Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #6514 - How to warm up SslConnection. #12151

Merged
merged 4 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,31 @@ public void setConnectionPool() throws Exception
// end::setConnectionPool[]
}

public void preCreateConnections() throws Exception
{
// tag::preCreateConnections[]
HttpClient httpClient = new HttpClient();
httpClient.start();

// For HTTP/1.1, you need to explicitly configure to initialize connections.
if (httpClient.getTransport() instanceof HttpClientTransportOverHTTP http1)
http1.setInitializeConnections(true);

// Create a dummy request to the server you want to pre-create connections to.
Request request = httpClient.newRequest("https://host/");

// Resolve the destination for that request.
Destination destination = httpClient.resolveDestination(request);

// Pre-create, for example, half of the connections.
int preCreate = httpClient.getMaxConnectionsPerDestination() / 2;
CompletableFuture<Void> completable = destination.getConnectionPool().preCreateConnections(preCreate);

// Wait for the connections to be created.
completable.get(5, TimeUnit.SECONDS);
// end::preCreateConnections[]
}

public void unixDomain() throws Exception
{
// tag::unixDomain[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Jetty's client library provides the following `ConnectionPool` implementations:
* `DuplexConnectionPool`, historically the first implementation, only used by the HTTP/1.1 transport.
* `MultiplexConnectionPool`, the generic implementation valid for any transport where connections are reused with a most recently used algorithm (that is, the connections most recently returned to the connection pool are the more likely to be used again).
* `RoundRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with a round-robin algorithm.
* `RandomRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly.
* `RandomConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly.

The `ConnectionPool` implementation can be customized for each destination in by setting a `ConnectionPool.Factory` on the `HttpClientTransport`:

Expand All @@ -167,6 +167,34 @@ The `ConnectionPool` implementation can be customized for each destination in by
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=setConnectionPool]
----

[[connection-pool-precreate-connections]]
=== Pre-Creating Connections

`ConnectionPool` offers the ability to pre-create connections by calling `ConnectionPool.preCreateConnections(int)`.

Pre-creating the connections saves the time and processing spent to establish the TCP connection, performing the TLS handshake (if necessary) and, for HTTP/2 and HTTP/3, perform the initial protocol setup.
This is particularly important for HTTP/2 because in the initial protocol setup the server informs the client of the maximum number of concurrent requests per connection (otherwise assumed to be just `1` by the client).

The scenarios where pre-creating connections is useful are, for example:

* Load testing, where you want to prepare the system with connections already created to avoid paying of cost of connection setup.
* Proxying scenarios, often in conjunction with the use of `RoundRobinConnectionPool` or `RandomConnectionPool`, where the proxy creates early the connections to the backend servers.

This is an example of how to pre-create connections:

[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=preCreateConnections]
----

[NOTE]
====
Pre-creating connections for secure HTTP/1.1 requires you to call `HttpClientTransportOverHTTP.setInitializeConnections(true)`, otherwise only the TCP connection is established, but the TLS handshake is not initiated.

To initialize connections for secure HTTP/1.1, the client sends an initial `OPTIONS * HTTP/1.1` request to the server.
The server must be able to handle this request without closing the connection (in particular it must not add the `Connection: close` header in the response).
====

[[request-processing]]
== HttpClient Request Processing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,23 @@ public class HttpClientConnectionFactory implements ClientConnectionFactory
*/
public static final Info HTTP11 = new HTTP11(new HttpClientConnectionFactory());

private boolean initializeConnections;

public boolean isInitializeConnections()
{
return initializeConnections;
}

public void setInitializeConnections(boolean initialize)
{
this.initializeConnections = initialize;
}

@Override
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context)
{
HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, context);
connection.setInitialize(isInitializeConnections());
return customize(connection, context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.eclipse.jetty.client.DuplexConnectionPool;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ProcessorUtils;
Expand All @@ -37,7 +36,7 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
public static final Origin.Protocol HTTP11 = new Origin.Protocol(List.of("http/1.1"), false);
private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportOverHTTP.class);

private final ClientConnectionFactory factory = new HttpClientConnectionFactory();
private final HttpClientConnectionFactory factory = new HttpClientConnectionFactory();
private int headerCacheSize = 1024;
private boolean headerCacheCaseSensitive;

Expand Down Expand Up @@ -100,4 +99,14 @@ public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
this.headerCacheCaseSensitive = headerCacheCaseSensitive;
}

public boolean isInitializeConnections()
{
return factory.isInitializeConnections();
}

public void setInitializeConnections(boolean initialize)
{
factory.setInitializeConnections(initialize);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.concurrent.atomic.LongAdder;

import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.Request;
Expand All @@ -40,6 +41,7 @@
import org.eclipse.jetty.client.transport.IConnection;
import org.eclipse.jetty.client.transport.SendFailure;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint;
Expand All @@ -61,6 +63,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
private final LongAdder bytesIn = new LongAdder();
private final LongAdder bytesOut = new LongAdder();
private long idleTimeout;
private boolean initialize;

public HttpConnectionOverHTTP(EndPoint endPoint, Map<String, Object> context)
{
Expand Down Expand Up @@ -159,12 +162,46 @@ public SendFailure send(HttpExchange exchange)
return delegate.send(exchange);
}

/**
* @return whether to initialize the connection with an {@code OPTIONS * HTTP/1.1} request.
*/
public boolean isInitialize()
{
return initialize;
}

/**
* @param initialize whether to initialize the connection with an {@code OPTIONS * HTTP/1.1} request.
*/
public void setInitialize(boolean initialize)
sbordet marked this conversation as resolved.
Show resolved Hide resolved
{
this.initialize = initialize;
}

@Override
public void onOpen()
{
super.onOpen();
fillInterested();
promise.succeeded(this);
boolean initialize = isInitialize();
if (initialize)
{
Destination destination = getHttpDestination();
Request request = destination.getHttpClient().newRequest(destination.getOrigin().asString())
.method(HttpMethod.OPTIONS)
.path("*");
sbordet marked this conversation as resolved.
Show resolved Hide resolved
send(request, result ->
{
if (result.isSucceeded())
promise.succeeded(this);
else
promise.failed(result.getFailure());
});
}
else
{
promise.succeeded(this);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ public void testCountersSweepToStringThroughLifecycle(ConnectionPoolFactory fact
assertThat(connectionPool.toString(), not(nullValue()));
}

private static class ConnectionPoolFactory
public static class ConnectionPoolFactory
{
private final String name;
private final ConnectionPool.Factory factory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ protected SslContextFactory.Server newSslContextFactoryServer()
}

protected void startClient(Transport transport) throws Exception
{
prepareClient(transport);
client.start();
}

protected void prepareClient(Transport transport) throws Exception
{
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
Expand All @@ -298,7 +304,6 @@ protected void startClient(Transport transport) throws Exception
client.setByteBufferPool(clientBufferPool);
client.setExecutor(clientThreads);
client.setSocketAddressResolver(new SocketAddressResolver.Sync());
client.start();
}

public AbstractConnector newConnector(Transport transport, Server server)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.test.client.transport;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.fcgi.server.internal.ServerFCGIConnection;
import org.eclipse.jetty.http2.server.internal.HTTP2ServerConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.quic.server.ServerQuicConnection;
import org.eclipse.jetty.server.internal.HttpConnection;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.not;

public class ConnectionPoolTest extends AbstractTest
{
@ParameterizedTest
@MethodSource("transports")
public void testPreCreateConnections(Transport transport) throws Exception
{
prepareServer(transport, new EmptyServerHandler());
ConnectionListener serverConnections = new ConnectionListener();
connector.addBean(serverConnections);
server.start();

startClient(transport);
client.setMaxConnectionsPerDestination(8);
if (transport == Transport.HTTPS)
((HttpClientTransportOverHTTP)client.getTransport()).setInitializeConnections(true);

var request = client.newRequest(newURI(transport));
Destination destination = client.resolveDestination(request);
destination.getConnectionPool().preCreateConnections(client.getMaxConnectionsPerDestination())
.get(5, TimeUnit.SECONDS);

// Verify that connections have been created.
List<Connection> connections = switch (transport)
{
case HTTP, HTTPS -> serverConnections.filter(HttpConnection.class);
case H2C, H2 -> serverConnections.filter(HTTP2ServerConnection.class);
case H3 -> serverConnections.filter(ServerQuicConnection.class);
case FCGI -> serverConnections.filter(ServerFCGIConnection.class);
};
assertThat(connections, not(empty()));

// Verify that TLS was performed.
List<Connection> sslConnections = switch (transport)
{
case HTTP, H2C, FCGI, H3 -> null;
case HTTPS, H2 -> serverConnections.filter(SslConnection.class);
};
if (sslConnections != null)
{
assertThat(sslConnections.size(), greaterThan(0));
sslConnections.forEach(c -> assertThat(c.getBytesIn(), greaterThan(0L)));
sslConnections.forEach(c -> assertThat(c.getBytesOut(), greaterThan(0L)));
}
}

private static class ConnectionListener implements Connection.Listener
{
private final List<Connection> connections = new ArrayList<>();

@Override
public void onOpened(Connection connection)
{
connections.add(connection);
}

@Override
public void onClosed(Connection connection)
{
connections.remove(connection);
}

private List<Connection> filter(Class<? extends Connection> klass)
{
return connections.stream()
.filter(klass::isInstance)
.toList();
}
}
}
Loading