> getRequestProperties() {
- return responseInfo.request().allHeaders();
+ return connectorResponse.request().allHeaders();
}
@Override
diff --git a/src/main/java/org/kohsuke/github/HttpConnector.java b/src/main/java/org/kohsuke/github/HttpConnector.java
index 0cb92ff40e..91471ec7d9 100644
--- a/src/main/java/org/kohsuke/github/HttpConnector.java
+++ b/src/main/java/org/kohsuke/github/HttpConnector.java
@@ -12,9 +12,11 @@
*
* For example, you can implement this to st custom timeouts.
*
+ * @deprecated Use {@link org.kohsuke.github.connector.GitHubConnector} instead.
* @author Kohsuke Kawaguchi
*/
@FunctionalInterface
+@Deprecated
public interface HttpConnector {
/**
* Opens a connection to the given URL.
diff --git a/src/main/java/org/kohsuke/github/RateLimitHandler.java b/src/main/java/org/kohsuke/github/RateLimitHandler.java
index b8a5461de6..58a5f89f1e 100644
--- a/src/main/java/org/kohsuke/github/RateLimitHandler.java
+++ b/src/main/java/org/kohsuke/github/RateLimitHandler.java
@@ -1,5 +1,7 @@
package org.kohsuke.github;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
@@ -21,19 +23,19 @@ public abstract class RateLimitHandler {
* 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.
*
- * @param responseInfo
+ * @param connectorResponse
* Response information for this request.
*
* @throws IOException
* the io exception
* @see API documentation from GitHub
*/
- void onError(GitHubResponse.ResponseInfo responseInfo) throws IOException {
+ void onError(GitHubConnectorResponse connectorResponse) throws IOException {
GHIOException e = new HttpException("Rate limit violation",
- responseInfo.statusCode(),
- responseInfo.header("Status"),
- responseInfo.url().toString()).withResponseHeaderFields(responseInfo.allHeaders());
- onError(e, new GitHubResponseInfoHttpURLConnectionAdapter(responseInfo));
+ connectorResponse.statusCode(),
+ connectorResponse.header("Status"),
+ connectorResponse.url().toString()).withResponseHeaderFields(connectorResponse.allHeaders());
+ onError(e, new GitHubResponseInfoHttpURLConnectionAdapter(connectorResponse));
}
/**
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index 924f3b7318..2f3154ec09 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -25,6 +25,8 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import org.apache.commons.io.IOUtils;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+import org.kohsuke.github.function.BodyHandler;
import org.kohsuke.github.function.InputStreamFunction;
import java.io.ByteArrayInputStream;
@@ -57,7 +59,7 @@ class Requester extends GitHubRequest.Builder {
public void send() throws IOException {
// Send expects there to be some body response, but doesn't care what it is.
// If there isn't a body, this will throw.
- client.sendRequest(this, (responseInfo) -> responseInfo.getBodyAsString());
+ client.sendRequest(this, (connectorResponse) -> GitHubResponse.getBodyAsString(connectorResponse));
}
/**
@@ -72,7 +74,8 @@ public void send() throws IOException {
* if the server returns 4xx/5xx responses.
*/
public T fetch(@Nonnull Class type) throws IOException {
- return client.sendRequest(this, (responseInfo) -> GitHubResponse.parseBody(responseInfo, type)).body();
+ return client.sendRequest(this, (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, type))
+ .body();
}
/**
@@ -87,7 +90,8 @@ public T fetch(@Nonnull Class type) throws IOException {
* the io exception
*/
public T fetchInto(@Nonnull T existingInstance) throws IOException {
- return client.sendRequest(this, (responseInfo) -> GitHubResponse.parseBody(responseInfo, existingInstance))
+ return client
+ .sendRequest(this, (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, existingInstance))
.body();
}
@@ -111,17 +115,17 @@ public int fetchHttpStatusCode() throws IOException {
* the io exception
*/
public T fetchStream(@Nonnull InputStreamFunction handler) throws IOException {
- return client.sendRequest(this, (responseInfo) -> handler.apply(responseInfo.bodyStream())).body();
+ return client.sendRequest(this, (connectorResponse) -> handler.apply(connectorResponse.bodyStream())).body();
}
/**
* Helper function to make it easy to pull streams.
*
* Copies an input stream to an in-memory input stream. The performance on this is not great but
- * {@link GitHubResponse.ResponseInfo#bodyStream()} is closed at the end of every call to
- * {@link GitHubClient#sendRequest(GitHubRequest, GitHubResponse.BodyHandler)}, so any reads to the original input
- * stream must be completed before then. There are a number of deprecated methods that return {@link InputStream}.
- * This method keeps all of them using the same code path.
+ * {@link GitHubConnectorResponse#bodyStream()} is closed at the end of every call to
+ * {@link GitHubClient#sendRequest(GitHubRequest, BodyHandler)}, so any reads to the original input stream must be
+ * completed before then. There are a number of deprecated methods that return {@link InputStream}. This method
+ * keeps all of them using the same code path.
*
* @param inputStream
* the input stream to be copied
diff --git a/src/main/java/org/kohsuke/github/connector/GitHubConnector.java b/src/main/java/org/kohsuke/github/connector/GitHubConnector.java
new file mode 100644
index 0000000000..21558e985b
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/connector/GitHubConnector.java
@@ -0,0 +1,29 @@
+package org.kohsuke.github.connector;
+
+import org.kohsuke.github.internal.DefaultGitHubConnector;
+import org.kohsuke.github.internal.GitHubConnectorHttpConnectorAdapter;
+
+import java.io.IOException;
+
+/**
+ * Pluggability for customizing HTTP request behaviors or using altogether different library.
+ *
+ * @author Liam Newman
+ */
+@FunctionalInterface
+public interface GitHubConnector {
+
+ GitHubConnectorResponse send(GitHubConnectorRequest request) throws IOException;
+
+ /**
+ * Default implementation used when connector is not set by user.
+ */
+ GitHubConnector DEFAULT = DefaultGitHubConnector.create();
+
+ /**
+ * Stub implementation that is always off-line.
+ */
+ GitHubConnector OFFLINE = new GitHubConnectorHttpConnectorAdapter(url -> {
+ throw new IOException("Offline");
+ });
+}
diff --git a/src/main/java/org/kohsuke/github/connector/GitHubConnectorRequest.java b/src/main/java/org/kohsuke/github/connector/GitHubConnectorRequest.java
new file mode 100644
index 0000000000..451987c0e3
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/connector/GitHubConnectorRequest.java
@@ -0,0 +1,34 @@
+package org.kohsuke.github.connector;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+public interface GitHubConnectorRequest {
+ @Nonnull
+ String method();
+
+ @Nonnull
+ Map> allHeaders();
+
+ @CheckForNull
+ String header(String name);
+
+ @CheckForNull
+ String contentType();
+
+ @CheckForNull
+ InputStream body();
+
+ @Nonnull
+ URL url();
+
+ boolean hasBody();
+
+ @Nonnull
+ Map injectedMappingValues();
+}
diff --git a/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java b/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java
new file mode 100644
index 0000000000..32cada1c8c
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java
@@ -0,0 +1,120 @@
+package org.kohsuke.github.connector;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.kohsuke.github.GitHubRequest;
+import org.kohsuke.github.function.BodyHandler;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.*;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * Response information supplied to a {@link BodyHandler} when a response is received and before the body is processed.
+ *
+ * Instances of this class are closed once the response is done being processed. This means that contents of the body
+ * stream will not be readable after a call is completed. Status code, response headers, and request details will still
+ * be readable.
+ */
+public abstract class GitHubConnectorResponse implements Closeable {
+
+ private static final Comparator nullableCaseInsensitiveComparator = Comparator
+ .nullsFirst(String.CASE_INSENSITIVE_ORDER);
+
+ private final int statusCode;
+ @Nonnull
+ private final GitHubConnectorRequest request;
+ @Nonnull
+ private final Map> headers;
+
+ protected GitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
+ int statusCode,
+ @Nonnull Map> headers) {
+ this.request = request;
+ this.statusCode = statusCode;
+
+ // Response header field names must be case-insensitive.
+ TreeMap> caseInsensitiveMap = new TreeMap<>(nullableCaseInsensitiveComparator);
+ for (Map.Entry> entry : headers.entrySet()) {
+ caseInsensitiveMap.put(entry.getKey(), Collections.unmodifiableList(new ArrayList<>(entry.getValue())));
+ }
+ this.headers = Collections.unmodifiableMap(caseInsensitiveMap);
+ }
+
+ /**
+ * Gets the value of a header field for this response.
+ *
+ * @param name
+ * the name of the header field.
+ * @return the value of the header field, or {@code null} if the header isn't set.
+ */
+ @CheckForNull
+ public String header(String name) {
+ String result = null;
+ if (headers.containsKey(name)) {
+ result = headers.get(name).get(0);
+ }
+ return result;
+ }
+
+ /**
+ * The response body as an {@link InputStream}.
+ *
+ * @return the response body
+ * @throws IOException
+ * if an I/O Exception occurs.
+ */
+ public abstract InputStream bodyStream() throws IOException;
+
+ /**
+ * The error message for this response.
+ *
+ * @return if there is an error with some error string, that is returned. If not, {@code null}.
+ */
+ public abstract String errorMessage();
+
+ /**
+ * The {@link URL} for this response.
+ *
+ * @return the {@link URL} for this response.
+ */
+ @Nonnull
+ public URL url() {
+ return request.url();
+ }
+
+ /**
+ * Gets the {@link GitHubRequest} for this response.
+ *
+ * @return the {@link GitHubRequest} for this response.
+ */
+ @Nonnull
+ public GitHubConnectorRequest request() {
+ return request;
+ }
+
+ /**
+ * The status code for this response.
+ *
+ * @return the status code for this response.
+ */
+ public int statusCode() {
+ return statusCode;
+ }
+
+ /**
+ * The headers for this response.
+ *
+ * @return the headers for this response.
+ */
+ @Nonnull
+ @SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable map of unmodifiable lists")
+ public Map> allHeaders() {
+ return headers;
+ }
+
+}
diff --git a/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpConnector.java b/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpConnector.java
index f415f832de..4c855eb16f 100644
--- a/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpConnector.java
+++ b/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpConnector.java
@@ -3,6 +3,9 @@
import okhttp3.*;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.*;
+import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.connector.GitHubConnectorRequest;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -67,7 +70,7 @@ public OkHttpConnector(OkHttpClient client, int cacheMaxAge) {
}
@Override
- public GitHubResponse.ResponseInfo send(GitHubRequest request) throws IOException {
+ public GitHubConnectorResponse send(GitHubConnectorRequest request) throws IOException {
Request.Builder builder = new Request.Builder().url(request.url());
if (maxAgeHeaderValue != null && request.header(HEADER_NAME) == null) {
// By default OkHttp honors max-age, meaning it will use local cache
@@ -87,14 +90,14 @@ public GitHubResponse.ResponseInfo send(GitHubRequest request) throws IOExceptio
}
RequestBody body = null;
- if (request.inBody()) {
+ if (request.hasBody()) {
body = RequestBody.create(IOUtils.toByteArray(request.body()));
}
builder.method(request.method(), body);
Request okhttpRequest = builder.build();
Response okhttpResponse = client.newCall(okhttpRequest).execute();
- return new OkHttpResponseInfo(request, okhttpResponse);
+ return new OkHttpGitHubConnectorResponse(request, okhttpResponse);
}
/** Returns connection spec with TLS v1.2 in it */
@@ -107,12 +110,12 @@ private List TlsConnectionSpecs() {
*
* Implementation specific to {@link okhttp3.Response}.
*/
- static class OkHttpResponseInfo extends GitHubResponse.ResponseInfo {
+ static class OkHttpGitHubConnectorResponse extends GitHubConnectorResponse {
@Nonnull
private final Response response;
- OkHttpResponseInfo(@Nonnull GitHubRequest request, @Nonnull Response response) {
+ OkHttpGitHubConnectorResponse(@Nonnull GitHubConnectorRequest request, @Nonnull Response response) {
super(request, response.code(), response.headers().toMultimap());
this.response = response;
}
diff --git a/src/main/java/org/kohsuke/github/function/BodyHandler.java b/src/main/java/org/kohsuke/github/function/BodyHandler.java
new file mode 100644
index 0000000000..0083dcd526
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/function/BodyHandler.java
@@ -0,0 +1,15 @@
+package org.kohsuke.github.function;
+
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
+import java.io.IOException;
+
+/**
+ * Represents a supplier of results that can throw.
+ *
+ * @param
+ * the type of results supplied by this supplier
+ */
+@FunctionalInterface
+public interface BodyHandler extends FunctionThrows {
+}
diff --git a/src/main/java/org/kohsuke/github/internal/DefaultGitHubConnector.java b/src/main/java/org/kohsuke/github/internal/DefaultGitHubConnector.java
new file mode 100644
index 0000000000..c8220c7fec
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/DefaultGitHubConnector.java
@@ -0,0 +1,22 @@
+package org.kohsuke.github.internal;
+
+import okhttp3.OkHttpClient;
+import org.kohsuke.github.HttpConnector;
+import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.extras.okhttp3.OkHttpConnector;
+
+public class DefaultGitHubConnector {
+ public static GitHubConnector create() {
+ String defaultConnectorProperty = System.getProperty("test.github.connector", "default");
+ if (defaultConnectorProperty.equalsIgnoreCase("okhttp")) {
+ return new OkHttpConnector(new OkHttpClient.Builder().build());
+ } else if (defaultConnectorProperty.equalsIgnoreCase("httpconnector")) {
+ return new GitHubConnectorHttpConnectorAdapter(HttpConnector.DEFAULT);
+ } else if (defaultConnectorProperty.equalsIgnoreCase("default")) {
+ return new GitHubConnectorHttpConnectorAdapter(HttpConnector.DEFAULT);
+ } else {
+ throw new IllegalStateException(
+ "Property 'test.github.connector' must reference a valid built-in connector - okhttp, httpconnector, or default.");
+ }
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubConnectorHttpConnectorAdapter.java b/src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java
similarity index 85%
rename from src/main/java/org/kohsuke/github/GitHubConnectorHttpConnectorAdapter.java
rename to src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java
index 6c4b586e4a..451da0a175 100644
--- a/src/main/java/org/kohsuke/github/GitHubConnectorHttpConnectorAdapter.java
+++ b/src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java
@@ -1,7 +1,11 @@
-package org.kohsuke.github;
+package org.kohsuke.github.internal;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
+import org.kohsuke.github.*;
+import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.connector.GitHubConnectorRequest;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
import java.io.IOException;
import java.io.InputStream;
@@ -30,7 +34,7 @@
* GitHubHttpUrlConnectionClient gets a new {@link HttpURLConnection} for each call to send.
*
*/
-class GitHubConnectorHttpConnectorAdapter implements GitHubConnector {
+public class GitHubConnectorHttpConnectorAdapter implements GitHubConnector, HttpConnector {
final HttpConnector httpConnector;
@@ -53,13 +57,13 @@ public static GitHubConnector adapt(HttpConnector connector) {
return gitHubConnector;
}
- @Override
+ @Nonnull
public HttpURLConnection connect(URL url) throws IOException {
return this.httpConnector.connect(url);
}
@Nonnull
- public GitHubResponse.ResponseInfo send(GitHubRequest request) throws IOException {
+ public GitHubConnectorResponse send(GitHubConnectorRequest request) throws IOException {
HttpURLConnection connection;
try {
connection = setupConnection(httpConnector, request);
@@ -73,11 +77,11 @@ public GitHubResponse.ResponseInfo send(GitHubRequest request) throws IOExceptio
int statusCode = connection.getResponseCode();
Map> headers = connection.getHeaderFields();
- return new HttpURLConnectionResponseInfo(request, statusCode, headers, connection);
+ return new HttpURLConnectionGitHubConnectorResponse(request, statusCode, headers, connection);
}
@Nonnull
- static HttpURLConnection setupConnection(@Nonnull HttpConnector connector, @Nonnull GitHubRequest request)
+ static HttpURLConnection setupConnection(@Nonnull HttpConnector connector, @Nonnull GitHubConnectorRequest request)
throws IOException {
HttpURLConnection connection = connector.connect(request.url());
setRequestMethod(request.method(), connection);
@@ -89,14 +93,14 @@ static HttpURLConnection setupConnection(@Nonnull HttpConnector connector, @Nonn
/**
* Set up the request parameters or POST payload.
*/
- private static void buildRequest(GitHubRequest request, HttpURLConnection connection) throws IOException {
+ private static void buildRequest(GitHubConnectorRequest request, HttpURLConnection connection) throws IOException {
for (Map.Entry> e : request.allHeaders().entrySet()) {
List v = e.getValue();
if (v != null)
connection.setRequestProperty(e.getKey(), String.join(", ", v));
}
- if (request.inBody()) {
+ if (request.hasBody()) {
connection.setDoOutput(true);
IOUtils.copyLarge(request.body(), connection.getOutputStream());
}
@@ -134,17 +138,17 @@ private static void setRequestMethod(String method, HttpURLConnection connection
}
/**
- * Initial response information supplied to a {@link GitHubResponse.BodyHandler} when a response is initially
- * received and before the body is processed.
+ * Initial response information supplied to a {@link org.kohsuke.github.function.BodyHandler} when a response is
+ * initially received and before the body is processed.
*
* Implementation specific to {@link HttpURLConnection}.
*/
- static class HttpURLConnectionResponseInfo extends GitHubResponse.ResponseInfo {
+ static class HttpURLConnectionGitHubConnectorResponse extends GitHubConnectorResponse {
@Nonnull
private final HttpURLConnection connection;
- HttpURLConnectionResponseInfo(@Nonnull GitHubRequest request,
+ HttpURLConnectionGitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
int statusCode,
@Nonnull Map> headers,
@Nonnull HttpURLConnection connection) {
@@ -195,7 +199,7 @@ private InputStream wrapStream(InputStream stream) throws IOException {
throw new UnsupportedOperationException("Unexpected Content-Encoding: " + encoding);
}
- private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());
+ private static final Logger LOGGER = Logger.getLogger(GitHub.class.getName());
@Override
public void close() throws IOException {
diff --git a/src/test/java/org/kohsuke/github/GitHubStaticTest.java b/src/test/java/org/kohsuke/github/GitHubStaticTest.java
index 0c57f1c0ce..37f67aa2ca 100644
--- a/src/test/java/org/kohsuke/github/GitHubStaticTest.java
+++ b/src/test/java/org/kohsuke/github/GitHubStaticTest.java
@@ -2,6 +2,8 @@
import org.junit.Assert;
import org.junit.Test;
+import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
import java.net.MalformedURLException;
import java.net.URL;
@@ -324,7 +326,7 @@ public void testMappingReaderWriter() throws Exception {
assertThat(repoString, not(nullValue()));
assertThat(repoString, containsString("testMappingReaderWriter"));
- GHRepository readRepo = GitHubClient.getMappingObjectReader((GitHubResponse.ResponseInfo) null)
+ GHRepository readRepo = GitHubClient.getMappingObjectReader((GitHubConnectorResponse) null)
.forType(GHRepository.class)
.readValue(repoString);
diff --git a/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenOver250/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json b/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenOver250/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json
index d0313d237e..0f14fb7af1 100644
--- a/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenOver250/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json
+++ b/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenOver250/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json
@@ -21136,7 +21136,7 @@
"blob_url": "https://github.com/hub4j-test-org/github-api/blob/94ff089e60064bfa43e374baeb10846f7ce82f40/src/main/java/org/kohsuke/github/GitHubClient.java",
"raw_url": "https://github.com/hub4j-test-org/github-api/raw/94ff089e60064bfa43e374baeb10846f7ce82f40/src/main/java/org/kohsuke/github/GitHubClient.java",
"contents_url": "https://api.github.com/repos/hub4j-test-org/github-api/contents/src/main/java/org/kohsuke/github/GitHubClient.java?ref=94ff089e60064bfa43e374baeb10846f7ce82f40",
- "patch": "@@ -14,6 +14,7 @@\n import java.time.format.DateTimeFormatter;\n import java.time.temporal.ChronoUnit;\n import java.util.*;\n+import java.util.concurrent.atomic.AtomicReference;\n import java.util.function.Consumer;\n import java.util.logging.Logger;\n \n@@ -52,10 +53,8 @@\n \n private HttpConnector connector;\n \n- private final Object rateLimitLock = new Object();\n-\n @Nonnull\n- private GHRateLimit rateLimit = GHRateLimit.DEFAULT;\n+ private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);\n \n private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());\n \n@@ -141,10 +140,9 @@ public boolean isCredentialValid() {\n getRateLimit();\n return true;\n } catch (IOException e) {\n- if (LOGGER.isLoggable(FINE))\n- LOGGER.log(FINE,\n- \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n- e);\n+ LOGGER.log(FINE,\n+ \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n+ e);\n return false;\n }\n }\n@@ -256,9 +254,7 @@ GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExce\n @Nonnull\n @Deprecated\n GHRateLimit lastRateLimit() {\n- synchronized (rateLimitLock) {\n- return rateLimit;\n- }\n+ return rateLimit.get();\n }\n \n /**\n@@ -278,12 +274,19 @@ GHRateLimit lastRateLimit() {\n */\n @Nonnull\n GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {\n- synchronized (rateLimitLock) {\n- if (rateLimit.getRecord(rateLimitTarget).isExpired()) {\n- getRateLimit(rateLimitTarget);\n+ GHRateLimit result = rateLimit.get();\n+ // Most of the time rate limit is not expired, so try to avoid locking.\n+ if (result.getRecord(rateLimitTarget).isExpired()) {\n+ // if the rate limit is expired, synchronize to ensure\n+ // only one call to getRateLimit() is made to refresh it.\n+ synchronized (this) {\n+ if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {\n+ getRateLimit(rateLimitTarget);\n+ }\n }\n- return rateLimit;\n+ result = rateLimit.get();\n }\n+ return result;\n }\n \n /**\n@@ -296,15 +299,9 @@ GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExcepti\n * {@link GHRateLimit.Record} constructed from the response header information\n */\n private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {\n- synchronized (rateLimitLock) {\n- observed = rateLimit.getMergedRateLimit(observed);\n-\n- if (rateLimit != observed) {\n- rateLimit = observed;\n- LOGGER.log(FINE, \"Rate limit now: {0}\", rateLimit);\n- }\n- return rateLimit;\n- }\n+ GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));\n+ LOGGER.log(FINEST, \"Rate limit now: {0}\", rateLimit.get());\n+ return result;\n }\n \n /**\n@@ -384,11 +381,7 @@ public String getApiUrl() {\n GitHubResponse.ResponseInfo responseInfo = null;\n try {\n try {\n- if (LOGGER.isLoggable(FINE)) {\n- LOGGER.log(FINE,\n- \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \"\n- + request.method() + \" \" + request.url().toString());\n- }\n+ logRequest(request);\n rateLimitChecker.checkRateLimit(this, request);\n \n responseInfo = getResponseInfo(request);\n@@ -424,6 +417,12 @@ public String getApiUrl() {\n throw new GHIOException(\"Ran out of retries for URL: \" + request.url().toString());\n }\n \n+ private void logRequest(@Nonnull final GitHubRequest request) {\n+ LOGGER.log(FINE,\n+ () -> \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \" + request.method() + \" \"\n+ + request.url().toString());\n+ }\n+\n @Nonnull\n protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;\n \n@@ -437,20 +436,15 @@ public String getApiUrl() {\n // special case handling for 304 unmodified, as the content will be \"\"\n } else if (responseInfo.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {\n \n- // Response code 202 means data is being generated.\n+ // Response code 202 means data is being generated or an action that can require some time is triggered.\n // This happens in specific cases:\n // statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching\n // fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork\n+ // workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run\n \n- if (responseInfo.url().toString().endsWith(\"/forks\")) {\n- LOGGER.log(INFO, \"The fork is being created. Please try again in 5 seconds.\");\n- } else if (responseInfo.url().toString().endsWith(\"/statistics\")) {\n- LOGGER.log(INFO, \"The statistics are being generated. Please try again in 5 seconds.\");\n- } else {\n- LOGGER.log(INFO,\n- \"Received 202 from \" + responseInfo.url().toString() + \" . Please try again in 5 seconds.\");\n- }\n- // Maybe throw an exception instead?\n+ LOGGER.log(FINE,\n+ \"Received HTTP_ACCEPTED(202) from \" + responseInfo.url().toString()\n+ + \" . Please try again in 5 seconds.\");\n } else if (handler != null) {\n body = handler.apply(responseInfo);\n }\n@@ -562,9 +556,7 @@ private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo responseInfo) {\n GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, responseInfo);\n updateRateLimit(GHRateLimit.fromRecord(observed, responseInfo.request().rateLimitTarget()));\n } catch (NumberFormatException | NullPointerException e) {\n- if (LOGGER.isLoggable(FINEST)) {\n- LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n- }\n+ LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n }\n }\n "
+ "patch": "@@ -14,6 +14,7 @@\n import java.time.format.DateTimeFormatter;\n import java.time.temporal.ChronoUnit;\n import java.util.*;\n+import java.util.concurrent.atomic.AtomicReference;\n import java.util.function.Consumer;\n import java.util.logging.Logger;\n \n@@ -52,10 +53,8 @@\n \n private HttpConnector connector;\n \n- private final Object rateLimitLock = new Object();\n-\n @Nonnull\n- private GHRateLimit rateLimit = GHRateLimit.DEFAULT;\n+ private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);\n \n private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());\n \n@@ -141,10 +140,9 @@ public boolean isCredentialValid() {\n getRateLimit();\n return true;\n } catch (IOException e) {\n- if (LOGGER.isLoggable(FINE))\n- LOGGER.log(FINE,\n- \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n- e);\n+ LOGGER.log(FINE,\n+ \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n+ e);\n return false;\n }\n }\n@@ -256,9 +254,7 @@ GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExce\n @Nonnull\n @Deprecated\n GHRateLimit lastRateLimit() {\n- synchronized (rateLimitLock) {\n- return rateLimit;\n- }\n+ return rateLimit.get();\n }\n \n /**\n@@ -278,12 +274,19 @@ GHRateLimit lastRateLimit() {\n */\n @Nonnull\n GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {\n- synchronized (rateLimitLock) {\n- if (rateLimit.getRecord(rateLimitTarget).isExpired()) {\n- getRateLimit(rateLimitTarget);\n+ GHRateLimit result = rateLimit.get();\n+ // Most of the time rate limit is not expired, so try to avoid locking.\n+ if (result.getRecord(rateLimitTarget).isExpired()) {\n+ // if the rate limit is expired, synchronize to ensure\n+ // only one call to getRateLimit() is made to refresh it.\n+ synchronized (this) {\n+ if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {\n+ getRateLimit(rateLimitTarget);\n+ }\n }\n- return rateLimit;\n+ result = rateLimit.get();\n }\n+ return result;\n }\n \n /**\n@@ -296,15 +299,9 @@ GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExcepti\n * {@link GHRateLimit.Record} constructed from the response header information\n */\n private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {\n- synchronized (rateLimitLock) {\n- observed = rateLimit.getMergedRateLimit(observed);\n-\n- if (rateLimit != observed) {\n- rateLimit = observed;\n- LOGGER.log(FINE, \"Rate limit now: {0}\", rateLimit);\n- }\n- return rateLimit;\n- }\n+ GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));\n+ LOGGER.log(FINEST, \"Rate limit now: {0}\", rateLimit.get());\n+ return result;\n }\n \n /**\n@@ -384,11 +381,7 @@ public String getApiUrl() {\n GitHubResponse.ResponseInfo connectorResponse = null;\n try {\n try {\n- if (LOGGER.isLoggable(FINE)) {\n- LOGGER.log(FINE,\n- \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \"\n- + request.method() + \" \" + request.url().toString());\n- }\n+ logRequest(request);\n rateLimitChecker.checkRateLimit(this, request);\n \n connectorResponse = getResponseInfo(request);\n@@ -424,6 +417,12 @@ public String getApiUrl() {\n throw new GHIOException(\"Ran out of retries for URL: \" + request.url().toString());\n }\n \n+ private void logRequest(@Nonnull final GitHubRequest request) {\n+ LOGGER.log(FINE,\n+ () -> \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \" + request.method() + \" \"\n+ + request.url().toString());\n+ }\n+\n @Nonnull\n protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;\n \n@@ -437,20 +436,15 @@ public String getApiUrl() {\n // special case handling for 304 unmodified, as the content will be \"\"\n } else if (connectorResponse.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {\n \n- // Response code 202 means data is being generated.\n+ // Response code 202 means data is being generated or an action that can require some time is triggered.\n // This happens in specific cases:\n // statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching\n // fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork\n+ // workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run\n \n- if (connectorResponse.url().toString().endsWith(\"/forks\")) {\n- LOGGER.log(INFO, \"The fork is being created. Please try again in 5 seconds.\");\n- } else if (connectorResponse.url().toString().endsWith(\"/statistics\")) {\n- LOGGER.log(INFO, \"The statistics are being generated. Please try again in 5 seconds.\");\n- } else {\n- LOGGER.log(INFO,\n- \"Received 202 from \" + connectorResponse.url().toString() + \" . Please try again in 5 seconds.\");\n- }\n- // Maybe throw an exception instead?\n+ LOGGER.log(FINE,\n+ \"Received HTTP_ACCEPTED(202) from \" + connectorResponse.url().toString()\n+ + \" . Please try again in 5 seconds.\");\n } else if (handler != null) {\n body = handler.apply(connectorResponse);\n }\n@@ -562,9 +556,7 @@ private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo connectorResponse) {\n GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, connectorResponse);\n updateRateLimit(GHRateLimit.fromRecord(observed, connectorResponse.request().rateLimitTarget()));\n } catch (NumberFormatException | NullPointerException e) {\n- if (LOGGER.isLoggable(FINEST)) {\n- LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n- }\n+ LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n }\n }\n "
},
{
"sha": "aea8b99b1f3301f65c8bc86c657bfd187db5c178",
diff --git a/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json b/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json
index a66e4891aa..7ff57c7ca2 100644
--- a/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json
+++ b/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-4.json
@@ -1098,7 +1098,7 @@
"blob_url": "https://github.com/hub4j-test-org/github-api/blob/94ff089e60064bfa43e374baeb10846f7ce82f40/src/main/java/org/kohsuke/github/GitHubClient.java",
"raw_url": "https://github.com/hub4j-test-org/github-api/raw/94ff089e60064bfa43e374baeb10846f7ce82f40/src/main/java/org/kohsuke/github/GitHubClient.java",
"contents_url": "https://api.github.com/repos/hub4j-test-org/github-api/contents/src/main/java/org/kohsuke/github/GitHubClient.java?ref=94ff089e60064bfa43e374baeb10846f7ce82f40",
- "patch": "@@ -14,6 +14,7 @@\n import java.time.format.DateTimeFormatter;\n import java.time.temporal.ChronoUnit;\n import java.util.*;\n+import java.util.concurrent.atomic.AtomicReference;\n import java.util.function.Consumer;\n import java.util.logging.Logger;\n \n@@ -52,10 +53,8 @@\n \n private HttpConnector connector;\n \n- private final Object rateLimitLock = new Object();\n-\n @Nonnull\n- private GHRateLimit rateLimit = GHRateLimit.DEFAULT;\n+ private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);\n \n private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());\n \n@@ -141,10 +140,9 @@ public boolean isCredentialValid() {\n getRateLimit();\n return true;\n } catch (IOException e) {\n- if (LOGGER.isLoggable(FINE))\n- LOGGER.log(FINE,\n- \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n- e);\n+ LOGGER.log(FINE,\n+ \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n+ e);\n return false;\n }\n }\n@@ -256,9 +254,7 @@ GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExce\n @Nonnull\n @Deprecated\n GHRateLimit lastRateLimit() {\n- synchronized (rateLimitLock) {\n- return rateLimit;\n- }\n+ return rateLimit.get();\n }\n \n /**\n@@ -278,12 +274,19 @@ GHRateLimit lastRateLimit() {\n */\n @Nonnull\n GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {\n- synchronized (rateLimitLock) {\n- if (rateLimit.getRecord(rateLimitTarget).isExpired()) {\n- getRateLimit(rateLimitTarget);\n+ GHRateLimit result = rateLimit.get();\n+ // Most of the time rate limit is not expired, so try to avoid locking.\n+ if (result.getRecord(rateLimitTarget).isExpired()) {\n+ // if the rate limit is expired, synchronize to ensure\n+ // only one call to getRateLimit() is made to refresh it.\n+ synchronized (this) {\n+ if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {\n+ getRateLimit(rateLimitTarget);\n+ }\n }\n- return rateLimit;\n+ result = rateLimit.get();\n }\n+ return result;\n }\n \n /**\n@@ -296,15 +299,9 @@ GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExcepti\n * {@link GHRateLimit.Record} constructed from the response header information\n */\n private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {\n- synchronized (rateLimitLock) {\n- observed = rateLimit.getMergedRateLimit(observed);\n-\n- if (rateLimit != observed) {\n- rateLimit = observed;\n- LOGGER.log(FINE, \"Rate limit now: {0}\", rateLimit);\n- }\n- return rateLimit;\n- }\n+ GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));\n+ LOGGER.log(FINEST, \"Rate limit now: {0}\", rateLimit.get());\n+ return result;\n }\n \n /**\n@@ -384,11 +381,7 @@ public String getApiUrl() {\n GitHubResponse.ResponseInfo responseInfo = null;\n try {\n try {\n- if (LOGGER.isLoggable(FINE)) {\n- LOGGER.log(FINE,\n- \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \"\n- + request.method() + \" \" + request.url().toString());\n- }\n+ logRequest(request);\n rateLimitChecker.checkRateLimit(this, request);\n \n responseInfo = getResponseInfo(request);\n@@ -424,6 +417,12 @@ public String getApiUrl() {\n throw new GHIOException(\"Ran out of retries for URL: \" + request.url().toString());\n }\n \n+ private void logRequest(@Nonnull final GitHubRequest request) {\n+ LOGGER.log(FINE,\n+ () -> \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \" + request.method() + \" \"\n+ + request.url().toString());\n+ }\n+\n @Nonnull\n protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;\n \n@@ -437,20 +436,15 @@ public String getApiUrl() {\n // special case handling for 304 unmodified, as the content will be \"\"\n } else if (responseInfo.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {\n \n- // Response code 202 means data is being generated.\n+ // Response code 202 means data is being generated or an action that can require some time is triggered.\n // This happens in specific cases:\n // statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching\n // fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork\n+ // workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run\n \n- if (responseInfo.url().toString().endsWith(\"/forks\")) {\n- LOGGER.log(INFO, \"The fork is being created. Please try again in 5 seconds.\");\n- } else if (responseInfo.url().toString().endsWith(\"/statistics\")) {\n- LOGGER.log(INFO, \"The statistics are being generated. Please try again in 5 seconds.\");\n- } else {\n- LOGGER.log(INFO,\n- \"Received 202 from \" + responseInfo.url().toString() + \" . Please try again in 5 seconds.\");\n- }\n- // Maybe throw an exception instead?\n+ LOGGER.log(FINE,\n+ \"Received HTTP_ACCEPTED(202) from \" + responseInfo.url().toString()\n+ + \" . Please try again in 5 seconds.\");\n } else if (handler != null) {\n body = handler.apply(responseInfo);\n }\n@@ -562,9 +556,7 @@ private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo responseInfo) {\n GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, responseInfo);\n updateRateLimit(GHRateLimit.fromRecord(observed, responseInfo.request().rateLimitTarget()));\n } catch (NumberFormatException | NullPointerException e) {\n- if (LOGGER.isLoggable(FINEST)) {\n- LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n- }\n+ LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n }\n }\n "
+ "patch": "@@ -14,6 +14,7 @@\n import java.time.format.DateTimeFormatter;\n import java.time.temporal.ChronoUnit;\n import java.util.*;\n+import java.util.concurrent.atomic.AtomicReference;\n import java.util.function.Consumer;\n import java.util.logging.Logger;\n \n@@ -52,10 +53,8 @@\n \n private HttpConnector connector;\n \n- private final Object rateLimitLock = new Object();\n-\n @Nonnull\n- private GHRateLimit rateLimit = GHRateLimit.DEFAULT;\n+ private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);\n \n private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());\n \n@@ -141,10 +140,9 @@ public boolean isCredentialValid() {\n getRateLimit();\n return true;\n } catch (IOException e) {\n- if (LOGGER.isLoggable(FINE))\n- LOGGER.log(FINE,\n- \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n- e);\n+ LOGGER.log(FINE,\n+ \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n+ e);\n return false;\n }\n }\n@@ -256,9 +254,7 @@ GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExce\n @Nonnull\n @Deprecated\n GHRateLimit lastRateLimit() {\n- synchronized (rateLimitLock) {\n- return rateLimit;\n- }\n+ return rateLimit.get();\n }\n \n /**\n@@ -278,12 +274,19 @@ GHRateLimit lastRateLimit() {\n */\n @Nonnull\n GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {\n- synchronized (rateLimitLock) {\n- if (rateLimit.getRecord(rateLimitTarget).isExpired()) {\n- getRateLimit(rateLimitTarget);\n+ GHRateLimit result = rateLimit.get();\n+ // Most of the time rate limit is not expired, so try to avoid locking.\n+ if (result.getRecord(rateLimitTarget).isExpired()) {\n+ // if the rate limit is expired, synchronize to ensure\n+ // only one call to getRateLimit() is made to refresh it.\n+ synchronized (this) {\n+ if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {\n+ getRateLimit(rateLimitTarget);\n+ }\n }\n- return rateLimit;\n+ result = rateLimit.get();\n }\n+ return result;\n }\n \n /**\n@@ -296,15 +299,9 @@ GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExcepti\n * {@link GHRateLimit.Record} constructed from the response header information\n */\n private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {\n- synchronized (rateLimitLock) {\n- observed = rateLimit.getMergedRateLimit(observed);\n-\n- if (rateLimit != observed) {\n- rateLimit = observed;\n- LOGGER.log(FINE, \"Rate limit now: {0}\", rateLimit);\n- }\n- return rateLimit;\n- }\n+ GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));\n+ LOGGER.log(FINEST, \"Rate limit now: {0}\", rateLimit.get());\n+ return result;\n }\n \n /**\n@@ -384,11 +381,7 @@ public String getApiUrl() {\n GitHubResponse.ResponseInfo connectorResponse = null;\n try {\n try {\n- if (LOGGER.isLoggable(FINE)) {\n- LOGGER.log(FINE,\n- \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \"\n- + request.method() + \" \" + request.url().toString());\n- }\n+ logRequest(request);\n rateLimitChecker.checkRateLimit(this, request);\n \n connectorResponse = getResponseInfo(request);\n@@ -424,6 +417,12 @@ public String getApiUrl() {\n throw new GHIOException(\"Ran out of retries for URL: \" + request.url().toString());\n }\n \n+ private void logRequest(@Nonnull final GitHubRequest request) {\n+ LOGGER.log(FINE,\n+ () -> \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \" + request.method() + \" \"\n+ + request.url().toString());\n+ }\n+\n @Nonnull\n protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;\n \n@@ -437,20 +436,15 @@ public String getApiUrl() {\n // special case handling for 304 unmodified, as the content will be \"\"\n } else if (connectorResponse.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {\n \n- // Response code 202 means data is being generated.\n+ // Response code 202 means data is being generated or an action that can require some time is triggered.\n // This happens in specific cases:\n // statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching\n // fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork\n+ // workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run\n \n- if (connectorResponse.url().toString().endsWith(\"/forks\")) {\n- LOGGER.log(INFO, \"The fork is being created. Please try again in 5 seconds.\");\n- } else if (connectorResponse.url().toString().endsWith(\"/statistics\")) {\n- LOGGER.log(INFO, \"The statistics are being generated. Please try again in 5 seconds.\");\n- } else {\n- LOGGER.log(INFO,\n- \"Received 202 from \" + connectorResponse.url().toString() + \" . Please try again in 5 seconds.\");\n- }\n- // Maybe throw an exception instead?\n+ LOGGER.log(FINE,\n+ \"Received HTTP_ACCEPTED(202) from \" + connectorResponse.url().toString()\n+ + \" . Please try again in 5 seconds.\");\n } else if (handler != null) {\n body = handler.apply(connectorResponse);\n }\n@@ -562,9 +556,7 @@ private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo connectorResponse) {\n GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, connectorResponse);\n updateRateLimit(GHRateLimit.fromRecord(observed, connectorResponse.request().rateLimitTarget()));\n } catch (NumberFormatException | NullPointerException e) {\n- if (LOGGER.isLoggable(FINEST)) {\n- LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n- }\n+ LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n }\n }\n "
},
{
"sha": "aea8b99b1f3301f65c8bc86c657bfd187db5c178",
diff --git a/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-5.json b/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-5.json
index 22c43a022f..3980686fae 100644
--- a/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-5.json
+++ b/src/test/resources/org/kohsuke/github/GHRepositoryTest/wiremock/getCommitsBetweenPaged/__files/repos_hub4j-test-org_github-api_compare_4261c42949915816a9f246eb14c3dfd21a637bc294ff089e60064bfa43e374baeb10846f7ce82f40-5.json
@@ -9064,7 +9064,7 @@
"blob_url": "https://github.com/hub4j-test-org/github-api/blob/94ff089e60064bfa43e374baeb10846f7ce82f40/src/main/java/org/kohsuke/github/GitHubClient.java",
"raw_url": "https://github.com/hub4j-test-org/github-api/raw/94ff089e60064bfa43e374baeb10846f7ce82f40/src/main/java/org/kohsuke/github/GitHubClient.java",
"contents_url": "https://api.github.com/repos/hub4j-test-org/github-api/contents/src/main/java/org/kohsuke/github/GitHubClient.java?ref=94ff089e60064bfa43e374baeb10846f7ce82f40",
- "patch": "@@ -14,6 +14,7 @@\n import java.time.format.DateTimeFormatter;\n import java.time.temporal.ChronoUnit;\n import java.util.*;\n+import java.util.concurrent.atomic.AtomicReference;\n import java.util.function.Consumer;\n import java.util.logging.Logger;\n \n@@ -52,10 +53,8 @@\n \n private HttpConnector connector;\n \n- private final Object rateLimitLock = new Object();\n-\n @Nonnull\n- private GHRateLimit rateLimit = GHRateLimit.DEFAULT;\n+ private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);\n \n private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());\n \n@@ -141,10 +140,9 @@ public boolean isCredentialValid() {\n getRateLimit();\n return true;\n } catch (IOException e) {\n- if (LOGGER.isLoggable(FINE))\n- LOGGER.log(FINE,\n- \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n- e);\n+ LOGGER.log(FINE,\n+ \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n+ e);\n return false;\n }\n }\n@@ -256,9 +254,7 @@ GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExce\n @Nonnull\n @Deprecated\n GHRateLimit lastRateLimit() {\n- synchronized (rateLimitLock) {\n- return rateLimit;\n- }\n+ return rateLimit.get();\n }\n \n /**\n@@ -278,12 +274,19 @@ GHRateLimit lastRateLimit() {\n */\n @Nonnull\n GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {\n- synchronized (rateLimitLock) {\n- if (rateLimit.getRecord(rateLimitTarget).isExpired()) {\n- getRateLimit(rateLimitTarget);\n+ GHRateLimit result = rateLimit.get();\n+ // Most of the time rate limit is not expired, so try to avoid locking.\n+ if (result.getRecord(rateLimitTarget).isExpired()) {\n+ // if the rate limit is expired, synchronize to ensure\n+ // only one call to getRateLimit() is made to refresh it.\n+ synchronized (this) {\n+ if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {\n+ getRateLimit(rateLimitTarget);\n+ }\n }\n- return rateLimit;\n+ result = rateLimit.get();\n }\n+ return result;\n }\n \n /**\n@@ -296,15 +299,9 @@ GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExcepti\n * {@link GHRateLimit.Record} constructed from the response header information\n */\n private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {\n- synchronized (rateLimitLock) {\n- observed = rateLimit.getMergedRateLimit(observed);\n-\n- if (rateLimit != observed) {\n- rateLimit = observed;\n- LOGGER.log(FINE, \"Rate limit now: {0}\", rateLimit);\n- }\n- return rateLimit;\n- }\n+ GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));\n+ LOGGER.log(FINEST, \"Rate limit now: {0}\", rateLimit.get());\n+ return result;\n }\n \n /**\n@@ -384,11 +381,7 @@ public String getApiUrl() {\n GitHubResponse.ResponseInfo responseInfo = null;\n try {\n try {\n- if (LOGGER.isLoggable(FINE)) {\n- LOGGER.log(FINE,\n- \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \"\n- + request.method() + \" \" + request.url().toString());\n- }\n+ logRequest(request);\n rateLimitChecker.checkRateLimit(this, request);\n \n responseInfo = getResponseInfo(request);\n@@ -424,6 +417,12 @@ public String getApiUrl() {\n throw new GHIOException(\"Ran out of retries for URL: \" + request.url().toString());\n }\n \n+ private void logRequest(@Nonnull final GitHubRequest request) {\n+ LOGGER.log(FINE,\n+ () -> \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \" + request.method() + \" \"\n+ + request.url().toString());\n+ }\n+\n @Nonnull\n protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;\n \n@@ -437,20 +436,15 @@ public String getApiUrl() {\n // special case handling for 304 unmodified, as the content will be \"\"\n } else if (responseInfo.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {\n \n- // Response code 202 means data is being generated.\n+ // Response code 202 means data is being generated or an action that can require some time is triggered.\n // This happens in specific cases:\n // statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching\n // fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork\n+ // workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run\n \n- if (responseInfo.url().toString().endsWith(\"/forks\")) {\n- LOGGER.log(INFO, \"The fork is being created. Please try again in 5 seconds.\");\n- } else if (responseInfo.url().toString().endsWith(\"/statistics\")) {\n- LOGGER.log(INFO, \"The statistics are being generated. Please try again in 5 seconds.\");\n- } else {\n- LOGGER.log(INFO,\n- \"Received 202 from \" + responseInfo.url().toString() + \" . Please try again in 5 seconds.\");\n- }\n- // Maybe throw an exception instead?\n+ LOGGER.log(FINE,\n+ \"Received HTTP_ACCEPTED(202) from \" + responseInfo.url().toString()\n+ + \" . Please try again in 5 seconds.\");\n } else if (handler != null) {\n body = handler.apply(responseInfo);\n }\n@@ -562,9 +556,7 @@ private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo responseInfo) {\n GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, responseInfo);\n updateRateLimit(GHRateLimit.fromRecord(observed, responseInfo.request().rateLimitTarget()));\n } catch (NumberFormatException | NullPointerException e) {\n- if (LOGGER.isLoggable(FINEST)) {\n- LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n- }\n+ LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n }\n }\n "
+ "patch": "@@ -14,6 +14,7 @@\n import java.time.format.DateTimeFormatter;\n import java.time.temporal.ChronoUnit;\n import java.util.*;\n+import java.util.concurrent.atomic.AtomicReference;\n import java.util.function.Consumer;\n import java.util.logging.Logger;\n \n@@ -52,10 +53,8 @@\n \n private HttpConnector connector;\n \n- private final Object rateLimitLock = new Object();\n-\n @Nonnull\n- private GHRateLimit rateLimit = GHRateLimit.DEFAULT;\n+ private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);\n \n private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());\n \n@@ -141,10 +140,9 @@ public boolean isCredentialValid() {\n getRateLimit();\n return true;\n } catch (IOException e) {\n- if (LOGGER.isLoggable(FINE))\n- LOGGER.log(FINE,\n- \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n- e);\n+ LOGGER.log(FINE,\n+ \"Exception validating credentials on \" + getApiUrl() + \" with login '\" + login + \"' \" + e,\n+ e);\n return false;\n }\n }\n@@ -256,9 +254,7 @@ GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExce\n @Nonnull\n @Deprecated\n GHRateLimit lastRateLimit() {\n- synchronized (rateLimitLock) {\n- return rateLimit;\n- }\n+ return rateLimit.get();\n }\n \n /**\n@@ -278,12 +274,19 @@ GHRateLimit lastRateLimit() {\n */\n @Nonnull\n GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {\n- synchronized (rateLimitLock) {\n- if (rateLimit.getRecord(rateLimitTarget).isExpired()) {\n- getRateLimit(rateLimitTarget);\n+ GHRateLimit result = rateLimit.get();\n+ // Most of the time rate limit is not expired, so try to avoid locking.\n+ if (result.getRecord(rateLimitTarget).isExpired()) {\n+ // if the rate limit is expired, synchronize to ensure\n+ // only one call to getRateLimit() is made to refresh it.\n+ synchronized (this) {\n+ if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {\n+ getRateLimit(rateLimitTarget);\n+ }\n }\n- return rateLimit;\n+ result = rateLimit.get();\n }\n+ return result;\n }\n \n /**\n@@ -296,15 +299,9 @@ GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOExcepti\n * {@link GHRateLimit.Record} constructed from the response header information\n */\n private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {\n- synchronized (rateLimitLock) {\n- observed = rateLimit.getMergedRateLimit(observed);\n-\n- if (rateLimit != observed) {\n- rateLimit = observed;\n- LOGGER.log(FINE, \"Rate limit now: {0}\", rateLimit);\n- }\n- return rateLimit;\n- }\n+ GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));\n+ LOGGER.log(FINEST, \"Rate limit now: {0}\", rateLimit.get());\n+ return result;\n }\n \n /**\n@@ -384,11 +381,7 @@ public String getApiUrl() {\n GitHubResponse.ResponseInfo connectorResponse = null;\n try {\n try {\n- if (LOGGER.isLoggable(FINE)) {\n- LOGGER.log(FINE,\n- \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \"\n- + request.method() + \" \" + request.url().toString());\n- }\n+ logRequest(request);\n rateLimitChecker.checkRateLimit(this, request);\n \n connectorResponse = getResponseInfo(request);\n@@ -424,6 +417,12 @@ public String getApiUrl() {\n throw new GHIOException(\"Ran out of retries for URL: \" + request.url().toString());\n }\n \n+ private void logRequest(@Nonnull final GitHubRequest request) {\n+ LOGGER.log(FINE,\n+ () -> \"GitHub API request [\" + (login == null ? \"anonymous\" : login) + \"]: \" + request.method() + \" \"\n+ + request.url().toString());\n+ }\n+\n @Nonnull\n protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;\n \n@@ -437,20 +436,15 @@ public String getApiUrl() {\n // special case handling for 304 unmodified, as the content will be \"\"\n } else if (connectorResponse.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {\n \n- // Response code 202 means data is being generated.\n+ // Response code 202 means data is being generated or an action that can require some time is triggered.\n // This happens in specific cases:\n // statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching\n // fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork\n+ // workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run\n \n- if (connectorResponse.url().toString().endsWith(\"/forks\")) {\n- LOGGER.log(INFO, \"The fork is being created. Please try again in 5 seconds.\");\n- } else if (connectorResponse.url().toString().endsWith(\"/statistics\")) {\n- LOGGER.log(INFO, \"The statistics are being generated. Please try again in 5 seconds.\");\n- } else {\n- LOGGER.log(INFO,\n- \"Received 202 from \" + connectorResponse.url().toString() + \" . Please try again in 5 seconds.\");\n- }\n- // Maybe throw an exception instead?\n+ LOGGER.log(FINE,\n+ \"Received HTTP_ACCEPTED(202) from \" + connectorResponse.url().toString()\n+ + \" . Please try again in 5 seconds.\");\n } else if (handler != null) {\n body = handler.apply(connectorResponse);\n }\n@@ -562,9 +556,7 @@ private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo connectorResponse) {\n GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, connectorResponse);\n updateRateLimit(GHRateLimit.fromRecord(observed, connectorResponse.request().rateLimitTarget()));\n } catch (NumberFormatException | NullPointerException e) {\n- if (LOGGER.isLoggable(FINEST)) {\n- LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n- }\n+ LOGGER.log(FINEST, \"Missing or malformed X-RateLimit header: \", e);\n }\n }\n "
},
{
"sha": "aea8b99b1f3301f65c8bc86c657bfd187db5c178",