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
This commit is contained in:
adrian.f.cole 2009-06-24 18:25:04 +00:00
parent 39e96d041f
commit 48c3155450
26 changed files with 838 additions and 349 deletions

View File

@ -33,7 +33,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
* *
* @author Adrian Cole * @author Adrian Cole
*/ */
public class FutureCommand<Q, R, T> implements Future<T> { public class FutureCommand<E, Q extends Request<E>, R, T> implements Future<T> {
private final Q request; private final Q request;
private final ResponseRunnableFuture<R, T> responseRunnableFuture; private final ResponseRunnableFuture<R, T> responseRunnableFuture;

View File

@ -28,6 +28,6 @@ package org.jclouds.command;
* *
* @author Adrian Cole * @author Adrian Cole
*/ */
public interface FutureCommandClient<O extends FutureCommand<?, ?, ?>> { public interface FutureCommandClient<O extends FutureCommand<?, ?, ?, ?>> {
void submit(O operation); void submit(O operation);
} }

View File

@ -0,0 +1,35 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.com>
*
* ====================================================================
* 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> {
E getEndPoint();
void setEndPoint(E endPoint);
}

View File

@ -32,76 +32,78 @@ import javax.annotation.Resource;
import org.jclouds.command.FutureCommand; import org.jclouds.command.FutureCommand;
import org.jclouds.logging.Logger; 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 * @author Adrian Cole
*/ */
public abstract class FutureCommandConnectionHandle<C, O extends FutureCommand<?, ?, ?>> { public abstract class FutureCommandConnectionHandle<E, C, O extends FutureCommand<E, ?, ?, ?>> {
protected final BlockingQueue<C> available; protected final BlockingQueue<C> available;
protected final Semaphore maxConnections; protected final Semaphore maxConnections;
protected final Semaphore completed; protected final Semaphore completed;
protected C conn; protected final E endPoint;
protected O command; protected C conn;
@Resource protected O command;
protected Logger logger = Logger.NULL; @Resource
protected Logger logger = Logger.NULL;
public FutureCommandConnectionHandle(Semaphore maxConnections, public FutureCommandConnectionHandle(Semaphore maxConnections, BlockingQueue<C> available,
@Assisted O command, @Assisted C conn, BlockingQueue<C> available) E endPoint, O command, C conn) throws InterruptedException {
throws InterruptedException { this.available = available;
this.maxConnections = maxConnections; this.maxConnections = maxConnections;
this.command = command; this.completed = new Semaphore(1);
this.conn = conn; this.endPoint = endPoint;
this.available = available; this.command = command;
this.completed = new Semaphore(1); this.conn = conn;
completed.acquire(); completed.acquire();
} }
public O getCommand() { public O getCommand() {
return command; return command;
} }
public abstract void startConnection(); public abstract void startConnection();
public boolean isCompleted() { public boolean isCompleted() {
return (completed.availablePermits() == 1); return (completed.availablePermits() == 1);
} }
public void release() throws InterruptedException { public void release() throws InterruptedException {
if (isCompleted()) { if (isCompleted() || alreadyReleased()) {
return; return;
} }
logger.trace("%1$s - %2$d - releasing to pool", conn, conn.hashCode()); logger.trace("%1$s - %2$d - releasing to pool", conn, conn.hashCode());
available.put(conn); available.put(conn);
conn = null; conn = null;
command = null; command = null;
completed.release(); completed.release();
} }
public void cancel() throws IOException { private boolean alreadyReleased() {
if (isCompleted()) { return conn == null;
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 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 { public abstract void shutdownConnection() throws IOException;
completed.acquire();
completed.release(); public void waitFor() throws InterruptedException {
} completed.acquire();
completed.release();
}
} }

View File

@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.jclouds.command.FutureCommand; import org.jclouds.command.FutureCommand;
import org.jclouds.lifecycle.BaseLifeCycle; import org.jclouds.lifecycle.BaseLifeCycle;
import com.google.inject.Provides; import com.google.inject.assistedinject.Assisted;
import com.google.inject.name.Named; import com.google.inject.name.Named;
/** /**
@ -41,130 +41,156 @@ import com.google.inject.name.Named;
* *
* @author Adrian Cole * @author Adrian Cole
*/ */
public abstract class FutureCommandConnectionPool<C, O extends FutureCommand<?, ?, ?>> public abstract class FutureCommandConnectionPool<E, C, O extends FutureCommand<E, ?, ?, ?>>
extends BaseLifeCycle { extends BaseLifeCycle {
protected final Semaphore allConnections;
protected final BlockingQueue<C> available;
protected final BlockingQueue<O> commandQueue;
protected final FutureCommandConnectionHandleFactory<C, O> futureCommandConnectionHandleFactory;
protected final int maxConnectionReuse;
protected final AtomicInteger currentSessionFailures = new AtomicInteger(0);
protected volatile boolean hitBottom = false;
public FutureCommandConnectionPool( protected final Semaphore allConnections;
ExecutorService executor, protected final BlockingQueue<C> available;
Semaphore allConnections,
BlockingQueue<O> commandQueue,
FutureCommandConnectionHandleFactory<C, O> futureCommandConnectionHandleFactory,
@Named("maxConnectionReuse") int maxConnectionReuse,
BlockingQueue<C> available, BaseLifeCycle... dependencies) {
super(executor, dependencies);
this.allConnections = allConnections;
this.commandQueue = commandQueue;
this.futureCommandConnectionHandleFactory = futureCommandConnectionHandleFactory;
this.maxConnectionReuse = maxConnectionReuse;
this.available = available;
}
protected void setResponseException(Exception ex, C conn) { /**
O command = getHandleFromConnection(conn).getCommand(); * inputOnly: nothing is taken from this queue.
command.getResponseFuture().setException(ex); */
} protected final BlockingQueue<O> 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) { public E getEndPoint() {
O command = getHandleFromConnection(conn).getCommand(); return endPoint;
command.cancel(true); }
}
@Provides public static interface Factory<E, C, O extends FutureCommand<E, ?, ?, ?>> {
public C getConnection() throws InterruptedException, TimeoutException { FutureCommandConnectionPool<E, C, O> create(E endPoint);
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");
logger.trace("%1$s - %2$d - aquired", conn, conn.hashCode()); public FutureCommandConnectionPool(ExecutorService executor, Semaphore allConnections,
if (connectionValid(conn)) { BlockingQueue<O> commandQueue, @Named("maxConnectionReuse") int maxConnectionReuse,
logger.debug("%1$s - %2$d - reusing", conn, conn.hashCode()); BlockingQueue<C> available, @Assisted E endPoint, BaseLifeCycle... dependencies) {
return conn; super(executor, dependencies);
} else { this.allConnections = allConnections;
logger.debug("%1$s - %2$d - unusable", conn, conn.hashCode()); this.resubmitQueue = commandQueue;
shutdownConnection(conn); this.maxConnectionReuse = maxConnectionReuse;
allConnections.release(); this.available = available;
return getConnection(); this.endPoint = endPoint;
} }
}
protected void fatalException(Exception ex, C conn) { protected void setResponseException(Exception ex, C conn) {
setResponseException(ex, conn); O command = getHandleFromConnection(conn).getCommand();
exception.set(ex); command.getResponseFuture().setException(ex);
shutdown(); }
}
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<C, O> getHandle(O command) logger.trace("%1$s - %2$d - aquired", conn, conn.hashCode());
throws InterruptedException, TimeoutException { if (connectionValid(conn)) {
exceptionIfNotActive(); logger.debug("%1$s - %2$d - reusing", conn, conn.hashCode());
C conn = getConnection(); return conn;
FutureCommandConnectionHandle<C, O> handle = futureCommandConnectionHandleFactory } else {
.create(command, conn); logger.debug("%1$s - %2$d - unusable", conn, conn.hashCode());
associateHandleWithConnection(handle, conn); shutdownConnection(conn);
return handle; allConnections.release();
} return getConnection();
}
}
protected void resubmitIfRequestIsReplayable(C connection, Exception e) { protected void fatalException(Exception ex, C conn) {
O command = getCommandFromConnection(connection); setResponseException(ex, conn);
if (command != null) { exception.set(ex);
if (isReplayable(command)) { shutdown();
logger.info("resubmitting command: %1$s", command); }
commandQueue.add(command);
} else {
command.setException(e);
}
}
}
protected abstract boolean isReplayable(O command); protected abstract void shutdownConnection(C conn);
O getCommandFromConnection(C connection) { protected abstract boolean connectionValid(C conn);
FutureCommandConnectionHandle<C, O> handle = getHandleFromConnection(connection);
if (handle != null && handle.getCommand() != null) {
return handle.getCommand();
}
return null;
}
protected void setExceptionOnCommand(C connection, Exception e) { public FutureCommandConnectionHandle<E, C, O> getHandle(O command) throws InterruptedException,
FutureCommand<?, ?, ?> command = getCommandFromConnection(connection); TimeoutException {
if (command != null) { exceptionIfNotActive();
logger.warn(e, "exception in command: %1$s", command); C conn = getConnection();
command.setException(e); FutureCommandConnectionHandle<E, C, O> handle = createHandle(command, conn);
} associateHandleWithConnection(handle, conn);
} return handle;
}
protected abstract void associateHandleWithConnection( protected abstract FutureCommandConnectionHandle<E, C, O> createHandle(O command, C conn);
FutureCommandConnectionHandle<C, O> handle, C connection);
protected abstract FutureCommandConnectionHandle<C, O> getHandleFromConnection( protected void resubmitIfRequestIsReplayable(C connection, Exception e) {
C connection); 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<E, C, O> handle = getHandleFromConnection(connection);
if (handle != null && handle.getCommand() != null) {
return handle.getCommand();
}
return null;
}
protected void setExceptionOnCommand(C connection, Exception e) {
FutureCommand<E, ?, ?, ?> command = getCommandFromConnection(connection);
if (command != null) {
logger.warn(e, "exception in command: %1$s", command);
command.setException(e);
}
}
protected abstract void associateHandleWithConnection(
FutureCommandConnectionHandle<E, C, O> handle, C connection);
protected abstract FutureCommandConnectionHandle<E, C, O> 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<C, O extends FutureCommand<?, ?, ?>> {
FutureCommandConnectionHandle<C, O> create(O command, C conn);
}
} }

View File

@ -24,6 +24,7 @@
package org.jclouds.command.pool; package org.jclouds.command.pool;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -33,6 +34,8 @@ import org.jclouds.command.FutureCommandClient;
import org.jclouds.lifecycle.BaseLifeCycle; import org.jclouds.lifecycle.BaseLifeCycle;
import org.jclouds.util.Utils; import org.jclouds.util.Utils;
import com.google.common.base.Function;
import com.google.common.collect.MapMaker;
import com.google.inject.Inject; import com.google.inject.Inject;
/** /**
@ -40,30 +43,30 @@ import com.google.inject.Inject;
* *
* @author Adrian Cole * @author Adrian Cole
*/ */
public class FutureCommandConnectionPoolClient<C, O extends FutureCommand<?, ?, ?>> extends public class FutureCommandConnectionPoolClient<E, C, O extends FutureCommand<E, ?, ?, ?>> extends
BaseLifeCycle implements FutureCommandClient<O> { BaseLifeCycle implements FutureCommandClient<O> {
private final FutureCommandConnectionPool<C, O> futureCommandConnectionPool;
private final ConcurrentMap<E, FutureCommandConnectionPool<E, C, O>> poolMap;
private final BlockingQueue<O> commandQueue; private final BlockingQueue<O> commandQueue;
private final FutureCommandConnectionPool.Factory<E, C, O> poolFactory;
@Inject @Inject
public FutureCommandConnectionPoolClient(ExecutorService executor, public FutureCommandConnectionPoolClient(ExecutorService executor,
FutureCommandConnectionPool<C, O> futureCommandConnectionPool, FutureCommandConnectionPool.Factory<E, C, O> pf, BlockingQueue<O> commandQueue) {
BlockingQueue<O> commandQueue) { super(executor);
super(executor, futureCommandConnectionPool); this.poolFactory = pf;
this.futureCommandConnectionPool = futureCommandConnectionPool; // TODO inject this.
poolMap = new MapMaker()
.makeComputingMap(new Function<E, FutureCommandConnectionPool<E, C, O>>() {
public FutureCommandConnectionPool<E, C, O> apply(E endPoint) {
FutureCommandConnectionPool<E, C, O> pool = poolFactory.create(endPoint);
addDependency(pool);
return pool;
}
});
this.commandQueue = commandQueue; this.commandQueue = commandQueue;
} }
/**
* {@inheritDoc}
* <p/>
* we continue while the connection pool is active
*/
@Override
protected boolean shouldDoWork() {
return super.shouldDoWork() && futureCommandConnectionPool.getStatus().equals(Status.ACTIVE);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
@ -72,9 +75,9 @@ public class FutureCommandConnectionPoolClient<C, O extends FutureCommand<?, ?,
*/ */
@Override @Override
protected void doShutdown() { protected void doShutdown() {
exception.compareAndSet(null, futureCommandConnectionPool.getException()); exception.compareAndSet(null, getExceptionFromDependenciesOrNull());
while (!commandQueue.isEmpty()) { while (!commandQueue.isEmpty()) {
FutureCommand<?, ?, ?> command = (FutureCommand<?, ?, ?>) commandQueue.remove(); FutureCommand<E, ?, ?, ?> command = (FutureCommand<E, ?, ?, ?>) commandQueue.remove();
if (command != null) { if (command != null) {
if (exception.get() != null) if (exception.get() != null)
command.setException(exception.get()); command.setException(exception.get());
@ -119,21 +122,29 @@ public class FutureCommandConnectionPoolClient<C, O extends FutureCommand<?, ?,
*/ */
protected void invoke(O command) { protected void invoke(O command) {
exceptionIfNotActive(); exceptionIfNotActive();
FutureCommandConnectionHandle<C, O> connectionHandle = null; FutureCommandConnectionPool<E, C, O> 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<E, C, O> connectionHandle = null;
try { try {
connectionHandle = futureCommandConnectionPool.getHandle(command); connectionHandle = pool.getHandle(command);
} catch (InterruptedException e) { } 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); commandQueue.add(command);
return; return;
} catch (TimeoutException e) { } 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); commandQueue.add(command);
return; return;
} }
if (connectionHandle == null) { 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); commandQueue.add(command);
return; return;
} }
@ -146,7 +157,7 @@ public class FutureCommandConnectionPoolClient<C, O extends FutureCommand<?, ?,
sb.append("FutureCommandConnectionPoolClient"); sb.append("FutureCommandConnectionPoolClient");
sb.append("{status=").append(status); sb.append("{status=").append(status);
sb.append(", commandQueue=").append((commandQueue != null) ? commandQueue.size() : 0); sb.append(", commandQueue=").append((commandQueue != null) ? commandQueue.size() : 0);
sb.append(", futureCommandConnectionPool=").append(futureCommandConnectionPool); sb.append(", poolMap=").append(poolMap);
sb.append('}'); sb.append('}');
return sb.toString(); return sb.toString();
} }

View File

@ -32,7 +32,6 @@ import org.jclouds.lifecycle.config.LifeCycleModule;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.name.Named; import com.google.inject.name.Named;
/** /**
@ -47,7 +46,7 @@ public abstract class FutureCommandConnectionPoolClientModule<C> extends
} }
@Provides @Provides
@Singleton // @Singleton per uri...
public abstract BlockingQueue<C> provideAvailablePool( public abstract BlockingQueue<C> provideAvailablePool(
@Named(PoolConstants.PROPERTY_POOL_MAX_CONNECTIONS) int max) @Named(PoolConstants.PROPERTY_POOL_MAX_CONNECTIONS) int max)
throws Exception; throws Exception;
@ -62,7 +61,7 @@ public abstract class FutureCommandConnectionPoolClientModule<C> extends
* @throws Exception * @throws Exception
*/ */
@Provides @Provides
@Singleton // @Singleton per uri...
public Semaphore provideTotalConnectionSemaphore( public Semaphore provideTotalConnectionSemaphore(
@Named(PoolConstants.PROPERTY_POOL_MAX_CONNECTIONS) int max) @Named(PoolConstants.PROPERTY_POOL_MAX_CONNECTIONS) int max)
throws Exception { throws Exception {

View File

@ -30,9 +30,9 @@ package org.jclouds.http;
*/ */
public interface HttpErrorHandler { public interface HttpErrorHandler {
public static final HttpErrorHandler NOOP = new HttpErrorHandler() { public static final HttpErrorHandler NOOP = new HttpErrorHandler() {
public void handle(HttpFutureCommand<?> command, HttpResponse response) { public void handleError(HttpFutureCommand<?> command, HttpResponse response) {
} }
}; };
void handle(HttpFutureCommand<?> command, HttpResponse response); void handleError(HttpFutureCommand<?> command, HttpResponse response);
} }

View File

@ -39,7 +39,16 @@ import org.jclouds.logging.Logger;
* *
* @author Adrian Cole * @author Adrian Cole
*/ */
public class HttpFutureCommand<T> extends FutureCommand<HttpRequest, HttpResponse, T> { public class HttpFutureCommand<T> extends FutureCommand<URI, HttpRequest, HttpResponse, T> {
private volatile int redirectCount;
public int incrementRedirectCount() {
return ++redirectCount;
}
public int getRedirectCount() {
return redirectCount;
}
public HttpFutureCommand(URI endPoint, HttpMethod method, String uri, public HttpFutureCommand(URI endPoint, HttpMethod method, String uri,
ResponseCallable<T> responseCallable) { ResponseCallable<T> responseCallable) {

View File

@ -25,65 +25,61 @@ package org.jclouds.http;
public interface HttpHeaders { public interface HttpHeaders {
/** /**
* Can be used to specify caching behavior along the request/reply chain. Go * Can be used to specify caching behavior along the request/reply chain. Go to
* to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.9. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.9.
*/ */
public static final String CACHE_CONTROL = "Cache-Control"; public static final String CACHE_CONTROL = "Cache-Control";
/** /**
* Specifies presentational information for the object. Go to * Specifies presentational information for the object. Go to
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html?sec19.5.1. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html?sec19.5.1.
*/ */
public static final String CONTENT_DISPOSITION = "Content-Disposition"; public static final String CONTENT_DISPOSITION = "Content-Disposition";
/** /**
* Specifies what content encodings have been applied to the object and thus * Specifies what content encodings have been applied to the object and thus what decoding
* what decoding mechanisms must be applied in order to obtain the * mechanisms must be applied in order to obtain the media-type referenced by the Content-Type
* media-type referenced by the Content-Type header field. Go to * header field. Go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.11.
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.11. */
*/ public static final String CONTENT_ENCODING = "Content-Encoding";
public static final String CONTENT_ENCODING = "Content-Encoding"; /**
/** * The size of the object, in bytes. This is required. Go to
* The size of the object, in bytes. This is required. Go to * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.13.
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.13. */
*/ public static final String CONTENT_LENGTH = "Content-Length";
public static final String CONTENT_LENGTH = "Content-Length"; /**
/** * A standard MIME type describing the format of the contents. If none is provided, the default
* A standard MIME type describing the format of the contents. If none is * is binary/octet-stream. Go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html?sec14.17.
* 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";
*/ /**
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
* The base64 encoded 128-bit MD5 digest of the message (without the * data that was originally sent.
* 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 public static final String CONTENT_MD5 = "Content-MD5";
* originally sent. /**
*/ * A user agent that wishes to authenticate itself with a server-- usually, but not necessarily,
public static final String CONTENT_MD5 = "Content-MD5"; * 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
* A user agent that wishes to authenticate itself with a server-- usually, * authentication information of the user agent for the realm of the resource being requested.
* but not necessarily, after receiving a 401 response--does so by including *
* an Authorization request-header field with the request. The Authorization * Authorization = "Authorization" ":" credentials
* field value consists of credentials containing the authentication *
* information of the user agent for the realm of the resource being * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html" />
* requested. */
* public static final String AUTHORIZATION = "Authorization";
* Authorization = "Authorization" ":" credentials public static final String HOST = "Host";
* public static final String DATE = "Date";
* @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html" /> public static final String TRANSFER_ENCODING = "Transfer-Encoding";
*/ public static final String LAST_MODIFIED = "Last-Modified";
public static final String AUTHORIZATION = "Authorization"; public static final String SERVER = "Server";
public static final String HOST = "Host"; public static final String ETAG = "ETag";
public static final String DATE = "Date"; public static final String RANGE = "Range";
public static final String TRANSFER_ENCODING = "Transfer-Encoding"; public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
public static final String LAST_MODIFIED = "Last-Modified"; public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
public static final String SERVER = "Server"; public static final String IF_MATCH = "If-Match";
public static final String ETAG = "ETag"; public static final String IF_NONE_MATCH = "If-None-Match";
public static final String RANGE = "Range"; public static final String CONTENT_RANGE = "Content-Range";
public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; public static final String LOCATION = "Location";
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";
} }

View File

@ -30,6 +30,7 @@ import java.net.URI;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.jclouds.command.Request;
import org.jclouds.logging.Logger; import org.jclouds.logging.Logger;
import org.jclouds.util.Utils; import org.jclouds.util.Utils;
@ -38,7 +39,7 @@ import org.jclouds.util.Utils;
* *
* @author Adrian Cole * @author Adrian Cole
*/ */
public class HttpRequest extends HttpMessage { public class HttpRequest extends HttpMessage implements Request<URI> {
private URI endPoint; private URI endPoint;
private final HttpMethod method; private final HttpMethod method;
@ -100,10 +101,16 @@ public class HttpRequest extends HttpMessage {
this.payload = content; this.payload = content;
} }
/**
* only the scheme, host, and port of the URI designates the endpoint
*/
public void setEndPoint(URI endPoint) { public void setEndPoint(URI endPoint) {
this.endPoint = endPoint; this.endPoint = endPoint;
} }
/**
* only the scheme, host, and port of the URI designates the endpoint
*/
public URI getEndPoint() { public URI getEndPoint() {
return endPoint; return endPoint;
} }

View File

@ -24,25 +24,21 @@
package org.jclouds.http; package org.jclouds.http;
/** /**
* Indicate whether a request should be retried after a server * Indicate whether a request should be retried after a server error response (HTTP status code >=
* error response (HTTP status code >= 500) based on the request's * 500) based on the request's replayable status and the number of attempts already performed.
* replayable status and the number of attempts already performed.
* *
* @author James Murty * @author James Murty
*/ */
public interface HttpRetryHandler { public interface HttpRetryHandler {
public static final HttpRetryHandler ALWAYS_RETRY = new 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;
} }
}; };
/** /**
* Return true if the command should be retried. This method should only be * Return true if the command should be retried. This method should only be invoked when the
* invoked when the response has failed with a HTTP 5xx error indicating a * response has failed with a HTTP 5xx error indicating a server-side error.
* server-side error.
*/ */
boolean shouldRetryRequest(HttpFutureCommand<?> command, HttpResponse response) boolean shouldRetryRequest(HttpFutureCommand<?> command, HttpResponse response);
throws InterruptedException;
} }

View File

@ -0,0 +1,44 @@
/**
*
* Copyright (C) 2009 Adrian Cole <adrian@jclouds.org>
*
* ====================================================================
* 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 {
}

View File

@ -0,0 +1,44 @@
/**
*
* Copyright (C) 2009 Adrian Cole <adrian@jclouds.org>
*
* ====================================================================
* 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 {
}

View File

@ -0,0 +1,44 @@
/**
*
* Copyright (C) 2009 Adrian Cole <adrian@jclouds.org>
*
* ====================================================================
* 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 {
}

View File

@ -23,17 +23,10 @@
*/ */
package org.jclouds.http.config; 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.HttpFutureCommandClient;
import org.jclouds.http.internal.JavaUrlHttpFutureCommandClient; import org.jclouds.http.internal.JavaUrlHttpFutureCommandClient;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
/** /**
* Configures {@link JavaUrlHttpFutureCommandClient}. * Configures {@link JavaUrlHttpFutureCommandClient}.
@ -53,15 +46,4 @@ public class JavaUrlHttpFutureCommandClientModule extends AbstractModule {
bind(HttpFutureCommandClient.class).to(JavaUrlHttpFutureCommandClient.class); 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));
}
} }

View File

@ -35,24 +35,42 @@ import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
/** /**
* Allow replayable request to be retried a limited number of times, and * Allow replayable request to be retried a limited number of times, and impose an exponential
* impose an exponential back-off delay before returning. * back-off delay before returning.
* <p> * <p>
* The back-off delay grows rapidly according to the formula * The back-off delay grows rapidly according to the formula
* <code>50 * (<i>{@link HttpFutureCommand#getFailureCount()}</i> ^ 2)</code>. For example: * <code>50 * (<i>{@link HttpFutureCommand#getFailureCount()}</i> ^ 2)</code>. For example:
* <table> * <table>
* <tr><th>Number of Attempts</th><th>Delay in milliseconds</th></tr> * <tr>
* <tr><td>1</td><td>50</td></tr> * <th>Number of Attempts</th>
* <tr><td>2</td><td>200</td></tr> * <th>Delay in milliseconds</th>
* <tr><td>3</td><td>450</td></tr> * </tr>
* <tr><td>4</td><td>800</td></tr> * <tr>
* <tr><td>5</td><td>1250</td></tr> * <td>1</td>
* <td>50</td>
* </tr>
* <tr>
* <td>2</td>
* <td>200</td>
* </tr>
* <tr>
* <td>3</td>
* <td>450</td>
* </tr>
* <tr>
* <td>4</td>
* <td>800</td>
* </tr>
* <tr>
* <td>5</td>
* <td>1250</td>
* </tr>
* </table> * </table>
* <p> * <p>
* This implementation has two side-effects. It increments the command's failure count * This implementation has two side-effects. It increments the command's failure count with
* with {@link HttpFutureCommand#incrementFailureCount()}, because this failure count * {@link HttpFutureCommand#incrementFailureCount()}, because this failure count value is used to
* value is used to determine how many times the command has already been tried. It * determine how many times the command has already been tried. It also closes the response's
* also closes the response's content input stream to ensure connections are cleaned up. * content input stream to ensure connections are cleaned up.
* *
* @author James Murty * @author James Murty
*/ */
@ -67,9 +85,7 @@ public class BackoffLimitedRetryHandler implements HttpRetryHandler {
this.retryCountLimit = retryCountLimit; this.retryCountLimit = retryCountLimit;
} }
public boolean shouldRetryRequest(HttpFutureCommand<?> command, HttpResponse response) public boolean shouldRetryRequest(HttpFutureCommand<?> command, HttpResponse response) {
throws InterruptedException
{
IOUtils.closeQuietly(response.getContent()); IOUtils.closeQuietly(response.getContent());
command.incrementFailureCount(); command.incrementFailureCount();
@ -78,22 +94,25 @@ public class BackoffLimitedRetryHandler implements HttpRetryHandler {
logger.warn("Cannot retry after server error, command is not replayable: %1$s", command); logger.warn("Cannot retry after server error, command is not replayable: %1$s", command);
return false; return false;
} else if (command.getFailureCount() > retryCountLimit) { } else if (command.getFailureCount() > retryCountLimit) {
logger.warn("Cannot retry after server error, command has exceeded retry limit %1$d: %2$s", logger.warn(
retryCountLimit, command); "Cannot retry after server error, command has exceeded retry limit %1$d: %2$s",
retryCountLimit, command);
return false; return false;
} else { } else {
imposeBackoffExponentialDelay(command.getFailureCount(), command.toString()); imposeBackoffExponentialDelay(command.getFailureCount(), command.toString());
return true; return true;
} }
} }
public void imposeBackoffExponentialDelay(int failureCound, String commandDescription) public void imposeBackoffExponentialDelay(int failureCound, String commandDescription) {
throws InterruptedException
{
long delayMs = (long) (50L * Math.pow(failureCound, 2)); 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", logger.debug("Retry %1$d/%2$d after server error, delaying for %3$d ms: %4$s", failureCound,
failureCound, retryCountLimit, delayMs, commandDescription); retryCountLimit, delayMs, commandDescription);
Thread.sleep(delayMs); try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
logger.error(e, "Interrupted imposing delay");
}
} }
} }

View File

@ -0,0 +1,41 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.com>
*
* ====================================================================
* 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;
}
}

View File

@ -25,19 +25,19 @@ package org.jclouds.http.handlers;
import java.io.IOException; import java.io.IOException;
import org.jclouds.http.HttpErrorHandler;
import org.jclouds.http.HttpFutureCommand; import org.jclouds.http.HttpFutureCommand;
import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpResponseException; import org.jclouds.http.HttpResponseException;
import org.jclouds.http.HttpErrorHandler;
import org.jclouds.util.Utils; import org.jclouds.util.Utils;
/** /**
* *
* @author Adrian Cole * @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; String content;
try { try {
content = response.getContent() != null ? Utils.toStringAndClose(response.getContent()) content = response.getContent() != null ? Utils.toStringAndClose(response.getContent())

View File

@ -0,0 +1,64 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.com>
*
* ====================================================================
* 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);
}
}
}

View File

@ -0,0 +1,68 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.com>
*
* ====================================================================
* 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;
}
}

View File

@ -0,0 +1,70 @@
/**
*
* Copyright (C) 2009 Global Cloud Specialists, Inc. <info@globalcloudspecialists.com>
*
* ====================================================================
* 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;
}
}
}

View File

@ -36,8 +36,8 @@ import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpRetryHandler; import org.jclouds.http.HttpRetryHandler;
import org.jclouds.http.handlers.BackoffLimitedRetryHandler; import org.jclouds.http.handlers.DelegatingErrorHandler;
import org.jclouds.http.handlers.CloseContentAndSetExceptionHandler; import org.jclouds.http.handlers.DelegatingRetryHandler;
import org.jclouds.logging.Logger; import org.jclouds.logging.Logger;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -51,10 +51,10 @@ public abstract class BaseHttpFutureCommandClient<Q> implements HttpFutureComman
protected List<HttpRequestFilter> requestFilters = Collections.emptyList(); protected List<HttpRequestFilter> requestFilters = Collections.emptyList();
@Inject(optional = true) @Inject(optional = true)
protected HttpErrorHandler httpErrorHandler = new CloseContentAndSetExceptionHandler(); private HttpRetryHandler retryHandler = new DelegatingRetryHandler();
@Inject(optional = true) @Inject(optional = true)
protected HttpRetryHandler httpRetryHandler = new BackoffLimitedRetryHandler(5); private HttpErrorHandler errorHandler = new DelegatingErrorHandler();
public void submit(HttpFutureCommand<?> command) { public void submit(HttpFutureCommand<?> command) {
HttpRequest request = command.getRequest(); HttpRequest request = command.getRequest();
@ -66,15 +66,22 @@ public abstract class BaseHttpFutureCommandClient<Q> implements HttpFutureComman
} }
HttpResponse response = null; HttpResponse response = null;
for (;;) { for (;;) {
logger.trace("%1$s - converting request %2$s", request.getEndPoint(), request); logger.trace("%s - converting request %s", request.getEndPoint(), request);
nativeRequest = convert(request); nativeRequest = convert(request);
response = invoke(nativeRequest); response = invoke(nativeRequest);
int statusCode = response.getStatusCode(); int statusCode = response.getStatusCode();
if (statusCode >= 500 && httpRetryHandler.shouldRetryRequest(command, response)) if (statusCode >= 300) {
continue; if (retryHandler.shouldRetryRequest(command, response)) {
break; continue;
} else {
errorHandler.handleError(command, response);
break;
}
} else {
processResponse(response, command);
break;
}
} }
handleResponse(command, response);
} catch (Exception e) { } catch (Exception e) {
command.setException(e); command.setException(e);
} finally { } finally {
@ -88,14 +95,9 @@ public abstract class BaseHttpFutureCommandClient<Q> implements HttpFutureComman
protected abstract void cleanup(Q nativeResponse); protected abstract void cleanup(Q nativeResponse);
protected void handleResponse(HttpFutureCommand<?> command, HttpResponse response) { protected void processResponse(HttpResponse response, HttpFutureCommand<?> command) {
int code = response.getStatusCode(); command.getResponseFuture().setResponse(response);
if (code >= 300) { command.getResponseFuture().run();
httpErrorHandler.handle(command, response);
} else {
command.getResponseFuture().setResponse(response);
command.getResponseFuture().run();
}
} }
} }

View File

@ -23,6 +23,9 @@
*/ */
package org.jclouds.lifecycle; 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.ExecutorService;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -41,18 +44,23 @@ public abstract class BaseLifeCycle implements Runnable, LifeCycle {
@Resource @Resource
protected Logger logger = Logger.NULL; protected Logger logger = Logger.NULL;
protected final ExecutorService executor; protected final ExecutorService executor;
protected final BaseLifeCycle[] dependencies; protected final List<LifeCycle> dependencies;
protected final Object statusLock; protected final Object statusLock;
protected volatile Status status; protected volatile Status status;
protected AtomicReference<Exception> exception = new AtomicReference<Exception>(); protected AtomicReference<Exception> exception = new AtomicReference<Exception>();
public BaseLifeCycle(ExecutorService executor, BaseLifeCycle... dependencies) { public BaseLifeCycle(ExecutorService executor, LifeCycle... dependencies) {
this.executor = executor; this.executor = executor;
this.dependencies = dependencies; this.dependencies = new ArrayList<LifeCycle>();
this.dependencies.addAll(Arrays.asList(dependencies));
this.statusLock = new Object(); this.statusLock = new Object();
this.status = Status.INACTIVE; this.status = Status.INACTIVE;
} }
public void addDependency(LifeCycle lifeCycle) {
dependencies.add(lifeCycle);
}
public Status getStatus() { public Status getStatus() {
return status; return status;
} }
@ -116,14 +124,23 @@ public abstract class BaseLifeCycle implements Runnable, LifeCycle {
} }
protected void exceptionIfDepedenciesNotActive() { protected void exceptionIfDepedenciesNotActive() {
for (BaseLifeCycle dependency : dependencies) { for (LifeCycle dependency : dependencies) {
if (dependency.status.compareTo(Status.ACTIVE) != 0) { if (dependency.getStatus().compareTo(Status.ACTIVE) != 0) {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format("Illegal state: %s for component: %s",
"Illegal state: %1$s for component: %2$s", dependency.status, dependency)); dependency.getStatus(), dependency));
} }
} }
} }
protected Exception getExceptionFromDependenciesOrNull() {
for (LifeCycle dependency : dependencies) {
if (dependency.getException() != null) {
return dependency.getException();
}
}
return null;
}
public Exception getException() { public Exception getException() {
return this.exception.get(); return this.exception.get();
} }

