/* * Copyright 2013 BiasedBit * * 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 com.biasedbit.http.client; import com.biasedbit.http.client.connection.*; import com.biasedbit.http.client.event.*; import com.biasedbit.http.client.future.DataSinkListener; import com.biasedbit.http.client.future.RequestFuture; import com.biasedbit.http.client.processor.DiscardProcessor; import com.biasedbit.http.client.processor.ResponseProcessor; import com.biasedbit.http.client.ssl.BogusSslContextFactory; import com.biasedbit.http.client.ssl.SslContextFactory; import com.biasedbit.http.client.timeout.HashedWheelTimeoutController; import com.biasedbit.http.client.timeout.TimeoutController; import com.biasedbit.http.client.util.*; import lombok.Getter; import lombok.SneakyThrows; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.*; import org.jboss.netty.channel.group.ChannelGroup; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; import org.jboss.netty.channel.socket.oio.OioClientSocketChannelFactory; import org.jboss.netty.handler.codec.http.*; import org.jboss.netty.handler.ssl.SslHandler; import org.jboss.netty.util.internal.ExecutorUtil; import javax.net.ssl.SSLEngine; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import static com.biasedbit.http.client.future.RequestFuture.*; import static com.biasedbit.http.client.util.Utils.*; import static org.jboss.netty.handler.codec.http.HttpHeaders.*; /** * Abstract implementation of the {@link HttpClient} interface. Contains most of the boilerplate code that other * {@link HttpClient} implementations would also need. * <p/> * This abstract implementation is in itself complete. If you extend this class, you needn't implement a single method * (just like {@link DefaultHttpClient} does). However you may want to override the specific behavior of one or other * method, rather than reimplement the whole class all over again (just like {@link StatsGatheringHttpClient} does - * only overrides the {@code eventHandlingLoop()} method to gather execution statistics). * <p/> * <h3>Thread safety and performance</h3> This default implementation is thread-safe and the performance does not * degrade when the instance is shared by multiple threads accessing it at the same time. * <p/> * <h3>Event queue (producer/consumer)</h3> When this implementation is initialised, it fires up an auxilliary thread, * the consumer. * <p/> * Every time one of the variants of the method {@code execute()} is called, a new * {@link com.biasedbit.http.client.event.ClientEvent} is generated * and introduced in a blocking event queue (on the caller's thread execution time). The consumer then grabs that * request and acts accordingly - it can either queue the request so that it is later executed in an available * connection, request a new connection in case no connections are available, directly execute this request, etc. * <p/> * <h3>Order</h3> By using an event queue, absolute order is guaranteed. If thread A calls {@code execute()} with a * request to host H prior to thread B (which also places a request for the same host), then the request provided by * thread A is guaranteed to be <strong>placed</strong> (i.e. written to the network) before the request placed by * thread B. * <p/> * This doesn't mean that request A will hit the server before request B or that the response for request A will arrive * before B. The reasons are obvious: * <p/> * <ul> * <li>A can end up in a connection slower than B's</li> * <li>Server can respond faster on one socket than on the other</li> * <li>Response for request B can have 10b and for request A 10bKb</li> * <li>etc</li> * </ul * <p/> * If you need to guarantee that a request B can only hit the server after a request A, you can either manually manage * that in your code through the {@link com.biasedbit.http.client.future.RequestFuture} API or configure the concrete * instance of this class to allow * at most 1 connection per host - although this last option will hurt performance globally. * <p/> * <div class="note"> * <div class="header">Note:</div> * Calling {@linkplain #execute(String, int, HttpRequest, com.biasedbit.http.client.processor.ResponseProcessor) one * of the variants of {@code execute}} * with the client configured with {@linkplain #setAutoDecompress(boolean) auto-inflation} turned on will cause a * 'ACCEPT_ENCODING' header to be added with value 'GZIP'. * </div> * * @author <a href="http://biasedbit.com/">Bruno de Carvalho</a> */ public class DefaultHttpClient implements HttpClient, ConnectionListener { // constants ------------------------------------------------------------------------------------------------------ protected static final ClientEvent POISON = new ClientEvent() { @Override public EventType getEventType() { return null; } }; // configuration defaults ----------------------------------------------------------------------------------------- public static final int CONNECTION_TIMEOUT = 10000; public static final int REQUEST_INACTIVITY_TIMEOUT = 10000; public static final boolean USE_SSL = false; public static final boolean USE_NIO = false; public static final boolean AUTO_DECOMPRESS = false; public static final int MAX_CONNECTIONS_PER_HOST = 3; public static final int MAX_QUEUED_REQUESTS = Short.MAX_VALUE; public static final int MAX_IO_WORKER_THREADS = 50; public static final int MAX_HELPER_THREADS = 20; public static final boolean CLEANUP_INACTIVE_HOST_CONTEXTS = true; // properties ----------------------------------------------------------------------------------------------------- @Getter private int connectionTimeout = CONNECTION_TIMEOUT; @Getter private int requestInactivityTimeout = REQUEST_INACTIVITY_TIMEOUT; @Getter private boolean useNio = USE_NIO; @Getter private boolean useSsl = USE_SSL; @Getter private int maxConnectionsPerHost = MAX_CONNECTIONS_PER_HOST; @Getter private int maxQueuedRequests = MAX_QUEUED_REQUESTS; @Getter private int maxIoWorkerThreads = MAX_IO_WORKER_THREADS; @Getter private int maxHelperThreads = MAX_HELPER_THREADS; @Getter private boolean autoDecompress = AUTO_DECOMPRESS; @Getter private boolean cleanupInactiveHostContexts = CLEANUP_INACTIVE_HOST_CONTEXTS; @Getter private ConnectionFactory connectionFactory; @Getter private TimeoutController timeoutController; @Getter private SslContextFactory sslContextFactory; // internal vars -------------------------------------------------------------------------------------------------- private final Map<String, HostController> hostControllers = new HashMap<>(); private final AtomicInteger queuedRequests = new AtomicInteger(0); private Executor executor; private ChannelFactory channelFactory; private ChannelPipelineFactory pipelineFactory; private ChannelGroup channelGroup; private BlockingQueue<ClientEvent> eventQueue; private int connectionCounter; private CountDownLatch eventConsumerLatch; private boolean internalTimeoutManager; private volatile boolean terminate; // HttpClient ----------------------------------------------------------------------------------------------------- @Override public boolean init() { if (timeoutController == null) { timeoutController = new HashedWheelTimeoutController(); // prefer lower resources consumption over precision timeoutController.init(); internalTimeoutManager = true; } if (connectionFactory == null) connectionFactory = new DefaultConnectionFactory(); if ((sslContextFactory == null) && isHttps()) sslContextFactory = BogusSslContextFactory.getInstance(); eventConsumerLatch = new CountDownLatch(1); eventQueue = new LinkedBlockingQueue<>(); // TODO instead of fixed size thread pool, use a cached thread pool with size limit (limited growth cached pool) executor = Executors.newFixedThreadPool(maxHelperThreads, new NamedThreadFactory("httpHelpers")); Executor workerPool = Executors.newFixedThreadPool(maxIoWorkerThreads, new NamedThreadFactory("httpWorkers")); if (useNio) { // It's only going to create 1 thread, so no harm done here. Executor bossPool = Executors.newCachedThreadPool(); channelFactory = new NioClientSocketChannelFactory(bossPool, workerPool); } else { channelFactory = new OioClientSocketChannelFactory(workerPool); } channelGroup = new CleanupChannelGroup(toString()); // Create a pipeline without the last handler (it will be added right before connecting). pipelineFactory = new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeline = Channels.pipeline(); if (useSsl) { SSLEngine engine = sslContextFactory.getClientContext().createSSLEngine(); engine.setUseClientMode(true); pipeline.addLast("ssl", new SslHandler(engine)); } pipeline.addLast("codec", new HttpClientCodec()); if (autoDecompress) pipeline.addLast("decompressor", new HttpContentDecompressor()); return pipeline; } }; executor.execute(new Runnable() { @Override public void run() { eventHandlingLoop(); } }); return true; } @Override public boolean isInitialized() { return (eventQueue != null) && !terminate; } @Override public void terminate() { if (terminate || eventQueue == null) return; // Stop accepting requests. terminate = true; // Copy any pending operations in order to signal execution request failures. Collection<ClientEvent> pendingEvents = new ArrayList<>(eventQueue); // Clear the queue and kill the consumer thread by "poisoning" the event queue. eventQueue.clear(); eventQueue.add(POISON); try { eventConsumerLatch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Fail all requests that were still in the event queue. for (ClientEvent event : pendingEvents) { switch (event.getEventType()) { case EXECUTE_REQUEST: ExecuteRequestEvent executeEvent = (ExecuteRequestEvent) event; executeEvent.getContext().getFuture().failedWithCause(SHUTTING_DOWN); break; case CONNECTION_CLOSED: ConnectionClosedEvent closedEvent = (ConnectionClosedEvent) event; if (closedEvent.hasRequestsToRetry()) { for (RequestContext context : closedEvent.getRetryRequests()) { context.getFuture().failedWithCause(SHUTTING_DOWN); } } break; } } // Kill all connections (will cause failure on requests executing in those connections) // and fail context-queued requests. for (HostController controller : hostControllers.values()) controller.shutdown(SHUTTING_DOWN); hostControllers.clear(); try { channelGroup.close().await(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } channelFactory.releaseExternalResources(); if (executor != null) ExecutorUtil.terminate(executor); if (internalTimeoutManager) timeoutController.terminate(); } @Override public RequestFuture<Object> execute(String host, int port, HttpRequest request) throws CannotExecuteRequestException { return executeWithDataSink(host, port, requestInactivityTimeout, request, DiscardProcessor.getInstance(), null); } @Override public <T> RequestFuture<T> execute(String host, int port, HttpRequest request, ResponseProcessor<T> processor) throws CannotExecuteRequestException { return executeWithDataSink(host, port, requestInactivityTimeout, request, processor, null); } @Override public <T> RequestFuture<T> execute(String host, int port, int timeout, HttpRequest request, ResponseProcessor<T> processor) throws CannotExecuteRequestException { return executeWithDataSink(host, port, timeout, request, processor, null); } @Override public <T> RequestFuture<T> executeWithDataSink(String host, int port, int timeout, HttpRequest request, ResponseProcessor<T> processor, DataSinkListener dataSinkListener) throws CannotExecuteRequestException { if (terminate) throw new CannotExecuteRequestException("HttpClient already terminated"); if (eventQueue == null) throw new CannotExecuteRequestException("HttpClient was not initialised"); if (dataSinkListener != null) { // Requests with data sink listener must be POST, PUT or PATCH (contain body) ensureValue((request.getMethod() == HttpMethod.POST) || (request.getMethod() == HttpMethod.PUT) || (request.getMethod() == HttpMethod.PATCH), "Requests with DataSink must be POST, PUT or PATCH"); } if (queuedRequests.incrementAndGet() > maxQueuedRequests) { queuedRequests.decrementAndGet(); throw new CannotExecuteRequestException("Request queue is full"); } // Perform these checks on the caller thread's time rather than the event dispatcher's. if (autoDecompress) setHeader(request, Names.ACCEPT_ENCODING, Values.GZIP); RequestContext<T> context = new RequestContext<>(host, port, timeout, request, processor); context.setDataSinkListener(dataSinkListener); if (!eventQueue.offer(new ExecuteRequestEvent(context))) { throw new CannotExecuteRequestException("Could not add request to queue"); } return context.getFuture(); } @Override public boolean isHttps() { return useSsl; } // ConnectionListener ----------------------------------------------------------------------------------------- @Override public void connectionOpened(Connection connection) { if (terminate) return; eventQueue.offer(new ConnectionOpenEvent(connection)); } @Override public void connectionTerminated(Connection connection, Collection<RequestContext> retryRequests) { if (terminate) { if ((retryRequests != null) && !retryRequests.isEmpty()) { for (RequestContext request : retryRequests) request.getFuture().failedWithCause(SHUTTING_DOWN); } } else { eventQueue.offer(new ConnectionClosedEvent(connection, retryRequests)); } } @Override public void connectionTerminated(Connection connection) { if (terminate) return; eventQueue.offer(new ConnectionClosedEvent(connection, null)); } @Override public void connectionFailed(Connection connection) { if (terminate) return; eventQueue.offer(new ConnectionFailedEvent(connection)); } @Override public void requestFinished(Connection connection, RequestContext context) { if (terminate) return; eventQueue.offer(new RequestCompleteEvent(context)); } // protected helpers ---------------------------------------------------------------------------------------------- protected ClientEvent popNextEvent() throws InterruptedException { return eventQueue.take(); } protected void eventQueuePoisoned() { eventConsumerLatch.countDown(); } protected void eventHandlingLoop() { while (true) { try { ClientEvent event = popNextEvent(); if (event == POISON) { eventQueuePoisoned(); return; } switch (event.getEventType()) { case EXECUTE_REQUEST: handleExecuteRequest((ExecuteRequestEvent) event); break; case REQUEST_COMPLETE: handleRequestComplete((RequestCompleteEvent) event); break; case CONNECTION_OPEN: handleConnectionOpen((ConnectionOpenEvent) event); break; case CONNECTION_CLOSED: handleConnectionClosed((ConnectionClosedEvent) event); break; case CONNECTION_FAILED: handleConnectionFailed((ConnectionFailedEvent) event); break; default: // Consume and do nothing, unknown event. } } catch (InterruptedException ignored) { /* poisoning the queue is the only way to stop this loop */ } } } // protected helpers ---------------------------------------------------------------------------------------------- protected void handleExecuteRequest(ExecuteRequestEvent event) { // First, add it to the queue (or create a queue for given host if one does not exist) String id = hostId(event.getContext()); HostController context = hostControllers.get(id); if (context == null) { context = HostController.createContextForRequest(event.getContext(), maxConnectionsPerHost); hostControllers.put(id, context); } context.addToQueue(event.getContext()); drainQueueAndProcessResult(context); } protected void handleRequestComplete(RequestCompleteEvent event) { queuedRequests.decrementAndGet(); HostController controller = hostControllers.get(hostId(event.getContext())); if (controller == null) return; // Can only happen if controller is cleaned meanwhile; just ignore. drainQueueAndProcessResult(controller); } protected void handleConnectionOpen(ConnectionOpenEvent event) { String id = hostId(event.getConnection()); HostController controller = hostControllers.get(id); controller.connectionOpen(event.getConnection()); // Rather than go through the whole process of drainQueue(), simply poll a single element from the head of // the queue into this connection (a newly opened connection is ALWAYS available). RequestContext nextRequest = controller.pollQueue(); if (nextRequest != null) event.getConnection().execute(nextRequest); } protected void handleConnectionClosed(ConnectionClosedEvent event) { // Update the list of available connections for the same host:port. String id = hostId(event.getConnection()); HostController controller = hostControllers.get(id); controller.connectionClosed(event.getConnection()); // Restore the requests to the queue prior to cleanup check - avoids unnecessary cleanup when there are request // to be retried. if (event.hasRequestsToRetry()) controller.restoreRequestsToQueue(event.getRetryRequests()); // If the pool has no connections and no requests are in queue for this host, then clean it up. // No requests in queue, no connections open or opening... Cleanup resources. if (controller.isCleanable() && cleanupInactiveHostContexts) hostControllers.remove(id); drainQueueAndProcessResult(controller); } protected void handleConnectionFailed(ConnectionFailedEvent event) { // Update the list of available connections for the same host:port. String id = hostId(event.getConnection()); HostController controller = hostControllers.get(id); controller.connectionFailed(); // If there are no more connections active or establishing we need to fail all queued requests. if (!controller.hasConnections()) controller.failAllRequests(CANNOT_CONNECT); } // private helpers ------------------------------------------------------------------------------------------------ private void drainQueueAndProcessResult(HostController controller) { HostController.DrainQueueResult result = controller.drainQueue(); switch (result) { case OPEN_CONNECTION: openConnection(controller); break; case QUEUE_EMPTY: case NOT_DRAINED: case DRAINED: default: } } private String hostId(Connection connection) { return hostId(connection.getHost(), connection.getPort()); } private String hostId(RequestContext context) { return hostId(context.getHost(), context.getPort()); } private String hostId(HostController context) { return hostId(context.getHost(), context.getPort()); } private String hostId(String host, int port) { return string(host, ":", port); } private void openConnection(final HostController controller) { // No need to recheck whether a connection can be opened or not, that was done already inside the HttpContext. // Try to create a pipeline before signalling a new connection is being open. // This should never throw exceptions but who knows... final ChannelPipeline pipeline = createChannelPipeline(); // Signal that a new connection is opening. controller.connectionOpening(); // server:port-X String id = new StringBuilder().append(hostId(controller)).append("-").append(connectionCounter++).toString(); // If not using NIO, then delegate the blocking write() call to the executor. Executor writeDelegator = useNio ? null : executor; final Connection connection = connectionFactory .createConnection(id, controller.getHost(), controller.getPort(), this, timeoutController, writeDelegator); pipeline.addLast("handler", connection); // Delegate actual connection to other thread, since calling connect is a blocking call. executor.execute(new Runnable() { @Override public void run() { ClientBootstrap bootstrap = new ClientBootstrap(channelFactory); bootstrap.setOption("reuseAddress", true); bootstrap.setOption("connectTimeoutMillis", connectionTimeout); bootstrap.setPipeline(pipeline); InetSocketAddress address = new InetSocketAddress(controller.getHost(), controller.getPort()); ChannelFuture future = bootstrap.connect(address); future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) channelGroup.add(future.getChannel()); } }); } }); } @SneakyThrows(Exception.class) // IDE still reports error, but it's OK... private ChannelPipeline createChannelPipeline() { return pipelineFactory.getPipeline(); } // getters & setters ---------------------------------------------------------------------------------------------- /** * Sets the connection to host timeout, in milliseconds. * <p/> * Defaults to 2000. * * @param connectionTimeoutInMillis Connection to host timeout, in milliseconds. */ public void setConnectionTimeout(int connectionTimeoutInMillis) { ensureState(eventQueue == null, "Cannot modify property after initialization"); ensureValue(connectionTimeoutInMillis >= 0, "connectionTimeoutInMillis must be >= 0 (0 means infinite)"); connectionTimeout = connectionTimeoutInMillis; } /** * Sets the default request timeout, in milliseconds. * <p/> * When {@link #execute(String, int, HttpRequest, com.biasedbit.http.client.processor.ResponseProcessor)} is * called (i.e. the variant without explicit request timeout) then this value is applied as the request timeout. * <p/> * Requests whose execution time exceeds (precision depends on the * {@link com.biasedbit.http.client.timeout.TimeoutController} chosen) this value will be considered failed and * their {@link com.biasedbit.http.client.future.RequestFuture} will be released with cause * {@link com.biasedbit.http.client.future.RequestFuture#TIMED_OUT}. * <p/> * Defaults to 2000. * * @param requestInactivityTimeoutInMillis Default request timeout, in milliseconds. */ public void setRequestInactivityTimeout(int requestInactivityTimeoutInMillis) { ensureState(eventQueue == null, "Cannot modify property after initialization"); ensureValue(requestInactivityTimeoutInMillis >= 0, "requestInactivityTimeoutInMillis must be >= 0 (0 means infinite)"); requestInactivityTimeout = requestInactivityTimeoutInMillis; } /** * Whether this client should use non-blocking IO (New I/O or NIO) or blocking IO (Plain Socket Old IO or OIO). * <p/> * NIO is generally better for higher throughput (scenarios with an elevated number of open connections) while OIO * is always better for latency (and scenarios where a low number of connections is open). * <p/> * If the number of connections open is not supposed to exceed 10~20, then use OIO as it typically presents better * results. * <p/> * Since the writes in OIO are blocking, the HTTP connections will delegate the call to * {@link org.jboss.netty.channel.Channel#write(Object)} to an executor (provided by this {@link HttpClient}). * <p/> * Defaults to {@code true}. * * @param useNio {@code true} if this client should use NIO, {@code false} if it should use OIO. */ public void setUseNio(boolean useNio) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.useNio = useNio; } /** * Whether this client should create SSL or non-SSL connections. * <p/> * All connections are affected by this flag. * <p/> * Defaults to {@code false}. * * @param useSsl {@code true} if all connections will have SSL support, {@code false} otherwise. */ public void setUseSsl(boolean useSsl) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.useSsl = useSsl; } /** * Sets the maximum number of active connections per host. * <p/> * This number also limits the number of connections being established so that * {@code connectionsOpen + connectionsOpening <= maxConnectionsPerHost} is always true. * <p/> * Defaults to 3. * * @param maxConnectionsPerHost Maximum number of total active connections (open + opening) per host at a given * time. Minimum value is 1. */ public void setMaxConnectionsPerHost(int maxConnectionsPerHost) { ensureState(eventQueue == null, "Cannot modify property after initialization"); ensureValue(maxConnectionsPerHost >= 1, "maxConnectionsPerHost must be >= 1"); this.maxConnectionsPerHost = maxConnectionsPerHost; } /** * Sets the maximum number of queued requests for this client. * <p/> * If the number of queued requests is exceeded, calling * {@linkplain #execute(String, int, HttpRequest, com.biasedbit.http.client.processor.ResponseProcessor) one of * the variants of {@code execute()}} will throw a {@link CannotExecuteRequestException}. * <p/> * Defaults to {@link Short#MAX_VALUE}. * * @param maxQueuedRequests Maximum number of queued requests at any given moment. */ public void setMaxQueuedRequests(int maxQueuedRequests) { ensureState(eventQueue == null, "Cannot modify property after initialization"); ensureValue(maxQueuedRequests > 1, "maxQueuedRequests must be > 1"); this.maxQueuedRequests = maxQueuedRequests; } /** * Maximum number of worker threads for the executor provided to Netty's {@link ChannelFactory}. * <p/> * Defaults to 50. * * @param maxIoWorkerThreads Maximum number of IO worker threads. */ public void setMaxIoWorkerThreads(int maxIoWorkerThreads) { ensureState(eventQueue == null, "Cannot modify property after initialization"); ensureValue(maxIoWorkerThreads > 1, "maxIoWorkerThreads must be > 1"); this.maxIoWorkerThreads = maxIoWorkerThreads; } /** * Maximum number of helper threads for the event processor. * <p/> * There are tasks performed by the internal event processor that are blocking and/or slow and need not be executed * in serial mode. Therefore the event processor delegates them to helper threads in order to keep doing what it's * supposed to do: consume events from the event queue. * <p/> * Defaults to 20. * * @param maxHelperThreads Maximum number of IO worker threads. */ public void setMaxHelperThreads(int maxHelperThreads) { ensureState(eventQueue == null, "Cannot modify property after initialization"); ensureValue(maxHelperThreads > 3, "maxHelperThreads must be > 3"); this.maxHelperThreads = maxHelperThreads; } /** * Whether responses should be auto inflated (decompressed) or not. * <p/> * Setting this flag to true will cause a 'Accept-Encoding' header with value 'gzip' to be added to the requests * submitted. * <p/> * Defaults to {@code true}. * * @param autoDecompress {@code true} if the connections should automatically decompress gzip content, * {@code false} otherwise. */ public void setAutoDecompress(boolean autoDecompress) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.autoDecompress = autoDecompress; } /** * Whether empty {@link HostController}s should be immediately cleaned up. * <p/> * When a {@linkplain HostController host controller} has no more queued requests nor active connections nor * connections opening, it is eligible for cleanup. Setting this flag to {@code true} will cause them to be * instantly reaped when such conditions are met. * <p/> * Unless your client will be performing requests to many different host/port combinations, you should set this flag * to {@code false}. While the overhead of creating/cleaning these contexts is minimal, it can be avoided in these * scenarios. * <p/> * Defaults to {@code true}. * * @param cleanupInactiveHostContexts {@code true} if inactive host contexts should be cleaned up, {@code false} * otherwise. * @see com.biasedbit.http.client.util.HostController */ public void setCleanupInactiveHostContexts(boolean cleanupInactiveHostContexts) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.cleanupInactiveHostContexts = cleanupInactiveHostContexts; } /** * The {@link com.biasedbit.http.client.connection.ConnectionFactory} that will be used to create new * {@link com.biasedbit.http.client.connection.Connection}. * <p/> * Defaults to {@link com.biasedbit.http.client.connection.DefaultConnectionFactory} if none is provided. * * @param connectionFactory The {@link com.biasedbit.http.client.connection.ConnectionFactory} to be used. * @see com.biasedbit.http.client.connection.ConnectionFactory * @see com.biasedbit.http.client.connection.Connection */ public void setConnectionFactory(ConnectionFactory connectionFactory) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.connectionFactory = connectionFactory; } /** * The {@link com.biasedbit.http.client.timeout.TimeoutController} that will be used to check request timeouts. * <p/> * If no instance is provided, a new instance is created upon calling {@link #init()}. This instance will be * automatically terminated when {@link #terminate()} is called. * <p/> * If an external {@link com.biasedbit.http.client.timeout.TimeoutController} is provided, then it must be * pre-initialised (i.e. its {@link com.biasedbit.http.client.timeout.TimeoutController#init()} must be called and * return {@code true}) and it must be post-terminated (i.e. its * {@link com.biasedbit.http.client.timeout.TimeoutController#terminate()} must be called after this instance * of {@link HttpClient} is disposed). * <p/> * Defaults to a new instance of {@link com.biasedbit.http.client.timeout.HashedWheelTimeoutController}. * * @param timeoutController The {@link com.biasedbit.http.client.timeout.TimeoutController} instance to use. * @see com.biasedbit.http.client.timeout.TimeoutController */ public void setTimeoutController(TimeoutController timeoutController) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.timeoutController = timeoutController; } public void setSslContextFactory(SslContextFactory sslContextFactory) { ensureState(eventQueue == null, "Cannot modify property after initialization"); this.sslContextFactory = sslContextFactory; } }