mirror of
https://github.com/apache/jclouds.git
synced 2026-09-01 00:44:28 +00:00
Move digital ocean rate limit handler to core to make it reusable
This commit is contained in:
@@ -352,6 +352,14 @@ public final class Constants {
|
||||
|
||||
/** Comma-separated list of methods considered idempotent for purposes of retries. By default jclouds uses DELETE,GET,HEAD,OPTIONS,PUT. */
|
||||
public static final String PROPERTY_IDEMPOTENT_METHODS = "jclouds.idempotent-methods";
|
||||
|
||||
/**
|
||||
* Maximum amount of time (in milliseconds) a request will wait until retrying if
|
||||
* the rate limit is exhausted.
|
||||
* <p>
|
||||
* Default value: 2 minutes.
|
||||
*/
|
||||
public static final String PROPERTY_MAX_RATE_LIMIT_WAIT = "jclouds.max-ratelimit-wait";
|
||||
|
||||
private Constants() {
|
||||
throw new AssertionError("intentionally unimplemented");
|
||||
|
||||
+41
-22
@@ -14,14 +14,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jclouds.digitalocean2.handlers;
|
||||
package org.jclouds.http.handlers;
|
||||
|
||||
import static org.jclouds.Constants.PROPERTY_MAX_RATE_LIMIT_WAIT;
|
||||
import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
|
||||
import static org.jclouds.digitalocean2.config.DigitalOcean2Properties.MAX_RATE_LIMIT_WAIT;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.jclouds.http.HttpCommand;
|
||||
import org.jclouds.http.HttpResponse;
|
||||
@@ -29,17 +28,15 @@ import org.jclouds.http.HttpRetryHandler;
|
||||
import org.jclouds.logging.Logger;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
/**
|
||||
* Retry handler that takes into account the DigitalOcean rate limit and delays
|
||||
* the requests until they are known to succeed.
|
||||
* Retry handler that takes into account the provider rate limit and delays the
|
||||
* requests until they are known to succeed.
|
||||
*/
|
||||
@Beta
|
||||
@Singleton
|
||||
public class RateLimitRetryHandler implements HttpRetryHandler {
|
||||
|
||||
static final String RATE_LIMIT_RESET_HEADER = "RateLimit-Reset";
|
||||
public abstract class RateLimitRetryHandler implements HttpRetryHandler {
|
||||
|
||||
@Resource
|
||||
protected Logger logger = Logger.NULL;
|
||||
@@ -49,15 +46,34 @@ public class RateLimitRetryHandler implements HttpRetryHandler {
|
||||
private int retryCountLimit = 5;
|
||||
|
||||
@Inject(optional = true)
|
||||
@Named(MAX_RATE_LIMIT_WAIT)
|
||||
private int maxRateLimitWait = 120000;
|
||||
@Named(PROPERTY_MAX_RATE_LIMIT_WAIT)
|
||||
private int maxRateLimitWait = 2 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Returns the response status that will be considered a rate limit error.
|
||||
* <p>
|
||||
* Providers can override this to customize which responses are retried.
|
||||
*/
|
||||
protected int rateLimitErrorStatus() {
|
||||
return 429;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the number of milliseconds that must pass until a request can be
|
||||
* performed.
|
||||
*
|
||||
* @param command The command being executed.
|
||||
* @param response The rate-limit error response.
|
||||
* @return The number of milliseconds to wait for an available request, if taht information is available.
|
||||
*/
|
||||
protected abstract Optional<Long> millisToNextAvailableRequest(final HttpCommand command, final HttpResponse response);
|
||||
|
||||
@Override
|
||||
public boolean shouldRetryRequest(final HttpCommand command, final HttpResponse response) {
|
||||
command.incrementFailureCount();
|
||||
|
||||
// Do not retry client errors that are not rate limit errors
|
||||
if (response.getStatusCode() != 429) {
|
||||
if (response.getStatusCode() != rateLimitErrorStatus()) {
|
||||
return false;
|
||||
} else if (!command.isReplayable()) {
|
||||
logger.error("Cannot retry after rate limit error, command is not replayable: %1$s", command);
|
||||
@@ -72,24 +88,22 @@ public class RateLimitRetryHandler implements HttpRetryHandler {
|
||||
}
|
||||
|
||||
private boolean delayRequestUntilAllowed(final HttpCommand command, final HttpResponse response) {
|
||||
// The header is the Unix epoch time when the next request can be done
|
||||
String epochForNextAvailableRequest = response.getFirstHeaderOrNull(RATE_LIMIT_RESET_HEADER);
|
||||
if (epochForNextAvailableRequest == null) {
|
||||
Optional<Long> millisToNextAvailableRequest = millisToNextAvailableRequest(command, response);
|
||||
if (!millisToNextAvailableRequest.isPresent()) {
|
||||
logger.error("Cannot retry after rate limit error, no retry information provided in the response");
|
||||
return false;
|
||||
}
|
||||
|
||||
long waitPeriod = millisUntilNextAvailableRequest(Long.parseLong(epochForNextAvailableRequest));
|
||||
|
||||
if (waitPeriod > 0) {
|
||||
long waitPeriod = millisToNextAvailableRequest.get();
|
||||
if (waitPeriod > 0L) {
|
||||
if (waitPeriod > maxRateLimitWait) {
|
||||
logger.error("Max wait for rate limited requests is %s seconds but need to wait %s seconds, aborting",
|
||||
logger.error("Max wait for rate limited requests is %sms but need to wait %sms, aborting",
|
||||
maxRateLimitWait, waitPeriod);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("Waiting %s seconds before retrying, as defined by the rate limit", waitPeriod);
|
||||
logger.debug("Waiting %sms before retrying, as defined by the rate limit", waitPeriod);
|
||||
// Do not use Uninterrumpibles or similar, to let the jclouds
|
||||
// tiemout configuration interrupt this thread
|
||||
Thread.sleep(waitPeriod);
|
||||
@@ -105,7 +119,12 @@ public class RateLimitRetryHandler implements HttpRetryHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static long millisUntilNextAvailableRequest(long epochForNextAvailableRequest) {
|
||||
return (epochForNextAvailableRequest * 1000) - System.currentTimeMillis();
|
||||
public int getRetryCountLimit() {
|
||||
return retryCountLimit;
|
||||
}
|
||||
|
||||
public int getMaxRateLimitWait() {
|
||||
return maxRateLimitWait;
|
||||
}
|
||||
|
||||
}
|
||||
+16
-17
@@ -14,9 +14,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jclouds.digitalocean2.handlers;
|
||||
package org.jclouds.http.handlers;
|
||||
|
||||
import static org.jclouds.digitalocean2.handlers.RateLimitRetryHandler.RATE_LIMIT_RESET_HEADER;
|
||||
import static com.google.common.net.HttpHeaders.RETRY_AFTER;
|
||||
import static org.jclouds.http.HttpUtils.releasePayload;
|
||||
import static org.jclouds.io.Payloads.newInputStreamPayload;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
@@ -32,6 +32,7 @@ import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.io.Payload;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
|
||||
@Test(groups = "unit", testName = "RateLimitRetryHandlerTest")
|
||||
@@ -41,7 +42,14 @@ public class RateLimitRetryHandlerTest {
|
||||
// stuck
|
||||
private static final long TEST_SAFE_TIMEOUT = 60000;
|
||||
|
||||
private final RateLimitRetryHandler rateLimitRetryHandler = new RateLimitRetryHandler();
|
||||
private final RateLimitRetryHandler rateLimitRetryHandler = new RateLimitRetryHandler() {
|
||||
@Override
|
||||
protected Optional<Long> millisToNextAvailableRequest(HttpCommand command, HttpResponse response) {
|
||||
String secondsToNextAvailableRequest = response.getFirstHeaderOrNull(RETRY_AFTER);
|
||||
return secondsToNextAvailableRequest != null ? Optional.of(Long.valueOf(secondsToNextAvailableRequest) * 1000)
|
||||
: Optional.<Long> absent();
|
||||
}
|
||||
};
|
||||
|
||||
@Test(timeOut = TEST_SAFE_TIMEOUT)
|
||||
public void testDoNotRetryIfNoRateLimit() {
|
||||
@@ -67,7 +75,7 @@ public class RateLimitRetryHandlerTest {
|
||||
}
|
||||
|
||||
@Test(timeOut = TEST_SAFE_TIMEOUT)
|
||||
public void testDoNotRetryIfNoRateLimitResetHeader() {
|
||||
public void testDoNotRetryIfNoRateLimitInfo() {
|
||||
HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429).build();
|
||||
|
||||
@@ -76,22 +84,16 @@ public class RateLimitRetryHandlerTest {
|
||||
|
||||
@Test(timeOut = TEST_SAFE_TIMEOUT)
|
||||
public void testDoNotRetryIfTooMuchWait() {
|
||||
// 5 minutes Unix epoch timestamp
|
||||
long rateLimitResetEpoch = (System.currentTimeMillis() + 300000) / 1000;
|
||||
HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429)
|
||||
.addHeader(RATE_LIMIT_RESET_HEADER, String.valueOf(rateLimitResetEpoch)).build();
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429).addHeader(RETRY_AFTER, "400").build();
|
||||
|
||||
assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
|
||||
}
|
||||
|
||||
@Test(timeOut = TEST_SAFE_TIMEOUT)
|
||||
public void testRequestIsDelayed() {
|
||||
// 5 seconds Unix epoch timestamp
|
||||
long rateLimitResetEpoch = (System.currentTimeMillis() + 5000) / 1000;
|
||||
HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429)
|
||||
.addHeader(RATE_LIMIT_RESET_HEADER, String.valueOf(rateLimitResetEpoch)).build();
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429).addHeader(RETRY_AFTER, "5").build();
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
@@ -104,12 +106,9 @@ public class RateLimitRetryHandlerTest {
|
||||
|
||||
@Test(timeOut = TEST_SAFE_TIMEOUT)
|
||||
public void testDoNotRetryIfRequestIsAborted() throws Exception {
|
||||
// 10 seconds Unix epoch timestamp
|
||||
long rateLimitResetEpoch = (System.currentTimeMillis() + 10000) / 1000;
|
||||
final HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost")
|
||||
.build());
|
||||
final HttpResponse response = HttpResponse.builder().statusCode(429)
|
||||
.addHeader(RATE_LIMIT_RESET_HEADER, String.valueOf(rateLimitResetEpoch)).build();
|
||||
final HttpResponse response = HttpResponse.builder().statusCode(429).addHeader(RETRY_AFTER, "10").build();
|
||||
|
||||
final Thread requestThread = Thread.currentThread();
|
||||
Thread killer = new Thread() {
|
||||
@@ -143,7 +142,7 @@ public class RateLimitRetryHandlerTest {
|
||||
@Test(timeOut = TEST_SAFE_TIMEOUT)
|
||||
public void testDisallowExcessiveRetries() {
|
||||
HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429).addHeader(RATE_LIMIT_RESET_HEADER, "0").build();
|
||||
HttpResponse response = HttpResponse.builder().statusCode(429).addHeader(RETRY_AFTER, "0").build();
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
assertTrue(rateLimitRetryHandler.shouldRetryRequest(command, response));
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jclouds.digitalocean2.config;
|
||||
|
||||
public final class DigitalOcean2Properties {
|
||||
|
||||
/**
|
||||
* Maximum amount of time (in milliseconds) a request will wait until retrying if
|
||||
* the rate limit is exhausted.
|
||||
* <p>
|
||||
* Default value: 2 minutes.
|
||||
*/
|
||||
public static final String MAX_RATE_LIMIT_WAIT = "jclouds.max-ratelimit-wait";
|
||||
|
||||
private DigitalOcean2Properties() {
|
||||
throw new AssertionError("intentionally unimplemented");
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
package org.jclouds.digitalocean2.config;
|
||||
|
||||
import org.jclouds.digitalocean2.handlers.RateLimitRetryHandler;
|
||||
import org.jclouds.digitalocean2.handlers.DigitalOcean2RateLimitRetryHandler;
|
||||
import org.jclouds.http.HttpRetryHandler;
|
||||
import org.jclouds.http.annotation.ClientError;
|
||||
|
||||
@@ -25,6 +25,6 @@ import com.google.inject.AbstractModule;
|
||||
public class DigitalOcean2RateLimitModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(RateLimitRetryHandler.class);
|
||||
bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(DigitalOcean2RateLimitRetryHandler.class);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
package org.jclouds.digitalocean2.exceptions;
|
||||
|
||||
import static org.jclouds.digitalocean2.handlers.RateLimitRetryHandler.millisUntilNextAvailableRequest;
|
||||
import static org.jclouds.digitalocean2.handlers.DigitalOcean2RateLimitRetryHandler.millisUntilNextAvailableRequest;
|
||||
|
||||
import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.rest.RateLimitExceededException;
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jclouds.digitalocean2.handlers;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.jclouds.http.HttpCommand;
|
||||
import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.http.handlers.RateLimitRetryHandler;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
@Singleton
|
||||
public class DigitalOcean2RateLimitRetryHandler extends RateLimitRetryHandler {
|
||||
|
||||
@Override
|
||||
protected Optional<Long> millisToNextAvailableRequest(HttpCommand command, HttpResponse response) {
|
||||
// The header is the Unix epoch time when the next request can be done
|
||||
String epochForNextAvailableRequest = response.getFirstHeaderOrNull("RateLimit-Reset");
|
||||
if (epochForNextAvailableRequest == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
return Optional.of(millisUntilNextAvailableRequest(Long.parseLong(epochForNextAvailableRequest)));
|
||||
}
|
||||
|
||||
public static long millisUntilNextAvailableRequest(long epochForNextAvailableRequest) {
|
||||
return (epochForNextAvailableRequest * 1000) - System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
package org.jclouds.digitalocean2.exceptions;
|
||||
|
||||
import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
|
||||
import static org.jclouds.digitalocean2.handlers.RateLimitRetryHandler.millisUntilNextAvailableRequest;
|
||||
import static org.jclouds.digitalocean2.handlers.DigitalOcean2RateLimitRetryHandler.millisUntilNextAvailableRequest;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
import static org.testng.Assert.fail;
|
||||
|
||||
Reference in New Issue
Block a user