From 878313582acf344847d439ef5478ea8005845c5e Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 29 Jan 2013 04:08:42 -0800 Subject: [PATCH] issue #1089 synchronous commands no longer spawn threads unless a timeout is specified --- core/src/main/java/org/jclouds/Fallback.java | 45 ++++ core/src/main/java/org/jclouds/Fallbacks.java | 151 ++++++----- .../config/ExecutorServiceModule.java | 8 + .../MapHttp4xxCodesToExceptions.java | 12 +- .../http/HttpCommandExecutorService.java | 18 +- .../BaseHttpCommandExecutorService.java | 143 +++++----- .../org/jclouds/rest/InputParamValidator.java | 2 - .../jclouds/rest/annotations/Fallback.java | 7 +- .../jclouds/rest/config/InvocationConfig.java | 55 ++++ .../config/ReadAnnotationsAndProperties.java | 118 +++++++++ .../org/jclouds/rest/config/RestModule.java | 53 ++-- .../jclouds/rest/internal/BlockOnFuture.java | 132 ---------- .../internal/InvokeAndCallGetOnFutures.java | 11 +- .../rest/internal/InvokeHttpMethod.java | 248 +++++++++++++----- .../internal/RestAnnotationProcessor.java | 2 +- .../rest/internal/TransformerForRequest.java | 8 +- .../HeaderToRetryAfterExceptionTest.java | 18 ++ .../MapHttp4xxCodesToExceptionsTest.java | 14 +- .../http/IntegrationTestAsyncClient.java | 8 +- .../BackoffLimitedRetryHandlerTest.java | 4 +- .../jclouds/rest/InputParamValidatorTest.java | 4 +- .../ReadAnnotationsAndPropertiesTest.java | 135 ++++++++++ .../rest/internal/BaseRestApiExpectTest.java | 9 + .../rest/internal/BaseRestApiTest.java | 12 + .../rest/internal/BlockOnFutureTest.java | 138 ---------- .../rest/internal/InvokeHttpMethodTest.java | 177 +++++++++++++ .../internal/RestAnnotationProcessorTest.java | 49 +++- 27 files changed, 1050 insertions(+), 531 deletions(-) create mode 100644 core/src/main/java/org/jclouds/Fallback.java create mode 100644 core/src/main/java/org/jclouds/rest/config/InvocationConfig.java create mode 100644 core/src/main/java/org/jclouds/rest/config/ReadAnnotationsAndProperties.java delete mode 100644 core/src/main/java/org/jclouds/rest/internal/BlockOnFuture.java create mode 100644 core/src/test/java/org/jclouds/rest/config/ReadAnnotationsAndPropertiesTest.java delete mode 100644 core/src/test/java/org/jclouds/rest/internal/BlockOnFutureTest.java create mode 100644 core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java diff --git a/core/src/main/java/org/jclouds/Fallback.java b/core/src/main/java/org/jclouds/Fallback.java new file mode 100644 index 0000000000..1af2b71ead --- /dev/null +++ b/core/src/main/java/org/jclouds/Fallback.java @@ -0,0 +1,45 @@ +/** + * 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; + +import com.google.common.annotations.Beta; +import com.google.common.util.concurrent.FutureFallback; + +/** + * Provides a backup value to replace an earlier exception. + * + * @param + * the result type of the backup value + * + * @author Adrian Cole + * @see FutureFallback + * @since 1.6 + */ +@Beta +public interface Fallback extends FutureFallback { + /** + * The exception is provided so that the {@code Fallback} implementation can + * conditionally determine whether to propagate the exception or to attempt + * to recover. + * + * @param t + * the exception that made the call fail. + */ + V createOrPropagate(Throwable t) throws Exception; +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/Fallbacks.java b/core/src/main/java/org/jclouds/Fallbacks.java index 812e7d8c79..f8479076d4 100644 --- a/core/src/main/java/org/jclouds/Fallbacks.java +++ b/core/src/main/java/org/jclouds/Fallbacks.java @@ -40,7 +40,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.FutureFallback; import com.google.common.util.concurrent.ListenableFuture; /** @@ -51,123 +50,153 @@ public final class Fallbacks { private Fallbacks() { } - public static final class NullOnNotFoundOr404 implements FutureFallback { - - @Override - public ListenableFuture create(Throwable t) { - return valOnNotFoundOr404(null, checkNotNull(t, "throwable")); + public static final class NullOnNotFoundOr404 implements Fallback { + public ListenableFuture create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); } - } - - public static final class VoidOnNotFoundOr404 implements FutureFallback { - - @Override - public ListenableFuture create(Throwable t) { + public Object createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(null, checkNotNull(t, "throwable")); } - } - public static final class TrueOnNotFoundOr404 implements FutureFallback { + public static final class VoidOnNotFoundOr404 implements Fallback { + public ListenableFuture create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } - @Override - public ListenableFuture create(Throwable t) { + public Void createOrPropagate(Throwable t) throws Exception { + return valOnNotFoundOr404(null, checkNotNull(t, "throwable")); + } + } + + public static final class TrueOnNotFoundOr404 implements Fallback { + public ListenableFuture create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public Boolean createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(true, checkNotNull(t, "throwable")); } - } - public static final class FalseOnNotFoundOr404 implements FutureFallback { + public static final class FalseOnNotFoundOr404 implements Fallback { + public ListenableFuture create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } - @Override - public ListenableFuture create(Throwable t) { + public Boolean createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(false, checkNotNull(t, "throwable")); } - } - public static final class FalseOnNotFoundOr422 implements FutureFallback { - - @Override - public ListenableFuture create(Throwable t) { - if (containsResourceNotFoundException(checkNotNull(t, "throwable")) - || returnValueOnCodeOrNull(t, true, equalTo(422)) != null) - return immediateFuture(false); - throw propagate(t); + public static final class FalseOnNotFoundOr422 implements Fallback { + public ListenableFuture create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); } + public Boolean createOrPropagate(Throwable t) throws Exception { + if (containsResourceNotFoundException(checkNotNull(t, "throwable")) + || returnValueOnCodeOrNull(t, true, equalTo(422)) != null) + return false; + throw propagate(t); + } } /** * @author Leander Beernaert */ - public static final class AbsentOn403Or404Or500 implements FutureFallback> { - @Override - public ListenableFuture> create(Throwable t) { - Boolean returnVal = returnValueOnCodeOrNull(checkNotNull(t, "throwable"), true, in(asList(403, 404, 500))); - if (returnVal != null) - return immediateFuture(Optional.absent()); - throw propagate(t); - + public static final class AbsentOn403Or404Or500 implements Fallback> { + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); } + public Optional createOrPropagate(Throwable t) throws Exception { + Boolean returnVal = returnValueOnCodeOrNull(checkNotNull(t, "throwable"), true, in(asList(403, 404, 500))); + if (returnVal != null) + return Optional.absent(); + throw propagate(t); + } } - public static final class EmptyFluentIterableOnNotFoundOr404 implements FutureFallback> { - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptyFluentIterableOnNotFoundOr404 implements Fallback> { + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public FluentIterable createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(FluentIterable.from(ImmutableSet.of()), checkNotNull(t, "throwable")); } } - public static final class EmptyIterableWithMarkerOnNotFoundOr404 implements - FutureFallback> { - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptyIterableWithMarkerOnNotFoundOr404 implements Fallback> { + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public IterableWithMarker createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(IterableWithMarkers.from(ImmutableSet.of()), checkNotNull(t, "throwable")); } } - public static final class EmptyPagedIterableOnNotFoundOr404 implements FutureFallback> { - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptyPagedIterableOnNotFoundOr404 implements Fallback> { + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public PagedIterable createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(PagedIterables.of(IterableWithMarkers.from(ImmutableSet.of())), checkNotNull(t, "throwable")); } } - public static final class EmptyListOnNotFoundOr404 implements FutureFallback> { // NO_UCD (unused code) - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptyListOnNotFoundOr404 implements Fallback> { // NO_UCD + // (unused + // code) + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public ImmutableList createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(ImmutableList.of(), checkNotNull(t, "throwable")); } } - public static final class EmptySetOnNotFoundOr404 implements FutureFallback> { - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptySetOnNotFoundOr404 implements Fallback> { + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public ImmutableSet createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(ImmutableSet.of(), checkNotNull(t, "throwable")); } } - public static final class EmptyMapOnNotFoundOr404 implements FutureFallback> { - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptyMapOnNotFoundOr404 implements Fallback> { + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public ImmutableMap createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(ImmutableMap.of(), checkNotNull(t, "throwable")); } } - public static final class EmptyMultimapOnNotFoundOr404 implements FutureFallback> { // NO_UCD (unused code) - @Override - public ListenableFuture> create(Throwable t) { + public static final class EmptyMultimapOnNotFoundOr404 implements Fallback> { // NO_UCD + // (unused + // code) + public ListenableFuture> create(Throwable t) throws Exception { + return immediateFuture(createOrPropagate(t)); + } + + public ImmutableMultimap createOrPropagate(Throwable t) throws Exception { return valOnNotFoundOr404(ImmutableMultimap.of(), checkNotNull(t, "throwable")); } } - public static ListenableFuture valOnNotFoundOr404(T val, Throwable t) { + public static T valOnNotFoundOr404(T val, Throwable t) { if (containsResourceNotFoundException(checkNotNull(t, "throwable")) || contains404(t)) - return immediateFuture(val); + return val; throw propagate(t); } diff --git a/core/src/main/java/org/jclouds/concurrent/config/ExecutorServiceModule.java b/core/src/main/java/org/jclouds/concurrent/config/ExecutorServiceModule.java index 3e908457f3..7f76c8091b 100644 --- a/core/src/main/java/org/jclouds/concurrent/config/ExecutorServiceModule.java +++ b/core/src/main/java/org/jclouds/concurrent/config/ExecutorServiceModule.java @@ -38,7 +38,9 @@ import org.jclouds.lifecycle.Closer; import org.jclouds.logging.Logger; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.SimpleTimeLimiter; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.common.util.concurrent.TimeLimiter; import com.google.inject.AbstractModule; import com.google.inject.Provides; @@ -98,6 +100,12 @@ public class ExecutorServiceModule extends AbstractModule { protected void configure() { // NO_UCD } + @Provides + @Singleton + TimeLimiter timeLimiter(@Named(PROPERTY_USER_THREADS) ListeningExecutorService userExecutor){ + return new SimpleTimeLimiter(userExecutor); + } + @Provides @Singleton @Named(PROPERTY_USER_THREADS) diff --git a/core/src/main/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptions.java b/core/src/main/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptions.java index 2748af68eb..662a8fd77c 100644 --- a/core/src/main/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptions.java +++ b/core/src/main/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptions.java @@ -20,15 +20,16 @@ package org.jclouds.fallbacks; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Throwables.propagate; +import static com.google.common.util.concurrent.Futures.immediateFuture; import javax.inject.Inject; import javax.inject.Singleton; +import org.jclouds.Fallback; import org.jclouds.http.HttpResponseException; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.ResourceNotFoundException; -import com.google.common.util.concurrent.FutureFallback; import com.google.common.util.concurrent.ListenableFuture; /** @@ -36,7 +37,7 @@ import com.google.common.util.concurrent.ListenableFuture; * @author Adrian Cole */ @Singleton -public final class MapHttp4xxCodesToExceptions implements FutureFallback { +public final class MapHttp4xxCodesToExceptions implements Fallback { private final PropagateIfRetryAfter propagateIfRetryAfter; @@ -46,7 +47,12 @@ public final class MapHttp4xxCodesToExceptions implements FutureFallback } @Override - public ListenableFuture create(Throwable t) { // NO_UCD + public ListenableFuture create(Throwable t) throws Exception { // NO_UCD + return immediateFuture(createOrPropagate(t)); + } + + @Override + public Object createOrPropagate(Throwable t) throws Exception { propagateIfRetryAfter.create(t); // if we pass here, we aren't a retry-after exception if (t instanceof HttpResponseException) { HttpResponseException responseException = HttpResponseException.class.cast(t); diff --git a/core/src/main/java/org/jclouds/http/HttpCommandExecutorService.java b/core/src/main/java/org/jclouds/http/HttpCommandExecutorService.java index 6408d9cc1e..ba59d47d19 100644 --- a/core/src/main/java/org/jclouds/http/HttpCommandExecutorService.java +++ b/core/src/main/java/org/jclouds/http/HttpCommandExecutorService.java @@ -27,15 +27,21 @@ import com.google.common.util.concurrent.ListenableFuture; * * @author Adrian Cole */ -public interface HttpCommandExecutorService { +public interface HttpCommandExecutorService { /** - * Asks the command to build a request relevant for an endpoint that produces responses of - * generic type {@code HttpResponse}. and invokes it on the endpoint, returning a future + * Returns a potentially deferred {@code HttpResponse} from a server responding to the + * {@code command}. The output {@code ListenableFuture} need not be + * {@linkplain Future#isDone done}, making {@code HttpCommandExecutorService} + * suitable for asynchronous derivations. * - * @param command - * that generates requests - * @return {@link Future} containing the response from the {@code endpoint} */ ListenableFuture submit(HttpCommand command); + + /** + * Returns a {@code HttpResponse} from the server which responded to the + * {@code command}. + */ + HttpResponse invoke(HttpCommand command); + } diff --git a/core/src/main/java/org/jclouds/http/internal/BaseHttpCommandExecutorService.java b/core/src/main/java/org/jclouds/http/internal/BaseHttpCommandExecutorService.java index 3c8dc3a4ee..aa9a5da0bc 100644 --- a/core/src/main/java/org/jclouds/http/internal/BaseHttpCommandExecutorService.java +++ b/core/src/main/java/org/jclouds/http/internal/BaseHttpCommandExecutorService.java @@ -19,10 +19,12 @@ package org.jclouds.http.internal; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Throwables.propagate; import static com.google.common.io.ByteStreams.copy; import static com.google.common.io.ByteStreams.nullOutputStream; import static org.jclouds.http.HttpUtils.checkRequestHasContentLengthOrChunkedEncoding; import static org.jclouds.http.HttpUtils.wirePayloadIfEnabled; +import static org.jclouds.util.Throwables2.getFirstThrowableOfType; import java.io.FilterInputStream; import java.io.IOException; @@ -46,7 +48,6 @@ import org.jclouds.http.handlers.DelegatingErrorHandler; import org.jclouds.http.handlers.DelegatingRetryHandler; import org.jclouds.io.ContentMetadataCodec; import org.jclouds.logging.Logger; -import org.jclouds.util.Throwables2; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -58,11 +59,11 @@ import com.google.common.util.concurrent.ListeningExecutorService; public abstract class BaseHttpCommandExecutorService implements HttpCommandExecutorService { protected final HttpUtils utils; protected final ContentMetadataCodec contentMetadataCodec; - - private final DelegatingRetryHandler retryHandler; - private final IOExceptionRetryHandler ioRetryHandler; - private final DelegatingErrorHandler errorHandler; - private final ListeningExecutorService ioExecutor; + + protected final DelegatingRetryHandler retryHandler; + protected final IOExceptionRetryHandler ioRetryHandler; + protected final DelegatingErrorHandler errorHandler; + protected final ListeningExecutorService ioExecutor; @Resource protected Logger logger = Logger.NULL; @@ -74,9 +75,9 @@ public abstract class BaseHttpCommandExecutorService implements HttpCommandEx @Inject protected BaseHttpCommandExecutorService(HttpUtils utils, ContentMetadataCodec contentMetadataCodec, - @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor, - DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler, - DelegatingErrorHandler errorHandler, HttpWire wire) { + @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor, + DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler, + DelegatingErrorHandler errorHandler, HttpWire wire) { this.utils = checkNotNull(utils, "utils"); this.contentMetadataCodec = checkNotNull(contentMetadataCodec, "contentMetadataCodec"); this.retryHandler = checkNotNull(retryHandler, "retryHandler"); @@ -123,12 +124,71 @@ public abstract class BaseHttpCommandExecutorService implements HttpCommandEx } + @Override + public HttpResponse invoke(HttpCommand command) { + HttpResponse response = null; + for (;;) { + HttpRequest request = command.getCurrentRequest(); + Q nativeRequest = null; + try { + for (HttpRequestFilter filter : request.getFilters()) { + request = filter.filter(request); + } + checkRequestHasContentLengthOrChunkedEncoding(request, + "After filtering, the request has neither chunked encoding nor content length: " + request); + logger.debug("Sending request %s: %s", request.hashCode(), request.getRequestLine()); + wirePayloadIfEnabled(wire, request); + utils.logRequest(headerLog, request, ">>"); + nativeRequest = convert(request); + response = invoke(nativeRequest); + + logger.debug("Receiving response %s: %s", request.hashCode(), response.getStatusLine()); + utils.logResponse(headerLog, response, "<<"); + if (response.getPayload() != null && wire.enabled()) + wire.input(response); + nativeRequest = null; // response took ownership of streams + int statusCode = response.getStatusCode(); + if (statusCode >= 300) { + if (shouldContinue(command, response)) + continue; + else + break; + } else { + break; + } + } catch (Exception e) { + IOException ioe = getFirstThrowableOfType(e, IOException.class); + if (ioe != null && ioRetryHandler.shouldRetryRequest(command, ioe)) { + continue; + } + command.setException(new HttpResponseException(e.getMessage() + " connecting to " + + command.getCurrentRequest().getRequestLine(), command, null, e)); + break; + + } finally { + cleanup(nativeRequest); + } + } + if (command.getException() != null) + throw propagate(command.getException()); + return response; + } + + private boolean shouldContinue(HttpCommand command, HttpResponse response) { + boolean shouldContinue = false; + if (retryHandler.shouldRetryRequest(command, response)) { + shouldContinue = true; + } else { + errorHandler.handleError(command, response); + } + return shouldContinue; + } + @Override public ListenableFuture submit(HttpCommand command) { HttpRequest request = command.getCurrentRequest(); checkRequestHasContentLengthOrChunkedEncoding(request, - "if the request has a payload, it must be set to chunked encoding or specify a content length: " - + request); + "if the request has a payload, it must be set to chunked encoding or specify a content length: " + request); return ioExecutor.submit(new HttpResponseCallable(command)); } @@ -140,63 +200,12 @@ public abstract class BaseHttpCommandExecutorService implements HttpCommandEx } public HttpResponse call() throws Exception { - - HttpResponse response = null; - for (;;) { - HttpRequest request = command.getCurrentRequest(); - Q nativeRequest = null; - try { - for (HttpRequestFilter filter : request.getFilters()) { - request = filter.filter(request); - } - checkRequestHasContentLengthOrChunkedEncoding(request, - "After filtering, the request has neither chunked encoding nor content length: " + request); - logger.debug("Sending request %s: %s", request.hashCode(), request.getRequestLine()); - wirePayloadIfEnabled(wire, request); - utils.logRequest(headerLog, request, ">>"); - nativeRequest = convert(request); - response = invoke(nativeRequest); - - logger.debug("Receiving response %s: %s", request.hashCode(), response.getStatusLine()); - utils.logResponse(headerLog, response, "<<"); - if (response.getPayload() != null && wire.enabled()) - wire.input(response); - nativeRequest = null; // response took ownership of streams - int statusCode = response.getStatusCode(); - if (statusCode >= 300) { - if (shouldContinue(response)) - continue; - else - break; - } else { - break; - } - } catch (Exception e) { - IOException ioe = Throwables2.getFirstThrowableOfType(e, IOException.class); - if (ioe != null && ioRetryHandler.shouldRetryRequest(command, ioe)) { - continue; - } - command.setException(new HttpResponseException(e.getMessage() + " connecting to " - + command.getCurrentRequest().getRequestLine(), command, null, e)); - break; - - } finally { - cleanup(nativeRequest); - } + try { + return invoke(command); + } finally { + if (command.getException() != null) + throw command.getException(); } - if (command.getException() != null) - throw command.getException(); - return response; - } - - private boolean shouldContinue(HttpResponse response) { - boolean shouldContinue = false; - if (retryHandler.shouldRetryRequest(command, response)) { - shouldContinue = true; - } else { - errorHandler.handleError(command, response); - } - return shouldContinue; } @Override diff --git a/core/src/main/java/org/jclouds/rest/InputParamValidator.java b/core/src/main/java/org/jclouds/rest/InputParamValidator.java index 85225b9e3b..36c61c3db4 100644 --- a/core/src/main/java/org/jclouds/rest/InputParamValidator.java +++ b/core/src/main/java/org/jclouds/rest/InputParamValidator.java @@ -19,7 +19,6 @@ package org.jclouds.rest; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.Collections2.filter; import java.util.List; @@ -29,7 +28,6 @@ import org.jclouds.predicates.Validator; import org.jclouds.reflect.Invocation; import org.jclouds.rest.annotations.ParamValidators; -import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.reflect.Parameter; diff --git a/core/src/main/java/org/jclouds/rest/annotations/Fallback.java b/core/src/main/java/org/jclouds/rest/annotations/Fallback.java index a652f82131..7495426408 100644 --- a/core/src/main/java/org/jclouds/rest/annotations/Fallback.java +++ b/core/src/main/java/org/jclouds/rest/annotations/Fallback.java @@ -24,10 +24,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import com.google.common.util.concurrent.FutureFallback; - /** - * Annotates the appropriate {@link FutureFallback} which propagates the exception or returns a valid fallback value. + * Annotates the appropriate {@link org.jclouds.Fallback} which propagates + * the exception or returns a valid fallback value. * * @since 1.6 * @author Adrian Cole @@ -35,5 +34,5 @@ import com.google.common.util.concurrent.FutureFallback; @Target(METHOD) @Retention(RUNTIME) public @interface Fallback { - Class> value(); + Class> value(); } diff --git a/core/src/main/java/org/jclouds/rest/config/InvocationConfig.java b/core/src/main/java/org/jclouds/rest/config/InvocationConfig.java new file mode 100644 index 0000000000..429c62a647 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/config/InvocationConfig.java @@ -0,0 +1,55 @@ +/** + * 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 current 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.config; + +import org.jclouds.Fallback; +import org.jclouds.reflect.Invocation; + +import com.google.common.annotations.Beta; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.UncheckedTimeoutException; +import com.google.inject.ImplementedBy; + +/** + * Provides the ability to decouple timeouts and fallbacks from what's built-in. + * + * @author Adrian Cole + */ +@Beta +@ImplementedBy(ReadAnnotationsAndProperties.class) +public interface InvocationConfig { + + /** + * If this is present, Sync method calls will block up to the specified nanos + * and throw an {@linkplain UncheckedTimeoutException}. If this is not + * present, Sync method calls will be invoked directly, typically through + * {@linkplain HttpCommandExecutorService#invoke}. + */ + Optional getTimeoutNanos(Invocation in); + + /** + * command named used in logging and configuration keys. + */ + String getCommandName(Invocation invocation); + + /** + * fallback used for Sync or Async commands. + */ + Fallback getFallback(Invocation invocation); +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/config/ReadAnnotationsAndProperties.java b/core/src/main/java/org/jclouds/rest/config/ReadAnnotationsAndProperties.java new file mode 100644 index 0000000000..3fc2176917 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/config/ReadAnnotationsAndProperties.java @@ -0,0 +1,118 @@ +/** + * 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 current 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.config; + +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.collect.Maps.transformValues; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.jclouds.Constants.PROPERTY_TIMEOUTS_PREFIX; +import static org.jclouds.util.Maps2.transformKeys; +import static org.jclouds.util.Predicates2.startsWith; + +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.annotations.Fallback; + +import com.google.common.annotations.Beta; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.reflect.Invokable; +import com.google.inject.Injector; + +@Beta +@Singleton +public class ReadAnnotationsAndProperties implements InvocationConfig { + private final Injector injector; + private final org.jclouds.Fallback defaultFallback; + private final Map timeouts; + + @Inject + ReadAnnotationsAndProperties(Injector injector, + Function, Map> filterStringsBoundByName, + org.jclouds.Fallback defaultFallback) { + this.injector = injector; + this.defaultFallback = defaultFallback; + this.timeouts = timeouts(filterStringsBoundByName); + } + + @Override + public Optional getTimeoutNanos(Invocation in) { + String commandName = getCommandName(in); + Optional defaultMillis = fromNullable(timeouts.get("default")); + Optional timeoutMillis = fromNullable(timeouts.get(commandName)); + Invokable invoked = in.getInvokable(); + if (invoked.isAnnotationPresent(Named.class)) { + timeoutMillis = timeoutMillis.or(defaultMillis); + } else { + // TODO: remove old logic once Named annotations are on all methods + String className = invoked.getOwnerType().getRawType().getSimpleName().replace("AsyncClient", "Client") + .replace("AsyncApi", "Api"); + timeoutMillis = timeoutMillis.or(fromNullable(timeouts.get(className))).or(defaultMillis); + } + if (timeoutMillis.isPresent()) + return Optional.of(MILLISECONDS.toNanos(timeoutMillis.get())); + return Optional.absent(); + } + + @Override + public String getCommandName(Invocation invocation) { + Invokable invoked = invocation.getInvokable(); + if (invoked.isAnnotationPresent(Named.class)) { + return invoked.getAnnotation(Named.class).value(); + } else { + // TODO: remove old logic once Named annotations are on all methods + String className = invoked.getOwnerType().getRawType().getSimpleName().replace("AsyncClient", "Client") + .replace("AsyncApi", "Api"); + return className + "." + invoked.getName(); + } + } + + @Override + public org.jclouds.Fallback getFallback(Invocation invocation) { + Fallback fallback = invocation.getInvokable().getAnnotation(Fallback.class); + if (fallback != null) { + return injector.getInstance(fallback.value()); + } + return defaultFallback; + } + + /** + * override timeout by values configured in properties(in ms) + */ + static Map timeouts(Function, Map> filterStringsBoundByName) { + Map stringBoundWithTimeoutPrefix = filterStringsBoundByName + .apply(startsWith(PROPERTY_TIMEOUTS_PREFIX)); + Map longsByName = transformValues(stringBoundWithTimeoutPrefix, new Function() { + public Long apply(String input) { + return Long.valueOf(String.valueOf(input)); + } + }); + return transformKeys(longsByName, new Function() { + public String apply(String input) { + return input.replaceFirst(PROPERTY_TIMEOUTS_PREFIX, ""); + } + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/config/RestModule.java b/core/src/main/java/org/jclouds/rest/config/RestModule.java index bd2724660f..661adc37f9 100644 --- a/core/src/main/java/org/jclouds/rest/config/RestModule.java +++ b/core/src/main/java/org/jclouds/rest/config/RestModule.java @@ -22,34 +22,34 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Iterables.toArray; import static com.google.common.collect.Iterables.transform; -import static com.google.common.collect.Maps.transformValues; import static com.google.common.util.concurrent.Atomics.newReference; -import static org.jclouds.Constants.PROPERTY_TIMEOUTS_PREFIX; import static org.jclouds.reflect.Reflection2.method; import static org.jclouds.reflect.Reflection2.methods; import static org.jclouds.rest.config.BinderUtils.bindHttpApi; -import static org.jclouds.util.Maps2.transformKeys; -import static org.jclouds.util.Predicates2.startsWith; import java.net.Proxy; import java.net.URI; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import javax.inject.Named; import javax.inject.Singleton; +import org.jclouds.fallbacks.MapHttp4xxCodesToExceptions; import org.jclouds.functions.IdentityFunction; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; import org.jclouds.http.functions.config.SaxParserModule; import org.jclouds.internal.FilterStringsBoundToInjectorByName; import org.jclouds.json.config.GsonModule; import org.jclouds.location.config.LocationModule; import org.jclouds.proxy.ProxyForURI; +import org.jclouds.reflect.Invocation; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.HttpAsyncClient; import org.jclouds.rest.HttpClient; import org.jclouds.rest.binders.BindToJsonPayloadWrappedWith; -import org.jclouds.rest.internal.BlockOnFuture; +import org.jclouds.rest.internal.RestAnnotationProcessor; +import org.jclouds.rest.internal.TransformerForRequest; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; @@ -90,6 +90,21 @@ public class RestModule extends AbstractModule { return seedKnownSync2AsyncInvokables(sync2Async); } + /** + * function view of above + */ + @Provides + @Singleton + protected Function sync2async(final Cache, Invokable> cache) { + return new Function() { + public Invocation apply(Invocation in) { + return Invocation.create( + checkNotNull(cache.getIfPresent(in.getInvokable()), "invokable %s not in %s", in.getInvokable(), + cache), in.getArgs()); + } + }; + } + @VisibleForTesting static Cache, Invokable> seedKnownSync2AsyncInvokables(Map, Class> sync2Async) { Cache, Invokable> sync2AsyncBuilder = CacheBuilder.newBuilder().build(); @@ -133,7 +148,12 @@ public class RestModule extends AbstractModule { install(new GsonModule()); install(new SetCaller.Module()); install(new FactoryModuleBuilder().build(BindToJsonPayloadWrappedWith.Factory.class)); - install(new FactoryModuleBuilder().build(BlockOnFuture.Factory.class)); + bind(new TypeLiteral>>() { + }).to(TransformerForRequest.class); + bind(new TypeLiteral>() { + }).to(MapHttp4xxCodesToExceptions.class); + bind(new TypeLiteral>() { + }).to(RestAnnotationProcessor.class); bind(IdentityFunction.class).toInstance(IdentityFunction.INSTANCE); bindHttpApi(binder(), HttpClient.class, HttpAsyncClient.class); // this will help short circuit scenarios that can otherwise lock out users @@ -145,23 +165,4 @@ public class RestModule extends AbstractModule { }).to(ProxyForURI.class); installLocations(); } - - @Provides - @Singleton - @Named("TIMEOUTS") - protected Map timeouts(Function, Map> filterStringsBoundByName) { - Map stringBoundWithTimeoutPrefix = filterStringsBoundByName - .apply(startsWith(PROPERTY_TIMEOUTS_PREFIX)); - Map longsByName = transformValues(stringBoundWithTimeoutPrefix, new Function() { - public Long apply(String input) { - return Long.valueOf(String.valueOf(input)); - } - }); - return transformKeys(longsByName, new Function() { - public String apply(String input) { - return input.replaceFirst(PROPERTY_TIMEOUTS_PREFIX, ""); - } - }); - - } } diff --git a/core/src/main/java/org/jclouds/rest/internal/BlockOnFuture.java b/core/src/main/java/org/jclouds/rest/internal/BlockOnFuture.java deleted file mode 100644 index 668b439a57..0000000000 --- a/core/src/main/java/org/jclouds/rest/internal/BlockOnFuture.java +++ /dev/null @@ -1,132 +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.rest.internal; - -import static com.google.common.base.Optional.fromNullable; -import static com.google.common.collect.ObjectArrays.concat; -import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly; -import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import javax.annotation.Resource; -import javax.inject.Inject; -import javax.inject.Named; - -import org.jclouds.logging.Logger; -import org.jclouds.reflect.Invocation; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.reflect.Invokable; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.UncheckedExecutionException; -import com.google.common.util.concurrent.UncheckedTimeoutException; -import com.google.inject.assistedinject.Assisted; - -public class BlockOnFuture implements Function, Object> { - - public static interface Factory { - /** - * @param invocation - * context for how the future was created - */ - BlockOnFuture create(Invocation invocation); - } - - @Resource - private Logger logger = Logger.NULL; - - private final Map timeouts; - private final Invocation invocation; - - @Inject - @VisibleForTesting - BlockOnFuture(@Named("TIMEOUTS") Map timeouts, @Assisted Invocation invocation) { - this.timeouts = timeouts; - this.invocation = invocation; - } - - @Override - public Object apply(ListenableFuture future) { - Optional timeoutNanos = timeoutInNanos(invocation.getInvokable(), timeouts); - return block(future, timeoutNanos); - } - - private Object block(ListenableFuture future, Optional timeoutNanos) { - try { - if (timeoutNanos.isPresent()) { - logger.debug(">> blocking on %s for %s", future, timeoutNanos); - return getUninterruptibly(future, timeoutNanos.get(), NANOSECONDS); - } else { - logger.debug(">> blocking on %s", future); - return getUninterruptibly(future); - } - } catch (ExecutionException e) { - throw propagateCause(e); - } catch (TimeoutException e) { - future.cancel(true); - throw new UncheckedTimeoutException(e); - } - } - - private static RuntimeException propagateCause(Exception e) { - Throwable cause = e.getCause(); - if (cause == null) { - UncheckedExecutionException unchecked = new UncheckedExecutionException(e.getMessage()) { - private static final long serialVersionUID = 1L; - }; - unchecked.setStackTrace(e.getStackTrace()); - throw unchecked; - } - StackTraceElement[] combined = concat(cause.getStackTrace(), e.getStackTrace(), StackTraceElement.class); - cause.setStackTrace(combined); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } - if (cause instanceof Error) { - throw (Error) cause; - } - // The cause is a weird kind of Throwable, so throw the outer exception. - throw new RuntimeException(e); - } - - // override timeout by values configured in properties(in ms) - private Optional timeoutInNanos(Invokable invoked, Map timeouts) { - Optional defaultMillis = fromNullable(timeouts.get("default")); - Optional timeoutMillis; - if (invoked.isAnnotationPresent(Named.class)) { - String commandName = invoked.getAnnotation(Named.class).value(); - timeoutMillis = fromNullable(timeouts.get(commandName)).or(defaultMillis); - } else { - // TODO: remove old logic, once Named annotations are present on all methods - String className = invoked.getOwnerType().getRawType().getSimpleName().replace("AsyncClient", "Client") - .replace("AsyncApi", "Api"); - timeoutMillis = fromNullable(timeouts.get(className + "." + invoked.getName())).or( - fromNullable(timeouts.get(className))).or(defaultMillis); - } - if (timeoutMillis.isPresent()) - return Optional.of(TimeUnit.MILLISECONDS.toNanos(timeoutMillis.get())); - return Optional.absent(); - } -} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java b/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java index 3478754353..cda07b0003 100644 --- a/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java +++ b/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java @@ -18,7 +18,6 @@ */ package org.jclouds.rest.internal; -import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Throwables.propagate; import static com.google.common.util.concurrent.Futures.getUnchecked; @@ -32,7 +31,6 @@ import org.jclouds.reflect.Invocation; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; -import com.google.common.cache.Cache; import com.google.common.reflect.Invokable; import com.google.common.util.concurrent.ListenableFuture; @@ -45,7 +43,7 @@ public final class InvokeAndCallGetOnFutures implements Function, Invokable> sync2AsyncInvokables; + private final Function sync2async; private final R receiver; /** @@ -55,8 +53,8 @@ public final class InvokeAndCallGetOnFutures implements Function, Invokable> sync2AsyncInvokables, R receiver) { - this.sync2AsyncInvokables = sync2AsyncInvokables; + InvokeAndCallGetOnFutures(Function sync2async, R receiver) { + this.sync2async = sync2async; this.receiver = receiver; } @@ -64,8 +62,7 @@ public final class InvokeAndCallGetOnFutures implements Function { @Resource private Logger logger = Logger.NULL; - private final Injector injector; - private final Cache, Invokable> sync2AsyncInvokables; - private final RestAnnotationProcessor annotationProcessor; + private final Function sync2async; + private final Function annotationProcessor; private final HttpCommandExecutorService http; - private final TransformerForRequest transformerForRequest; private final ListeningExecutorService userExecutor; - private final BlockOnFuture.Factory blocker; + private final TimeLimiter timeLimiter; + private final Function> transformerForRequest; + private final InvocationConfig config; @Inject - private InvokeHttpMethod(Injector injector, Cache, Invokable> sync2AsyncInvokables, - RestAnnotationProcessor annotationProcessor, HttpCommandExecutorService http, - TransformerForRequest transformerForRequest, - @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, BlockOnFuture.Factory blocker) { - this.injector = injector; - this.sync2AsyncInvokables = sync2AsyncInvokables; + @VisibleForTesting + InvokeHttpMethod(Function sync2async, Function annotationProcessor, + HttpCommandExecutorService http, Function> transformerForRequest, + TimeLimiter timeLimiter, InvocationConfig config, + @Named(PROPERTY_USER_THREADS) ListeningExecutorService userExecutor) { + this.sync2async = sync2async; this.annotationProcessor = annotationProcessor; this.http = http; this.userExecutor = userExecutor; - this.blocker = blocker; + this.timeLimiter = timeLimiter; this.transformerForRequest = transformerForRequest; + this.config = config; } - private final LoadingCache, FutureFallback> fallbacks = CacheBuilder.newBuilder().build( - new CacheLoader, FutureFallback>() { - - @Override - public FutureFallback load(Invokable key) throws Exception { - Fallback annotation = key.getAnnotation(Fallback.class); - if (annotation != null) { - return injector.getInstance(annotation.value()); - } - return injector.getInstance(MapHttp4xxCodesToExceptions.class); - } - - }); - @Override public Object apply(Invocation in) { if (isFuture(in.getInvokable())) { - return createFuture(in); + return submit(in); } - Invocation async = Invocation.create( - checkNotNull(sync2AsyncInvokables.getIfPresent(in.getInvokable()), "invokable %s not in %s", - in.getInvokable(), sync2AsyncInvokables), in.getArgs()); + Invocation async = toAsync(in); + Optional timeoutNanos = config.getTimeoutNanos(async); + if (timeoutNanos.isPresent()) { + return invokeWithTimeout(async, timeoutNanos.get()); + } + return invoke(async); + } + + /** + * submits the {@linkplain HttpCommand} associated with {@code invocation}, + * {@link #getTransformer(String, HttpCommand) parses its response}, and + * applies a {@link #getFallback(String, Invocation, HttpCommand) fallback} + * if a {@code Throwable} is encountered. Parsing and Fallback occur on the + * {@code userExecutor} thread. + */ + public ListenableFuture submit(Invocation invocation) { + String commandName = config.getCommandName(invocation); + HttpCommand command = toCommand(commandName, invocation); + Function transformer = getTransformer(commandName, command); + org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); + + logger.debug(">> submitting %s", commandName); + return withFallback(transform(http.submit(command), transformer, userExecutor), fallback); + } + + /** + * invokes the {@linkplain HttpCommand} associated with {@code invocation}, + * {@link #getTransformer(String, HttpCommand) parses its response}, and + * applies a {@link #getFallback(String, Invocation, HttpCommand) fallback} + * if a {@code Throwable} is encountered. + */ + public Object invoke(Invocation invocation) { + String commandName = config.getCommandName(invocation); + HttpCommand command = toCommand(commandName, invocation); + Function transformer = getTransformer(commandName, command); + org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); + + logger.debug(">> invoking %s", commandName); + try { + return transformer.apply(http.invoke(command)); + } catch (Throwable t) { + try { + return fallback.createOrPropagate(t); + } catch (Exception e) { + throw propagate(e); + } + } + } + + /** + * calls {@link #invoke(Invocation)}, timing out after the specified time + * limit. If the target method call finished before the limit is reached, the + * return value or exception is propagated to the caller exactly as-is. If, + * on the other hand, the time limit is reached, we attempt to abort the call + * to the target, and throw an {@link UncheckedTimeoutException} to the + * caller. + * + * @param invocation + * the Invocation to invoke via {@link #invoke(Invocation)} + * @param limitNanos + * with timeoutUnit, the maximum length of time to wait in + * nanoseconds + * @throws InterruptedException + * if our thread is interrupted during execution + * @throws UncheckedTimeoutException + * if the time limit is reached + * @see TimeLimiter#callWithTimeout(Callable, long, TimeUnit, boolean) + */ + public Object invokeWithTimeout(final Invocation invocation, final long limitNanos) { + String commandName = config.getCommandName(invocation); + HttpCommand command = toCommand(commandName, invocation); + org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); + + logger.debug(">> blocking on %s for %s", invocation, limitNanos); + try { + return timeLimiter + .callWithTimeout(new InvokeAndTransform(commandName, command), limitNanos, NANOSECONDS, true); + } catch (Throwable t) { + try { + return fallback.createOrPropagate(t); + } catch (Exception e) { + throw propagate(e); + } + } + } + + private org.jclouds.Fallback getFallback(String commandName, Invocation invocation, HttpCommand command) { + HttpRequest request = command.getCurrentRequest(); + org.jclouds.Fallback fallback = config.getFallback(invocation); + if (fallback instanceof InvocationContext) + InvocationContext.class.cast(fallback).setContext(request); + logger.trace("<< exceptions from %s are parsed by %s", commandName, fallback.getClass().getSimpleName()); + return fallback; + } + + @VisibleForTesting + final class InvokeAndTransform implements Callable { + private final String commandName; + private final HttpCommand command; + private final Function transformer; + + InvokeAndTransform(String commandName, HttpCommand command) { + this.commandName = commandName; + this.command = command; + this.transformer = getTransformer(commandName, command); + } + + @Override + public Object call() throws Exception { + return transformer.apply(http.invoke(command)); + } + + @Override + public int hashCode() { + return Objects.hashCode(commandName, command, transformer); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + InvokeAndTransform that = InvokeAndTransform.class.cast(obj); + return equal(this.commandName, that.commandName) && equal(this.command, that.command) + && equal(this.transformer, that.transformer); + } + + @Override + public String toString() { + return toStringHelper(this).add("commandName", commandName).add("command", command) + .add("transformer", transformer).toString(); + } + } + + /** + * looks up the corresponding {@code Invocation} that returns a + * {@code Future}. Only Invokables that return {@code Futures} are annotated + * in a way that can be parsed into an {@linkplain HttpRequest}. + */ + private Invocation toAsync(Invocation in) { + Invocation async = sync2async.apply(in); checkState(isFuture(async.getInvokable()), "not a future: %s", async); - return blocker.create(async).apply(createFuture(async)); + return async; + } + + private HttpCommand toCommand(String commandName, Invocation invocation) { + logger.trace(">> converting %s", commandName); + HttpRequest request = annotationProcessor.apply(invocation); + logger.trace("<< converted %s to %s", commandName, request.getRequestLine()); + return new HttpCommand(request); + } + + private Function getTransformer(String commandName, HttpCommand command) { + HttpRequest request = command.getCurrentRequest(); + Function transformer = transformerForRequest.apply(request); + logger.trace("<< response from %s is parsed by %s", commandName, transformer.getClass().getSimpleName()); + return transformer; } private boolean isFuture(Invokable in) { return in.getReturnType().getRawType().equals(ListenableFuture.class); } - public ListenableFuture createFuture(Invocation invocation) { - String name = invocation.getInvokable().toString(); - logger.trace(">> converting %s", name); - GeneratedHttpRequest request = annotationProcessor.apply(invocation); - logger.trace("<< converted %s to %s", name, request.getRequestLine()); - - Function transformer = transformerForRequest.apply(request); - logger.trace("<< response from %s is parsed by %s", name, transformer.getClass().getSimpleName()); - - logger.debug(">> invoking %s", name); - ListenableFuture result = transform(http.submit(new HttpCommand(request)), transformer, userExecutor); - - FutureFallback fallback = fallbacks.getUnchecked(invocation.getInvokable()); - if (fallback instanceof InvocationContext) { - InvocationContext.class.cast(fallback).setContext(request); - } - logger.trace("<< exceptions from %s are parsed by %s", name, fallback.getClass().getSimpleName()); - return withFallback(result, fallback); - } - @Override public boolean equals(Object o) { if (this == o) diff --git a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java index d4101ce74c..bdcbd15e77 100644 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -131,7 +131,7 @@ import com.google.inject.TypeLiteral; * * @author Adrian Cole */ -public class RestAnnotationProcessor implements Function { +public class RestAnnotationProcessor implements Function { @Resource protected Logger logger = Logger.NULL; diff --git a/core/src/main/java/org/jclouds/rest/internal/TransformerForRequest.java b/core/src/main/java/org/jclouds/rest/internal/TransformerForRequest.java index bd004c1f9a..351f14d137 100644 --- a/core/src/main/java/org/jclouds/rest/internal/TransformerForRequest.java +++ b/core/src/main/java/org/jclouds/rest/internal/TransformerForRequest.java @@ -35,6 +35,7 @@ import javax.lang.model.type.NullType; import org.jclouds.functions.IdentityFunction; import org.jclouds.functions.OnlyElementOrNull; +import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; import org.jclouds.http.functions.ParseFirstJsonValueNamed; import org.jclouds.http.functions.ParseJson; @@ -69,7 +70,7 @@ import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.TypeLiteral; -public class TransformerForRequest implements Function> { +public class TransformerForRequest implements Function> { private final ParseSax.Factory parserFactory; private final Injector injector; private final GetAcceptHeaders getAcceptHeaders; @@ -83,7 +84,8 @@ public class TransformerForRequest implements Function apply(GeneratedHttpRequest request) { + public Function apply(HttpRequest in) { + GeneratedHttpRequest request = GeneratedHttpRequest.class.cast(in); Function transformer; Class> handler = getSaxResponseParserClassOrNull(request.getInvocation() .getInvokable()); @@ -93,7 +95,7 @@ public class TransformerForRequest implements Function) { - ((InvocationContext) transformer).setContext(request); + InvocationContext.class.cast(transformer).setContext(request); } if (request.getInvocation().getInvokable().isAnnotationPresent(Transform.class)) { Function wrappingTransformer = injector.getInstance(request.getInvocation().getInvokable() diff --git a/core/src/test/java/org/jclouds/fallbacks/HeaderToRetryAfterExceptionTest.java b/core/src/test/java/org/jclouds/fallbacks/HeaderToRetryAfterExceptionTest.java index 0655aad755..178fe41d2f 100644 --- a/core/src/test/java/org/jclouds/fallbacks/HeaderToRetryAfterExceptionTest.java +++ b/core/src/test/java/org/jclouds/fallbacks/HeaderToRetryAfterExceptionTest.java @@ -1,3 +1,21 @@ +/** + * 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 current 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.fallbacks; import static java.util.concurrent.TimeUnit.MILLISECONDS; diff --git a/core/src/test/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptionsTest.java b/core/src/test/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptionsTest.java index fb41da8e6e..0ed67012cc 100644 --- a/core/src/test/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptionsTest.java +++ b/core/src/test/java/org/jclouds/fallbacks/MapHttp4xxCodesToExceptionsTest.java @@ -36,27 +36,27 @@ import com.google.common.net.HttpHeaders; public class MapHttp4xxCodesToExceptionsTest { @Test(expectedExceptions = AuthorizationException.class) - public void test401ToAuthorizationException() { + public void test401ToAuthorizationException() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder().statusCode(401).build())); } @Test(expectedExceptions = AuthorizationException.class) - public void test403ToAuthorizationException() { + public void test403ToAuthorizationException() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder().statusCode(403).build())); } @Test(expectedExceptions = ResourceNotFoundException.class) - public void test404ToResourceNotFoundException() { + public void test404ToResourceNotFoundException() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder().statusCode(404).build())); } @Test(expectedExceptions = IllegalStateException.class) - public void test409ToIllegalStateException() { + public void test409ToIllegalStateException() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder().statusCode(409).build())); } @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry now") - public void testHttpResponseExceptionWithRetryAfterDate() { + public void testHttpResponseExceptionWithRetryAfterDate() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder() .statusCode(503) @@ -64,7 +64,7 @@ public class MapHttp4xxCodesToExceptionsTest { } @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry in 700 seconds") - public void testHttpResponseExceptionWithRetryAfterOffset(){ + public void testHttpResponseExceptionWithRetryAfterOffset() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder() .statusCode(503) @@ -72,7 +72,7 @@ public class MapHttp4xxCodesToExceptionsTest { } @Test(expectedExceptions = RetryAfterException.class, expectedExceptionsMessageRegExp = "retry in 86400 seconds") - public void testHttpResponseExceptionWithRetryAfterPastIsZero(){ + public void testHttpResponseExceptionWithRetryAfterPastIsZero() throws Exception { fn.create(new HttpResponseException(command, HttpResponse.builder() .statusCode(503) diff --git a/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java b/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java index bc9ea7b926..44df644a27 100644 --- a/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java +++ b/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java @@ -52,7 +52,6 @@ import org.jclouds.util.Strings2; import com.google.common.base.Function; import com.google.common.collect.Multimap; -import com.google.common.util.concurrent.FutureFallback; import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.Provides; @@ -92,13 +91,14 @@ public interface IntegrationTestAsyncClient { @Fallback(FooOnException.class) ListenableFuture downloadException(@PathParam("id") String id, HttpRequestOptions options); - static class FooOnException implements FutureFallback { - - @Override + static class FooOnException implements org.jclouds.Fallback { public ListenableFuture create(Throwable t) throws Exception { return immediateFuture("foo"); } + public String createOrPropagate(Throwable t) throws Exception { + return "foo"; + } } @GET 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 a2058ae90f..ab8717a436 100644 --- a/core/src/test/java/org/jclouds/http/handlers/BackoffLimitedRetryHandlerTest.java +++ b/core/src/test/java/org/jclouds/http/handlers/BackoffLimitedRetryHandlerTest.java @@ -27,6 +27,7 @@ import java.util.Properties; import org.jclouds.http.BaseJettyTest; import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; import org.jclouds.http.IntegrationTestAsyncClient; import org.jclouds.io.Payloads; @@ -34,6 +35,7 @@ import org.jclouds.reflect.Invocation; import org.jclouds.rest.internal.RestAnnotationProcessor; import org.testng.annotations.Test; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.reflect.Invokable; @@ -122,7 +124,7 @@ public class BackoffLimitedRetryHandlerTest { assertEquals(response.getPayload().getInput().read(), -1); } - private final RestAnnotationProcessor processor = BaseJettyTest.newBuilder(8100, new Properties()).buildInjector() + private final Function processor = BaseJettyTest.newBuilder(8100, new Properties()).buildInjector() .getInstance(RestAnnotationProcessor.class); private HttpCommand createCommand() throws SecurityException, NoSuchMethodException { diff --git a/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java b/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java index 55927c7c89..3bb143a2a8 100644 --- a/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java +++ b/core/src/test/java/org/jclouds/rest/InputParamValidatorTest.java @@ -24,6 +24,7 @@ import javax.ws.rs.POST; import javax.ws.rs.PathParam; import org.jclouds.ContextBuilder; +import org.jclouds.http.HttpRequest; import org.jclouds.http.IntegrationTestAsyncClient; import org.jclouds.http.IntegrationTestClient; import org.jclouds.predicates.validators.AllLowerCaseValidator; @@ -35,6 +36,7 @@ import org.testng.TestException; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.reflect.Invokable; import com.google.inject.Injector; @@ -105,7 +107,7 @@ public class InputParamValidatorTest { } Injector injector; - RestAnnotationProcessor restAnnotationProcessor; + Function restAnnotationProcessor; @BeforeClass void setupFactory() { diff --git a/core/src/test/java/org/jclouds/rest/config/ReadAnnotationsAndPropertiesTest.java b/core/src/test/java/org/jclouds/rest/config/ReadAnnotationsAndPropertiesTest.java new file mode 100644 index 0000000000..61bcd4fbc9 --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/config/ReadAnnotationsAndPropertiesTest.java @@ -0,0 +1,135 @@ +/** + * 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 current 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.config; + +import static org.jclouds.reflect.Reflection2.method; +import static org.testng.Assert.assertEquals; + +import java.util.Properties; + +import javax.inject.Named; + +import org.jclouds.Fallbacks.FalseOnNotFoundOr404; +import org.jclouds.Fallbacks.NullOnNotFoundOr404; +import org.jclouds.http.HttpResponse; +import org.jclouds.internal.FilterStringsBoundToInjectorByName; +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.annotations.Fallback; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.name.Names; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit", singleThreaded = true) +public class ReadAnnotationsAndPropertiesTest { + + public static interface ThingApi { + HttpResponse get(); + + HttpResponse namedGet(); + } + + public static interface ThingAsyncApi { + ListenableFuture get(); + + @Named("ns:get") + @Fallback(FalseOnNotFoundOr404.class) + ListenableFuture namedGet(); + } + + private Invocation asyncGet; + private Invocation asyncNamedGet; + private org.jclouds.Fallback defaultFallback; + + @BeforeClass + void setupInvocations() throws SecurityException, NoSuchMethodException { + asyncGet = Invocation.create(method(ThingAsyncApi.class, "get"), ImmutableList.of()); + asyncNamedGet = Invocation.create(method(ThingAsyncApi.class, "namedGet"), ImmutableList.of()); + defaultFallback = new NullOnNotFoundOr404(); + } + + /** + * this functionality will be removed once Named annotations are on all async + * classes. + */ + public void testInvocationsSetDefaultTimeoutOnAsyncMethods() throws Exception { + final Properties props = new Properties(); + props.setProperty("jclouds.timeouts.default", "250"); + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + Names.bindProperties(binder(), props); + } + }); + ReadAnnotationsAndProperties config = new ReadAnnotationsAndProperties(injector, + new FilterStringsBoundToInjectorByName(injector), defaultFallback); + assertEquals(config.getTimeoutNanos(asyncGet), Optional.of(250000000l)); + assertEquals(config.getTimeoutNanos(asyncNamedGet), Optional.of(250000000l)); + } + + public void testNamedInvocationGetsTimeoutOverrideOnAsyncMethods() throws Exception { + final Properties props = new Properties(); + props.setProperty("jclouds.timeouts.default", "50"); + props.setProperty("jclouds.timeouts.ThingApi", "100"); + props.setProperty("jclouds.timeouts.ns:get", "250"); + Injector injector = Guice.createInjector(new AbstractModule() { + protected void configure() { + Names.bindProperties(binder(), props); + } + }); + ReadAnnotationsAndProperties config = new ReadAnnotationsAndProperties(injector, + new FilterStringsBoundToInjectorByName(injector), defaultFallback); + assertEquals(config.getTimeoutNanos(asyncNamedGet), Optional.of(250000000l)); + } + + /** + * this functionality will be removed once Named annotations are on all async + * classes. + */ + public void testNamingConventionOfUnnamedMethods() throws Exception { + Injector injector = Guice.createInjector(); + ReadAnnotationsAndProperties config = new ReadAnnotationsAndProperties(injector, + new FilterStringsBoundToInjectorByName(injector), defaultFallback); + assertEquals(config.getCommandName(asyncGet), "ThingApi.get"); + } + + public void testNamingConventionOfNamedAsyncMethods() throws Exception { + Injector injector = Guice.createInjector(); + ReadAnnotationsAndProperties config = new ReadAnnotationsAndProperties(injector, + new FilterStringsBoundToInjectorByName(injector), defaultFallback); + assertEquals(config.getCommandName(asyncNamedGet), "ns:get"); + } + + public void testFallbackOverride() throws Exception { + Injector injector = Guice.createInjector(); + ReadAnnotationsAndProperties config = new ReadAnnotationsAndProperties(injector, + new FilterStringsBoundToInjectorByName(injector), defaultFallback); + assertEquals(config.getFallback(asyncNamedGet).getClass(), FalseOnNotFoundOr404.class); + assertEquals(config.getFallback(asyncGet), defaultFallback); + } +} 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 f20deafa89..4154bab67e 100644 --- a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java @@ -84,12 +84,15 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.InputSupplier; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.SimpleTimeLimiter; +import com.google.common.util.concurrent.TimeLimiter; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Module; +import com.google.inject.Provides; import com.google.inject.TypeLiteral; /** @@ -236,6 +239,12 @@ public abstract class BaseRestApiExpectTest { }).toInstance(fn); bind(HttpCommandExecutorService.class).to(ExpectHttpCommandExecutorService.class); } + + @Provides + @Singleton + TimeLimiter timeLimiter(@Named(PROPERTY_USER_THREADS) ListeningExecutorService userExecutor){ + return new SimpleTimeLimiter(userExecutor); + } } /** diff --git a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiTest.java b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiTest.java index 687b8f2e9c..f41f3e3d1e 100644 --- a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiTest.java @@ -33,6 +33,9 @@ import java.io.IOException; import java.util.Date; import java.util.Map.Entry; +import javax.inject.Named; +import javax.inject.Singleton; + import org.jclouds.concurrent.config.ConfiguresExecutorService; import org.jclouds.fallbacks.MapHttp4xxCodesToExceptions; import org.jclouds.http.HttpCommandExecutorService; @@ -51,8 +54,11 @@ import com.google.common.collect.SortedSetMultimap; import com.google.common.collect.TreeMultimap; import com.google.common.reflect.Invokable; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.SimpleTimeLimiter; +import com.google.common.util.concurrent.TimeLimiter; import com.google.inject.AbstractModule; import com.google.inject.Injector; +import com.google.inject.Provides; /** * @@ -83,6 +89,12 @@ public abstract class BaseRestApiTest { bind(ListeningExecutorService.class).annotatedWith(named(PROPERTY_IO_WORKER_THREADS)).toInstance(sameThreadExecutor()); bind(HttpCommandExecutorService.class).toInstance(mock); } + + @Provides + @Singleton + TimeLimiter timeLimiter(@Named(PROPERTY_USER_THREADS) ListeningExecutorService userExecutor){ + return new SimpleTimeLimiter(userExecutor); + } } protected void assertPayloadEquals(HttpRequest request, String toMatch, String contentType, boolean contentMD5) { diff --git a/core/src/test/java/org/jclouds/rest/internal/BlockOnFutureTest.java b/core/src/test/java/org/jclouds/rest/internal/BlockOnFutureTest.java deleted file mode 100644 index d9673c9a57..0000000000 --- a/core/src/test/java/org/jclouds/rest/internal/BlockOnFutureTest.java +++ /dev/null @@ -1,138 +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.rest.internal; - -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.jclouds.reflect.Reflection2.method; -import static org.testng.Assert.assertEquals; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import javax.inject.Named; - -import org.jclouds.reflect.Invocation; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import com.google.common.base.Function; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.ListenableFuture; - -/** - * - * @author Adrian Cole - */ -@Test(groups = "unit", singleThreaded = true) -public class BlockOnFutureTest { - - static ListenableFuture future; - - public static class ThingAsyncApi { - public ListenableFuture get() { - return future; - } - - @Named("ns:get") - public ListenableFuture namedGet() { - return future; - } - } - - private Invocation get; - private Invocation namedGet; - - @BeforeClass - void setupInvocations() throws SecurityException, NoSuchMethodException { - get = Invocation.create(method(ThingAsyncApi.class, "get"), ImmutableList.of()); - namedGet = Invocation.create(method(ThingAsyncApi.class, "namedGet"), ImmutableList.of()); - } - - @SuppressWarnings("unchecked") - @BeforeMethod - void createMockedFuture() throws InterruptedException, ExecutionException, TimeoutException { - future = createMock(ListenableFuture.class); - expect(future.get(250000000, TimeUnit.NANOSECONDS)).andReturn("foo"); - replay(future); - } - - public void testUnnamedMethodWithDefaultPropTimeout() throws Exception { - Function, Object> withOverride = new BlockOnFuture(ImmutableMap.of("default", 250L), get); - assertEquals(withOverride.apply(future), "foo"); - verify(future); - } - - public void testUnnamedMethodWithClassPropTimeout() throws Exception { - Function, Object> withOverride = new BlockOnFuture(ImmutableMap.of("default", 50L, - "ThingApi", 250L), get); - assertEquals(withOverride.apply(future), "foo"); - verify(future); - } - - public void testUnnamedMethodWithMethodPropTimeout() throws Exception { - Function, Object> withOverride = new BlockOnFuture(ImmutableMap.of("default", 50L, - "ThingApi", 100L, "ThingApi.get", 250L), get); - assertEquals(withOverride.apply(future), "foo"); - verify(future); - } - - @SuppressWarnings("unchecked") - public void testUnnamedMethodWithNoTimeoutsCallGetDirectly() throws Exception { - future = createMock(ListenableFuture.class); - expect(future.get()).andReturn("foo"); - replay(future); - - Function, Object> noOverrides = new BlockOnFuture(ImmutableMap. of(), get); - - assertEquals(noOverrides.apply(future), "foo"); - verify(future); - } - - public void testNamedMethodWithDefaultPropTimeout() throws Exception { - Function, Object> withOverride = new BlockOnFuture(ImmutableMap.of("default", 250L), namedGet); - assertEquals(withOverride.apply(future), "foo"); - verify(future); - } - - public void testNamedMethodWithMethodPropTimeout() throws Exception { - Function, Object> withOverride = new BlockOnFuture(ImmutableMap.of("default", 50L, - "ThingApi", 100L, "ns:get", 250L), namedGet); - assertEquals(withOverride.apply(future), "foo"); - verify(future); - } - - @SuppressWarnings("unchecked") - public void testNamedMethodWithNoTimeoutsCallGetDirectly() throws Exception { - future = createMock(ListenableFuture.class); - expect(future.get()).andReturn("foo"); - replay(future); - - Function, Object> noOverrides = new BlockOnFuture(ImmutableMap. of(), namedGet); - - assertEquals(noOverrides.apply(future), "foo"); - verify(future); - } - -} diff --git a/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java b/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java new file mode 100644 index 0000000000..71dade0220 --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java @@ -0,0 +1,177 @@ +/** + * 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.internal; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.jclouds.reflect.Reflection2.method; +import static org.testng.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Named; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpCommandExecutorService; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.config.InvocationConfig; +import org.jclouds.rest.internal.InvokeHttpMethod.InvokeAndTransform; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.TimeLimiter; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit", singleThreaded = true) +public class InvokeHttpMethodTest { + + public static interface ThingApi { + HttpResponse get(); + } + + public static interface ThingAsyncApi { + @Named("ns:get") + ListenableFuture get(); + } + + private Invocation get; + private Invocation asyncGet; + private Function sync2async; + private HttpRequest getRequest = HttpRequest.builder().method("GET").endpoint("http://get").build(); + private HttpCommand getCommand = new HttpCommand(getRequest); + private Function toRequest; + + @BeforeClass + void setupInvocations() throws SecurityException, NoSuchMethodException { + get = Invocation.create(method(ThingApi.class, "get"), ImmutableList.of()); + asyncGet = Invocation.create(method(ThingAsyncApi.class, "get"), ImmutableList.of()); + sync2async = Functions.forMap(ImmutableMap.of(get, asyncGet)); + toRequest = Functions.forMap(ImmutableMap.of(asyncGet, getRequest)); + } + + @SuppressWarnings("unchecked") + private Function> transformerForRequest = Function.class.cast(Functions + .constant(Functions.identity())); + private ListeningExecutorService userThreads = MoreExecutors.sameThreadExecutor(); + + private HttpResponse response = HttpResponse.builder().statusCode(200).payload("foo").build(); + private HttpCommandExecutorService http; + private TimeLimiter timeLimiter; + @SuppressWarnings("rawtypes") + private org.jclouds.Fallback fallback; + private InvocationConfig config; + private InvokeHttpMethod invokeHttpMethod; + + private ListenableFuture future; + + @SuppressWarnings("unchecked") + @BeforeMethod + void createMocks() { + http = createMock(HttpCommandExecutorService.class); + timeLimiter = createMock(TimeLimiter.class); + fallback = createMock(org.jclouds.Fallback.class); + config = createMock(InvocationConfig.class); + future = createMock(ListenableFuture.class); + invokeHttpMethod = new InvokeHttpMethod(sync2async, toRequest, http, transformerForRequest, timeLimiter, config, + userThreads); + expect(config.getCommandName(asyncGet)).andReturn("ns:get"); + expect(config.getFallback(asyncGet)).andReturn(fallback); + } + + @AfterMethod + void verifyMocks() { + verify(http, timeLimiter, fallback, config, future); + } + + public void testMethodWithTimeoutRunsTimeLimiter() throws Exception { + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional.of(250000000l)); + InvokeAndTransform invoke = invokeHttpMethod.new InvokeAndTransform("ns:get", getCommand); + expect(timeLimiter.callWithTimeout(invoke, 250000000, TimeUnit.NANOSECONDS, true)).andReturn(response); + replay(http, timeLimiter, fallback, config, future); + invokeHttpMethod.apply(get); + } + + public void testMethodWithNoTimeoutCallGetDirectly() throws Exception { + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional. absent()); + expect(http.invoke(new HttpCommand(getRequest))).andReturn(response); + replay(http, timeLimiter, fallback, config, future); + invokeHttpMethod.apply(get); + } + + public void testAsyncMethodSubmitsRequest() throws Exception { + expect(http.submit(new HttpCommand(getRequest))).andReturn(future); + future.addListener(anyObject(Runnable.class), eq(userThreads)); + replay(http, timeLimiter, fallback, config, future); + invokeHttpMethod.apply(asyncGet); + } + + private HttpResponse fallbackResponse = HttpResponse.builder().statusCode(200).payload("bar").build(); + + public void testDirectCallRunsFallbackCreateOrPropagate() throws Exception { + IllegalStateException exception = new IllegalStateException(); + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional. absent()); + expect(http.invoke(new HttpCommand(getRequest))).andThrow(exception); + expect(fallback.createOrPropagate(exception)).andReturn(fallbackResponse); + replay(http, timeLimiter, fallback, config, future); + assertEquals(invokeHttpMethod.apply(get), fallbackResponse); + } + + public void testTimeLimitedRunsFallbackCreateOrPropagate() throws Exception { + IllegalStateException exception = new IllegalStateException(); + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional.of(250000000l)); + InvokeAndTransform invoke = invokeHttpMethod.new InvokeAndTransform("ns:get", getCommand); + expect(timeLimiter.callWithTimeout(invoke, 250000000, TimeUnit.NANOSECONDS, true)).andThrow(exception); + expect(fallback.createOrPropagate(exception)).andReturn(fallbackResponse); + replay(http, timeLimiter, fallback, config, future); + assertEquals(invokeHttpMethod.apply(get), fallbackResponse); + } + + @SuppressWarnings("unchecked") + public void testSubmitRunsFallbackCreateOnGet() throws Exception { + IllegalStateException exception = new IllegalStateException(); + expect(http.submit(new HttpCommand(getRequest))).andReturn( + Futures. immediateFailedFuture(exception)); + expect(fallback.create(exception)).andReturn(Futures. immediateFuture(fallbackResponse)); + // not using the field, as you can see above we are making an immediate + // failed future instead. + future = createMock(ListenableFuture.class); + replay(http, timeLimiter, fallback, config, future); + assertEquals(ListenableFuture.class.cast(invokeHttpMethod.apply(asyncGet)).get(), fallbackResponse); + } +} diff --git a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java index 114087ea1c..9134fc691b 100644 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -249,9 +249,14 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://localhost:9999/client/1/foo HTTP/1.1"); - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } }); @@ -274,6 +279,11 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { if (callCounter == 1) assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://localhost:1111/client/1/bar/2 HTTP/1.1"); @@ -281,7 +291,7 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://localhost:1111/client/1/foo HTTP/1.1"); callCounter++; - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } }); @@ -304,8 +314,13 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://howdyboys/client/1/foo HTTP/1.1"); - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } }); @@ -329,9 +344,14 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://howdyboys/testing/testing/thepathparam/client/1/foo HTTP/1.1"); - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } }); @@ -355,8 +375,13 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://howdyboys/client/1/foo HTTP/1.1"); - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } }); @@ -380,8 +405,13 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://howdyboys/client/1/foo HTTP/1.1"); - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } }, new AbstractModule() { @@ -419,8 +449,13 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @Override public ListenableFuture submit(HttpCommand command) { + return Futures.immediateFuture(invoke(command)); + } + + @Override + public HttpResponse invoke(HttpCommand command) { assertEquals(command.getCurrentRequest().getRequestLine(), "GET http://howdyboys/client/1/foo HTTP/1.1"); - return Futures.immediateFuture(HttpResponse.builder().build()); + return HttpResponse.builder().build(); } });