/**
*
* Copyright (C) 2009 Cloud Conscious, LLC. <info@cloudconscious.com>
*
* ====================================================================
* Licensed 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.httpnio.pool;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.UnmappableCharacterException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
import javax.inject.Named;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpException;
import org.apache.http.impl.nio.DefaultClientIOEventDispatch;
import org.apache.http.impl.nio.SSLClientIOEventDispatch;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.nio.NHttpConnection;
import org.apache.http.nio.protocol.AsyncNHttpClientHandler;
import org.apache.http.nio.protocol.EventListener;
import org.apache.http.nio.reactor.IOEventDispatch;
import org.apache.http.nio.reactor.IOReactorStatus;
import org.apache.http.nio.reactor.SessionRequest;
import org.apache.http.nio.reactor.SessionRequestCallback;
import org.apache.http.params.HttpParams;
import org.jclouds.Constants;
import org.jclouds.http.HttpCommandRendezvous;
import org.jclouds.http.TransformingHttpCommand;
import org.jclouds.http.pool.HttpCommandConnectionHandle;
import org.jclouds.http.pool.HttpCommandConnectionPool;
import com.google.common.annotations.VisibleForTesting;
/**
* Connection Pool for HTTP requests that utilizes Apache HTTPNio
*
* @author Adrian Cole
*/
public class NioHttpCommandConnectionPool extends HttpCommandConnectionPool<NHttpConnection>
implements EventListener {
@Override
public String toString() {
return "NioHttpCommandConnectionPool [ target=" + target + ", endPoint=" + getEndPoint()
+ ", hashCode=" + hashCode() + " ]";
}
private final NHttpClientConnectionPoolSessionRequestCallback sessionCallback;
private final DefaultConnectingIOReactor ioReactor;
private final IOEventDispatch dispatch;
private final InetSocketAddress target;
public static interface Factory extends HttpCommandConnectionPool.Factory<NHttpConnection> {
NioHttpCommandConnectionPool create(URI endPoint);
}
@Inject
public NioHttpCommandConnectionPool(ExecutorService executor, Semaphore allConnections,
BlockingQueue<HttpCommandRendezvous<?>> commandQueue,
BlockingQueue<NHttpConnection> available, AsyncNHttpClientHandler clientHandler,
DefaultConnectingIOReactor ioReactor, HttpParams params, URI endPoint,
@Named(Constants.PROPERTY_MAX_CONNECTION_REUSE) int maxConnectionReuse,
@Named(Constants.PROPERTY_MAX_SESSION_FAILURES) int maxSessionFailures) {
super(executor, allConnections, commandQueue, available, endPoint, maxConnectionReuse,
maxSessionFailures);
String host = checkNotNull(checkNotNull(endPoint, "endPoint").getHost(), String.format(
"Host null for endpoint %s", endPoint));
int port = endPoint.getPort();
if (endPoint.getScheme().equals("https")) {
try {
this.dispatch = provideSSLClientEventDispatch(clientHandler, params);
} catch (KeyManagementException e) {
throw new RuntimeException("SSL error creating a connection to " + endPoint, e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SSL error creating a connection to " + endPoint, e);
}
if (port == -1)
port = 443;
} else {
this.dispatch = provideClientEventDispatch(clientHandler, params);
if (port == -1)
port = 80;
}
checkArgument(port > 0, String.format("Port %d not in range for endpoint %s", endPoint
.getPort(), endPoint));
this.ioReactor = ioReactor;
this.sessionCallback = new NHttpClientConnectionPoolSessionRequestCallback();
this.target = new InetSocketAddress(host, port);
clientHandler.setEventListener(this);
}
public static IOEventDispatch provideSSLClientEventDispatch(AsyncNHttpClientHandler handler,
HttpParams params) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, null, null);
return new SSLClientIOEventDispatch(handler, context, params);
}
public static IOEventDispatch provideClientEventDispatch(AsyncNHttpClientHandler handler,
HttpParams params) {
return new DefaultClientIOEventDispatch(handler, params);
}
@Override
public void start() {
synchronized (this.statusLock) {
if (this.status.compareTo(Status.INACTIVE) == 0) {
executorService.execute(new Runnable() {
public void run() {
try {
ioReactor.execute(dispatch);
} catch (IOException e) {
exception.set(e);
logger.error(e, "Error dispatching %1$s", dispatch);
status = Status.SHUTDOWN_REQUEST;
}
}
});
}
super.start();
}
}
public void shutdownReactor(long waitMs) {
try {
this.ioReactor.shutdown(waitMs);
} catch (IOException e) {
logger.error(e, "Error shutting down reactor");
}
}
@Override
public boolean connectionValid(NHttpConnection conn) {
boolean isOpen = conn.isOpen();
boolean isStale = conn.isStale();
long requestCount = conn.getMetrics().getRequestCount();
return isOpen && !isStale && requestCount < maxConnectionReuse;
}
@Override
public void shutdownConnection(NHttpConnection conn) {
if (conn.getStatus() == NHttpConnection.ACTIVE) {
try {
conn.shutdown();
} catch (IOException e) {
logger.error(e, "Error shutting down connection");
}
}
}
@Override
protected void doWork() throws Exception {
createNewConnection();
}
@Override
protected void doShutdown() {
// Give the I/O reactor 1 sec to shut down
shutdownReactor(1000);
assert this.ioReactor.getStatus().equals(IOReactorStatus.SHUT_DOWN) : "incorrect status after io reactor shutdown :"
+ this.ioReactor.getStatus();
}
@Override
protected void createNewConnection() throws InterruptedException {
boolean acquired = allConnections.tryAcquire(1, TimeUnit.SECONDS);
if (acquired) {
if (shouldDoWork()) {
logger.trace("Opening: %s", getTarget());
ioReactor.connect(getTarget(), null, null, sessionCallback);
} else {
allConnections.release();
}
}
}
@Override
protected void associateHandleWithConnection(
HttpCommandConnectionHandle<NHttpConnection> handle, NHttpConnection connection) {
connection.getContext().setAttribute("command-handle", handle);
}
@Override
protected NioHttpCommandConnectionHandle getHandleFromConnection(NHttpConnection connection) {
return (NioHttpCommandConnectionHandle) connection.getContext()
.getAttribute("command-handle");
}
class NHttpClientConnectionPoolSessionRequestCallback implements SessionRequestCallback {
/**
* {@inheritDoc}
*/
@Override
public void completed(SessionRequest request) {
}
/**
* @see releaseConnectionAndSetResponseException
*/
@Override
public void cancelled(SessionRequest request) {
releaseConnectionAndSetResponseException(request, new CancellationException(
"Cancelled request: " + request.getRemoteAddress()));
}
/**
* Releases a connection and associates the current exception with the request using the
* session.
*/
@VisibleForTesting
void releaseConnectionAndSetResponseException(SessionRequest request, Exception e) {
allConnections.release();
TransformingHttpCommand<?> frequest = (TransformingHttpCommand<?>) request.getAttachment();
if (frequest != null) {
frequest.setException(e);
}
}
/**
* Disables the pool, if {@code maxSessionFailures} is reached}
*
* @see releaseConnectionAndSetResponseException
*/
@Override
public void failed(SessionRequest request) {
int count = currentSessionFailures.getAndIncrement();
releaseConnectionAndSetResponseException(request, request.getException());
if (count >= maxSessionFailures) {
logger.error(request.getException(),
"Exceeded maximum Session failures: %d, Disabling pool for %s",
maxSessionFailures, getTarget());
exception.set(request.getException());
}
}
/**
* @see releaseConnectionAndSetResponseException
*/
@Override
public void timeout(SessionRequest request) {
releaseConnectionAndSetResponseException(request, new TimeoutException("Timeout on: "
+ request.getRemoteAddress()));
}
}
public void connectionOpen(NHttpConnection conn) {
conn.setSocketTimeout(0);
available.offer(conn);
logger.trace("Opened: %s", getTarget());
}
public void connectionTimeout(NHttpConnection conn) {
String message = String.format("Timeout on : %s - timeout %d", getTarget(), conn
.getSocketTimeout());
logger.warn(message);
resubmitIfRequestIsReplayable(conn, new TimeoutException(message));
}
public void connectionClosed(NHttpConnection conn) {
logger.trace("Closed: %s", getTarget());
}
public void fatalIOException(IOException ex, NHttpConnection conn) {
logger.error(ex, "IO Exception: %s", getTarget());
HttpCommandRendezvous<?> rendezvous = getCommandFromConnection(conn);
if (rendezvous != null) {
/**
* these exceptions, while technically i/o are unresolvable. set the error on the command
* itself so that it doesn't replay.
*/
if (ex instanceof UnmappableCharacterException) {
setExceptionOnCommand(ex, rendezvous);
} else {
resubmitIfRequestIsReplayable(conn, ex);
}
}
}
public void fatalProtocolException(HttpException ex, NHttpConnection conn) {
logger.error(ex, "Protocol Exception: %s", getTarget());
setExceptionOnCommand(conn, ex);
}
@Override
protected NioHttpCommandConnectionHandle createHandle(HttpCommandRendezvous<?> command,
NHttpConnection conn) {
try {
return new NioHttpCommandConnectionHandle(allConnections, available, endPoint, command,
conn);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted creating a handle to " + conn, e);
}
}
@Override
protected boolean isReplayable(HttpCommandRendezvous<?> rendezvous) {
return rendezvous.getCommand().isReplayable();
}
@VisibleForTesting
InetSocketAddress getTarget() {
return target;
}
}