Skip to content

Commit

Permalink
Adds ability to close idle connections after a certain time using con…
Browse files Browse the repository at this point in the history
…neciton-idle-timeout.

Signed-off-by: Santiago Pericas-Geertsen <[email protected]>
  • Loading branch information
spericas committed Aug 26, 2024
1 parent 1e8a953 commit 308e621
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 5 deletions.
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 @@ -36,6 +36,7 @@
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.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
Expand All @@ -51,6 +52,8 @@
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
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.AsciiString;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;
Expand Down Expand Up @@ -218,6 +221,20 @@ public void initChannel(SocketChannel ch) {
requestDecoder,
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 @@ -211,6 +211,11 @@ public boolean requestedUriDiscoveryEnabled() {
return isRequestedUriDiscoveryEnabled;
}

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

static class SocketConfig implements SocketConfiguration {

private final int port;
Expand All @@ -234,6 +239,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 @@ -261,6 +267,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 @@ -387,5 +394,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 @@ -619,6 +619,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 @@ -273,6 +273,16 @@ default boolean continueImmediately() {
*/
AllowList trustedProxies();

/**
* Timeout seconds after which any idle connection will be automatically closed
* 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 @@ -574,6 +584,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 seconds after which an idle connection will be automatically
* 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 @@ -626,6 +645,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 @@ -671,6 +693,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 @@ -958,6 +981,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 @@ -981,6 +1005,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 @@ -1072,6 +1102,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 @@ -701,6 +701,12 @@ public Builder experimental(ExperimentalConfiguration experimental) {
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,108 @@
/*
* 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.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.is;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;

/**
* 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(Routing.builder().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!"));

// second request while connection is active
client.request(Http.Method.GET,
"/hello",
null);
res = client.receive();
assertThat(res, containsString("Hello World!"));

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

// try again and either get nothing or an exception
try {
client.request(Http.Method.GET,
"/hello",
null);
res = client.receive();
assertThat(res, is(""));
} catch (SocketException e) {
// falls through as possible outcome
}
}
}
}

0 comments on commit 308e621

Please sign in to comment.