/** * * 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.pool; import static com.google.common.base.Preconditions.checkArgument; import static org.jclouds.concurrent.ConcurrentUtils.makeListenable; import java.net.URI; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.inject.Inject; import org.jclouds.http.HttpCommand; import org.jclouds.http.HttpCommandRendezvous; import org.jclouds.http.HttpResponse; import org.jclouds.http.TransformingHttpCommandExecutorService; import org.jclouds.lifecycle.BaseLifeCycle; import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.collect.MapMaker; import com.google.common.util.concurrent.ListenableFuture; /** * * @author Adrian Cole */ public class ConnectionPoolTransformingHttpCommandExecutorService<C> extends BaseLifeCycle implements TransformingHttpCommandExecutorService { private final ConcurrentMap<URI, HttpCommandConnectionPool<C>> poolMap; private final BlockingQueue<HttpCommandRendezvous<?>> commandQueue; private final HttpCommandConnectionPool.Factory<C> poolFactory; @Inject public ConnectionPoolTransformingHttpCommandExecutorService(ExecutorService executor, HttpCommandConnectionPool.Factory<C> pf, BlockingQueue<HttpCommandRendezvous<?>> commandQueue) { super(executor); this.poolFactory = pf; // TODO inject this. poolMap = new MapMaker().makeComputingMap(new Function<URI, HttpCommandConnectionPool<C>>() { public HttpCommandConnectionPool<C> apply(URI endPoint) { checkArgument(endPoint.getHost() != null, String.format( "endPoint.getHost() is null for %s", endPoint)); try { HttpCommandConnectionPool<C> pool = poolFactory.create(endPoint); addDependency(pool); return pool; } catch (RuntimeException e) { logger.error(e, "error creating entry for %s", endPoint); throw e; } } }); this.commandQueue = commandQueue; } /** * {@inheritDoc} * * If the reason we are shutting down is due an exception, we set that exception on all pending * commands. Otherwise, we cancel the pending commands. */ @Override protected void doShutdown() { exception.compareAndSet(null, getExceptionFromDependenciesOrNull()); while (!commandQueue.isEmpty()) { HttpCommandRendezvous<?> rendezvous = (HttpCommandRendezvous<?>) commandQueue.remove(); if (rendezvous != null) { try { if (exception.get() != null) rendezvous.setException(exception.get()); else rendezvous.setException(new CancellationException("shutdown")); } catch (InterruptedException e) { logger.error(e, "Error cancelling command %s", rendezvous.getCommand()); } } } } @Override protected void doWork() throws InterruptedException { takeACommandOffTheQueueAndInvokeIt(); } private void takeACommandOffTheQueueAndInvokeIt() throws InterruptedException { HttpCommandRendezvous<?> rendezvous = commandQueue.poll(1, TimeUnit.SECONDS); if (rendezvous != null) { try { invoke(rendezvous); } catch (Exception e) { Throwables.propagateIfPossible(e, InterruptedException.class); logger.error(e, "Error processing command %s", rendezvous.getCommand()); } } } /** * This is an asynchronous operation that puts the <code>command</code> onto a queue. Later, it * will be processed via the {@link #invoke(HttpCommandRendezvous) invoke} * method. */ public <T> ListenableFuture<T> submit(HttpCommand command, final Function<HttpResponse, T> responseTransformer) { exceptionIfNotActive(); final SynchronousQueue<?> channel = new SynchronousQueue<Object>(); // should block and immediately parse the response on exit. ListenableFuture<T> future = makeListenable(executorService.submit(new Callable<T>() { public T call() throws Exception { Object o = channel.take(); if (o instanceof Exception) { throw (Exception) o; } return responseTransformer.apply((HttpResponse) o); } }), executorService); HttpCommandRendezvous<T> rendezvous = new HttpCommandRendezvous<T>(command, channel, future); commandQueue.add(rendezvous); return rendezvous.getListenableFuture(); } /** * Invoke binds a command with a connection from the pool. This binding is called a * {@link HttpCommandConnectionHandle handle}. The handle will keep this binding until the * command's response is parsed or an exception is set on the Command object. * * @param command */ protected void invoke(HttpCommandRendezvous<?> command) { exceptionIfNotActive(); URI endpoint = createBaseEndpointFor(command); HttpCommandConnectionPool<C> pool = poolMap.get(endpoint); if (pool == null) { // TODO limit; logger.warn("pool not available for command %s; retrying", command.getCommand()); commandQueue.add(command); return; } HttpCommandConnectionHandle<C> connectionHandle = null; try { connectionHandle = pool.getHandle(command); } catch (InterruptedException e) { logger.warn(e, "Interrupted getting a connection for command %s; retrying", command .getCommand()); commandQueue.add(command); return; } catch (TimeoutException e) { logger.warn(e, "Timeout getting a connection for command %s on pool %s; retrying", command .getCommand(), pool); commandQueue.add(command); return; } catch (RuntimeException e) { logger.warn(e, "Error getting a connection for command %s on pool %s; retrying", command .getCommand(), pool); discardPool(endpoint, pool); commandQueue.add(command); return; } if (connectionHandle == null) { logger.error("Failed to obtain connection for command %s; retrying", command.getCommand()); commandQueue.add(command); return; } connectionHandle.startConnection(); } private void discardPool(URI endpoint, HttpCommandConnectionPool<C> pool) { poolMap.remove(endpoint, pool); pool.shutdown(); this.dependencies.remove(pool); } /** * keys to the map are only used for socket information, not path. In this case, you should * remove any path or query details from the URI. */ private URI createBaseEndpointFor(HttpCommandRendezvous<?> command) { URI endpoint = command.getCommand().getRequest().getEndpoint(); if (endpoint.getPort() == -1) { return URI.create(String.format("%s://%s", endpoint.getScheme(), endpoint.getHost())); } else { return URI.create(String.format("%s://%s:%d", endpoint.getScheme(), endpoint.getHost(), endpoint.getPort())); } } }