View Javadoc
1   /**
2    * Copyright (c) 2013-2023, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.github.wire;
31  
32  import com.jcabi.aspects.Immutable;
33  import com.jcabi.http.Request;
34  import com.jcabi.http.Response;
35  import com.jcabi.http.Wire;
36  import com.jcabi.log.Logger;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.util.Collection;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.concurrent.TimeUnit;
43  import lombok.EqualsAndHashCode;
44  import lombok.ToString;
45  
46  /**
47   * Wire that waits if number of remaining request per hour is less than
48   * a given threshold.
49   *
50   * <p>Github sets following headers in each response:
51   * {@code X-RateLimit-Limit}, {@code X-RateLimit-Remaining}, and
52   * {@code X-RateLimit-Reset}. If {@code X-RateLimit-Remaining} is
53   * less than a given threshold, {@code CarefulWire} will sleep until a time
54   * specified in the {@code X-RateLimit-Reset} header. For further information
55   * about the Github rate limiting see
56   * <a href="https://developer.github.com/v3/#rate-limiting">API
57   * documentation</a>.
58   *
59   * <p>You can use {@code CarefulWire} with a {@link com.jcabi.github.Github}
60   * object:
61   * <pre>
62   * {@code
63   * Github github = new RtGithub(
64   *     new RtGithub().entry().through(CarefulWire.class, 50)
65   * );
66   * }
67   * </pre>
68   *
69   * @author Alexander Sinyagin (sinyagin.alexander@gmail.com)
70   * @version $Id: cb6ac78caff6221a20b35c8a3656f45c91156a7f $
71   */
72  @Immutable
73  @ToString
74  @EqualsAndHashCode(of = { "origin", "threshold" })
75  @SuppressWarnings("PMD.AvoidDuplicateLiterals")
76  public final class CarefulWire implements Wire {
77  
78      /**
79       * Original wire.
80       */
81      private final transient Wire origin;
82  
83      /**
84       * Threshold of number of remaining requests, below which requests are
85       * blocked until reset.
86       */
87      private final transient int threshold;
88  
89      /**
90       * Public ctor.
91       *
92       * @param wire Original wire
93       * @param thrshld Threshold of number of remaining requests, below which
94       *  requests are blocked until reset
95       */
96      public CarefulWire(final Wire wire, final int thrshld) {
97          this.origin = wire;
98          this.threshold = thrshld;
99      }
100 
101     @Override
102     // @checkstyle ParameterNumber (8 lines)
103     public Response send(
104         final Request req,
105         final String home,
106         final String method,
107         final Collection<Map.Entry<String, String>> headers,
108         final InputStream content,
109         final int connect, final int read
110     ) throws IOException {
111         final Response resp = this.origin
112             .send(req, home, method, headers, content, connect, read);
113         final int remaining = this.remainingHeader(resp);
114         if (remaining < this.threshold) {
115             final long reset = this.resetHeader(resp);
116             final long now = TimeUnit.MILLISECONDS
117                 .toSeconds(System.currentTimeMillis());
118             if (reset > now) {
119                 final long length = reset - now;
120                 Logger.info(
121                     this,
122                     // @checkstyle LineLength (1 line)
123                     "Remaining number of requests per hour is less than %d. Waiting for %d seconds.",
124                     this.threshold, length
125                 );
126                 try {
127                     TimeUnit.SECONDS.sleep(length);
128                 } catch (final InterruptedException ex) {
129                     Thread.currentThread().interrupt();
130                     throw new IllegalStateException(ex);
131                 }
132             }
133         }
134         return resp;
135     }
136 
137     /**
138      * Get the header with the given name from the response.
139      * If there is no such header, returns null.
140      * @param resp Response to get header from
141      * @param headername Name of header to get
142      * @return The value of the first header with the given name, or null.
143      */
144     private String headerOrNull(
145         final Response resp,
146         final String headername) {
147         final List<String> values = resp.headers().get(headername);
148         String value = null;
149         if (values != null && !values.isEmpty()) {
150             value = values.get(0);
151         }
152         return value;
153     }
154 
155     /**
156      * Returns the value of the X-RateLimit-Remaining header.
157      * If there is no such header, returns Integer.MAX_VALUE (no rate limit).
158      * @param resp Response to get header from
159      * @return Number of requests remaining before the rate limit will be hit
160      */
161     private int remainingHeader(
162         final Response resp) {
163         final String remainingstr = this.headerOrNull(
164             resp,
165             "X-RateLimit-Remaining"
166         );
167         int remaining = Integer.MAX_VALUE;
168         if (remainingstr != null) {
169             remaining = Integer.parseInt(remainingstr);
170         }
171         return remaining;
172     }
173 
174     /**
175      * Returns the value of the X-RateLimit-Reset header.
176      * If there is no such header, returns 0 (reset immediately).
177      * @param resp Response to get header from
178      * @return Timestamp (in seconds) at which the rate limit will reset
179      */
180     private long resetHeader(
181         final Response resp) {
182         final String resetstr = this.headerOrNull(resp, "X-RateLimit-Reset");
183         long reset = 0;
184         if (resetstr != null) {
185             reset = Long.parseLong(resetstr);
186         }
187         return reset;
188     }
189 
190 }