From 48c31554500b1a500c7664b1d56ebbb42eaac667 Mon Sep 17 00:00:00 2001 From: "adrian.f.cole" Date: Wed, 24 Jun 2009 18:25:04 +0000 Subject: [PATCH] issue 69: refactored http clients to not be bound to a single endpoint such that redirects can be assigned to another host git-svn-id: http://jclouds.googlecode.com/svn/trunk@1456 3d8758e0-26b5-11de-8745-db77d3ebf521 --- .../org/jclouds/command/FutureCommand.java | 2 +- .../jclouds/command/FutureCommandClient.java | 4 +- .../java/org/jclouds/command/Request.java | 35 +++ .../pool/FutureCommandConnectionHandle.java | 122 ++++----- .../pool/FutureCommandConnectionPool.java | 246 ++++++++++-------- .../FutureCommandConnectionPoolClient.java | 59 +++-- ...tureCommandConnectionPoolClientModule.java | 5 +- .../org/jclouds/http/HttpErrorHandler.java | 4 +- .../org/jclouds/http/HttpFutureCommand.java | 11 +- .../java/org/jclouds/http/HttpHeaders.java | 116 ++++----- .../java/org/jclouds/http/HttpRequest.java | 9 +- .../org/jclouds/http/HttpRetryHandler.java | 18 +- .../jclouds/http/annotation/ClientError.java | 44 ++++ .../jclouds/http/annotation/Redirection.java | 44 ++++ .../jclouds/http/annotation/ServerError.java | 44 ++++ .../JavaUrlHttpFutureCommandClientModule.java | 18 -- .../handlers/BackoffLimitedRetryHandler.java | 77 +++--- .../http/handlers/CannotRetryHandler.java | 41 +++ ...seContentAndSetExceptionErrorHandler.java} | 6 +- .../http/handlers/DelegatingErrorHandler.java | 64 +++++ .../http/handlers/DelegatingRetryHandler.java | 68 +++++ .../handlers/RedirectionRetryHandler.java | 70 +++++ .../internal/BaseHttpFutureCommandClient.java | 36 +-- .../org/jclouds/lifecycle/BaseLifeCycle.java | 31 ++- .../http/BaseHttpFutureCommandClientTest.java | 11 + .../java/org/jclouds/http/BaseJettyTest.java | 2 + 26 files changed, 838 insertions(+), 349 deletions(-) create mode 100644 core/src/main/java/org/jclouds/command/Request.java create mode 100644 core/src/main/java/org/jclouds/http/annotation/ClientError.java create mode 100644 core/src/main/java/org/jclouds/http/annotation/Redirection.java create mode 100644 core/src/main/java/org/jclouds/http/annotation/ServerError.java create mode 100644 core/src/main/java/org/jclouds/http/handlers/CannotRetryHandler.java rename core/src/main/java/org/jclouds/http/handlers/{CloseContentAndSetExceptionHandler.java => CloseContentAndSetExceptionErrorHandler.java} (91%) create mode 100644 core/src/main/java/org/jclouds/http/handlers/DelegatingErrorHandler.java create mode 100644 core/src/main/java/org/jclouds/http/handlers/DelegatingRetryHandler.java create mode 100644 core/src/main/java/org/jclouds/http/handlers/RedirectionRetryHandler.java diff --git a/core/src/main/java/org/jclouds/command/FutureCommand.java b/core/src/main/java/org/jclouds/command/FutureCommand.java index 7f28a88121..aaeb4a6314 100644 --- a/core/src/main/java/org/jclouds/command/FutureCommand.java +++ b/core/src/main/java/org/jclouds/command/FutureCommand.java @@ -33,7 +33,7 @@ import static com.google.common.base.Preconditions.checkNotNull; * * @author Adrian Cole */ -public class FutureCommand implements Future { +public class FutureCommand, R, T> implements Future { private final Q request; private final ResponseRunnableFuture responseRunnableFuture; diff --git a/core/src/main/java/org/jclouds/command/FutureCommandClient.java b/core/src/main/java/org/jclouds/command/FutureCommandClient.java index e8911b5d3a..81360ab0af 100644 --- a/core/src/main/java/org/jclouds/command/FutureCommandClient.java +++ b/core/src/main/java/org/jclouds/command/FutureCommandClient.java @@ -28,6 +28,6 @@ package org.jclouds.command; * * @author Adrian Cole */ -public interface FutureCommandClient> { - void submit(O operation); +public interface FutureCommandClient> { + void submit(O operation); } diff --git a/core/src/main/java/org/jclouds/command/Request.java b/core/src/main/java/org/jclouds/command/Request.java new file mode 100644 index 0000000000..895cc0a68c --- /dev/null +++ b/core/src/main/java/org/jclouds/command/Request.java @@ -0,0 +1,35 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.command; + +/** + * A service request must have an endPoint associated with it. + * + * @author Adrian Cole + */ +public interface Request { + E getEndPoint(); + + void setEndPoint(E endPoint); +} diff --git a/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionHandle.java b/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionHandle.java index 491e048f02..6cf84fad7b 100644 --- a/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionHandle.java +++ b/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionHandle.java @@ -32,76 +32,78 @@ import javax.annotation.Resource; import org.jclouds.command.FutureCommand; import org.jclouds.logging.Logger; -import com.google.inject.assistedinject.Assisted; - /** - * // TODO: Adrian: Document this! + * Associates a command with an open connection to a service. * * @author Adrian Cole */ -public abstract class FutureCommandConnectionHandle> { - protected final BlockingQueue available; - protected final Semaphore maxConnections; - protected final Semaphore completed; - protected C conn; - protected O command; - @Resource - protected Logger logger = Logger.NULL; +public abstract class FutureCommandConnectionHandle> { + protected final BlockingQueue available; + protected final Semaphore maxConnections; + protected final Semaphore completed; + protected final E endPoint; + protected C conn; + protected O command; + @Resource + protected Logger logger = Logger.NULL; - public FutureCommandConnectionHandle(Semaphore maxConnections, - @Assisted O command, @Assisted C conn, BlockingQueue available) - throws InterruptedException { - this.maxConnections = maxConnections; - this.command = command; - this.conn = conn; - this.available = available; - this.completed = new Semaphore(1); - completed.acquire(); - } + public FutureCommandConnectionHandle(Semaphore maxConnections, BlockingQueue available, + E endPoint, O command, C conn) throws InterruptedException { + this.available = available; + this.maxConnections = maxConnections; + this.completed = new Semaphore(1); + this.endPoint = endPoint; + this.command = command; + this.conn = conn; + completed.acquire(); + } - public O getCommand() { - return command; - } + public O getCommand() { + return command; + } - public abstract void startConnection(); + public abstract void startConnection(); - public boolean isCompleted() { - return (completed.availablePermits() == 1); - } + public boolean isCompleted() { + return (completed.availablePermits() == 1); + } - public void release() throws InterruptedException { - if (isCompleted()) { - return; - } - logger.trace("%1$s - %2$d - releasing to pool", conn, conn.hashCode()); - available.put(conn); - conn = null; - command = null; - completed.release(); - } + public void release() throws InterruptedException { + if (isCompleted() || alreadyReleased()) { + return; + } + logger.trace("%1$s - %2$d - releasing to pool", conn, conn.hashCode()); + available.put(conn); + conn = null; + command = null; + completed.release(); + } - public void cancel() throws IOException { - if (isCompleted()) { - return; - } - if (conn != null) { - logger.trace("%1$s - %2$d - cancelled; shutting down connection", - conn, conn.hashCode()); - try { - shutdownConnection(); - } finally { - conn = null; - command = null; - maxConnections.release(); - } - } - completed.release(); - } + private boolean alreadyReleased() { + return conn == null; + } - public abstract void shutdownConnection() throws IOException; + public void cancel() throws IOException { + if (isCompleted()) { + return; + } + if (conn != null) { + logger.trace("%1$s - %2$d - cancelled; shutting down connection", conn, conn.hashCode()); + try { + shutdownConnection(); + } finally { + conn = null; + command = null; + maxConnections.release(); + } + } + completed.release(); + } - public void waitFor() throws InterruptedException { - completed.acquire(); - completed.release(); - } + public abstract void shutdownConnection() throws IOException; + + public void waitFor() throws InterruptedException { + completed.acquire(); + completed.release(); + } } diff --git a/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPool.java b/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPool.java index 1a5bf32817..18b9bda458 100644 --- a/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPool.java +++ b/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPool.java @@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.jclouds.command.FutureCommand; import org.jclouds.lifecycle.BaseLifeCycle; -import com.google.inject.Provides; +import com.google.inject.assistedinject.Assisted; import com.google.inject.name.Named; /** @@ -41,130 +41,156 @@ import com.google.inject.name.Named; * * @author Adrian Cole */ -public abstract class FutureCommandConnectionPool> - extends BaseLifeCycle { - protected final Semaphore allConnections; - protected final BlockingQueue available; - protected final BlockingQueue commandQueue; - protected final FutureCommandConnectionHandleFactory futureCommandConnectionHandleFactory; - protected final int maxConnectionReuse; - protected final AtomicInteger currentSessionFailures = new AtomicInteger(0); - protected volatile boolean hitBottom = false; +public abstract class FutureCommandConnectionPool> + extends BaseLifeCycle { - public FutureCommandConnectionPool( - ExecutorService executor, - Semaphore allConnections, - BlockingQueue commandQueue, - FutureCommandConnectionHandleFactory futureCommandConnectionHandleFactory, - @Named("maxConnectionReuse") int maxConnectionReuse, - BlockingQueue available, BaseLifeCycle... dependencies) { - super(executor, dependencies); - this.allConnections = allConnections; - this.commandQueue = commandQueue; - this.futureCommandConnectionHandleFactory = futureCommandConnectionHandleFactory; - this.maxConnectionReuse = maxConnectionReuse; - this.available = available; - } + protected final Semaphore allConnections; + protected final BlockingQueue available; - protected void setResponseException(Exception ex, C conn) { - O command = getHandleFromConnection(conn).getCommand(); - command.getResponseFuture().setException(ex); - } + /** + * inputOnly: nothing is taken from this queue. + */ + protected final BlockingQueue resubmitQueue; + protected final int maxConnectionReuse; + protected final AtomicInteger currentSessionFailures = new AtomicInteger(0); + protected volatile boolean hitBottom = false; + protected final E endPoint; - protected void cancel(C conn) { - O command = getHandleFromConnection(conn).getCommand(); - command.cancel(true); - } + public E getEndPoint() { + return endPoint; + } - @Provides - public C getConnection() throws InterruptedException, TimeoutException { - exceptionIfNotActive(); - if (!hitBottom) { - hitBottom = available.size() == 0 - && allConnections.availablePermits() == 0; - if (hitBottom) - logger.warn("%1$s - saturated connection pool", this); - } - logger - .debug( - "%1$s - attempting to acquire connection; %d currently available", - this, available.size()); - C conn = available.poll(5, TimeUnit.SECONDS); - if (conn == null) - throw new TimeoutException( - "could not obtain a pooled connection within 5 seconds"); + public static interface Factory> { + FutureCommandConnectionPool create(E endPoint); + } - logger.trace("%1$s - %2$d - aquired", conn, conn.hashCode()); - if (connectionValid(conn)) { - logger.debug("%1$s - %2$d - reusing", conn, conn.hashCode()); - return conn; - } else { - logger.debug("%1$s - %2$d - unusable", conn, conn.hashCode()); - shutdownConnection(conn); - allConnections.release(); - return getConnection(); - } - } + public FutureCommandConnectionPool(ExecutorService executor, Semaphore allConnections, + BlockingQueue commandQueue, @Named("maxConnectionReuse") int maxConnectionReuse, + BlockingQueue available, @Assisted E endPoint, BaseLifeCycle... dependencies) { + super(executor, dependencies); + this.allConnections = allConnections; + this.resubmitQueue = commandQueue; + this.maxConnectionReuse = maxConnectionReuse; + this.available = available; + this.endPoint = endPoint; + } - protected void fatalException(Exception ex, C conn) { - setResponseException(ex, conn); - exception.set(ex); - shutdown(); - } + protected void setResponseException(Exception ex, C conn) { + O command = getHandleFromConnection(conn).getCommand(); + command.getResponseFuture().setException(ex); + } - protected abstract void shutdownConnection(C conn); + protected void cancel(C conn) { + O command = getHandleFromConnection(conn).getCommand(); + command.cancel(true); + } - protected abstract boolean connectionValid(C conn); + protected C getConnection() throws InterruptedException, TimeoutException { + exceptionIfNotActive(); + if (!hitBottom) { + hitBottom = available.size() == 0 && allConnections.availablePermits() == 0; + if (hitBottom) + logger.warn("%1$s - saturated connection pool", this); + } + logger.debug("%s - attempting to acquire connection; %s currently available", this, available + .size()); + C conn = available.poll(5, TimeUnit.SECONDS); + if (conn == null) + throw new TimeoutException("could not obtain a pooled connection within 5 seconds"); - public FutureCommandConnectionHandle getHandle(O command) - throws InterruptedException, TimeoutException { - exceptionIfNotActive(); - C conn = getConnection(); - FutureCommandConnectionHandle handle = futureCommandConnectionHandleFactory - .create(command, conn); - associateHandleWithConnection(handle, conn); - return handle; - } + logger.trace("%1$s - %2$d - aquired", conn, conn.hashCode()); + if (connectionValid(conn)) { + logger.debug("%1$s - %2$d - reusing", conn, conn.hashCode()); + return conn; + } else { + logger.debug("%1$s - %2$d - unusable", conn, conn.hashCode()); + shutdownConnection(conn); + allConnections.release(); + return getConnection(); + } + } - protected void resubmitIfRequestIsReplayable(C connection, Exception e) { - O command = getCommandFromConnection(connection); - if (command != null) { - if (isReplayable(command)) { - logger.info("resubmitting command: %1$s", command); - commandQueue.add(command); - } else { - command.setException(e); - } - } - } + protected void fatalException(Exception ex, C conn) { + setResponseException(ex, conn); + exception.set(ex); + shutdown(); + } - protected abstract boolean isReplayable(O command); + protected abstract void shutdownConnection(C conn); - O getCommandFromConnection(C connection) { - FutureCommandConnectionHandle handle = getHandleFromConnection(connection); - if (handle != null && handle.getCommand() != null) { - return handle.getCommand(); - } - return null; - } + protected abstract boolean connectionValid(C conn); - protected void setExceptionOnCommand(C connection, Exception e) { - FutureCommand command = getCommandFromConnection(connection); - if (command != null) { - logger.warn(e, "exception in command: %1$s", command); - command.setException(e); - } - } + public FutureCommandConnectionHandle getHandle(O command) throws InterruptedException, + TimeoutException { + exceptionIfNotActive(); + C conn = getConnection(); + FutureCommandConnectionHandle handle = createHandle(command, conn); + associateHandleWithConnection(handle, conn); + return handle; + } - protected abstract void associateHandleWithConnection( - FutureCommandConnectionHandle handle, C connection); + protected abstract FutureCommandConnectionHandle createHandle(O command, C conn); - protected abstract FutureCommandConnectionHandle getHandleFromConnection( - C connection); + protected void resubmitIfRequestIsReplayable(C connection, Exception e) { + O command = getCommandFromConnection(connection); + if (command != null) { + if (isReplayable(command)) { + logger.info("resubmitting command: %1$s", command); + resubmitQueue.add(command); + } else { + command.setException(e); + } + } + } - protected abstract void createNewConnection() throws InterruptedException; + protected abstract boolean isReplayable(O command); + + O getCommandFromConnection(C connection) { + FutureCommandConnectionHandle handle = getHandleFromConnection(connection); + if (handle != null && handle.getCommand() != null) { + return handle.getCommand(); + } + return null; + } + + protected void setExceptionOnCommand(C connection, Exception e) { + FutureCommand command = getCommandFromConnection(connection); + if (command != null) { + logger.warn(e, "exception in command: %1$s", command); + command.setException(e); + } + } + + protected abstract void associateHandleWithConnection( + FutureCommandConnectionHandle handle, C connection); + + protected abstract FutureCommandConnectionHandle getHandleFromConnection(C connection); + + protected abstract void createNewConnection() throws InterruptedException; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((endPoint == null) ? 0 : endPoint.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FutureCommandConnectionPool other = (FutureCommandConnectionPool) obj; + if (endPoint == null) { + if (other.endPoint != null) + return false; + } else if (!endPoint.equals(other.endPoint)) + return false; + return true; + } - public interface FutureCommandConnectionHandleFactory> { - FutureCommandConnectionHandle create(O command, C conn); - } } diff --git a/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPoolClient.java b/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPoolClient.java index 4891a23478..3dc658c9af 100644 --- a/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPoolClient.java +++ b/core/src/main/java/org/jclouds/command/pool/FutureCommandConnectionPoolClient.java @@ -24,6 +24,7 @@ package org.jclouds.command.pool; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -33,6 +34,8 @@ import org.jclouds.command.FutureCommandClient; import org.jclouds.lifecycle.BaseLifeCycle; import org.jclouds.util.Utils; +import com.google.common.base.Function; +import com.google.common.collect.MapMaker; import com.google.inject.Inject; /** @@ -40,30 +43,30 @@ import com.google.inject.Inject; * * @author Adrian Cole */ -public class FutureCommandConnectionPoolClient> extends +public class FutureCommandConnectionPoolClient> extends BaseLifeCycle implements FutureCommandClient { - private final FutureCommandConnectionPool futureCommandConnectionPool; + + private final ConcurrentMap> poolMap; private final BlockingQueue commandQueue; + private final FutureCommandConnectionPool.Factory poolFactory; @Inject public FutureCommandConnectionPoolClient(ExecutorService executor, - FutureCommandConnectionPool futureCommandConnectionPool, - BlockingQueue commandQueue) { - super(executor, futureCommandConnectionPool); - this.futureCommandConnectionPool = futureCommandConnectionPool; + FutureCommandConnectionPool.Factory pf, BlockingQueue commandQueue) { + super(executor); + this.poolFactory = pf; + // TODO inject this. + poolMap = new MapMaker() + .makeComputingMap(new Function>() { + public FutureCommandConnectionPool apply(E endPoint) { + FutureCommandConnectionPool pool = poolFactory.create(endPoint); + addDependency(pool); + return pool; + } + }); this.commandQueue = commandQueue; } - /** - * {@inheritDoc} - *

