diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index c56022749..8217af5cd 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -75,6 +75,8 @@ public class GitHub { private final String apiUrl; + /*package*/ final RateLimitHandler rateLimitHandler; + private HttpConnector connector = HttpConnector.DEFAULT; /** @@ -113,7 +115,7 @@ public class GitHub { * @param connector * HttpConnector to use. Pass null to use default connector. */ - /* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector) throws IOException { + /* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler) throws IOException { if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize this.apiUrl = apiUrl; if (null != connector) this.connector = connector; @@ -132,6 +134,7 @@ public class GitHub { if (login==null && encodedAuthorization!=null) login = getMyself().getLogin(); this.login = login; + this.rateLimitHandler = rateLimitHandler; } /** diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java index e794fb14c..f35c76502 100644 --- a/src/main/java/org/kohsuke/github/GitHubBuilder.java +++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java @@ -25,6 +25,8 @@ public class GitHubBuilder { private HttpConnector connector; + private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT; + public GitHubBuilder() { } @@ -155,6 +157,10 @@ public class GitHubBuilder { this.connector = connector; return this; } + public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) { + this.rateLimitHandler = handler; + return this; + } /** * Configures {@linkplain #withConnector(HttpConnector) connector} @@ -170,6 +176,6 @@ public class GitHubBuilder { } public GitHub build() throws IOException { - return new GitHub(endpoint, user, oauthToken, password, connector); + return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler); } } diff --git a/src/main/java/org/kohsuke/github/RateLimitHandler.java b/src/main/java/org/kohsuke/github/RateLimitHandler.java new file mode 100644 index 000000000..b00624ce2 --- /dev/null +++ b/src/main/java/org/kohsuke/github/RateLimitHandler.java @@ -0,0 +1,61 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; + +/** + * Pluggable strategy to determine what to do when the API rate limit is reached. + * + * @author Kohsuke Kawaguchi + * @see GitHubBuilder#withRateLimitHandler(RateLimitHandler) + */ +public abstract class RateLimitHandler { + /** + * Called when the library encounters HTTP error indicating that the API rate limit is reached. + * + *
+ * Any exception thrown from this method will cause the request to fail, and the caller of github-api + * will receive an exception. If this method returns normally, another request will be attempted. + * For that to make sense, the implementation needs to wait for some time. + * + * @see API documentation from GitHub + * @param e + * Exception from Java I/O layer. If you decide to fail the processing, you can throw + * this exception (or wrap this exception into another exception and throw it.) + * @param uc + * Connection that resulted in an error. Useful for accessing other response headers. + */ + public abstract void onError(IOException e, HttpURLConnection uc) throws IOException; + + /** + * Block until the API rate limit is reset. Useful for long-running batch processing. + */ + public static final RateLimitHandler WAIT = new RateLimitHandler() { + @Override + public void onError(IOException e, HttpURLConnection uc) throws IOException { + try { + Thread.sleep(parseWaitTime(uc)); + } catch (InterruptedException _) { + throw (InterruptedIOException)new InterruptedIOException().initCause(e); + } + } + + private long parseWaitTime(HttpURLConnection uc) { + String v = uc.getHeaderField("X-RateLimit-Reset"); + if (v==null) return 10000; // can't tell + + return Math.max(10000, Long.parseLong(v)*1000 - System.currentTimeMillis()); + } + }; + + /** + * Fail immediately. + */ + public static final RateLimitHandler FAIL = new RateLimitHandler() { + @Override + public void onError(IOException e, HttpURLConnection uc) throws IOException { + throw (IOException)new IOException("API rate limit reached").initCause(e); + } + }; +} diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index e2b0362a4..896bbd605 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -417,18 +417,11 @@ class Requester { } /** - * If the error is because of the API limit, wait 10 sec and return normally. - * Otherwise throw an exception reporting an error. + * Handle API error by either throwing it or by returning normally to retry. */ /*package*/ void handleApiError(IOException e, HttpURLConnection uc) throws IOException { if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) { - // API limit reached. wait 10 secs and return normally - try { - Thread.sleep(10000); - return; - } catch (InterruptedException _) { - throw (InterruptedIOException)new InterruptedIOException().initCause(e); - } + root.rateLimitHandler.onError(e,uc); } if (e instanceof FileNotFoundException)