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

3.x: Adds ability to close idle HTTP connections after a certain time #9194

Merged
merged 2 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2022 Oracle and/or its affiliates.
* Copyright (c) 2017, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,6 +33,8 @@
import io.helidon.webserver.ReferenceHoldingQueue.IndirectReference;

import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
Expand All @@ -42,6 +44,8 @@
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;

Expand Down Expand Up @@ -193,6 +197,20 @@ public void initChannel(SocketChannel ch) {
directHandlers));
}

// Set up idle handler to close inactive connections based on config
int idleTimeout = serverConfig.connectionIdleTimeout();
if (idleTimeout > 0) {
p.addLast(new IdleStateHandler(idleTimeout, idleTimeout, idleTimeout));
p.addLast(new ChannelDuplexHandler() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
ctx.close(); // async close of idle connection
}
}
});
}

// Cleanup queues as part of event loop
ch.eventLoop().execute(this::clearQueues);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
* Copyright (c) 2017, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -208,6 +208,11 @@ public boolean requestedUriDiscoveryEnabled() {
return isRequestedUriDiscoveryEnabled;
}

@Override
public int connectionIdleTimeout() {
return socketConfig.connectionIdleTimeout();
}

static class SocketConfig implements SocketConfiguration {

private final int port;
Expand All @@ -232,6 +237,7 @@ static class SocketConfig implements SocketConfiguration {
private final List<RequestedUriDiscoveryType> requestedUriDiscoveryTypes;
private final AllowList trustedProxies;
private final boolean isRequestedUriDiscoveryEnabled;
private final int connectionIdleTimeout;

/**
* Creates new instance.
Expand Down Expand Up @@ -260,6 +266,7 @@ static class SocketConfig implements SocketConfiguration {
this.requestedUriDiscoveryTypes = builder.requestedUriDiscoveryTypes();
this.trustedProxies = builder.trustedProxies();
this.isRequestedUriDiscoveryEnabled = builder.requestedUriDiscoveryEnabled();
this.connectionIdleTimeout = builder.connectionIdleTimeout();
}

@Override
Expand Down Expand Up @@ -395,5 +402,10 @@ public AllowList trustedProxies() {
public boolean requestedUriDiscoveryEnabled() {
return isRequestedUriDiscoveryEnabled;
}

@Override
public int connectionIdleTimeout() {
return connectionIdleTimeout;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
* Copyright (c) 2017, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -606,6 +606,12 @@ public Builder trustedProxies(AllowList trustedProxies) {
return this;
}

@Override
public Builder connectionIdleTimeout(int seconds) {
defaultSocketBuilder().connectionIdleTimeout(seconds);
return this;
}

/**
* Configure the maximum amount of time that the server will wait to shut
* down regardless of the value of any additionally requested
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
* Copyright (c) 2017, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -296,6 +296,16 @@ default int maxUpgradeContentLength() {
return 64 * 1024;
}

/**
* Timeout millis after which any idle connection will be automatically closed
spericas marked this conversation as resolved.
Show resolved Hide resolved
* by the server.
*
* @return idle connection timeout in seconds
*/
default int connectionIdleTimeout() {
return 0;
}

/**
* Types of discovery of frontend uri. Defaults to {@link #HOST} when frontend uri discovery is disabled (uses only Host
* header and information about current request to determine scheme, host, port, and path).
Expand Down Expand Up @@ -587,6 +597,15 @@ default B tls(Supplier<WebServerTls> tlsConfig) {
@ConfiguredOption(key = REQUESTED_URI_DISCOVERY_CONFIG_KEY_PREFIX + "trusted-proxies")
B trustedProxies(AllowList trustedProxies);

/**
* Sets the number of millis after which an idle connection will be automatically
spericas marked this conversation as resolved.
Show resolved Hide resolved
* closed by the server.
*
* @param seconds time in seconds
* @return updated builder
*/
B connectionIdleTimeout(int seconds);

/**
* Update this socket configuration from a {@link io.helidon.config.Config}.
*
Expand Down Expand Up @@ -639,6 +658,9 @@ default B config(Config config) {
config.get("requested-uri-discovery.trusted-proxies").as(AllowList::create)
.ifPresent(this::trustedProxies);

// idle connections
config.get("connection-idle-timeout").asInt().ifPresent(this::connectionIdleTimeout);

return (B) this;
}
}
Expand Down Expand Up @@ -683,6 +705,7 @@ final class Builder implements SocketConfigurationBuilder<Builder>, io.helidon.c
private final List<RequestedUriDiscoveryType> requestedUriDiscoveryTypes = new ArrayList<>();
private Boolean requestedUriDiscoveryEnabled;
private AllowList trustedProxies;
private int connectionIdleTimeout;

private Builder() {
}
Expand Down Expand Up @@ -977,6 +1000,7 @@ public Builder config(Config config) {
config.get("enable-compression").asBoolean().ifPresent(this::enableCompression);
config.get("backpressure-buffer-size").asLong().ifPresent(this::backpressureBufferSize);
config.get("backpressure-strategy").as(BackpressureStrategy.class).ifPresent(this::backpressureStrategy);
config.get("connection-idle-timeout").asInt().ifPresent(this::connectionIdleTimeout);

return this;
}
Expand All @@ -1000,6 +1024,12 @@ public Builder requestedUriDiscoveryEnabled(boolean enabled) {
return this;
}

@Override
public Builder connectionIdleTimeout(int seconds) {
this.connectionIdleTimeout = seconds;
return this;
}

int port() {
return port;
}
Expand Down Expand Up @@ -1091,6 +1121,10 @@ boolean requestedUriDiscoveryEnabled() {
return requestedUriDiscoveryEnabled;
}

int connectionIdleTimeout() {
return connectionIdleTimeout;
}

/**
* Checks validity of requested URI settings and supplies defaults for omitted settings.
* <p>The behavior of `requested-uri-discovery` settings can be summarized as follows:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
* Copyright (c) 2017, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -639,6 +639,12 @@ public Builder trustedProxies(AllowList trustedProxies) {
return this;
}

@Override
public Builder connectionIdleTimeout(int seconds) {
defaultSocket(it -> it.connectionIdleTimeout(seconds));
return this;
}

/**
* A helper method to support fluentAPI when invoking another method.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* 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 io.helidon.webserver;

import java.net.SocketException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Logger;

import io.helidon.common.http.Http;
import io.helidon.webserver.utils.SocketHttpClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

/**
* Tests support for idle connection timeouts.
*/
public class ConnectionIdleTest {
private static final Logger LOGGER = Logger.getLogger(ConnectionIdleTest.class.getName());
private static final Duration TIMEOUT = Duration.ofSeconds(10);

private static final int IDLE_TIMEOUT = 1000;

private static WebServer webServer;

@BeforeAll
public static void startServer() throws Exception {
startServer(0);
}

@AfterAll
public static void close() throws Exception {
if (webServer != null) {
webServer.shutdown().await(TIMEOUT);
}
}

/**
* Start the Web Server
*
* @param port the port on which to start the server
*/
private static void startServer(int port) {
webServer = WebServer.builder()
.host("localhost")
.port(port)
.connectionIdleTimeout(IDLE_TIMEOUT / 1000) // in seconds
.routing(r -> r.get("/hello", (req, res) -> res.send("Hello World!")))
.build()
.start()
.await(TIMEOUT);

LOGGER.info("Started server at: https://localhost:" + webServer.port());
}

@Test
public void testIdleConnectionClosed() throws Exception {
try (SocketHttpClient client = new SocketHttpClient(webServer)) {
// initial request with keep-alive to open connection
client.request(Http.Method.GET,
"/hello",
null,
List.of("Connection: keep-alive"));
String res = client.receive();
assertThat(res, containsString("Hello World!"));

// wait for connection to time out due to inactivity
Thread.sleep(2 * IDLE_TIMEOUT);

// now fail attempting to use connection again
assertEventuallyThrows(SocketException.class, () -> {
client.request(Http.Method.GET,
"/hello",
null);
client.receive();
return null;
}, 5 * IDLE_TIMEOUT);
}
}

private static void assertEventuallyThrows(Class<?> exc, Callable<?> runnable, long millis)
throws InterruptedException {
long start = System.currentTimeMillis();
do {
try {
runnable.call();
} catch (Throwable t) {
if (t.getClass().equals(exc)) {
return;
}
}
Thread.sleep(millis / 3);
} while (System.currentTimeMillis() - start <= millis);
fail("Predicate failed after " + millis + " milliseconds");
}
}
Loading