/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.jdk.connector;
import java.io.IOException;
import java.net.CookieManager;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ScheduledExecutorService;
/**
* @author Petr Janouch (petr.janouch at oracle.com)
*/
class DestinationConnectionPool {
private final ConnectorConfiguration configuration;
private final Queue<HttpConnection> idleConnections = new ConcurrentLinkedDeque<>();
private final Set<HttpConnection> connections = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Queue<RequestRecord> pendingRequests = new ConcurrentLinkedDeque<>();
private final Map<HttpConnection, RequestRecord> requestsInProgress = new HashMap<>();
private final CookieManager cookieManager;
private final ScheduledExecutorService scheduler;
private final ConnectionStateListener connectionStateListener;
private volatile ConnectionCloseListener connectionCloseListener;
private int connectionCounter = 0;
private boolean closed = false;
DestinationConnectionPool(ConnectorConfiguration configuration,
CookieManager cookieManager,
ScheduledExecutorService scheduler) {
this.configuration = configuration;
this.cookieManager = cookieManager;
this.scheduler = scheduler;
this.connectionStateListener = new ConnectionStateListener();
}
void setConnectionCloseListener(ConnectionCloseListener connectionCloseListener) {
this.connectionCloseListener = connectionCloseListener;
}
void send(HttpRequest httpRequest, CompletionHandler<HttpResponse> completionHandler) {
pendingRequests.add(new RequestRecord(httpRequest, completionHandler));
processPendingRequests();
}
private void processPendingRequests() {
HttpConnection connection;
HttpRequest httpRequest;
CompletionHandler<HttpResponse> completionHandler;
synchronized (this) {
/* this is synchronized so that another thread does not steal the pending request at the head of the queue
while we investigate if we can execute it. */
RequestRecord pendingHead = pendingRequests.peek();
if (pendingHead == null) {
// no pending requests
return;
}
httpRequest = pendingHead.request;
completionHandler = pendingHead.completionHandler;
connection = idleConnections.poll();
if (connection != null) {
pendingRequests.poll();
}
}
if (connection != null) {
// if there was a connection available just use it
requestsInProgress.put(connection, new RequestRecord(httpRequest, completionHandler));
connection.send(httpRequest);
return;
}
// if there was not a connection available keep this requests in pending list and try to create a connection
synchronized (this) {
// synchronized because other thread might open/close connections, so we have to make sure we get the limits right.
if (configuration.getMaxConnectionsPerDestination() == connectionCounter) {
// we are at the limit for this destination, just wait for a connection to become idle or close
return;
}
// create a connection
connection = new HttpConnection(httpRequest.getUri(), cookieManager, configuration, scheduler,
connectionStateListener);
connections.add(connection);
connectionCounter++;
}
// we don't want to connect inside the synchronized block
connection.connect();
}
synchronized void close() {
if (closed) {
return;
}
closed = true;
connections.forEach(HttpConnection::close);
}
private RequestRecord getRequest(HttpConnection connection) {
RequestRecord requestRecord = requestsInProgress.get(connection);
if (requestRecord == null) {
throw new IllegalStateException("Request not found");
}
return requestRecord;
}
private RequestRecord removeRequest(HttpConnection connection) {
RequestRecord requestRecord = requestsInProgress.get(connection);
if (requestRecord == null) {
throw new IllegalStateException("Request not found");
}
return requestRecord;
}
private void handleIdleConnection(HttpConnection connection) {
idleConnections.add(connection);
processPendingRequests();
}
private void cleanClosedConnection(HttpConnection connection) {
if (closed) {
return;
}
RequestRecord pendingRequest;
synchronized (this) {
idleConnections.remove(connection);
connections.remove(connection);
connectionCounter--;
pendingRequest = pendingRequests.peek();
if (pendingRequest == null) {
if (connectionCounter == 0) {
connectionCloseListener.onLastConnectionClosed();
}
return;
}
}
processPendingRequests();
}
private void handleIllegalStateTransition(HttpConnection.State oldState, HttpConnection.State newState) {
throw new IllegalStateException("Illegal state transition, old state: " + oldState + " new state: " + newState);
}
private synchronized void removeAllPendingWithError(Throwable t) {
for (RequestRecord requestRecord : pendingRequests) {
requestRecord.completionHandler.failed(t);
}
pendingRequests.clear();
}
private class ConnectionStateListener implements HttpConnection.StateChangeListener {
@Override
public void onStateChanged(HttpConnection connection, HttpConnection.State oldState, HttpConnection.State newState) {
switch (newState) {
case IDLE: {
switch (oldState) {
case RECEIVED:
case CONNECTING: {
handleIdleConnection(connection);
return;
}
default: {
handleIllegalStateTransition(oldState, newState);
return;
}
}
}
case RECEIVED: {
switch (oldState) {
case RECEIVING_HEADER: {
RequestRecord request = removeRequest(connection);
request.completionHandler.completed(connection.getHttResponse());
return;
}
case RECEIVING_BODY: {
removeRequest(connection);
return;
}
default: {
handleIllegalStateTransition(oldState, newState);
return;
}
}
}
case RECEIVING_BODY: {
switch (oldState) {
case RECEIVING_HEADER: {
RequestRecord request = getRequest(connection);
request.response = connection.getHttResponse();
request.completionHandler.completed(connection.getHttResponse());
return;
}
default: {
handleIllegalStateTransition(oldState, newState);
return;
}
}
}
case ERROR: {
switch (oldState) {
case SENDING_REQUEST: {
RequestRecord request = removeRequest(connection);
request.completionHandler.failed(connection.getError());
return;
}
case RECEIVING_HEADER: {
RequestRecord request = removeRequest(connection);
request.completionHandler.failed(connection.getError());
return;
}
case RECEIVING_BODY: {
requestsInProgress.remove(connection);
return;
}
case CONNECTING: {
removeAllPendingWithError(connection.getError());
return;
}
default: {
connection.getError().printStackTrace();
handleIllegalStateTransition(oldState, newState);
return;
}
}
}
case RESPONSE_TIMEOUT: {
switch (oldState) {
case RECEIVING_HEADER: {
RequestRecord request = removeRequest(connection);
request.completionHandler
.failed(new IOException(LocalizationMessages.TIMEOUT_RECEIVING_RESPONSE()));
return;
}
case RECEIVING_BODY: {
RequestRecord request = requestsInProgress.remove(connection);
request.response.getBodyStream()
.notifyError(new IOException(LocalizationMessages.TIMEOUT_RECEIVING_RESPONSE_BODY()));
return;
}
default: {
handleIllegalStateTransition(oldState, newState);
return;
}
}
}
case CLOSED_BY_SERVER: {
switch (oldState) {
case SENDING_REQUEST: {
RequestRecord request = removeRequest(connection);
request.completionHandler
.failed(new IOException(LocalizationMessages.CLOSED_WHILE_SENDING_REQUEST()));
return;
}
case RECEIVING_HEADER: {
RequestRecord request = removeRequest(connection);
request.completionHandler
.failed(new IOException(LocalizationMessages.CLOSED_WHILE_RECEIVING_RESPONSE(),
connection.getError()));
return;
}
case RECEIVING_BODY: {
RequestRecord request = requestsInProgress.remove(connection);
request.response.getBodyStream().notifyError(
new IOException(LocalizationMessages.CLOSED_WHILE_RECEIVING_BODY(),
connection.getError()));
return;
}
case CONNECTING: {
removeAllPendingWithError(new IOException(LocalizationMessages.CONNECTION_CLOSED()));
return;
}
}
}
case CLOSED: {
switch (oldState) {
case SENDING_REQUEST: {
RequestRecord request = removeRequest(connection);
request.completionHandler
.failed(new IOException(LocalizationMessages.CLOSED_BY_CLIENT_WHILE_SENDING()));
cleanClosedConnection(connection);
return;
}
case RECEIVING_HEADER: {
RequestRecord request = removeRequest(connection);
request.completionHandler
.failed(new IOException(LocalizationMessages.CLOSED_WHILE_RECEIVING_RESPONSE()));
cleanClosedConnection(connection);
return;
}
case RECEIVING_BODY: {
RequestRecord request = requestsInProgress.remove(connection);
request.response.getBodyStream().notifyError(
new IOException(LocalizationMessages.CLOSED_BY_CLIENT_WHILE_RECEIVING_BODY(),
connection.getError()));
cleanClosedConnection(connection);
return;
}
default: {
cleanClosedConnection(connection);
return;
}
}
}
case CONNECT_TIMEOUT: {
switch (oldState) {
case CONNECTING: {
removeAllPendingWithError(new IOException(LocalizationMessages.CONNECTION_TIMEOUT()));
return;
}
default: {
cleanClosedConnection(connection);
}
}
}
}
}
}
private static class RequestRecord {
private final HttpRequest request;
private final CompletionHandler<HttpResponse> completionHandler;
private HttpResponse response;
public RequestRecord(HttpRequest request, CompletionHandler<HttpResponse> completionHandler) {
this.request = request;
this.completionHandler = completionHandler;
}
}
static class DestinationKey {
private final String host;
private final int port;
private final boolean secure;
DestinationKey(URI uri) {
host = uri.getHost();
port = Utils.getPort(uri);
secure = Constants.HTTPS.equalsIgnoreCase(uri.getScheme());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DestinationKey that = (DestinationKey) o;
return port == that.port && secure == that.secure && host.equals(that.host);
}
@Override
public int hashCode() {
int result = host.hashCode();
result = 31 * result + port;
result = 31 * result + (secure ? 1 : 0);
return result;
}
}
interface ConnectionCloseListener {
void onLastConnectionClosed();
}
}