/* * Copyright 2010 Ning, Inc. * * Ning 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 com.ning.http.client.providers.netty; import java.net.MalformedURLException; import java.net.URI; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.jboss.netty.channel.Channel; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import com.ning.http.client.AsyncHandler; import com.ning.http.client.Request; import com.ning.http.client.listenable.AbstractListenableFuture; /** * A {@link Future} that can be used to track when an asynchronous HTTP request * has been fully processed. * * @param <V> */ public final class NettyResponseFuture<V> extends AbstractListenableFuture<V> { public final static String MAX_RETRY = "com.ning.http.client.providers.netty.maxRetry"; enum STATE { NEW, POOLED, RECONNECTED, CLOSED, } private final CountDownLatch latch = new CountDownLatch(1); private final AtomicBoolean isDone = new AtomicBoolean(false); private final AtomicBoolean isCancelled = new AtomicBoolean(false); private AsyncHandler<V> asyncHandler; private final int responseTimeoutInMs; private final int idleConnectionTimeoutInMs; private Request request; private HttpRequest nettyRequest; private final AtomicReference<V> content = new AtomicReference<V>(); private URI uri; private boolean keepAlive = true; private HttpResponse httpResponse; private final AtomicReference<ExecutionException> exEx = new AtomicReference<ExecutionException>(); private final AtomicInteger redirectCount = new AtomicInteger(); private volatile Future<?> reaperFuture; private final AtomicBoolean inAuth = new AtomicBoolean(false); private final AtomicBoolean statusReceived = new AtomicBoolean(false); private final AtomicLong touch = new AtomicLong(System.currentTimeMillis()); private final long start = System.currentTimeMillis(); private final NettyAsyncHttpProvider asyncHttpProvider; private final AtomicReference<STATE> state = new AtomicReference<STATE>( STATE.NEW); private final AtomicBoolean contentProcessed = new AtomicBoolean(false); private Channel channel; private boolean reuseChannel = false; private final AtomicInteger currentRetry = new AtomicInteger(0); private final int maxRetry; private boolean writeHeaders; private boolean writeBody; private final AtomicBoolean throwableCalled = new AtomicBoolean(false); private boolean allowConnect = false; public NettyResponseFuture(final URI uri, final Request request, final AsyncHandler<V> asyncHandler, final HttpRequest nettyRequest, final int responseTimeoutInMs, final int idleConnectionTimeoutInMs, final NettyAsyncHttpProvider asyncHttpProvider) { this.asyncHandler = asyncHandler; this.responseTimeoutInMs = responseTimeoutInMs; this.idleConnectionTimeoutInMs = idleConnectionTimeoutInMs; this.request = request; this.nettyRequest = nettyRequest; this.uri = uri; this.asyncHttpProvider = asyncHttpProvider; if (System.getProperty(MAX_RETRY) != null) { maxRetry = Integer.valueOf(System.getProperty(MAX_RETRY)); } else { maxRetry = asyncHttpProvider.getConfig().getMaxRequestRetry(); } writeHeaders = true; writeBody = true; } protected URI getURI() throws MalformedURLException { return uri; } protected void setURI(final URI uri) { this.uri = uri; } /** * {@inheritDoc} */ /* @Override */ @Override public boolean isDone() { return isDone.get(); } /** * {@inheritDoc} */ /* @Override */ @Override public boolean isCancelled() { return isCancelled.get(); } void setAsyncHandler(final AsyncHandler<V> asyncHandler) { this.asyncHandler = asyncHandler; } /** * {@inheritDoc} */ /* @Override */ @Override public boolean cancel(final boolean force) { cancelReaper(); if (isCancelled.get()) return false; try { channel.getPipeline().getContext(NettyAsyncHttpProvider.class) .setAttachment(new NettyAsyncHttpProvider.DiscardEvent()); channel.close(); } catch (final Throwable t) { // Ignore } if (!throwableCalled.getAndSet(true)) { try { asyncHandler.onThrowable(new CancellationException()); } catch (final Throwable t) { } } latch.countDown(); isCancelled.set(true); super.done(); return true; } /** * Is the Future still valid * * @return <code>true</code> if response has expired and should be * terminated. */ public boolean hasExpired() { final long now = System.currentTimeMillis(); return idleConnectionTimeoutInMs != -1 && ((now - touch.get()) >= idleConnectionTimeoutInMs) || responseTimeoutInMs != -1 && ((now - start) >= responseTimeoutInMs); } /** * {@inheritDoc} */ /* @Override */ @Override public V get() throws InterruptedException, ExecutionException { try { return get(responseTimeoutInMs, TimeUnit.MILLISECONDS); } catch (final TimeoutException e) { cancelReaper(); throw new ExecutionException(e); } } void cancelReaper() { if (reaperFuture != null) { reaperFuture.cancel(true); } } /** * {@inheritDoc} */ /* @Override */ @Override public V get(final long l, final TimeUnit tu) throws InterruptedException, TimeoutException, ExecutionException { if (!isDone() && !isCancelled()) { boolean expired = false; if (l == -1) { latch.await(); } else { expired = !latch.await(l, tu); } if (expired) { isCancelled.set(true); final TimeoutException te = new TimeoutException(String.format( "No response received after %s", l)); if (!throwableCalled.getAndSet(true)) { try { asyncHandler.onThrowable(te); } catch (final Throwable t) { } finally { cancelReaper(); throw new ExecutionException(te); } } } isDone.set(true); final ExecutionException e = exEx.getAndSet(null); if (e != null) { throw e; } } return getContent(); } V getContent() throws ExecutionException { final ExecutionException e = exEx.getAndSet(null); if (e != null) { throw e; } V update = content.get(); // No more retry currentRetry.set(maxRetry); if (exEx.get() == null && !contentProcessed.getAndSet(true)) { try { update = asyncHandler.onCompleted(); } catch (final Throwable ex) { if (!throwableCalled.getAndSet(true)) { try { asyncHandler.onThrowable(ex); } catch (final Throwable t) { } finally { cancelReaper(); throw new RuntimeException(ex); } } } content.compareAndSet(null, update); } return update; } @Override public final void done(final Callable callable) { try { cancelReaper(); if (exEx.get() != null) { return; } getContent(); isDone.set(true); if (callable != null) { try { callable.call(); } catch (final Exception ex) { throw new RuntimeException(ex); } } } catch (final ExecutionException t) { return; } catch (final RuntimeException t) { exEx.compareAndSet(null, new ExecutionException(t)); } finally { latch.countDown(); } super.done(); } @Override public final void abort(final Throwable t) { cancelReaper(); if (isDone.get() || isCancelled.get()) return; exEx.compareAndSet(null, new ExecutionException(t)); if (!throwableCalled.getAndSet(true)) { try { asyncHandler.onThrowable(t); } catch (final Throwable te) { } finally { isCancelled.set(true); } } latch.countDown(); super.done(); } @Override public void content(final V v) { content.set(v); } protected final Request getRequest() { return request; } public final HttpRequest getNettyRequest() { return nettyRequest; } protected final void setNettyRequest(final HttpRequest nettyRequest) { this.nettyRequest = nettyRequest; } protected final AsyncHandler<V> getAsyncHandler() { return asyncHandler; } protected final boolean getKeepAlive() { return keepAlive; } protected final void setKeepAlive(final boolean keepAlive) { this.keepAlive = keepAlive; } protected final HttpResponse getHttpResponse() { return httpResponse; } protected final void setHttpResponse(final HttpResponse httpResponse) { this.httpResponse = httpResponse; } protected int incrementAndGetCurrentRedirectCount() { return redirectCount.incrementAndGet(); } protected void setReaperFuture(final Future<?> reaperFuture) { cancelReaper(); this.reaperFuture = reaperFuture; } protected boolean isInAuth() { return inAuth.get(); } protected boolean getAndSetAuth(final boolean inDigestAuth) { return inAuth.getAndSet(inDigestAuth); } protected STATE getState() { return state.get(); } protected void setState(final STATE state) { this.state.set(state); } public boolean getAndSetStatusReceived(final boolean sr) { return statusReceived.getAndSet(sr); } /** * {@inheritDoc} */ /* @Override */ @Override public void touch() { touch.set(System.currentTimeMillis()); } /** * {@inheritDoc} */ /* @Override */ @Override public boolean getAndSetWriteHeaders(final boolean writeHeaders) { final boolean b = this.writeHeaders; this.writeHeaders = writeHeaders; return b; } /** * {@inheritDoc} */ /* @Override */ @Override public boolean getAndSetWriteBody(final boolean writeBody) { final boolean b = this.writeBody; this.writeBody = writeBody; return b; } protected NettyAsyncHttpProvider provider() { return asyncHttpProvider; } protected void attachChannel(final Channel channel) { this.channel = channel; } public void setReuseChannel(final boolean reuseChannel) { this.reuseChannel = reuseChannel; } public boolean isConnectAllowed() { return allowConnect; } public void setConnectAllowed(final boolean allowConnect) { this.allowConnect = allowConnect; } protected void attachChannel(final Channel channel, final boolean reuseChannel) { this.channel = channel; this.reuseChannel = reuseChannel; } protected Channel channel() { return channel; } protected boolean reuseChannel() { return reuseChannel; } protected boolean canRetry() { if (currentRetry.incrementAndGet() > maxRetry) { return false; } return true; } public void setRequest(final Request request) { this.request = request; } /** * Return true if the {@link Future} cannot be recovered. There is some * scenario where a connection can be closed by an unexpected IOException, * and in some situation we can recover from that exception. * * @return true if that {@link Future} cannot be recovered. */ public boolean cannotBeReplay() { return isDone() || !canRetry() || isCancelled() || (channel() != null && channel().isOpen() && uri.getScheme() .compareToIgnoreCase("https") != 0) || isInAuth(); } @Override public String toString() { return "NettyResponseFuture{" + "currentRetry=" + currentRetry + ",\n\tisDone=" + isDone + ",\n\tisCancelled=" + isCancelled + ",\n\tasyncHandler=" + asyncHandler + ",\n\tresponseTimeoutInMs=" + responseTimeoutInMs + ",\n\tnettyRequest=" + nettyRequest + ",\n\tcontent=" + content + ",\n\turi=" + uri + ",\n\tkeepAlive=" + keepAlive + ",\n\thttpResponse=" + httpResponse + ",\n\texEx=" + exEx + ",\n\tredirectCount=" + redirectCount + ",\n\treaperFuture=" + reaperFuture + ",\n\tinAuth=" + inAuth + ",\n\tstatusReceived=" + statusReceived + ",\n\ttouch=" + touch + '}'; } }