Skip to content

Commit 1266dcc

Browse files
authored
Merge pull request hub4j#327 from stephenc/expose-rate-limit-headers
Expose Rate Limit Headers
2 parents 470da06 + 6fcddf4 commit 1266dcc

2 files changed

Lines changed: 139 additions & 25 deletions

File tree

src/main/java/org/kohsuke/github/GitHub.java

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@
2323
*/
2424
package org.kohsuke.github;
2525

26-
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
27-
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
28-
import static java.util.logging.Level.FINE;
29-
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
30-
import static org.kohsuke.github.Previews.DRAX;
31-
26+
import com.fasterxml.jackson.databind.DeserializationFeature;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std;
29+
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
3230
import java.io.ByteArrayInputStream;
3331
import java.io.FileNotFoundException;
3432
import java.io.IOException;
@@ -49,17 +47,19 @@
4947
import java.util.Map;
5048
import java.util.Set;
5149
import java.util.TimeZone;
52-
50+
import java.util.concurrent.TimeUnit;
51+
import java.util.logging.Level;
52+
import java.util.logging.Logger;
53+
import javax.annotation.CheckForNull;
54+
import javax.annotation.Nonnull;
5355
import org.apache.commons.codec.Charsets;
5456
import org.apache.commons.codec.binary.Base64;
5557

56-
import com.fasterxml.jackson.databind.DeserializationFeature;
57-
import com.fasterxml.jackson.databind.ObjectMapper;
58-
import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std;
59-
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
60-
61-
import javax.annotation.Nonnull;
62-
import java.util.logging.Logger;
58+
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
59+
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
60+
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
61+
import static java.util.logging.Level.FINE;
62+
import static org.kohsuke.github.Previews.DRAX;
6363

