Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change backoff strategy to exponential for BitbucketCloudApiClient when rate-limited #841

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
private static final int MAX_AVATAR_LENGTH = 16384;
private static final int MAX_PAGE_LENGTH = 100;
private static final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
private static final int RETRY_DURATION_BASE_MILLIS = 5000;
private CloseableHttpClient client;
private HttpClientContext context;
private final String owner;
Expand Down Expand Up @@ -787,131 +788,135 @@
.set("pagelen", MAX_PAGE_LENGTH);
if (StringUtils.isNotBlank(projectKey)) {
template.set("q", "project.key=" + "\"" + projectKey + "\""); // q=project.key="<projectKey>"
cacheKey.append("::").append(projectKey);
} else {
cacheKey.append("::<undefined>");
}
if (role != null && authenticator != null) {
template.set("role", role.getId());
cacheKey.append("::").append(role.getId());
} else {
cacheKey.append("::<undefined>");
}

Callable<List<BitbucketCloudRepository>> request = () -> {
List<BitbucketCloudRepository> repositories = new ArrayList<>();
Integer pageNumber = 1;
String url, response;
PaginatedBitbucketRepository page;
do {
response = getRequest(url = template.set("page", pageNumber).expand());
try {
page = JsonParser.toJava(response, PaginatedBitbucketRepository.class);
repositories.addAll(page.getValues());
} catch (IOException e) {
throw new IOException("I/O error when parsing response from URL: " + url, e);
}
pageNumber++;
} while (page.getNext() != null);
repositories.sort(Comparator.comparing(BitbucketCloudRepository::getRepositoryName));
return repositories;
};
try {
if (enableCache) {
return cachedRepositories.get(cacheKey.toString(), request);
} else {
return request.call();
}
} catch (Exception ex) {
throw new IOException("Error while loading repositories from cache", ex);
}
}

/** {@inheritDoc} */
@NonNull
@Override
public List<BitbucketCloudRepository> getRepositories() throws IOException, InterruptedException {
return getRepositories(null);
}

private void setClientProxyParams(String host, HttpClientBuilder builder) {
Jenkins jenkins = Jenkins.getInstanceOrNull();
ProxyConfiguration proxyConfig = null;
if (jenkins != null) {
proxyConfig = jenkins.proxy;
}

Proxy proxy = Proxy.NO_PROXY;
if (proxyConfig != null) {
proxy = proxyConfig.createProxy(host);
}

if (proxy.type() != Proxy.Type.DIRECT) {
final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
LOGGER.fine("Jenkins proxy: " + proxy.address());
HttpHost proxyHttpHost = new HttpHost(proxyAddress.getHostName(), proxyAddress.getPort());
builder.setProxy(proxyHttpHost);
String username = proxyConfig.getUserName();
String password = proxyConfig.getPassword();
if (username != null && !"".equals(username.trim())) {
LOGGER.fine("Using proxy authentication (user=" + username + ")");
if (context == null) {
// may have been already set in com.cloudbees.jenkins.plugins.bitbucket.api.credentials.BitbucketUsernamePasswordAuthenticator.configureContext(HttpClientContext, HttpHost)
context = HttpClientContext.create();
}
CredentialsProvider credentialsProvider = context.getCredentialsProvider();
if (credentialsProvider == null) {
credentialsProvider = new BasicCredentialsProvider();
// may have been already set in com.cloudbees.jenkins.plugins.bitbucket.api.credentials.BitbucketUsernamePasswordAuthenticator.configureContext(HttpClientContext, HttpHost)
context.setCredentialsProvider(credentialsProvider);
}
credentialsProvider.setCredentials(new AuthScope(proxyHttpHost), new UsernamePasswordCredentials(username, password));
AuthCache authCache = context.getAuthCache();
if (authCache == null) {
authCache = new BasicAuthCache();
context.setAuthCache(authCache);
}
authCache.put(proxyHttpHost, new BasicScheme());
}
}
}

@Restricted(ProtectedExternally.class)
protected CloseableHttpResponse executeMethod(HttpRequestBase httpMethod) throws InterruptedException, IOException {
return executeMethod(API_HOST, httpMethod);
}

@Restricted(ProtectedExternally.class)
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws InterruptedException, IOException {
HttpClientContext requestContext = null;
if (API_HOST.equals(host)) {
requestContext = context;
if (authenticator != null) {
authenticator.configureRequest(httpMethod);
}
}

RequestConfig.Builder requestConfig = RequestConfig.custom();
String connectTimeout = System.getProperty("http.connect.timeout", "10");
requestConfig.setConnectTimeout(Integer.parseInt(connectTimeout) * 1000);
String connectionRequestTimeout = System.getProperty("http.connect.request.timeout", "60");
requestConfig.setConnectionRequestTimeout(Integer.parseInt(connectionRequestTimeout) * 1000);
String socketTimeout = System.getProperty("http.socket.timeout", "60");
requestConfig.setSocketTimeout(Integer.parseInt(socketTimeout) * 1000);
httpMethod.setConfig(requestConfig.build());

CloseableHttpResponse response = client.execute(host, httpMethod, requestContext);
int retryCount = 0;
while (response.getStatusLine().getStatusCode() == API_RATE_LIMIT_CODE) {
release(httpMethod);
if (Thread.interrupted()) {
throw new InterruptedException();
}
/*
TODO: When bitbucket starts supporting rate limit expiration time, remove 5 sec wait and put code
TODO: When bitbucket starts supporting rate limit expiration time, remove current wait strategy and put code

Check warning on line 912 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: When bitbucket starts supporting rate limit expiration time, remove current wait strategy and put code
to wait till expiration time is over. It should also fix the wait for ever loop.
*/
LOGGER.fine("Bitbucket Cloud API rate limit reached, sleeping for 5 sec then retry...");
Thread.sleep(5000);
long sleepTime = RETRY_DURATION_BASE_MILLIS * (long)Math.pow(2, retryCount);
LOGGER.log(Level.FINE, "Bitbucket Cloud API rate limit reached for {0}, sleeping for {1} sec then retry...", new Object[]{httpMethod.getURI(), sleepTime / 1000});
Thread.sleep(sleepTime);
retryCount++;
LOGGER.log(Level.FINE, "Retrying Bitbucket Cloud API request for {0}, retryCount={1}", new Object[]{httpMethod.getURI(), retryCount});

Check warning on line 919 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 791-919 are not covered by tests
response = client.execute(host, httpMethod, requestContext);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this close the previous CloseableHttpResponse response? If so, that isn't a new bug though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a different issue that is not really related to my changes, I guess it makes more sense to move this to a separate ticket and PR, if this causes issues. I think it doesn't really belong to this one.

}
return response;
Expand Down