From b8eb49f0a9719ff122b86bec34a36a77a982a250 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 9 Dec 2012 00:29:19 -0800 Subject: [PATCH 1/3] added rfc822 + iso8601Seconds to DateCodecFactory and simplified ctor --- .../main/java/org/jclouds/date/DateCodec.java | 1 + .../org/jclouds/date/DateCodecFactory.java | 6 ++ .../internal/DateServiceDateCodecFactory.java | 81 +++++++++++++++++-- .../DateServiceDateCodecFactoryTest.java | 43 +++++++++- .../BackoffLimitedRetryHandlerTest.java | 5 +- .../rest/internal/BaseRestApiExpectTest.java | 5 +- .../jclouds/gae/ConvertToGaeRequestTest.java | 6 +- .../gae/ConvertToJcloudsResponseTest.java | 5 +- 8 files changed, 125 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/org/jclouds/date/DateCodec.java b/core/src/main/java/org/jclouds/date/DateCodec.java index 8570b58c7a..bd4068e913 100644 --- a/core/src/main/java/org/jclouds/date/DateCodec.java +++ b/core/src/main/java/org/jclouds/date/DateCodec.java @@ -24,6 +24,7 @@ import java.util.Date; * converting from Date->String and vice versa. * * @author aled + * @see DateCodecFactory */ public interface DateCodec { diff --git a/core/src/main/java/org/jclouds/date/DateCodecFactory.java b/core/src/main/java/org/jclouds/date/DateCodecFactory.java index 596030811a..a99cf32249 100644 --- a/core/src/main/java/org/jclouds/date/DateCodecFactory.java +++ b/core/src/main/java/org/jclouds/date/DateCodecFactory.java @@ -26,11 +26,17 @@ import com.google.inject.ImplementedBy; * Codecs for converting from Date->String and vice versa. * * @author aled + * @see DateCodec */ @ImplementedBy(DateServiceDateCodecFactory.class) public interface DateCodecFactory { + DateCodec rfc822(); + DateCodec rfc1123(); DateCodec iso8601(); + + DateCodec iso8601Seconds(); + } diff --git a/core/src/main/java/org/jclouds/date/internal/DateServiceDateCodecFactory.java b/core/src/main/java/org/jclouds/date/internal/DateServiceDateCodecFactory.java index b462186156..9e9552956e 100644 --- a/core/src/main/java/org/jclouds/date/internal/DateServiceDateCodecFactory.java +++ b/core/src/main/java/org/jclouds/date/internal/DateServiceDateCodecFactory.java @@ -33,13 +33,45 @@ import com.google.inject.Inject; @Singleton public class DateServiceDateCodecFactory implements DateCodecFactory { + private final DateCodec rfc822Codec; private final DateCodec rfc1123Codec; - private final DateServiceIso8601Codec iso8601Codec; + private final DateCodec iso8601Codec; + private final DateCodec iso8601SecondsCodec; @Inject - public DateServiceDateCodecFactory(DateServiceRfc1123Codec rfc1123Codec, DateServiceIso8601Codec iso8601Codec) { - this.rfc1123Codec = checkNotNull(rfc1123Codec, "rfc1123Codec"); - this.iso8601Codec = checkNotNull(iso8601Codec, "iso8601Codec"); + public DateServiceDateCodecFactory(DateService dateService) { + checkNotNull(dateService, "dateService"); + this.rfc822Codec = new DateServiceRfc822Codec(dateService); + this.rfc1123Codec = new DateServiceRfc1123Codec(dateService); + this.iso8601Codec = new DateServiceIso8601Codec(dateService); + this.iso8601SecondsCodec = new DateServiceIso8601SecondsCodec(dateService); + } + + @Singleton + public static class DateServiceRfc822Codec implements DateCodec { + + protected final DateService dateService; + + @Inject + public DateServiceRfc822Codec(final DateService dateService) { + this.dateService = checkNotNull(dateService, "dateService"); + } + + @Override + public Date toDate(String date) throws IllegalArgumentException { + return dateService.rfc822DateParse(date); + } + + @Override + public String toString(Date date) { + return dateService.rfc822DateFormat(date); + } + + @Override + public String toString() { + return "rfc822()"; + } + } @Singleton @@ -68,7 +100,7 @@ public class DateServiceDateCodecFactory implements DateCodecFactory { } } - + @Singleton public static class DateServiceIso8601Codec implements DateCodec { @@ -95,15 +127,52 @@ public class DateServiceDateCodecFactory implements DateCodecFactory { } } + + @Singleton + public static class DateServiceIso8601SecondsCodec implements DateCodec { + + protected final DateService dateService; + + @Inject + public DateServiceIso8601SecondsCodec(DateService dateService) { + this.dateService = checkNotNull(dateService, "dateService"); + } + + @Override + public Date toDate(String date) throws IllegalArgumentException { + return dateService.iso8601SecondsDateParse(date); + } + + @Override + public String toString(Date date) { + return dateService.iso8601SecondsDateFormat(date); + } + + @Override + public String toString() { + return "iso8601Seconds()"; + } + + } + + @Override + public DateCodec rfc822() { + return rfc822Codec; + } @Override public DateCodec rfc1123() { return rfc1123Codec; } - + @Override public DateCodec iso8601() { return iso8601Codec; } + @Override + public DateCodec iso8601Seconds() { + return iso8601SecondsCodec; + } + } diff --git a/core/src/test/java/org/jclouds/date/internal/DateServiceDateCodecFactoryTest.java b/core/src/test/java/org/jclouds/date/internal/DateServiceDateCodecFactoryTest.java index 02f8416011..68f2a7fdd1 100644 --- a/core/src/test/java/org/jclouds/date/internal/DateServiceDateCodecFactoryTest.java +++ b/core/src/test/java/org/jclouds/date/internal/DateServiceDateCodecFactoryTest.java @@ -24,8 +24,6 @@ import static org.testng.Assert.fail; import java.util.Date; import org.jclouds.date.DateCodec; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceIso8601Codec; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceRfc1123Codec; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -38,15 +36,35 @@ import org.testng.annotations.Test; public class DateServiceDateCodecFactoryTest { private DateServiceDateCodecFactory simpleDateCodecFactory; + private DateCodec rfc822Codec; private DateCodec rfc1123Codec; private DateCodec iso8601Codec; + private DateCodec iso8601SecondsCodec; @BeforeTest public void setUp() { - simpleDateCodecFactory = new DateServiceDateCodecFactory(new DateServiceRfc1123Codec( - new SimpleDateFormatDateService()), new DateServiceIso8601Codec(new SimpleDateFormatDateService())); + simpleDateCodecFactory = new DateServiceDateCodecFactory(new SimpleDateFormatDateService()); + rfc822Codec = simpleDateCodecFactory.rfc822(); rfc1123Codec = simpleDateCodecFactory.rfc1123(); iso8601Codec = simpleDateCodecFactory.iso8601(); + iso8601SecondsCodec = simpleDateCodecFactory.iso8601Seconds(); + } + + @Test + public void testCodecForRfc822() { + Date date = new Date(1000); + assertEquals(rfc822Codec.toDate(rfc822Codec.toString(date)), date); + + assertEquals(rfc822Codec.toDate("Thu, 01 Dec 1994 16:00:00 GMT"), new Date(786297600000L)); + } + + @Test + public void testCodecForRfc822ThrowsParseExceptionWhenMalformed() { + try { + rfc822Codec.toDate("wrong"); + fail(); + } catch (IllegalArgumentException e) { + } } @Test @@ -82,4 +100,21 @@ public class DateServiceDateCodecFactoryTest { } catch (IllegalArgumentException e) { } } + + @Test + public void testCodecForIso8601Seconds() { + Date date = new Date(1000); + assertEquals(iso8601SecondsCodec.toDate(iso8601SecondsCodec.toString(date)), date); + + assertEquals(iso8601SecondsCodec.toDate("2012-11-14T21:51:28UTC").getTime(), 1352929888000l); + } + + @Test + public void testCodecForIso8601SecondsThrowsParseExceptionWhenMalformed() { + try { + iso8601SecondsCodec.toDate("-"); + fail(); + } catch (IllegalArgumentException e) { + } + } } diff --git a/core/src/test/java/org/jclouds/http/handlers/BackoffLimitedRetryHandlerTest.java b/core/src/test/java/org/jclouds/http/handlers/BackoffLimitedRetryHandlerTest.java index a16c481a55..7779761a0e 100644 --- a/core/src/test/java/org/jclouds/http/handlers/BackoffLimitedRetryHandlerTest.java +++ b/core/src/test/java/org/jclouds/http/handlers/BackoffLimitedRetryHandlerTest.java @@ -35,8 +35,6 @@ import javax.net.ssl.SSLSession; import javax.ws.rs.core.UriBuilder; import org.jclouds.date.internal.DateServiceDateCodecFactory; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceIso8601Codec; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceRfc1123Codec; import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.http.BaseJettyTest; import org.jclouds.http.HttpCommand; @@ -118,8 +116,7 @@ public class BackoffLimitedRetryHandlerTest { BackoffLimitedRetryHandler backoff = new BackoffLimitedRetryHandler(); HttpUtils utils = new HttpUtils(0, 500, 1, 1); ContentMetadataCodec contentMetadataCodec = new DefaultContentMetadataCodec(new DateServiceDateCodecFactory( - new DateServiceRfc1123Codec(new SimpleDateFormatDateService()), new DateServiceIso8601Codec( - new SimpleDateFormatDateService()))); + new SimpleDateFormatDateService())); RedirectionRetryHandler retry = new RedirectionRetryHandler(uriBuilderProvider, backoff); JavaUrlHttpCommandExecutorService httpService = new JavaUrlHttpCommandExecutorService(utils, contentMetadataCodec, execService, diff --git a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java index ea0c26deff..fd178c9a2d 100644 --- a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java @@ -50,8 +50,6 @@ import org.jclouds.concurrent.MoreExecutors; import org.jclouds.concurrent.SingleThreaded; import org.jclouds.concurrent.config.ConfiguresExecutorService; import org.jclouds.date.internal.DateServiceDateCodecFactory; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceIso8601Codec; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceRfc1123Codec; import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.http.HttpCommandExecutorService; import org.jclouds.http.HttpRequest; @@ -122,8 +120,7 @@ public abstract class BaseRestApiExpectTest { protected String provider = "mock"; protected ContentMetadataCodec contentMetadataCodec = new DefaultContentMetadataCodec( - new DateServiceDateCodecFactory(new DateServiceRfc1123Codec(new SimpleDateFormatDateService()), - new DateServiceIso8601Codec(new SimpleDateFormatDateService()))); + new DateServiceDateCodecFactory(new SimpleDateFormatDateService())); /** * Override this to supply alternative bindings for use in the test. This is commonly used to diff --git a/drivers/gae/src/test/java/org/jclouds/gae/ConvertToGaeRequestTest.java b/drivers/gae/src/test/java/org/jclouds/gae/ConvertToGaeRequestTest.java index 9dbaa0c9c0..de0c140cf5 100644 --- a/drivers/gae/src/test/java/org/jclouds/gae/ConvertToGaeRequestTest.java +++ b/drivers/gae/src/test/java/org/jclouds/gae/ConvertToGaeRequestTest.java @@ -32,8 +32,6 @@ import javax.ws.rs.core.HttpHeaders; import org.jclouds.crypto.Crypto; import org.jclouds.date.internal.DateServiceDateCodecFactory; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceIso8601Codec; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceRfc1123Codec; import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.encryption.internal.JCECrypto; import org.jclouds.http.HttpRequest; @@ -75,9 +73,7 @@ public class ConvertToGaeRequestTest { void setupClient() { endPoint = URI.create("http://localhost:80/foo"); req = new ConvertToGaeRequest(new HttpUtils(0, 0, 0, 0), new DefaultContentMetadataCodec( - new DateServiceDateCodecFactory(new DateServiceRfc1123Codec(new SimpleDateFormatDateService()), - new DateServiceIso8601Codec(new SimpleDateFormatDateService())))); - + new DateServiceDateCodecFactory(new SimpleDateFormatDateService()))); } @Test diff --git a/drivers/gae/src/test/java/org/jclouds/gae/ConvertToJcloudsResponseTest.java b/drivers/gae/src/test/java/org/jclouds/gae/ConvertToJcloudsResponseTest.java index 9a308026a2..4285209d83 100644 --- a/drivers/gae/src/test/java/org/jclouds/gae/ConvertToJcloudsResponseTest.java +++ b/drivers/gae/src/test/java/org/jclouds/gae/ConvertToJcloudsResponseTest.java @@ -33,8 +33,6 @@ import javax.ws.rs.core.HttpHeaders; import org.jclouds.crypto.Crypto; import org.jclouds.date.internal.DateServiceDateCodecFactory; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceIso8601Codec; -import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceRfc1123Codec; import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.encryption.internal.JCECrypto; import org.jclouds.http.HttpResponse; @@ -72,8 +70,7 @@ public class ConvertToJcloudsResponseTest { void setupClient() { endPoint = URI.create("http://localhost:80/foo"); req = new ConvertToJcloudsResponse(new DefaultContentMetadataCodec(new DateServiceDateCodecFactory( - new DateServiceRfc1123Codec(new SimpleDateFormatDateService()), new DateServiceIso8601Codec( - new SimpleDateFormatDateService())))); + new SimpleDateFormatDateService()))); } @Test From 24f8e665d49d730074aac736a1b46bf0244c766a Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 9 Dec 2012 00:30:35 -0800 Subject: [PATCH 2/3] added RetryAfterException and placed in default exception handling --- .../HeaderToRetryAfterException.java | 123 ++++++++++++++++ .../org/jclouds/rest/RetryAfterException.java | 101 ++++++++++++++ .../MapHttp4xxCodesToExceptions.java | 12 ++ .../rest/functions/PropagateIfRetryAfter.java | 36 +++++ .../HeaderToRetryAfterExceptionTest.java | 131 ++++++++++++++++++ .../MapHttp4xxCodesToExceptionsTest.java | 82 ++++++----- 6 files changed, 447 insertions(+), 38 deletions(-) create mode 100644 core/src/main/java/org/jclouds/http/functions/HeaderToRetryAfterException.java create mode 100644 core/src/main/java/org/jclouds/rest/RetryAfterException.java create mode 100644 core/src/main/java/org/jclouds/rest/functions/PropagateIfRetryAfter.java create mode 100644 core/src/test/java/org/jclouds/http/functions/HeaderToRetryAfterExceptionTest.java diff --git a/core/src/main/java/org/jclouds/http/functions/HeaderToRetryAfterException.java b/core/src/main/java/org/jclouds/http/functions/HeaderToRetryAfterException.java new file mode 100644 index 0000000000..22ad810749 --- /dev/null +++ b/core/src/main/java/org/jclouds/http/functions/HeaderToRetryAfterException.java @@ -0,0 +1,123 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.http.functions; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import javax.inject.Inject; + +import org.jclouds.date.DateCodec; +import org.jclouds.date.DateCodecFactory; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpResponseException; +import org.jclouds.rest.RetryAfterException; +import org.jclouds.rest.functions.PropagateIfRetryAfter; + +import com.google.common.annotations.Beta; +import com.google.common.base.Optional; +import com.google.common.base.Ticker; +import com.google.common.net.HttpHeaders; + +/** + * propagates as {@link RetryAfterException} if a Throwable is an + * {@link HttpResponseException} with a {@link HttpResponse set} and a valid + * {@link HttpHeaders#RETRY_AFTER} header set. + * + * @author Adrian Cole + * @see Retry-After + * format + */ +@Beta +public final class HeaderToRetryAfterException implements PropagateIfRetryAfter { + + private final Ticker ticker; + private final DateCodec dateCodec; + + /** + * + * @param ticker + * how to read current time + * @param dateParser + * how to parse the {@link HttpHeaders#RETRY_AFTER} header, if it + * is a Date. + * @return + */ + public static HeaderToRetryAfterException create(Ticker ticker, DateCodec dateCodec) { + return new HeaderToRetryAfterException(ticker, dateCodec); + } + + /** + * uses {@link Ticker#systemTicker()} and {@link DateCodecFactory#rfc822()} + */ + @Inject + private HeaderToRetryAfterException(DateCodecFactory factory) { + this(Ticker.systemTicker(), factory.rfc822()); + } + + private HeaderToRetryAfterException(Ticker ticker, DateCodec dateCodec) { + this.ticker = checkNotNull(ticker, "ticker"); + this.dateCodec = checkNotNull(dateCodec, "dateCodec"); + } + + @Override + public Void apply(Throwable in) { + if (!(in instanceof HttpResponseException)) + return null; + HttpResponse response = HttpResponseException.class.cast(in).getResponse(); + if (response == null) + return null; + + // https://tools.ietf.org/html/rfc2616#section-14.37 + String retryAfter = response.getFirstHeaderOrNull(HttpHeaders.RETRY_AFTER); + if (retryAfter != null) { + Optional retryException = tryCreateRetryAfterException(in, retryAfter); + if (retryException.isPresent()) + throw retryException.get(); + } + + return null; + } + + /** + * returns a {@link RetryAfterException} if parameter {@code retryAfter} + * corresponds to known formats. + * + * @see Retry-After + * format + */ + public Optional tryCreateRetryAfterException(Throwable in, String retryAfter) { + checkNotNull(in, "throwable"); + checkNotNull(retryAfter, "retryAfter"); + + if (retryAfter.matches("^[0-9]+$")) + return Optional.of(new RetryAfterException(in, Integer.parseInt(retryAfter))); + try { + long retryTimeMillis = dateCodec.toDate(retryAfter).getTime(); + long currentTimeMillis = NANOSECONDS.toMillis(ticker.read()); + return Optional.of(new RetryAfterException(in, (int) MILLISECONDS.toSeconds(retryTimeMillis + - currentTimeMillis))); + } catch (IllegalArgumentException e) { + // ignored + } + return Optional.absent(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/RetryAfterException.java b/core/src/main/java/org/jclouds/rest/RetryAfterException.java new file mode 100644 index 0000000000..1000c121ed --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/RetryAfterException.java @@ -0,0 +1,101 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.rest; + +import com.google.common.net.HttpHeaders; +import com.google.common.primitives.Ints; + +/** + * This exception is raised when an http endpoint returns with a response + * telling the caller to make the same request after a certain period of time. + * + * Typically, this is returned with a {@code 503} status code, as specified in + * the {@link HttpHeaders#RETRY_AFTER} header. + */ +public class RetryAfterException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Delta in seconds + */ + private final int seconds; + + /** + * Construct an exception instance to happen at a time in the future + * + * @param message + * message + * @param seconds + * retry after delta. Negative values are converted to zero + */ + public RetryAfterException(String message, int seconds) { + super(message); + this.seconds = Ints.max(seconds, 0); + } + + /** + * Construct an exception instance to happen at a time in the future + * + * @param cause + * cause + * @param seconds + * retry after delta. Negative values are converted to zero + */ + public RetryAfterException(Throwable cause, int seconds) { + super(defaultMessage(seconds = Ints.max(seconds, 0)), cause); + this.seconds = seconds; + } + + private static String defaultMessage(int seconds) { + switch (seconds) { + case 0: + return "retry now"; + case 1: + return "retry in 1 second"; + default: + return String.format("retry in %d seconds", seconds); + } + } + + /** + * Construct an exception instance to happen at a time in the future + * + * @param message + * message + * @param cause + * cause + * @param seconds + * retry after delta. Negative values are converted to zero + */ + public RetryAfterException(String message, Throwable cause, int seconds) { + super(message, cause); + this.seconds = Ints.max(seconds, 0); + } + + /** + * Get the value of the retry time + * + * @return the retry time, in seconds. This is always zero or positive. + */ + public int getSeconds() { + return seconds; + } + +} diff --git a/core/src/main/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptions.java b/core/src/main/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptions.java index 6675519683..a082db6a2b 100644 --- a/core/src/main/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptions.java +++ b/core/src/main/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptions.java @@ -18,6 +18,9 @@ */ package org.jclouds.rest.functions; +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.inject.Inject; import javax.inject.Singleton; import org.jclouds.http.HttpResponseException; @@ -34,7 +37,16 @@ import com.google.common.base.Throwables; @Singleton public class MapHttp4xxCodesToExceptions implements Function { + private final PropagateIfRetryAfter propagateIfRetryAfter; + + @Inject + protected MapHttp4xxCodesToExceptions(PropagateIfRetryAfter propagateIfRetryAfter) { + this.propagateIfRetryAfter = checkNotNull(propagateIfRetryAfter, "propagateIfRetryAfter"); + } + + @Override public Object apply(Exception from) { + propagateIfRetryAfter.apply(from); if (from instanceof HttpResponseException) { HttpResponseException responseException = (HttpResponseException) from; if (responseException.getResponse() != null) diff --git a/core/src/main/java/org/jclouds/rest/functions/PropagateIfRetryAfter.java b/core/src/main/java/org/jclouds/rest/functions/PropagateIfRetryAfter.java new file mode 100644 index 0000000000..d651c380c8 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/functions/PropagateIfRetryAfter.java @@ -0,0 +1,36 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.rest.functions; + +import org.jclouds.http.functions.HeaderToRetryAfterException; +import org.jclouds.rest.RetryAfterException; + +import com.google.common.base.Function; +import com.google.inject.ImplementedBy; + +/** + * propagates as {@link RetryAfterException} if a Throwable contains information + * such as a retry offset. + * + * @author Adrian Cole + */ +@ImplementedBy(HeaderToRetryAfterException.class) +public interface PropagateIfRetryAfter extends Function { + +} \ No newline at end of file diff --git a/core/src/test/java/org/jclouds/http/functions/HeaderToRetryAfterExceptionTest.java b/core/src/test/java/org/jclouds/http/functions/HeaderToRetryAfterExceptionTest.java new file mode 100644 index 0000000000..4224bce703 --- /dev/null +++ b/core/src/test/java/org/jclouds/http/functions/HeaderToRetryAfterExceptionTest.java @@ -0,0 +1,131 @@ +package org.jclouds.http.functions; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import org.jclouds.date.DateCodec; +import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceRfc822Codec; +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpResponseException; +import org.jclouds.rest.RetryAfterException; +import org.testng.annotations.Test; + +import com.google.common.base.Function; +import com.google.common.base.Ticker; +import com.google.common.net.HttpHeaders; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public class HeaderToRetryAfterExceptionTest { + + public void testArbitraryExceptionDoesntPropagate(){ + fn.apply(new RuntimeException()); + } + + public void testHttpResponseExceptionWithoutResponseDoesntPropagate(){ + fn.apply(new HttpResponseException("message", command, null)); + } + + public void testHttpResponseExceptionWithoutRetryAfterHeaderDoesntPropagate(){ + fn.apply(new HttpResponseException(command, HttpResponse.builder().statusCode(500).build())); + } + + public void testHttpResponseExceptionWithMalformedRetryAfterHeaderDoesntPropagate(){ + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "Fri, 31 Dec 1999 23:59:59 ZBW").build())); + } + + @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry now") + public void testHttpResponseExceptionWithRetryAfterDate() { + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "Fri, 31 Dec 1999 23:59:59 GMT").build())); + } + + @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry in 700 seconds") + public void testHttpResponseExceptionWithRetryAfterOffset(){ + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "700").build())); + } + + @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry in 86400 seconds") + public void testHttpResponseExceptionWithRetryAfterPastIsZero(){ + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "Sun, 2 Jan 2000 00:00:00 GMT").build())); + } + + public static HttpCommand command = new HttpCommand() { + + @Override + public int getRedirectCount() { + return 0; + } + + @Override + public int incrementRedirectCount() { + return 0; + } + + @Override + public boolean isReplayable() { + return false; + } + + @Override + public Exception getException() { + return null; + } + + @Override + public int getFailureCount() { + return 0; + } + + @Override + public int incrementFailureCount() { + return 0; + } + + @Override + public void setException(Exception exception) { + + } + + @Override + public HttpRequest getCurrentRequest() { + return HttpRequest.builder().method("GET").endpoint("http://stub").build(); + } + + @Override + public void setCurrentRequest(HttpRequest request) { + + } + + }; + + static DateCodec rfc822 = new DateServiceRfc822Codec(new SimpleDateFormatDateService()); + + static Ticker y2k = new Ticker(){ + + @Override + public long read() { + return MILLISECONDS.toNanos(rfc822.toDate("Sat, 1 Jan 2000 00:00:00 GMT").getTime()); + } + + }; + + public static HeaderToRetryAfterException fn = HeaderToRetryAfterException.create(y2k, rfc822); + + +} diff --git a/core/src/test/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptionsTest.java b/core/src/test/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptionsTest.java index 7a48286332..365ca92ad4 100644 --- a/core/src/test/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptionsTest.java +++ b/core/src/test/java/org/jclouds/rest/functions/MapHttp4xxCodesToExceptionsTest.java @@ -18,20 +18,16 @@ */ package org.jclouds.rest.functions; -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.fail; - +import org.jclouds.http.HttpCommand; import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpResponseException; +import org.jclouds.http.functions.HeaderToRetryAfterExceptionTest; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.ResourceNotFoundException; +import org.jclouds.rest.RetryAfterException; import org.testng.annotations.Test; -import com.google.common.base.Function; +import com.google.common.net.HttpHeaders; /** * @@ -40,42 +36,52 @@ import com.google.common.base.Function; @Test(groups = { "unit" }) public class MapHttp4xxCodesToExceptionsTest { - @Test - public void test401And403ToAuthorizationException() { - assertCodeMakes(401, AuthorizationException.class); - assertCodeMakes(403, AuthorizationException.class); + @Test(expectedExceptions = AuthorizationException.class) + public void test401ToAuthorizationException() { + fn.apply(new HttpResponseException(command, HttpResponse.builder().statusCode(401).build())); } - @Test + @Test(expectedExceptions = AuthorizationException.class) + public void test403ToAuthorizationException() { + fn.apply(new HttpResponseException(command, HttpResponse.builder().statusCode(403).build())); + } + + @Test(expectedExceptions = ResourceNotFoundException.class) public void test404ToResourceNotFoundException() { - assertCodeMakes(404, ResourceNotFoundException.class); + fn.apply(new HttpResponseException(command, HttpResponse.builder().statusCode(404).build())); } - @Test + @Test(expectedExceptions = IllegalStateException.class) public void test409ToIllegalStateException() { - assertCodeMakes(409, IllegalStateException.class); + fn.apply(new HttpResponseException(command, HttpResponse.builder().statusCode(409).build())); } - - private void assertCodeMakes(int statuscode, Class expected) { - Function function = new MapHttp4xxCodesToExceptions(); - HttpResponseException responseException = createMock(HttpResponseException.class); - - HttpResponse response = createMock(HttpResponse.class); - expect(response.getStatusCode()).andReturn(statuscode).atLeastOnce(); - expect(responseException.getResponse()).andReturn(response).atLeastOnce(); - - replay(responseException); - replay(response); - - try { - function.apply(responseException); - fail(); - } catch (Exception e) { - assertEquals(e.getClass(), expected); - } - - verify(responseException); - verify(response); + + @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry now") + public void testHttpResponseExceptionWithRetryAfterDate() { + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "Fri, 31 Dec 1999 23:59:59 GMT").build())); } - + + @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry in 700 seconds") + public void testHttpResponseExceptionWithRetryAfterOffset(){ + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "700").build())); + } + + @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry in 86400 seconds") + public void testHttpResponseExceptionWithRetryAfterPastIsZero(){ + fn.apply(new HttpResponseException(command, + HttpResponse.builder() + .statusCode(503) + .addHeader(HttpHeaders.RETRY_AFTER, "Sun, 2 Jan 2000 00:00:00 GMT").build())); + } + + MapHttp4xxCodesToExceptions fn = new MapHttp4xxCodesToExceptions(HeaderToRetryAfterExceptionTest.fn); + + HttpCommand command = HeaderToRetryAfterExceptionTest.command; + } From 04a528e82353620f339b06d331abe07f73ee3ebf Mon Sep 17 00:00:00 2001 From: Steve Loughran Date: Wed, 21 Nov 2012 18:42:43 +0000 Subject: [PATCH 3/3] integrated RetryAfterException into OpenStack Nova error response --- .../nova/v2_0/functions/OverLimitParser.java | 94 ++++++ .../nova/v2_0/handlers/NovaErrorHandler.java | 70 ++++- .../nova/v2_0/NovaErrorHandlerTest.java | 149 ---------- .../v2_0/handlers/NovaErrorHandlerTest.java | 276 ++++++++++++++++++ 4 files changed, 432 insertions(+), 157 deletions(-) create mode 100644 apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/functions/OverLimitParser.java delete mode 100644 apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/NovaErrorHandlerTest.java create mode 100644 apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandlerTest.java diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/functions/OverLimitParser.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/functions/OverLimitParser.java new file mode 100644 index 0000000000..85567e12ea --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/functions/OverLimitParser.java @@ -0,0 +1,94 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.openstack.nova.v2_0.functions; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; + +import javax.annotation.Resource; +import javax.inject.Inject; + +import org.jclouds.json.Json; +import org.jclouds.logging.Logger; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; + +/** + * + * The expected body contains the time as in this (real) response + * + *
+ *   {
+ * "overLimit" : {
+ *  "code" : 413,
+ *  "message" : "OverLimit Retry...",
+ *  "details" : "Error Details...",
+ *  "retryAt" : "2012-11-14T21:51:28UTC"
+ *  }
+ * }
+ * 
+ * + * or + * + *
+ *    {
+ *      "overLimit": {
+ *        "message": "This request was rate-limited.",
+ *        "code": 413,
+ *        "retryAfter": "54",
+ *        "details": "Only 1 POST request(s) can be made to \"*\" every minute."
+ *      }
+ *    }
+ * 
+ * + * @author Adrian Cole, Steve Loughran + * + */ +public class OverLimitParser implements Function> { + + @Resource + private Logger logger = Logger.NULL; + private final Json json; + + @Inject + public OverLimitParser(Json json) { + this.json = checkNotNull(json, "json"); + } + + private static class Holder { + Map overLimit = ImmutableMap.of(); + } + + /** + * parses or returns an empty map. + */ + @Override + public Map apply(String in) { + try { + return json.fromJson(in, OverLimitParser.Holder.class).overLimit; + } catch (RuntimeException e) { + // an error was raised during parsing -which can include badly + // formatted fields. + logger.error("Failed to parse " + in + "", e); + return ImmutableMap.of(); + } + } +} \ No newline at end of file diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandler.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandler.java index 5afd04196c..6592c7d6b7 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandler.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandler.java @@ -18,37 +18,72 @@ */ package org.jclouds.openstack.nova.v2_0.handlers; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Strings.emptyToNull; +import static com.google.common.collect.Maps.filterKeys; import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream; +import java.util.Set; + +import javax.annotation.Resource; +import javax.inject.Inject; import javax.inject.Singleton; +import org.jclouds.date.DateCodecFactory; import org.jclouds.http.HttpCommand; import org.jclouds.http.HttpErrorHandler; import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpResponseException; +import org.jclouds.http.functions.HeaderToRetryAfterException; +import org.jclouds.logging.Logger; +import org.jclouds.openstack.nova.v2_0.functions.OverLimitParser; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.InsufficientResourcesException; import org.jclouds.rest.ResourceNotFoundException; +import org.jclouds.rest.RetryAfterException; + +import com.google.common.base.Optional; +import com.google.common.base.Ticker; +import com.google.common.collect.ImmutableSet; /** * This will parse and set an appropriate exception on the command object. * - * @author Adrian Cole + * @author Adrian Cole, Steve Loughran * */ // TODO: is there error spec someplace? let's type errors, etc. @Singleton public class NovaErrorHandler implements HttpErrorHandler { + @Resource + protected Logger logger = Logger.NULL; + protected final HeaderToRetryAfterException retryAfterParser; + protected final OverLimitParser overLimitParser; + + protected NovaErrorHandler(HeaderToRetryAfterException retryAfterParser, OverLimitParser overLimitParser) { + this.retryAfterParser = checkNotNull(retryAfterParser, "retryAfterParser"); + this.overLimitParser = checkNotNull(overLimitParser, "overLimitParser"); + } + + /** + * in current format, retryAt has a value of {@code 2012-11-14T21:51:28UTC}, which is an ISO-8601 seconds (not milliseconds) format. + */ + @Inject + public NovaErrorHandler(DateCodecFactory factory, OverLimitParser overLimitParser) { + this(HeaderToRetryAfterException.create(Ticker.systemTicker(), factory.iso8601Seconds()), overLimitParser); + } + public void handleError(HttpCommand command, HttpResponse response) { // it is important to always read fully and close streams byte[] data = closeClientButKeepContentStream(response); - String message = data != null ? new String(data) : null; + String content = data != null ? emptyToNull(new String(data)) : null; - Exception exception = message != null ? new HttpResponseException(command, response, message) - : new HttpResponseException(command, response); - message = message != null ? message : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(), - response.getStatusLine()); + Exception exception = content != null ? new HttpResponseException(command, response, content) + : new HttpResponseException(command, response); + String requestLine = command.getCurrentRequest().getRequestLine(); + String message = content != null ? content : String.format("%s -> %s", requestLine, response.getStatusLine()); switch (response.getStatusCode()) { case 400: if (message.indexOf("quota exceeded") != -1) @@ -68,10 +103,29 @@ public class NovaErrorHandler implements HttpErrorHandler { } break; case 413: - exception = new InsufficientResourcesException(message, exception); - break; + if (content == null) { + exception = new InsufficientResourcesException(message, exception); + break; + } + exception = parseAndBuildRetryException(content, message, exception); } command.setException(exception); } + /** + * Build an exception from the response. If it contains the JSON payload then + * that is parsed to create a {@link RetryAfterException}, otherwise a + * {@link InsufficientResourcesException} is returned + * + */ + private Exception parseAndBuildRetryException(String json, String message, Exception exception) { + Set retryFields = ImmutableSet.of("retryAfter", "retryAt"); + for (String value : filterKeys(overLimitParser.apply(json), in(retryFields)).values()) { + Optional retryException = retryAfterParser.tryCreateRetryAfterException(exception, value); + if (retryException.isPresent()) + return retryException.get(); + } + return new InsufficientResourcesException(message, exception); + } + } diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/NovaErrorHandlerTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/NovaErrorHandlerTest.java deleted file mode 100644 index 2691715488..0000000000 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/NovaErrorHandlerTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Licensed to jclouds, Inc. (jclouds) under one or more - * contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. jclouds 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.openstack.nova.v2_0; - -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.reportMatcher; -import static org.easymock.EasyMock.verify; - -import java.net.URI; - -import org.easymock.IArgumentMatcher; -import org.jclouds.http.HttpCommand; -import org.jclouds.http.HttpRequest; -import org.jclouds.http.HttpResponse; -import org.jclouds.openstack.nova.v2_0.handlers.NovaErrorHandler; -import org.jclouds.rest.AuthorizationException; -import org.jclouds.rest.InsufficientResourcesException; -import org.jclouds.rest.ResourceNotFoundException; -import org.testng.annotations.Test; - -import com.google.inject.Guice; - -/** - * - * @author Adrian Cole - */ -@Test(groups = { "unit" }) -public class NovaErrorHandlerTest { - - @Test - public void test401MakesAuthorizationException() { - assertCodeMakes("GET", URI.create("https://api.openstack.nova.com/foo"), 401, "", "Unauthorized", - AuthorizationException.class); - } - - @Test - public void test400MakesIllegalStateExceptionOnQuotaExceededOnNoFixedIps() { - // should wait until ips are associated w/the server - assertCodeMakes( - "POST", - URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/servers/71554/action"), - 400, - "HTTP/1.1 400 Bad Request", - "{\"badRequest\": {\"message\": \"instance |71554| has no fixed_ips. unable to associate floating ip\", \"code\": 400}}", - IllegalStateException.class); - } - - @Test - public void test400MakesIllegalStateExceptionOnAlreadyExists() { - assertCodeMakes( - "POST", - URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/servers"), - 400, - "HTTP/1.1 400 Bad Request", - "{\"badRequest\": {\"message\": \"Server with the name 'test' already exists\", \"code\": 400}}", - IllegalStateException.class); - } - - @Test - public void test400MakesInsufficientResourcesExceptionOnQuotaExceeded() { - assertCodeMakes( - "POST", - URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/os-floating-ips"), - 400, - "HTTP/1.1 400 Bad Request", - "{\"badRequest\": {\"message\": \"AddressLimitExceeded: Address quota exceeded. You cannot create any more addresses\", \"code\": 400}}", - InsufficientResourcesException.class); - } - - @Test - public void test413MakesInsufficientResourcesException() { - assertCodeMakes( - "POST", - URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/os-volumes"), - 413, - "HTTP/1.1 413 Request Entity Too Large", - "{\"badRequest\": {\"message\": \"Volume quota exceeded. You cannot create a volume of size 1G\", \"code\": 413, \"retryAfter\": 0}}", - InsufficientResourcesException.class); - } - - @Test - public void test404MakesResourceNotFoundException() { - assertCodeMakes("GET", URI.create("https://api.openstack.nova.com/foo"), 404, "", "Not Found", - ResourceNotFoundException.class); - } - - private void assertCodeMakes(String method, URI uri, int statusCode, String message, String content, - Class expected) { - assertCodeMakes(method, uri, statusCode, message, "text/json", content, expected); - } - - private void assertCodeMakes(String method, URI uri, int statusCode, String message, String contentType, - String content, Class expected) { - - NovaErrorHandler function = Guice.createInjector().getInstance(NovaErrorHandler.class); - - HttpCommand command = createMock(HttpCommand.class); - HttpRequest request = HttpRequest.builder().method(method).endpoint(uri).build(); - HttpResponse response = HttpResponse.builder().statusCode(statusCode).message(message).payload(content).build(); - response.getPayload().getContentMetadata().setContentType(contentType); - - expect(command.getCurrentRequest()).andReturn(request).atLeastOnce(); - command.setException(classEq(expected)); - - replay(command); - - function.handleError(command, response); - - verify(command); - } - - public static Exception classEq(final Class in) { - reportMatcher(new IArgumentMatcher() { - - @Override - public void appendTo(StringBuffer buffer) { - buffer.append("classEq("); - buffer.append(in); - buffer.append(")"); - } - - @Override - public boolean matches(Object arg) { - return arg.getClass() == in; - } - - }); - return null; - } - -} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandlerTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandlerTest.java new file mode 100644 index 0000000000..08c8fe85c0 --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/handlers/NovaErrorHandlerTest.java @@ -0,0 +1,276 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.openstack.nova.v2_0.handlers; + +import static org.testng.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; + +import org.jclouds.date.DateCodec; +import org.jclouds.date.internal.DateServiceDateCodecFactory.DateServiceIso8601SecondsCodec; +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.functions.HeaderToRetryAfterException; +import org.jclouds.json.internal.GsonWrapper; +import org.jclouds.openstack.nova.v2_0.functions.OverLimitParser; +import org.jclouds.rest.AuthorizationException; +import org.jclouds.rest.InsufficientResourcesException; +import org.jclouds.rest.ResourceNotFoundException; +import org.jclouds.rest.RetryAfterException; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.google.common.base.Ticker; +import com.google.gson.Gson; + +/** + * + * @author Adrian Cole, Steve Loughran + */ +@Test(groups = { "unit" }) +public class NovaErrorHandlerTest { + + private HttpCommand command; + + @BeforeTest + void setupCommand(){ + command = command(); + } + + @Test + public void test401MakesAuthorizationException() { + fn.handleError(command, HttpResponse.builder().statusCode(401).message("Unauthorized").build()); + + assertEquals(command.getException().getClass(), AuthorizationException.class); + assertEquals(command.getException().getMessage(), + "POST https://nova/v1.1/servers HTTP/1.1 -> HTTP/1.1 401 Unauthorized"); + } + + @Test + public void test404MakesResourceNotFoundException() { + fn.handleError(command, HttpResponse.builder().statusCode(404).message("Not Found").build()); + + assertEquals(command.getException().getClass(), ResourceNotFoundException.class); + assertEquals(command.getException().getMessage(), + "POST https://nova/v1.1/servers HTTP/1.1 -> HTTP/1.1 404 Not Found"); + } + + // should wait until ips are associated w/the server + HttpResponse noFixedIps = HttpResponse.builder().statusCode(400) + .message("HTTP/1.1 400 Bad Request") + .payload("{\"badRequest\": {\"message\": "+ + "\"instance |71554| has no fixed_ips. unable to associate floating ip\", \"code\": 400}}") + .build(); + + @Test + public void test400MakesIllegalStateExceptionOnQuotaExceededOnNoFixedIps() { + fn.handleError(command, noFixedIps); + + assertEquals(command.getException().getClass(), IllegalStateException.class); + assertEquals(command.getException().getMessage(), noFixedIps.getPayload().getRawContent()); + } + + HttpResponse alreadyExists = HttpResponse.builder().statusCode(400) + .message("HTTP/1.1 400 Bad Request") + .payload("{\"badRequest\": {\"message\": \"Server with the name 'test' already exists\", \"code\": 400}}") + .build(); + + @Test + public void test400MakesIllegalStateExceptionOnAlreadyExists() { + fn.handleError(command, alreadyExists); + + assertEquals(command.getException().getClass(), IllegalStateException.class); + assertEquals(command.getException().getMessage(), alreadyExists.getPayload().getRawContent()); + } + + HttpResponse quotaExceeded = HttpResponse.builder().statusCode(400) + .message("HTTP/1.1 400 Bad Request") + .payload("{\"badRequest\": {\"message\": \"AddressLimitExceeded: Address quota exceeded. " + + "You cannot create any more addresses\", \"code\": 400}}") + .build(); + + @Test + public void test400MakesInsufficientResourcesExceptionOnQuotaExceeded() { + fn.handleError(command, quotaExceeded); + + assertEquals(command.getException().getClass(), InsufficientResourcesException.class); + assertEquals(command.getException().getMessage(), quotaExceeded.getPayload().getRawContent()); + } + + HttpResponse tooLarge = HttpResponse.builder().statusCode(413) + .message("HTTP/1.1 413 Request Entity Too Large") + .payload("{\"badRequest\": {\"message\": \"Volume quota exceeded. You cannot create a volume of size 1G\", " + + "\"code\": 413, \"retryAfter\": 0}}") + .build(); + + @Test + public void test413MakesInsufficientResourcesException() { + fn.handleError(command, tooLarge); + + assertEquals(command.getException().getClass(), InsufficientResourcesException.class); + assertEquals(command.getException().getMessage(), tooLarge.getPayload().getRawContent()); + } + + /** + * Reponse received from Rackspace UK on November 14, 2012. + */ + HttpResponse retryAt = HttpResponse.builder().statusCode(413) + .message("HTTP/1.1 413 Request Entity Too Large") + .payload("{ 'overLimit' : { 'code' : 413," + + " 'message' : 'OverLimit Retry...', " + + " 'details' : 'Error Details...'," + + " 'retryAt' : '2012-11-14T21:51:28UTC' }}") + .build(); + + @Test + public void test413WithRetryAtExceptionParsesDelta() { + fn.handleError(command, retryAt); + + assertEquals(command.getException().getClass(), RetryAfterException.class); + assertEquals(command.getException().getMessage(), "retry in 3600 seconds"); + } + + /** + * Folsom response. This contains a delta in seconds to retry after, not a + * fixed time. + * + */ + HttpResponse retryAfter = HttpResponse.builder().statusCode(413) + .message("HTTP/1.1 413 Request Entity Too Large") + .payload("{ 'overLimit': { 'message': 'This request was rate-limited.', " + + " 'retryAfter': '54', " + + " 'details': 'Only 1 POST request(s) can be made to \\'*\\' every minute.'" + " }}") + .build(); + + @Test + public void test413WithRetryAfterExceptionFolsom() { + fn.handleError(command, retryAfter); + + assertEquals(command.getException().getClass(), RetryAfterException.class); + assertEquals(command.getException().getMessage(), "retry in 54 seconds"); + } + + /** + * Folsom response with a retryAt field inserted -at a different date. This + * can be used to verify that the retryAfter field is picked up first + */ + HttpResponse retryAfterTrumps = HttpResponse.builder().statusCode(413) + .message("HTTP/1.1 413 Request Entity Too Large") + .payload("{ 'overLimit': {" + + " 'message': 'This request was rate-limited.', " + + " 'retryAfter': '54', " + + " 'retryAt' : '2012-11-14T21:51:28UTC'," + + " 'details': 'Only 1 POST request(s) can be made to \\'*\\' every minute.' }}") + .build(); + + @Test + public void test413WithRetryAfterTrumpsRetryAt() { + fn.handleError(command, retryAfterTrumps); + + assertEquals(command.getException().getClass(), RetryAfterException.class); + assertEquals(command.getException().getMessage(), "retry in 54 seconds"); + } + + HttpResponse badRetryAt = HttpResponse.builder().statusCode(413) + .message("HTTP/1.1 413 Request Entity Too Large") + .payload("{ 'overLimit' : { 'code' : 413," + + " 'message' : 'OverLimit Retry...', " + + " 'details' : 'Error Details...'," + + " 'retryAt' : '2012-11-~~~:51:28UTC' }}") + .build(); + + @Test + public void test413WithBadRetryAtFormatFallsBack() { + fn.handleError(command, badRetryAt); + + assertEquals(command.getException().getClass(), InsufficientResourcesException.class); + assertEquals(command.getException().getMessage(), badRetryAt.getPayload().getRawContent()); + } + + + DateCodec iso8601Seconds = new DateServiceIso8601SecondsCodec(new SimpleDateFormatDateService()); + + Ticker y2k = new Ticker(){ + + @Override + public long read() { + return TimeUnit.MILLISECONDS.toNanos(iso8601Seconds.toDate("2012-11-14T20:51:28UTC").getTime()); + } + + }; + + NovaErrorHandler fn = new NovaErrorHandler(HeaderToRetryAfterException.create(y2k, iso8601Seconds), + new OverLimitParser(new GsonWrapper(new Gson()))); + + private HttpCommand command() { + return new HttpCommand() { + + private Exception exception; + + @Override + public int getRedirectCount() { + return 0; + } + + @Override + public int incrementRedirectCount() { + return 0; + } + + @Override + public boolean isReplayable() { + return false; + } + + @Override + public Exception getException() { + return exception; + } + + @Override + public int getFailureCount() { + return 0; + } + + @Override + public int incrementFailureCount() { + return 0; + } + + @Override + public void setException(Exception exception) { + this.exception = exception; + } + + @Override + public HttpRequest getCurrentRequest() { + return HttpRequest.builder().method("POST").endpoint("https://nova/v1.1/servers").build(); + } + + @Override + public void setCurrentRequest(HttpRequest request) { + + } + + }; + } + +}