View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2013-2025 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.jcabi.github.wire;
6   
7   import com.jcabi.aspects.Immutable;
8   import com.jcabi.http.Request;
9   import com.jcabi.http.Response;
10  import com.jcabi.http.Wire;
11  import com.jcabi.log.Logger;
12  import java.io.IOException;
13  import java.io.InputStream;
14  import java.util.Collection;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.concurrent.TimeUnit;
18  import lombok.EqualsAndHashCode;
19  import lombok.ToString;
20  
21  /**
22   * Wire that waits if number of remaining request per hour is less than
23   * a given threshold.
24   *
25   * <p>GitHub sets following headers in each response:
26   * {@code X-RateLimit-Limit}, {@code X-RateLimit-Remaining}, and
27   * {@code X-RateLimit-Reset}. If {@code X-RateLimit-Remaining} is
28   * less than a given threshold, {@code CarefulWire} will sleep until a time
29   * specified in the {@code X-RateLimit-Reset} header. For further information
30   * about the GitHub rate limiting see
31   * <a href="https://developer.github.com/v3/#rate-limiting">API
32   * documentation</a>.
33   *
34   * <p>You can use {@code CarefulWire} with a {@link com.jcabi.github.GitHub}
35   * object:
36   * <pre>
37   * {@code
38   * GitHub github = new RtGitHub(
39   *     new RtGitHub().entry().through(CarefulWire.class, 50)
40   * );
41   * }
42   * </pre>
43   * @since 0.4
44   */
45  @Immutable
46  @ToString
47  @EqualsAndHashCode(of = { "origin", "threshold" })
48  @SuppressWarnings("PMD.AvoidDuplicateLiterals")
49  public final class CarefulWire implements Wire {
50  
51      /**
52       * Original wire.
53       */
54      private final transient Wire origin;
55  
56      /**
57       * Threshold of number of remaining requests, below which requests are
58       * blocked until reset.
59       */
60      private final transient int threshold;
61  
62      /**
63       * Public ctor.
64       *
65       * @param wire Original wire
66       * @param thrshld Threshold of number of remaining requests, below which
67       *  requests are blocked until reset
68       */
69      public CarefulWire(final Wire wire, final int thrshld) {
70          this.origin = wire;
71          this.threshold = thrshld;
72      }
73  
74      @Override
75      // @checkstyle ParameterNumber (8 lines)
76      public Response send(
77          final Request req,
78          final String home,
79          final String method,
80          final Collection<Map.Entry<String, String>> headers,
81          final InputStream content,
82          final int connect, final int read
83      ) throws IOException {
84          final Response resp = this.origin
85              .send(req, home, method, headers, content, connect, read);
86          final int remaining = CarefulWire.remainingHeader(resp);
87          if (remaining < this.threshold) {
88              final long reset = CarefulWire.resetHeader(resp);
89              final long now = TimeUnit.MILLISECONDS
90                  .toSeconds(System.currentTimeMillis());
91              if (reset > now) {
92                  final long length = reset - now;
93                  Logger.info(
94                      this,
95                      // @checkstyle LineLength (1 line)
96                      "Remaining number of requests per hour is less than %d. Waiting for %d seconds.",
97                      this.threshold, length
98                  );
99                  try {
100                     TimeUnit.SECONDS.sleep(length);
101                 } catch (final InterruptedException ex) {
102                     Thread.currentThread().interrupt();
103                     throw new IllegalStateException(ex);
104                 }
105             }
106         }
107         return resp;
108     }
109 
110     /**
111      * Get the header with the given name from the response.
112      * If there is no such header, returns null.
113      * @param resp Response to get header from
114      * @param headername Name of header to get
115      * @return The value of the first header with the given name, or null.
116      * @checkstyle NonStaticMethodCheck (5 lines)
117      */
118     @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
119     private static String headerOrNull(
120         final Response resp,
121         final String headername) {
122         final List<String> values = resp.headers().get(headername);
123         String value = null;
124         if (values != null && !values.isEmpty()) {
125             value = values.get(0);
126         }
127         return value;
128     }
129 
130     /**
131      * Returns the value of the X-RateLimit-Remaining header.
132      * If there is no such header, returns Integer.MAX_VALUE (no rate limit).
133      * @param resp Response to get header from
134      * @return Number of requests remaining before the rate limit will be hit
135      */
136     private static int remainingHeader(
137         final Response resp) {
138         final String remainingstr = CarefulWire.headerOrNull(
139             resp,
140             "X-RateLimit-Remaining"
141         );
142         int remaining = Integer.MAX_VALUE;
143         if (remainingstr != null) {
144             remaining = Integer.parseInt(remainingstr);
145         }
146         return remaining;
147     }
148 
149     /**
150      * Returns the value of the X-RateLimit-Reset header.
151      * If there is no such header, returns 0 (reset immediately).
152      * @param resp Response to get header from
153      * @return Timestamp (in seconds) at which the rate limit will reset
154      */
155     private static long resetHeader(
156         final Response resp) {
157         final String resetstr = CarefulWire.headerOrNull(resp, "X-RateLimit-Reset");
158         long reset = 0;
159         if (resetstr != null) {
160             reset = Long.parseLong(resetstr);
161         }
162         return reset;
163     }
164 
165 }