Java tutorial
/* * Copyright 2010-2013 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 org.asynchttpclient.providers.netty.request; import static org.asynchttpclient.providers.netty.util.HttpUtil.WEBSOCKET; import static org.asynchttpclient.providers.netty.util.HttpUtil.isSecure; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.AsyncHandlerExtensions; import org.asynchttpclient.AsyncHttpClientConfig; import org.asynchttpclient.ConnectionPoolKeyStrategy; import org.asynchttpclient.FluentCaseInsensitiveStringsMap; import org.asynchttpclient.ListenableFuture; import org.asynchttpclient.ProxyServer; import org.asynchttpclient.Request; import org.asynchttpclient.filter.FilterContext; import org.asynchttpclient.filter.FilterException; import org.asynchttpclient.filter.IOExceptionFilter; import org.asynchttpclient.listener.TransferCompletionHandler; import org.asynchttpclient.providers.netty.NettyAsyncHttpProviderConfig; import org.asynchttpclient.providers.netty.channel.Channels; import org.asynchttpclient.providers.netty.future.NettyResponseFuture; import org.asynchttpclient.providers.netty.request.timeout.IdleConnectionTimeoutTimerTask; import org.asynchttpclient.providers.netty.request.timeout.RequestTimeoutTimerTask; import org.asynchttpclient.providers.netty.request.timeout.TimeoutsHolder; import org.asynchttpclient.util.AsyncHttpProviderUtils; import org.asynchttpclient.util.ProxyUtils; import org.asynchttpclient.websocket.WebSocketUpgradeHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.Timeout; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.util.Map; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; public class NettyRequestSender { private static final Logger LOGGER = LoggerFactory.getLogger(NettyRequestSender.class); private final AtomicBoolean closed; private final AsyncHttpClientConfig config; private final Channels channels; private final NettyRequestFactory requestFactory; public NettyRequestSender(AtomicBoolean closed, // AsyncHttpClientConfig config, // NettyAsyncHttpProviderConfig nettyConfig, // Channels channels) { this.closed = closed; this.config = config; this.channels = channels; requestFactory = new NettyRequestFactory(config, nettyConfig); } public boolean retry(NettyResponseFuture<?> future, Channel channel) { boolean success = false; if (!closed.get()) { channels.removeAll(channel); if (future == null) { Object attachment = Channels.getDefaultAttribute(channel); if (attachment instanceof NettyResponseFuture) future = (NettyResponseFuture<?>) attachment; } if (future != null && future.canBeReplayed()) { future.setState(NettyResponseFuture.STATE.RECONNECTED); future.getAndSetStatusReceived(false); LOGGER.debug("Trying to recover request {}\n", future.getNettyRequest()); if (future.getAsyncHandler() instanceof AsyncHandlerExtensions) { AsyncHandlerExtensions.class.cast(future.getAsyncHandler()).onRetry(); } try { sendNextRequest(future.getRequest(), future); success = true; } catch (IOException iox) { future.setState(NettyResponseFuture.STATE.CLOSED); future.abort(iox); LOGGER.error("Remotely Closed, unable to recover", iox); } } else { LOGGER.debug("Unable to recover future {}\n", future); } } return success; } public boolean applyIoExceptionFiltersAndReplayRequest(NettyResponseFuture<?> future, IOException e, Channel channel) throws IOException { boolean replayed = false; FilterContext<?> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler()) .request(future.getRequest()).ioException(e).build(); for (IOExceptionFilter asyncFilter : config.getIOExceptionFilters()) { try { fc = asyncFilter.filter(fc); if (fc == null) { throw new NullPointerException("FilterContext is null"); } } catch (FilterException efe) { channels.abort(future, efe); } } if (fc.replayRequest()) { replayRequest(future, fc, channel); replayed = true; } return replayed; } public <T> void sendNextRequest(final Request request, final NettyResponseFuture<T> f) throws IOException { sendRequest(request, f.getAsyncHandler(), f, true); } // FIXME is this useful? Can't we do that when building the request? private final boolean validateWebSocketRequest(Request request, AsyncHandler<?> asyncHandler) { return request.getMethod().equals(HttpMethod.GET.name()) && asyncHandler instanceof WebSocketUpgradeHandler; } private Channel getCachedChannel(NettyResponseFuture<?> future, URI uri, ConnectionPoolKeyStrategy poolKeyGen, ProxyServer proxyServer) { if (future != null && future.reuseChannel() && isChannelValid(future.channel())) return future.channel(); else return channels.pollAndVerifyCachedChannel(uri, proxyServer, poolKeyGen); } private <T> ListenableFuture<T> sendRequestWithCachedChannel(Request request, URI uri, ProxyServer proxy, NettyResponseFuture<T> future, AsyncHandler<T> asyncHandler, Channel channel) throws IOException { future.setState(NettyResponseFuture.STATE.POOLED); future.attachChannel(channel, false); LOGGER.debug("\nUsing cached Channel {}\n for request \n{}\n", channel, future.getNettyRequest().getHttpRequest()); Channels.setDefaultAttribute(channel, future); try { writeRequest(future, channel); } catch (Exception ex) { LOGGER.debug("writeRequest failure", ex); if (ex.getMessage() != null && ex.getMessage().contains("SSLEngine")) { LOGGER.debug("SSLEngine failure", ex); future = null; } else { try { asyncHandler.onThrowable(ex); } catch (Throwable t) { LOGGER.warn("doConnect.writeRequest()", t); } IOException ioe = new IOException(ex.getMessage()); ioe.initCause(ex); throw ioe; } } return future; } private InetSocketAddress remoteAddress(Request request, URI uri, ProxyServer proxy, boolean useProxy) { if (request.getInetAddress() != null) return new InetSocketAddress(request.getInetAddress(), AsyncHttpProviderUtils.getPort(uri)); else if (!useProxy || ProxyUtils.avoidProxy(proxy, uri.getHost())) return new InetSocketAddress(AsyncHttpProviderUtils.getHost(uri), AsyncHttpProviderUtils.getPort(uri)); else return new InetSocketAddress(proxy.getHost(), proxy.getPort()); } private ChannelFuture connect(Request request, URI uri, ProxyServer proxy, boolean useProxy, Bootstrap bootstrap) { InetSocketAddress remoteAddress = remoteAddress(request, uri, proxy, useProxy); if (request.getLocalAddress() != null) return bootstrap.connect(remoteAddress, new InetSocketAddress(request.getLocalAddress(), 0)); else return bootstrap.connect(remoteAddress); } private <T> ListenableFuture<T> sendRequestWithNewChannel(// Request request, // URI uri, // ProxyServer proxy, // boolean useProxy, // NettyResponseFuture<T> future, // AsyncHandler<T> asyncHandler, // boolean reclaimCache) throws IOException { boolean useSSl = isSecure(uri) && !useProxy; // Do not throw an exception when we need an extra connection for a redirect // FIXME why? This violate the max connection per host handling, right? boolean acquiredConnection = !reclaimCache && channels.acquireConnection(asyncHandler); Bootstrap bootstrap = channels.getBootstrap(request.getURI(), useSSl, useProxy); NettyConnectListener<T> connectListener = new NettyConnectListener<T>(config, this, future); ChannelFuture channelFuture; try { channelFuture = connect(request, uri, proxy, useProxy, bootstrap); } catch (Throwable t) { if (acquiredConnection) { channels.releaseFreeConnections(); } channels.abort(connectListener.future(), t.getCause() == null ? t : t.getCause()); return connectListener.future(); } channelFuture.addListener(connectListener); LOGGER.debug("\nNon cached request \n{}\n\nusing Channel \n{}\n", connectListener.future().getNettyRequest().getHttpRequest(), channelFuture.channel()); if (!connectListener.future().isCancelled() || !connectListener.future().isDone()) { channels.registerChannel(channelFuture.channel()); connectListener.future().attachChannel(channelFuture.channel(), false); } else if (acquiredConnection) { channels.releaseFreeConnections(); } return connectListener.future(); } private <T> NettyResponseFuture<T> newNettyResponseFuture(URI uri, Request request, AsyncHandler<T> asyncHandler, NettyRequest nettyRequest, ProxyServer proxyServer) { int requestTimeout = AsyncHttpProviderUtils.requestTimeout(config, request); NettyResponseFuture<T> f = new NettyResponseFuture<T>(// uri, // request, // asyncHandler, // nettyRequest, // requestTimeout, // config, // request.getConnectionPoolKeyStrategy(), // proxyServer); String expectHeader = request.getHeaders().getFirstValue(HttpHeaders.Names.EXPECT); if (expectHeader != null && expectHeader.equalsIgnoreCase(HttpHeaders.Values.CONTINUE)) { f.setDontWriteBodyBecauseExpectContinue(true); } return f; } private <T> NettyResponseFuture<T> newNettyRequestAndResponseFuture(final Request request, final AsyncHandler<T> asyncHandler, NettyResponseFuture<T> originalFuture, URI uri, ProxyServer proxy, boolean forceConnect) throws IOException { NettyRequest nettyRequest = requestFactory.newNettyRequest(request, uri, forceConnect, proxy); if (originalFuture == null) { return newNettyResponseFuture(uri, request, asyncHandler, nettyRequest, proxy); } else { originalFuture.setNettyRequest(nettyRequest); originalFuture.setRequest(request); return originalFuture; } } private boolean isChannelValid(Channel channel) { return channel != null && channel.isOpen() && channel.isActive(); } private <T> ListenableFuture<T> sendRequestThroughSslProxy(// Request request, // AsyncHandler<T> asyncHandler, // NettyResponseFuture<T> future, // boolean reclaimCache, // URI uri, // ProxyServer proxyServer) throws IOException { // Using CONNECT depends on wither we can fetch a valid channel or not // Loop until we get a valid channel from the pool and it's still valid once the request is built NettyResponseFuture<T> newFuture = null; for (int i = 0; i < 3; i++) { Channel channel = getCachedChannel(future, uri, request.getConnectionPoolKeyStrategy(), proxyServer); if (isChannelValid(channel)) { if (newFuture == null) newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future, uri, proxyServer, false); if (isChannelValid(channel)) // if the channel is still active, we can use it, otherwise try gain return sendRequestWithCachedChannel(request, uri, proxyServer, newFuture, asyncHandler, channel); } else // pool is empty break; } newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future, uri, proxyServer, true); return sendRequestWithNewChannel(request, uri, proxyServer, true, newFuture, asyncHandler, reclaimCache); } private <T> ListenableFuture<T> sendRequestWithCertainForceConnect(// Request request, // AsyncHandler<T> asyncHandler, // NettyResponseFuture<T> future, // boolean reclaimCache, // URI uri, // ProxyServer proxyServer, // boolean useProxy, // boolean forceConnect) throws IOException { // We know for sure if we have to force to connect or not, so we can build the HttpRequest right away // This reduces the probability of having a pooled channel closed by the server by the time we build the request NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future, uri, proxyServer, forceConnect); Channel channel = getCachedChannel(future, uri, request.getConnectionPoolKeyStrategy(), proxyServer); if (isChannelValid(channel)) return sendRequestWithCachedChannel(request, uri, proxyServer, newFuture, asyncHandler, channel); else return sendRequestWithNewChannel(request, uri, proxyServer, useProxy, newFuture, asyncHandler, reclaimCache); } public <T> ListenableFuture<T> sendRequest(final Request request, // final AsyncHandler<T> asyncHandler, // NettyResponseFuture<T> future, // boolean reclaimCache) throws IOException { if (closed.get()) { throw new IOException("Closed"); } // FIXME really useful? Why not do this check when building the request? if (request.getURI().getScheme().startsWith(WEBSOCKET) && !validateWebSocketRequest(request, asyncHandler)) { throw new IOException("WebSocket method must be a GET"); } URI uri = config.isUseRawUrl() ? request.getRawURI() : request.getURI(); ProxyServer proxyServer = ProxyUtils.getProxyServer(config, request); boolean resultOfAConnect = future != null && future.getNettyRequest() != null && future.getNettyRequest().getHttpRequest().getMethod() == HttpMethod.CONNECT; boolean useProxy = proxyServer != null && !resultOfAConnect; if (useProxy && isSecure(uri)) { // SSL proxy, have to handle CONNECT if (future != null && future.isConnectAllowed()) // CONNECT forced return sendRequestWithCertainForceConnect(request, asyncHandler, future, reclaimCache, uri, proxyServer, true, true); else return sendRequestThroughSslProxy(request, asyncHandler, future, reclaimCache, uri, proxyServer); } else return sendRequestWithCertainForceConnect(request, asyncHandler, future, reclaimCache, uri, proxyServer, useProxy, false); } private void configureTransferAdapter(AsyncHandler<?> handler, HttpRequest httpRequest) { FluentCaseInsensitiveStringsMap h = new FluentCaseInsensitiveStringsMap(); for (Map.Entry<String, String> entries : httpRequest.headers()) { h.add(entries.getKey(), entries.getValue()); } TransferCompletionHandler.class.cast(handler).headers(h); } private void scheduleTimeouts(NettyResponseFuture<?> nettyResponseFuture) { try { nettyResponseFuture.touch(); int requestTimeoutInMs = AsyncHttpProviderUtils.requestTimeout(config, nettyResponseFuture.getRequest()); TimeoutsHolder timeoutsHolder = new TimeoutsHolder(); if (requestTimeoutInMs != -1) { Timeout requestTimeout = channels.newTimeoutInMs( new RequestTimeoutTimerTask(nettyResponseFuture, channels, timeoutsHolder, closed), requestTimeoutInMs); timeoutsHolder.requestTimeout = requestTimeout; } int idleConnectionTimeoutInMs = config.getIdleConnectionTimeoutInMs(); if (idleConnectionTimeoutInMs != -1 && idleConnectionTimeoutInMs < requestTimeoutInMs) { // no need for a idleConnectionTimeout that's less than the requestTimeoutInMs Timeout idleConnectionTimeout = channels .newTimeoutInMs( new IdleConnectionTimeoutTimerTask(nettyResponseFuture, channels, timeoutsHolder, closed, requestTimeoutInMs, idleConnectionTimeoutInMs), idleConnectionTimeoutInMs); timeoutsHolder.idleConnectionTimeout = idleConnectionTimeout; } nettyResponseFuture.setTimeoutsHolder(timeoutsHolder); } catch (RejectedExecutionException ex) { channels.abort(nettyResponseFuture, ex); } } public final <T> void writeRequest(NettyResponseFuture<T> future, Channel channel) { try { // if the channel is dead because it was pooled and the remote server decided to close it, // we just let it go and the channelInactive do its work if (!channel.isOpen() || !channel.isActive()) { return; } NettyRequest nettyRequest = future.getNettyRequest(); HttpRequest httpRequest = nettyRequest.getHttpRequest(); AsyncHandler<T> handler = future.getAsyncHandler(); if (handler instanceof TransferCompletionHandler) { configureTransferAdapter(handler, httpRequest); } if (!future.isHeadersAlreadyWrittenOnContinue()) { try { if (future.getAsyncHandler() instanceof AsyncHandlerExtensions) { AsyncHandlerExtensions.class.cast(future.getAsyncHandler()).onRequestSent(); } channel.writeAndFlush(httpRequest, channel.newProgressivePromise()) .addListener(new ProgressListener(config, future.getAsyncHandler(), future, true, 0L)); } catch (Throwable cause) { // FIXME why not notify? LOGGER.debug(cause.getMessage(), cause); try { channel.close(); } catch (RuntimeException ex) { LOGGER.debug(ex.getMessage(), ex); } return; } } if (!future.isDontWriteBodyBecauseExpectContinue() && !httpRequest.getMethod().equals(HttpMethod.CONNECT) && nettyRequest.getBody() != null) nettyRequest.getBody().write(channel, future, config); } catch (Throwable ioe) { try { channel.close(); } catch (RuntimeException ex) { LOGGER.debug(ex.getMessage(), ex); } } scheduleTimeouts(future); } public void replayRequest(final NettyResponseFuture<?> future, FilterContext fc, Channel channel) throws IOException { Request newRequest = fc.getRequest(); future.setAsyncHandler(fc.getAsyncHandler()); future.setState(NettyResponseFuture.STATE.NEW); future.touch(); LOGGER.debug("\n\nReplaying Request {}\n for Future {}\n", newRequest, future); if (future.getAsyncHandler() instanceof AsyncHandlerExtensions) { AsyncHandlerExtensions.class.cast(future.getAsyncHandler()).onRetry(); } channels.drainChannel(channel, future); sendNextRequest(newRequest, future); } }