/* * 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.util; import com.biasedbit.http.client.connection.Connection; import lombok.*; import java.util.Collection; import java.util.LinkedList; import static com.biasedbit.http.client.util.HostController.DrainQueueResult.*; import static com.biasedbit.http.client.util.Utils.ensureValue; /** * HostController stores context on a per-host basis, serving as a helper component to help keep HttpClient * implementations cleaner. * * @author <a href="http://biasedbit.com/">Bruno de Carvalho</a> */ @RequiredArgsConstructor public class HostController { // enums ---------------------------------------------------------------------------------------------------------- public static enum DrainQueueResult {QUEUE_EMPTY, DRAINED, NOT_DRAINED, OPEN_CONNECTION} // properties ----------------------------------------------------------------------------------------------------- @Getter private final String host; @Getter private final int port; @Delegate private final ConnectionPool connectionPool; // exposes connection pool API on this class // internal vars -------------------------------------------------------------------------------------------------- protected final LinkedList<RequestContext> queue = new LinkedList<>(); // class interface ------------------------------------------------------------------------------------------------ public static HostController createContextForRequest(RequestContext context, int maxConnections) { return new HostController(context.getHost(), context.getPort(), new ConnectionPool(maxConnections)); } // interface ------------------------------------------------------------------------------------------------------ public boolean isCleanable() { return !connectionPool.hasConnections() && queue.isEmpty(); } /** * Used to restore requests to the head of the queue. * <p/> * An example of the usage of this method is when a pipelining HTTP connection disconnects unexpectedly while some * of the requests were still waiting for a response. Instead of simply failing those requests, they can be retried * in a different connection, provided that their execution order is maintained (i.e. they go back to the head of * the queue and not the tail). * * @param requests Collection of requests to add to the queue head. */ public void restoreRequestsToQueue(Collection<RequestContext> requests) { queue.addAll(0, requests); } /** * Adds a request to the end of the queue. * * @param request Request to add. */ public void addToQueue(RequestContext request) { ensureValue(request.getHost().equals(host), "Request host (%s) and context host (%s) are different", request.getHost(), host); ensureValue(request.getPort() == port, "Request port (%s) and context port (%s) are different", request.getPort(), port); queue.add(request); } /** * Drains one (or more) elements of the queue into one (or more) connections in the connection pool. * <p/> * This is the method used to move queue elements into connections and thus advance the request dispatching process. * * @return The result of the drain operation. */ public DrainQueueResult drainQueue() { // 1. Test if there's anything to drain if (queue.isEmpty()) return QUEUE_EMPTY; // 2. There are contents to drain, test if there are any connections created. if (!connectionPool.hasConnections()) { // 2a. No connections open, test if there is still room to create a new one. if (connectionPool.hasAvailableSlots()) return OPEN_CONNECTION; else return NOT_DRAINED; } // 3. There is content to drain and there are connections, drain as much as possible in a single loop. boolean drained = false; for (Connection connection : connectionPool.getConnections()) { // Connection not available, immediately try next one if (!connection.isAvailable()) continue; // Feed requests off the queue to the connection until it stops reporting itself as available or // execution submission fails. boolean executionAccepted = false; do { // Peek the next request and see if the connection is able to accept it. RequestContext context = queue.peek(); executionAccepted = connection.execute(context); if (executionAccepted) { // Request was accepted by the connection, remove it from the queue. queue.remove(); // Prematurely exit in case there are no further requests to execute. if (queue.isEmpty()) return DRAINED; // Otherwise, result will be DRAINED whether we manage do execute another request or not. drained = true; } } while (connection.isAvailable() && executionAccepted); } if (drained) return DRAINED; // 4. There were connections open but none of them was available; if possible, request a new one. if (connectionPool.hasAvailableSlots()) return OPEN_CONNECTION; else return NOT_DRAINED; } /** * Retrieves the first element of the queue (head). * * @return The first element of the queue. */ public RequestContext pollQueue() { return queue.poll(); } /** * Fails all queued requests with the given cause. * * @param cause Cause to fail all queued requests. */ public void failAllRequests(Throwable cause) { for (RequestContext context : queue) context.getFuture().failedWithCause(cause); queue.clear(); } public void shutdown(Throwable cause) { failAllRequests(cause); for (Connection connection : connectionPool.getConnections()) connection.terminate(cause); } }