Move digital ocean rate limit handler to core to make it reusable

This commit is contained in:
Ignasi Barrera
2016-10-19 16:56:03 +02:00
parent edbb2c0e6f
commit 8030e53f32
8 changed files with 114 additions and 76 deletions
@@ -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");
@@ -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;
}
}
@@ -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));
@@ -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");
}
}
@@ -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);
}
}
@@ -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;
@@ -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();
}
}
@@ -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;