- * we continue while the connection pool is active - */ - @Override - protected boolean shouldDoWork() { - return super.shouldDoWork() && futureCommandConnectionPool.getStatus().equals(Status.ACTIVE); - } - /** * {@inheritDoc} * @@ -72,9 +75,9 @@ public class FutureCommandConnectionPoolClient command = (FutureCommand) commandQueue.remove(); + FutureCommand command = (FutureCommand) commandQueue.remove(); if (command != null) { if (exception.get() != null) command.setException(exception.get()); @@ -119,21 +122,29 @@ public class FutureCommandConnectionPoolClient connectionHandle = null; + FutureCommandConnectionPool pool = poolMap.get(command.getRequest().getEndPoint()); + if (pool == null) { + //TODO limit; + logger.warn("pool not available for command %s; retrying", command); + commandQueue.add(command); + return; + } + + FutureCommandConnectionHandle connectionHandle = null; try { - connectionHandle = futureCommandConnectionPool.getHandle(command); + connectionHandle = pool.getHandle(command); } catch (InterruptedException e) { - logger.warn(e, "Interrupted getting a connection for command %1$s; retrying", command); + logger.warn(e, "Interrupted getting a connection for command %s; retrying", command); commandQueue.add(command); return; } catch (TimeoutException e) { - logger.warn(e, "Timeout getting a connection for command %1$s; retrying", command); + logger.warn(e, "Timeout getting a connection for command %s on pool %s; retrying", command, pool); commandQueue.add(command); return; } if (connectionHandle == null) { - logger.error("Failed to obtain connection for command %1$s; retrying", command); + logger.error("Failed to obtain connection for command %s; retrying", command); commandQueue.add(command); return; } @@ -146,7 +157,7 @@ public class FutureCommandConnectionPoolClient - */ - public static final String AUTHORIZATION = "Authorization"; - public static final String HOST = "Host"; - public static final String DATE = "Date"; - public static final String TRANSFER_ENCODING = "Transfer-Encoding"; - public static final String LAST_MODIFIED = "Last-Modified"; - public static final String SERVER = "Server"; - public static final String ETAG = "ETag"; - public static final String RANGE = "Range"; - public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; - public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; - public static final String IF_MATCH = "If-Match"; - public static final String IF_NONE_MATCH = "If-None-Match"; - public static final String CONTENT_RANGE = "Content-Range"; + /** + * Can be used to specify caching behavior along the request/reply chain. Go to + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.9. + */ + public static final String CACHE_CONTROL = "Cache-Control"; + /** + * Specifies presentational information for the object. Go to + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html?sec19.5.1. + */ + public static final String CONTENT_DISPOSITION = "Content-Disposition"; + /** + * Specifies what content encodings have been applied to the object and thus what decoding + * mechanisms must be applied in order to obtain the media-type referenced by the Content-Type + * header field. Go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.11. + */ + public static final String CONTENT_ENCODING = "Content-Encoding"; + /** + * The size of the object, in bytes. This is required. Go to + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.13. + */ + public static final String CONTENT_LENGTH = "Content-Length"; + /** + * A standard MIME type describing the format of the contents. If none is provided, the default + * is binary/octet-stream. Go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.17. + */ + public static final String CONTENT_TYPE = "Content-Type"; + /** + * The base64 encoded 128-bit MD5 digest of the message (without the headers) according to RFC + * 1864. This header can be used as a message integrity check to verify that the data is the same + * data that was originally sent. + */ + public static final String CONTENT_MD5 = "Content-MD5"; + /** + * A user agent that wishes to authenticate itself with a server-- usually, but not necessarily, + * after receiving a 401 response--does so by including an Authorization request-header field + * with the request. The Authorization field value consists of credentials containing the + * authentication information of the user agent for the realm of the resource being requested. + * + * Authorization = "Authorization" ":" credentials + * + * @see + */ + public static final String AUTHORIZATION = "Authorization"; + public static final String HOST = "Host"; + public static final String DATE = "Date"; + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; + public static final String LAST_MODIFIED = "Last-Modified"; + public static final String SERVER = "Server"; + public static final String ETAG = "ETag"; + public static final String RANGE = "Range"; + public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + public static final String IF_MATCH = "If-Match"; + public static final String IF_NONE_MATCH = "If-None-Match"; + public static final String CONTENT_RANGE = "Content-Range"; + public static final String LOCATION = "Location"; } \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/http/HttpRequest.java b/core/src/main/java/org/jclouds/http/HttpRequest.java index 5c93acdf82..7bf3614f42 100644 --- a/core/src/main/java/org/jclouds/http/HttpRequest.java +++ b/core/src/main/java/org/jclouds/http/HttpRequest.java @@ -30,6 +30,7 @@ import java.net.URI; import javax.annotation.Resource; +import org.jclouds.command.Request; import org.jclouds.logging.Logger; import org.jclouds.util.Utils; @@ -38,7 +39,7 @@ import org.jclouds.util.Utils; * * @author Adrian Cole */ -public class HttpRequest extends HttpMessage { +public class HttpRequest extends HttpMessage implements Request { private URI endPoint; private final HttpMethod method; @@ -100,10 +101,16 @@ public class HttpRequest extends HttpMessage { this.payload = content; } + /** + * only the scheme, host, and port of the URI designates the endpoint + */ public void setEndPoint(URI endPoint) { this.endPoint = endPoint; } + /** + * only the scheme, host, and port of the URI designates the endpoint + */ public URI getEndPoint() { return endPoint; } diff --git a/core/src/main/java/org/jclouds/http/HttpRetryHandler.java b/core/src/main/java/org/jclouds/http/HttpRetryHandler.java index 9665492460..9fe9a809ae 100644 --- a/core/src/main/java/org/jclouds/http/HttpRetryHandler.java +++ b/core/src/main/java/org/jclouds/http/HttpRetryHandler.java @@ -24,25 +24,21 @@ package org.jclouds.http; /** - * Indicate whether a request should be retried after a server - * error response (HTTP status code >= 500) based on the request's - * replayable status and the number of attempts already performed. + * Indicate whether a request should be retried after a server error response (HTTP status code >= + * 500) based on the request's replayable status and the number of attempts already performed. * * @author James Murty */ public interface HttpRetryHandler { public static final HttpRetryHandler ALWAYS_RETRY = new HttpRetryHandler() { - public boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) - { + public boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) { return true; - } + } }; /** - * Return true if the command should be retried. This method should only be - * invoked when the response has failed with a HTTP 5xx error indicating a - * server-side error. + * Return true if the command should be retried. This method should only be invoked when the + * response has failed with a HTTP 5xx error indicating a server-side error. */ - boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) - throws InterruptedException; + boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response); } diff --git a/core/src/main/java/org/jclouds/http/annotation/ClientError.java b/core/src/main/java/org/jclouds/http/annotation/ClientError.java new file mode 100644 index 0000000000..928c418983 --- /dev/null +++ b/core/src/main/java/org/jclouds/http/annotation/ClientError.java @@ -0,0 +1,44 @@ +/** + * + * Copyright (C) 2009 Adrian Cole + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.annotation; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +/** + * Implies that the object can address {@link org.jclouds.http.HttpResponse}s + * that contain status code 4xx. + * + * @author Adrian Cole + */ +@BindingAnnotation +@Target( { FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +public @interface ClientError { +} diff --git a/core/src/main/java/org/jclouds/http/annotation/Redirection.java b/core/src/main/java/org/jclouds/http/annotation/Redirection.java new file mode 100644 index 0000000000..8b21721889 --- /dev/null +++ b/core/src/main/java/org/jclouds/http/annotation/Redirection.java @@ -0,0 +1,44 @@ +/** + * + * Copyright (C) 2009 Adrian Cole + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.annotation; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +/** + * Implies that the object can address {@link org.jclouds.http.HttpResponse}s that contain status + * code 3xx. + * + * @author Adrian Cole + */ +@BindingAnnotation +@Target( { FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +public @interface Redirection { +} diff --git a/core/src/main/java/org/jclouds/http/annotation/ServerError.java b/core/src/main/java/org/jclouds/http/annotation/ServerError.java new file mode 100644 index 0000000000..232f952ffe --- /dev/null +++ b/core/src/main/java/org/jclouds/http/annotation/ServerError.java @@ -0,0 +1,44 @@ +/** + * + * Copyright (C) 2009 Adrian Cole + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.annotation; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +/** + * Implies that the object can address {@link org.jclouds.http.HttpResponse}s that contain status + * code 5xx. + * + * @author Adrian Cole + */ +@BindingAnnotation +@Target( { FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +public @interface ServerError { +} diff --git a/core/src/main/java/org/jclouds/http/config/JavaUrlHttpFutureCommandClientModule.java b/core/src/main/java/org/jclouds/http/config/JavaUrlHttpFutureCommandClientModule.java index 23eb0cc828..764980661c 100644 --- a/core/src/main/java/org/jclouds/http/config/JavaUrlHttpFutureCommandClientModule.java +++ b/core/src/main/java/org/jclouds/http/config/JavaUrlHttpFutureCommandClientModule.java @@ -23,17 +23,10 @@ */ package org.jclouds.http.config; -import java.net.MalformedURLException; -import java.net.URI; - -import org.jclouds.http.HttpConstants; import org.jclouds.http.HttpFutureCommandClient; import org.jclouds.http.internal.JavaUrlHttpFutureCommandClient; import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import com.google.inject.Singleton; -import com.google.inject.name.Named; /** * Configures {@link JavaUrlHttpFutureCommandClient}. @@ -53,15 +46,4 @@ public class JavaUrlHttpFutureCommandClientModule extends AbstractModule { bind(HttpFutureCommandClient.class).to(JavaUrlHttpFutureCommandClient.class); } - @Singleton - @Provides - protected URI provideAddress(@Named(HttpConstants.PROPERTY_HTTP_ADDRESS) String address, - @Named(HttpConstants.PROPERTY_HTTP_PORT) int port, - @Named(HttpConstants.PROPERTY_HTTP_SECURE) boolean isSecure) - throws MalformedURLException { - - return URI.create(String.format("%1$s://%2$s:%3$s", isSecure ? "https" : "http", address, - port)); - } - } \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/http/handlers/BackoffLimitedRetryHandler.java b/core/src/main/java/org/jclouds/http/handlers/BackoffLimitedRetryHandler.java index 42a0810e86..060961b9cc 100644 --- a/core/src/main/java/org/jclouds/http/handlers/BackoffLimitedRetryHandler.java +++ b/core/src/main/java/org/jclouds/http/handlers/BackoffLimitedRetryHandler.java @@ -35,30 +35,48 @@ import com.google.inject.Inject; import com.google.inject.name.Named; /** - * Allow replayable request to be retried a limited number of times, and - * impose an exponential back-off delay before returning. + * Allow replayable request to be retried a limited number of times, and impose an exponential + * back-off delay before returning. *

- * The back-off delay grows rapidly according to the formula + * The back-off delay grows rapidly according to the formula * 50 * ({@link HttpFutureCommand#getFailureCount()} ^ 2). For example: * - * - * - * - * - * - * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * *
Number of AttemptsDelay in milliseconds
150
2200
3450
4800
51250
Number of AttemptsDelay in milliseconds
150
2200
3450
4800
51250
*

- * This implementation has two side-effects. It increments the command's failure count - * with {@link HttpFutureCommand#incrementFailureCount()}, because this failure count - * value is used to determine how many times the command has already been tried. It - * also closes the response's content input stream to ensure connections are cleaned up. + * This implementation has two side-effects. It increments the command's failure count with + * {@link HttpFutureCommand#incrementFailureCount()}, because this failure count value is used to + * determine how many times the command has already been tried. It also closes the response's + * content input stream to ensure connections are cleaned up. * * @author James Murty */ public class BackoffLimitedRetryHandler implements HttpRetryHandler { private final int retryCountLimit; - + @Resource protected Logger logger = Logger.NULL; @@ -67,33 +85,34 @@ public class BackoffLimitedRetryHandler implements HttpRetryHandler { this.retryCountLimit = retryCountLimit; } - public boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) - throws InterruptedException - { + public boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) { IOUtils.closeQuietly(response.getContent()); - + command.incrementFailureCount(); if (!command.getRequest().isReplayable()) { logger.warn("Cannot retry after server error, command is not replayable: %1$s", command); return false; } else if (command.getFailureCount() > retryCountLimit) { - logger.warn("Cannot retry after server error, command has exceeded retry limit %1$d: %2$s", - retryCountLimit, command); + logger.warn( + "Cannot retry after server error, command has exceeded retry limit %1$d: %2$s", + retryCountLimit, command); return false; } else { - imposeBackoffExponentialDelay(command.getFailureCount(), command.toString()); - return true; + imposeBackoffExponentialDelay(command.getFailureCount(), command.toString()); + return true; } } - - public void imposeBackoffExponentialDelay(int failureCound, String commandDescription) - throws InterruptedException - { + + public void imposeBackoffExponentialDelay(int failureCound, String commandDescription) { long delayMs = (long) (50L * Math.pow(failureCound, 2)); - logger.debug("Retry %1$d/%2$d after server error, delaying for %3$d ms: %4$s", - failureCound, retryCountLimit, delayMs, commandDescription); - Thread.sleep(delayMs); + logger.debug("Retry %1$d/%2$d after server error, delaying for %3$d ms: %4$s", failureCound, + retryCountLimit, delayMs, commandDescription); + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + logger.error(e, "Interrupted imposing delay"); + } } } diff --git a/core/src/main/java/org/jclouds/http/handlers/CannotRetryHandler.java b/core/src/main/java/org/jclouds/http/handlers/CannotRetryHandler.java new file mode 100644 index 0000000000..8aa57931c5 --- /dev/null +++ b/core/src/main/java/org/jclouds/http/handlers/CannotRetryHandler.java @@ -0,0 +1,41 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.handlers; + +import org.jclouds.http.HttpFutureCommand; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpRetryHandler; + +/** + * Always returns false. + * + * @author Adrian Cole + */ +public class CannotRetryHandler implements HttpRetryHandler { + + public boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) { + return false; + } + +} diff --git a/core/src/main/java/org/jclouds/http/handlers/CloseContentAndSetExceptionHandler.java b/core/src/main/java/org/jclouds/http/handlers/CloseContentAndSetExceptionErrorHandler.java similarity index 91% rename from core/src/main/java/org/jclouds/http/handlers/CloseContentAndSetExceptionHandler.java rename to core/src/main/java/org/jclouds/http/handlers/CloseContentAndSetExceptionErrorHandler.java index 72795750e0..8c20cc03e4 100644 --- a/core/src/main/java/org/jclouds/http/handlers/CloseContentAndSetExceptionHandler.java +++ b/core/src/main/java/org/jclouds/http/handlers/CloseContentAndSetExceptionErrorHandler.java @@ -25,19 +25,19 @@ package org.jclouds.http.handlers; import java.io.IOException; +import org.jclouds.http.HttpErrorHandler; import org.jclouds.http.HttpFutureCommand; import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpResponseException; -import org.jclouds.http.HttpErrorHandler; import org.jclouds.util.Utils; /** * * @author Adrian Cole */ -public class CloseContentAndSetExceptionHandler implements HttpErrorHandler { +public class CloseContentAndSetExceptionErrorHandler implements HttpErrorHandler { - public void handle(HttpFutureCommand command, HttpResponse response) { + public void handleError(HttpFutureCommand command, HttpResponse response) { String content; try { content = response.getContent() != null ? Utils.toStringAndClose(response.getContent()) diff --git a/core/src/main/java/org/jclouds/http/handlers/DelegatingErrorHandler.java b/core/src/main/java/org/jclouds/http/handlers/DelegatingErrorHandler.java new file mode 100644 index 0000000000..e98ab79c8c --- /dev/null +++ b/core/src/main/java/org/jclouds/http/handlers/DelegatingErrorHandler.java @@ -0,0 +1,64 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.handlers; + +import org.jclouds.http.HttpErrorHandler; +import org.jclouds.http.HttpFutureCommand; +import org.jclouds.http.annotation.ClientError; +import org.jclouds.http.annotation.Redirection; +import org.jclouds.http.annotation.ServerError; + +import com.google.inject.Inject; + +/** + * Delegates to {@link HttpErrorHandler HttpErrorHandlers} who are annotated according to the + * response codes they relate to. + * + * @author Adrian Cole + */ +public class DelegatingErrorHandler implements HttpErrorHandler { + + @Redirection + @Inject(optional = true) + private HttpErrorHandler redirectionHandler = new CloseContentAndSetExceptionErrorHandler(); + + @ClientError + @Inject(optional = true) + private HttpErrorHandler clientErrorHandler = new CloseContentAndSetExceptionErrorHandler(); + + @ServerError + @Inject(optional = true) + private HttpErrorHandler serverErrorHandler = new CloseContentAndSetExceptionErrorHandler(); + + public void handleError(HttpFutureCommand command, org.jclouds.http.HttpResponse response) { + int statusCode = response.getStatusCode(); + if (statusCode >= 300 && statusCode < 400) { + redirectionHandler.handleError(command, response); + } else if (statusCode >= 400 && statusCode < 500) { + clientErrorHandler.handleError(command, response); + } else if (statusCode >= 500) { + serverErrorHandler.handleError(command, response); + } + } +} diff --git a/core/src/main/java/org/jclouds/http/handlers/DelegatingRetryHandler.java b/core/src/main/java/org/jclouds/http/handlers/DelegatingRetryHandler.java new file mode 100644 index 0000000000..5d4fe0424b --- /dev/null +++ b/core/src/main/java/org/jclouds/http/handlers/DelegatingRetryHandler.java @@ -0,0 +1,68 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.handlers; + +import org.jclouds.http.HttpFutureCommand; +import org.jclouds.http.HttpRetryHandler; +import org.jclouds.http.annotation.ClientError; +import org.jclouds.http.annotation.Redirection; +import org.jclouds.http.annotation.ServerError; + +import com.google.inject.Inject; + +/** + * Delegates to {@link HttpRetryHandler HttpRetryHandlers} who are annotated according to the + * response codes they relate to. + * + * @author Adrian Cole + */ +public class DelegatingRetryHandler implements HttpRetryHandler { + + @Redirection + @Inject(optional = true) + private HttpRetryHandler redirectionRetryHandler = new RedirectionRetryHandler(5); + + @ClientError + @Inject(optional = true) + private HttpRetryHandler clientErrorRetryHandler = new CannotRetryHandler(); + + @ServerError + @Inject(optional = true) + private HttpRetryHandler serverErrorRetryHandler = new BackoffLimitedRetryHandler(5); + + public boolean shouldRetryRequest(HttpFutureCommand command, + org.jclouds.http.HttpResponse response) { + int statusCode = response.getStatusCode(); + boolean retryRequest = false; + if (statusCode >= 300 && statusCode < 400) { + retryRequest = redirectionRetryHandler.shouldRetryRequest(command, response); + } else if (statusCode >= 400 && statusCode < 500) { + retryRequest = clientErrorRetryHandler.shouldRetryRequest(command, response); + } else if (statusCode >= 500) { + retryRequest = serverErrorRetryHandler.shouldRetryRequest(command, response); + } + return retryRequest; + } + +} diff --git a/core/src/main/java/org/jclouds/http/handlers/RedirectionRetryHandler.java b/core/src/main/java/org/jclouds/http/handlers/RedirectionRetryHandler.java new file mode 100644 index 0000000000..9afc6505b6 --- /dev/null +++ b/core/src/main/java/org/jclouds/http/handlers/RedirectionRetryHandler.java @@ -0,0 +1,70 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +package org.jclouds.http.handlers; + +import java.net.URI; + +import javax.annotation.Resource; + +import org.apache.commons.io.IOUtils; +import org.jclouds.http.HttpFutureCommand; +import org.jclouds.http.HttpHeaders; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpRetryHandler; +import org.jclouds.logging.Logger; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +/** + * Handles Retryable responses with error codes in the 3xx range + * + * @author Adrian Cole + */ +public class RedirectionRetryHandler implements HttpRetryHandler { + private final int retryCountLimit; + + @Resource + protected Logger logger = Logger.NULL; + + @Inject + public RedirectionRetryHandler(@Named("jclouds.http.max-redirects") int retryCountLimit) { + this.retryCountLimit = retryCountLimit; + } + + public boolean shouldRetryRequest(HttpFutureCommand command, HttpResponse response) { + IOUtils.closeQuietly(response.getContent()); + command.incrementRedirectCount(); + + String hostHeader = response.getFirstHeaderOrNull(HttpHeaders.LOCATION); + if (hostHeader != null && command.getRedirectCount() < retryCountLimit) { + URI redirectURI = URI.create(hostHeader); + command.getRequest().setEndPoint(redirectURI); + return true; + } else { + return false; + } + } + +} diff --git a/core/src/main/java/org/jclouds/http/internal/BaseHttpFutureCommandClient.java b/core/src/main/java/org/jclouds/http/internal/BaseHttpFutureCommandClient.java index a0e1a1c904..d46f9bdc6f 100644 --- a/core/src/main/java/org/jclouds/http/internal/BaseHttpFutureCommandClient.java +++ b/core/src/main/java/org/jclouds/http/internal/BaseHttpFutureCommandClient.java @@ -36,8 +36,8 @@ import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpRetryHandler; -import org.jclouds.http.handlers.BackoffLimitedRetryHandler; -import org.jclouds.http.handlers.CloseContentAndSetExceptionHandler; +import org.jclouds.http.handlers.DelegatingErrorHandler; +import org.jclouds.http.handlers.DelegatingRetryHandler; import org.jclouds.logging.Logger; import com.google.inject.Inject; @@ -51,10 +51,10 @@ public abstract class BaseHttpFutureCommandClient implements HttpFutureComman protected List requestFilters = Collections.emptyList(); @Inject(optional = true) - protected HttpErrorHandler httpErrorHandler = new CloseContentAndSetExceptionHandler(); + private HttpRetryHandler retryHandler = new DelegatingRetryHandler(); @Inject(optional = true) - protected HttpRetryHandler httpRetryHandler = new BackoffLimitedRetryHandler(5); + private HttpErrorHandler errorHandler = new DelegatingErrorHandler(); public void submit(HttpFutureCommand command) { HttpRequest request = command.getRequest(); @@ -66,15 +66,22 @@ public abstract class BaseHttpFutureCommandClient implements HttpFutureComman } HttpResponse response = null; for (;;) { - logger.trace("%1$s - converting request %2$s", request.getEndPoint(), request); + logger.trace("%s - converting request %s", request.getEndPoint(), request); nativeRequest = convert(request); response = invoke(nativeRequest); int statusCode = response.getStatusCode(); - if (statusCode >= 500 && httpRetryHandler.shouldRetryRequest(command, response)) - continue; - break; + if (statusCode >= 300) { + if (retryHandler.shouldRetryRequest(command, response)) { + continue; + } else { + errorHandler.handleError(command, response); + break; + } + } else { + processResponse(response, command); + break; + } } - handleResponse(command, response); } catch (Exception e) { command.setException(e); } finally { @@ -88,14 +95,9 @@ public abstract class BaseHttpFutureCommandClient implements HttpFutureComman protected abstract void cleanup(Q nativeResponse); - protected void handleResponse(HttpFutureCommand command, HttpResponse response) { - int code = response.getStatusCode(); - if (code >= 300) { - httpErrorHandler.handle(command, response); - } else { - command.getResponseFuture().setResponse(response); - command.getResponseFuture().run(); - } + protected void processResponse(HttpResponse response, HttpFutureCommand command) { + command.getResponseFuture().setResponse(response); + command.getResponseFuture().run(); } } \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/lifecycle/BaseLifeCycle.java b/core/src/main/java/org/jclouds/lifecycle/BaseLifeCycle.java index ddf17b1baf..f636e2fed1 100644 --- a/core/src/main/java/org/jclouds/lifecycle/BaseLifeCycle.java +++ b/core/src/main/java/org/jclouds/lifecycle/BaseLifeCycle.java @@ -23,6 +23,9 @@ */ package org.jclouds.lifecycle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -41,18 +44,23 @@ public abstract class BaseLifeCycle implements Runnable, LifeCycle { @Resource protected Logger logger = Logger.NULL; protected final ExecutorService executor; - protected final BaseLifeCycle[] dependencies; + protected final List dependencies; protected final Object statusLock; protected volatile Status status; protected AtomicReference exception = new AtomicReference(); - public BaseLifeCycle(ExecutorService executor, BaseLifeCycle... dependencies) { + public BaseLifeCycle(ExecutorService executor, LifeCycle... dependencies) { this.executor = executor; - this.dependencies = dependencies; + this.dependencies = new ArrayList(); + this.dependencies.addAll(Arrays.asList(dependencies)); this.statusLock = new Object(); this.status = Status.INACTIVE; } + public void addDependency(LifeCycle lifeCycle) { + dependencies.add(lifeCycle); + } + public Status getStatus() { return status; } @@ -116,14 +124,23 @@ public abstract class BaseLifeCycle implements Runnable, LifeCycle { } protected void exceptionIfDepedenciesNotActive() { - for (BaseLifeCycle dependency : dependencies) { - if (dependency.status.compareTo(Status.ACTIVE) != 0) { - throw new IllegalStateException(String.format( - "Illegal state: %1$s for component: %2$s", dependency.status, dependency)); + for (LifeCycle dependency : dependencies) { + if (dependency.getStatus().compareTo(Status.ACTIVE) != 0) { + throw new IllegalStateException(String.format("Illegal state: %s for component: %s", + dependency.getStatus(), dependency)); } } } + protected Exception getExceptionFromDependenciesOrNull() { + for (LifeCycle dependency : dependencies) { + if (dependency.getException() != null) { + return dependency.getException(); + } + } + return null; + } + public Exception getException() { return this.exception.get(); } diff --git a/core/src/test/java/org/jclouds/http/BaseHttpFutureCommandClientTest.java b/core/src/test/java/org/jclouds/http/BaseHttpFutureCommandClientTest.java index 889e8aaf1c..632eb29d7a 100644 --- a/core/src/test/java/org/jclouds/http/BaseHttpFutureCommandClientTest.java +++ b/core/src/test/java/org/jclouds/http/BaseHttpFutureCommandClientTest.java @@ -81,10 +81,21 @@ public abstract class BaseHttpFutureCommandClientTest extends BaseJettyTest { assertEquals(get.get(10, TimeUnit.SECONDS).trim(), XML2); } + @Test(invocationCount = 50, timeOut = 3000) + public void testGetStringPermanentRedirect() throws MalformedURLException, ExecutionException, + InterruptedException, TimeoutException { + // GetString get = factory.createGetString("/permanentredirect"); + // assert get != null; + // client.submit(get); + // assertEquals(get.get(10, TimeUnit.SECONDS).trim(), XML2); + // TODO assert misses are only one, as permanent redirects paths should be remembered. + } + @Test(invocationCount = 50, timeOut = 3000) public void testPutRedirect() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { Put put = factory.createPut("/redirect", "foo"); + put.getRequest().getHeaders().put(HttpHeaders.CONTENT_LENGTH, "foo".getBytes().length + ""); assert put != null; client.submit(put); assertEquals(put.get(10, TimeUnit.SECONDS), new Boolean(true)); diff --git a/core/src/test/java/org/jclouds/http/BaseJettyTest.java b/core/src/test/java/org/jclouds/http/BaseJettyTest.java index a6bb0bd249..9674643087 100644 --- a/core/src/test/java/org/jclouds/http/BaseJettyTest.java +++ b/core/src/test/java/org/jclouds/http/BaseJettyTest.java @@ -24,6 +24,7 @@ package org.jclouds.http; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -135,6 +136,7 @@ public abstract class BaseJettyTest { @Override protected void configure() { Names.bindProperties(binder(), properties); + bind(URI.class).toInstance(URI.create("http://localhost:" + testPort)); } }, new JDKLoggingModule(), new HttpCommandsModule(), createClientModule(), new AbstractModule() {