View File

@ -81,10 +81,21 @@ public abstract class BaseHttpFutureCommandClientTest extends BaseJettyTest {
assertEquals(get.get(10, TimeUnit.SECONDS).trim(), XML2); 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) @Test(invocationCount = 50, timeOut = 3000)
public void testPutRedirect() throws MalformedURLException, ExecutionException, public void testPutRedirect() throws MalformedURLException, ExecutionException,
InterruptedException, TimeoutException { InterruptedException, TimeoutException {
Put put = factory.createPut("/redirect", "foo"); Put put = factory.createPut("/redirect", "foo");
put.getRequest().getHeaders().put(HttpHeaders.CONTENT_LENGTH, "foo".getBytes().length + "");
assert put != null; assert put != null;
client.submit(put); client.submit(put);
assertEquals(put.get(10, TimeUnit.SECONDS), new Boolean(true)); assertEquals(put.get(10, TimeUnit.SECONDS), new Boolean(true));

View File

@ -24,6 +24,7 @@
package org.jclouds.http; package org.jclouds.http;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
@ -135,6 +136,7 @@ public abstract class BaseJettyTest {
@Override @Override
protected void configure() { protected void configure() {
Names.bindProperties(binder(), properties); Names.bindProperties(binder(), properties);
bind(URI.class).toInstance(URI.create("http://localhost:" + testPort));
} }
}, new JDKLoggingModule(), new HttpCommandsModule(), createClientModule(), }, new JDKLoggingModule(), new HttpCommandsModule(), createClientModule(),
new AbstractModule() { new AbstractModule() {