6464
/**
6565
* Root of the GitHub API.
@@ -90,6 +90,10 @@ public class GitHub {
9090

9191
private HttpConnector connector = HttpConnector.DEFAULT;
9292

93+
private final Object headerRateLimitLock = new Object();
94+
private GHRateLimit headerRateLimit = null;
95+
private volatile GHRateLimit rateLimit = null;
96+
9397
/**
9498
* Creates a client API root object.
9599
*
@@ -254,6 +258,10 @@ public HttpConnector getConnector() {
254258
return connector;
255259
}
256260

261+
public String getApiUrl() {
262+
return apiUrl;
263+
}
264+
257265
/**
258266
* Sets the custom connector used to make requests to GitHub.
259267
*/
@@ -287,17 +295,61 @@ public void setConnector(HttpConnector connector) {
287295
*/
288296
public GHRateLimit getRateLimit() throws IOException {
289297
try {
290-
return retrieve().to("/rate_limit", JsonRateLimit.class).rate;
298+
return rateLimit = retrieve().to("/rate_limit", JsonRateLimit.class).rate;
291299
} catch (FileNotFoundException e) {
292300
// GitHub Enterprise doesn't have the rate limit, so in that case
293301
// return some big number that's not too big.
294302
// see issue #78
295303
GHRateLimit r = new GHRateLimit();
296304
r.limit = r.remaining = 1000000;
297-
long hours = 1000L * 60 * 60;
298-
r.reset = new Date(System.currentTimeMillis() + 1 * hours );
299-
return r;
305+
long hour = 60L * 60L; // this is madness, storing the date as seconds in a Date object
306+
r.reset = new Date((System.currentTimeMillis() + hour) / 1000L );
307+
return rateLimit = r;
308+
}
309+
}
310+
311+
/*package*/ void updateRateLimit(@Nonnull GHRateLimit observed) {
312+
synchronized (headerRateLimitLock) {
313+
if (headerRateLimit == null
314+
|| headerRateLimit.getResetDate().getTime() < observed.getResetDate().getTime()
315+
|| headerRateLimit.remaining > observed.remaining) {
316+
headerRateLimit = observed;
317+
LOGGER.log(Level.INFO, "Rate limit now: {0}", headerRateLimit);
318+
}
319+
}
320+
}
321+
322+
/**
323+
* Returns the most recently observed rate limit data or {@code null} if either there is no rate limit
324+
* (for example GitHub Enterprise) or if no requests have been made.
325+
*
326+
* @return the most recently observed rate limit data or {@code null}.
327+
*/
328+
@CheckForNull
329+
public GHRateLimit lastRateLimit() {
330+
synchronized (headerRateLimitLock) {
331+
return headerRateLimit;
332+
}
333+
}
334+
335+
/**
336+
* Gets the current rate limit while trying not to actually make any remote requests unless absolutely necessary.
337+
*
338+
* @return the current rate limit data.
339+
* @throws IOException if we couldn't get the current rate limit data.
340+
*/
341+
@Nonnull
342+
public GHRateLimit rateLimit() throws IOException {
343+
synchronized (headerRateLimitLock) {
344+
if (headerRateLimit != null) {
345+
return headerRateLimit;
346+
}
347+
}
348+
GHRateLimit rateLimit = this.rateLimit;
349+
if (rateLimit == null || rateLimit.getResetDate().getTime() < System.currentTimeMillis()) {
350+
rateLimit = getRateLimit();
300351
}
352+
return rateLimit;
301353
}
302354

303355
/**

src/main/java/org/kohsuke/github/Requester.java

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525

2626
import com.fasterxml.jackson.databind.JsonMappingException;
2727
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28-
import org.apache.commons.io.IOUtils;
29-
3028
import java.io.FileNotFoundException;
3129
import java.io.IOException;
3230
import java.io.InputStream;
@@ -42,23 +40,26 @@
4240
import java.net.URLEncoder;
4341
import java.util.ArrayList;
4442
import java.util.Collection;
43+
import java.util.Date;
4544
import java.util.HashMap;
4645
import java.util.Iterator;
4746
import java.util.LinkedHashMap;
4847
import java.util.List;
4948
import java.util.Locale;
5049
import java.util.Map;
5150
import java.util.NoSuchElementException;
51+
import java.util.logging.Level;
5252
import java.util.logging.Logger;
5353
import java.util.regex.Matcher;
5454
import java.util.regex.Pattern;
5555
import java.util.zip.GZIPInputStream;
56-
5756
import javax.annotation.WillClose;
57+
import org.apache.commons.io.IOUtils;
58+
import org.apache.commons.lang.StringUtils;
5859

5960
import static java.util.Arrays.asList;
6061
import static java.util.logging.Level.FINE;
61-
import static org.kohsuke.github.GitHub.*;
62+
import static org.kohsuke.github.GitHub.MAPPER;
6263

6364
/**
6465
* A builder pattern for making HTTP call and parsing its output.
@@ -281,6 +282,8 @@ private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOExcepti
281282
return result;
282283
} catch (IOException e) {
283284
handleApiError(e);
285+
} finally {
286+
noteRateLimit(tailApiUrl);
284287
}
285288
}
286289
}
@@ -299,6 +302,8 @@ public int asHttpStatusCode(String tailApiUrl) throws IOException {
299302
return uc.getResponseCode();
300303
} catch (IOException e) {
301304
handleApiError(e);
305+
} finally {
306+
noteRateLimit(tailApiUrl);
302307
}
303308
}
304309
}
@@ -313,6 +318,59 @@ public InputStream asStream(String tailApiUrl) throws IOException {
313318
return wrapStream(uc.getInputStream());
314319
} catch (IOException e) {
315320
handleApiError(e);
321+
} finally {
322+
noteRateLimit(tailApiUrl);
323+
}
324+
}
325+
}
326+
327+
private void noteRateLimit(String tailApiUrl) {
328+
if ("/rate_limit".equals(tailApiUrl)) {
329+
// the rate_limit API is "free"
330+
return;
331+
}
332+
if (tailApiUrl.startsWith("/search")) {
333+
// the search API uses a different rate limit
334+
return;
335+
}
336+
String limit = uc.getHeaderField("X-RateLimit-Limit");
337+
if (StringUtils.isBlank(limit)) {
338+
// if we are missing a header, return fast
339+
return;
340+
}
341+
String remaining = uc.getHeaderField("X-RateLimit-Remaining");
342+
if (StringUtils.isBlank(remaining)) {
343+
// if we are missing a header, return fast
344+
return;
345+
}
346+
String reset = uc.getHeaderField("X-RateLimit-Reset");
347+
if (StringUtils.isBlank(reset)) {
348+
// if we are missing a header, return fast
349+
return;
350+
}
351+
GHRateLimit observed = new GHRateLimit();
352+
try {
353+
observed.limit = Integer.parseInt(limit);
354+
} catch (NumberFormatException e) {
355+
if (LOGGER.isLoggable(Level.FINEST)) {
356+
LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Limit header value " + limit, e);
357+
}
358+
return;
359+
}
360+
try {
361+
observed.remaining = Integer.parseInt(remaining);
362+
} catch (NumberFormatException e) {
363+
if (LOGGER.isLoggable(Level.FINEST)) {
364+
LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Remaining header value " + remaining, e);
365+
}
366+
return;
367+
}
368+
try {
369+
observed.reset = new Date(Long.parseLong(reset)); // this is madness, storing the date as seconds
370+
root.updateRateLimit(observed);
371+
} catch (NumberFormatException e) {
372+
if (LOGGER.isLoggable(Level.FINEST)) {
373+
LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Reset header value " + reset, e);
316374
}
317375
}
318376
}
@@ -382,7 +440,7 @@ private boolean isMethodWithBody() {
382440
}
383441

384442
try {
385-
return new PagingIterator<T>(type, root.getApiURL(s.toString()));
443+
return new PagingIterator<T>(type, tailApiUrl, root.getApiURL(s.toString()));
386444
} catch (IOException e) {
387445
throw new Error(e);
388446
}
@@ -391,6 +449,7 @@ private boolean isMethodWithBody() {
391449
class PagingIterator<T> implements Iterator<T> {
392450

393451
private final Class<T> type;
452+
private final String tailApiUrl;
394453

395454
/**
396455
* The next batch to be returned from {@link #next()}.
@@ -402,9 +461,10 @@ class PagingIterator<T> implements Iterator<T> {
402461
*/
403462
private URL url;
404463

405-
PagingIterator(Class<T> type, URL url) {
406-
this.url = url;
464+
PagingIterator(Class<T> type, String tailApiUrl, URL url) {
407465
this.type = type;
466+
this.tailApiUrl = tailApiUrl;
467+
this.url = url;
408468
}
409469

410470
public boolean hasNext() {
@@ -438,6 +498,8 @@ private void fetch() {
438498
return;
439499
} catch (IOException e) {
440500
handleApiError(e);
501+
} finally {
502+
noteRateLimit(tailApiUrl);
441503
}
442504
}
443505
} catch (IOException e) {

0 commit comments

Comments
 (0)