From f30dba20f244cbe32f5ae0371e6266a4d68aeef3 Mon Sep 17 00:00:00 2001 From: "bodong.ybd" Date: Fri, 1 Sep 2023 17:00:20 +0800 Subject: [PATCH] Support TCP_USER_TIMEOUT option --- .../io/lettuce/core/ConnectionBuilder.java | 12 ++ .../java/io/lettuce/core/SocketOptions.java | 117 ++++++++++++++++++ .../lettuce/core/resource/EpollProvider.java | 7 ++ .../core/resource/IOUringProvider.java | 7 ++ .../lettuce/core/SocketOptionsUnitTests.java | 15 +++ 5 files changed, 158 insertions(+) diff --git a/src/main/java/io/lettuce/core/ConnectionBuilder.java b/src/main/java/io/lettuce/core/ConnectionBuilder.java index 3031a407e2..2c0429cd20 100644 --- a/src/main/java/io/lettuce/core/ConnectionBuilder.java +++ b/src/main/java/io/lettuce/core/ConnectionBuilder.java @@ -284,6 +284,18 @@ public void configureBootstrap(boolean domainSocket, logger.warn("Cannot apply extended TCP keepalive options to channel type " + channelClass.getName()); } } + + if (options.isEnableTcpUserTimeout()) { + SocketOptions.TcpUserTimeoutOptions tcpUserTimeoutOptions = options.getTcpUserTimeout(); + + if (IOUringProvider.isAvailable()) { + IOUringProvider.applyTcpUserTimeout(bootstrap, tcpUserTimeoutOptions.getTcpUserTimeout()); + } else if (io.lettuce.core.resource.EpollProvider.isAvailable()) { + EpollProvider.applyTcpUserTimeout(bootstrap, tcpUserTimeoutOptions.getTcpUserTimeout()); + } else { + logger.warn("Cannot apply tcp user timeout options to channel type " + channelClass.getName()); + } + } } public RedisChannelHandler connection() { diff --git a/src/main/java/io/lettuce/core/SocketOptions.java b/src/main/java/io/lettuce/core/SocketOptions.java index 061f1ae3a4..fbc6e49a79 100644 --- a/src/main/java/io/lettuce/core/SocketOptions.java +++ b/src/main/java/io/lettuce/core/SocketOptions.java @@ -36,12 +36,16 @@ public class SocketOptions { public static final boolean DEFAULT_SO_KEEPALIVE = false; + public static final boolean DEFAULT_TCP_USER_TIMEOUT_ENABLED = false; + public static final boolean DEFAULT_SO_NO_DELAY = true; private final Duration connectTimeout; private final KeepAliveOptions keepAlive; + private final TcpUserTimeoutOptions tcpUserTimeout; + private final boolean extendedKeepAlive; private final boolean tcpNoDelay; @@ -52,6 +56,7 @@ protected SocketOptions(Builder builder) { this.keepAlive = builder.keepAlive; this.extendedKeepAlive = builder.extendedKeepAlive; this.tcpNoDelay = builder.tcpNoDelay; + this.tcpUserTimeout = builder.tcpUserTimeout; } protected SocketOptions(SocketOptions original) { @@ -59,6 +64,7 @@ protected SocketOptions(SocketOptions original) { this.keepAlive = original.getKeepAlive(); this.extendedKeepAlive = original.isExtendedKeepAlive(); this.tcpNoDelay = original.isTcpNoDelay(); + this.tcpUserTimeout = original.tcpUserTimeout; } /** @@ -98,6 +104,9 @@ public static class Builder { private KeepAliveOptions keepAlive = KeepAliveOptions.builder().enable(DEFAULT_SO_KEEPALIVE).build(); + private TcpUserTimeoutOptions tcpUserTimeout = TcpUserTimeoutOptions.builder() + .enable(DEFAULT_TCP_USER_TIMEOUT_ENABLED).build(); + private boolean tcpNoDelay = DEFAULT_SO_NO_DELAY; private boolean extendedKeepAlive = false; @@ -174,6 +183,14 @@ public Builder keepAlive(KeepAliveOptions keepAlive) { return this; } + public Builder tcpUserTimeout(TcpUserTimeoutOptions tcpUserTimeout) { + LettuceAssert.notNull(tcpUserTimeout, "tcpUserTimeout options must not be null"); + + this.tcpUserTimeout = tcpUserTimeout; + + return this; + } + /** * Set whether to disable/enable Nagle's algorithm. Defaults to {@code true} (Nagle disabled). See * {@link #DEFAULT_SO_NO_DELAY}. @@ -265,6 +282,14 @@ public boolean isTcpNoDelay() { return tcpNoDelay; } + public boolean isEnableTcpUserTimeout() { + return tcpUserTimeout.isEnabled(); + } + + public TcpUserTimeoutOptions getTcpUserTimeout() { + return tcpUserTimeout; + } + /** * Extended Keep-Alive options (idle, interval, count). Extended options should not be used in code intended to be portable * as options are applied only when using NIO sockets with Java 11 or newer epoll sockets, or io_uring sockets. Not @@ -483,4 +508,96 @@ public Duration getInterval() { } + /** + * TCP_USER_TIMEOUT TCP_USER_TIMEOUT comes from RFC5482 + * , configuring this parameter can allow the user TCP to initiate a reconnection to solve this problem when the + * network is abnormal: #2082 + */ + public static class TcpUserTimeoutOptions { + + /** + * Recommended default TCP_USER_TIMEOUT == TCP_KEEPIDLE(2 hour) + TCP_KEEPINTVL(75 s) * TCP_KEEPCNT(9) + * 2 * 3600 + 75 * 9 = 7875 + */ + public static final Duration DEFAULT_TCP_USER_TIMEOUT = Duration.ofSeconds(7875); + + private final Duration tcpUserTimeout; + + private final boolean enabled; + + private TcpUserTimeoutOptions(TcpUserTimeoutOptions.Builder builder) { + + this.tcpUserTimeout = builder.tcpUserTimeout; + this.enabled = builder.enabled; + } + + public static TcpUserTimeoutOptions.Builder builder() { + return new TcpUserTimeoutOptions.Builder(); + } + + /** + * Builder class for TcpUserTimeoutOptions. + */ + public static class Builder { + + private Duration tcpUserTimeout = DEFAULT_TCP_USER_TIMEOUT; + + private boolean enabled = DEFAULT_TCP_USER_TIMEOUT_ENABLED; + + private Builder() { + } + + public TcpUserTimeoutOptions.Builder enable() { + return enable(true); + } + + public TcpUserTimeoutOptions.Builder disable() { + return enable(false); + } + + public TcpUserTimeoutOptions.Builder enable(boolean enabled) { + + this.enabled = enabled; + return this; + } + + public TcpUserTimeoutOptions.Builder tcpUserTimeout(Duration tcpUserTimeout) { + + LettuceAssert.notNull(tcpUserTimeout, "tcpUserTimeout must not be null"); + LettuceAssert.isTrue(!tcpUserTimeout.isNegative(), "tcpUserTimeout must not be begative"); + + this.tcpUserTimeout = tcpUserTimeout; + return this; + } + + public TcpUserTimeoutOptions build() { + return new TcpUserTimeoutOptions(this); + } + + } + + /** + * Creates a new Builder instance with the current TcpUserTimeoutOptions state. + * + * @return a new Builder with the current state + */ + public TcpUserTimeoutOptions.Builder mutate() { + + TcpUserTimeoutOptions.Builder builder = builder(); + + builder.enabled = this.isEnabled(); + builder.tcpUserTimeout = this.getTcpUserTimeout(); + + return builder; + } + + public boolean isEnabled() { + return enabled; + } + + public Duration getTcpUserTimeout() { + return tcpUserTimeout; + } + } + } diff --git a/src/main/java/io/lettuce/core/resource/EpollProvider.java b/src/main/java/io/lettuce/core/resource/EpollProvider.java index 1ae3d6c6b9..a108ba9845 100644 --- a/src/main/java/io/lettuce/core/resource/EpollProvider.java +++ b/src/main/java/io/lettuce/core/resource/EpollProvider.java @@ -122,6 +122,13 @@ public static void applyKeepAlive(Bootstrap bootstrap, int count, Duration idle, bootstrap.option(EpollChannelOption.TCP_KEEPINTVL, Math.toIntExact(interval.getSeconds())); } + /** + * Apply TcpUserTimeout options. + */ + public static void applyTcpUserTimeout(Bootstrap bootstrap, Duration timeout) { + bootstrap.option(EpollChannelOption.TCP_USER_TIMEOUT, Math.toIntExact(timeout.toMillis())); + } + /** * {@link EventLoopResources} for available Epoll. */ diff --git a/src/main/java/io/lettuce/core/resource/IOUringProvider.java b/src/main/java/io/lettuce/core/resource/IOUringProvider.java index 53c7952c4f..06d7a3aaff 100644 --- a/src/main/java/io/lettuce/core/resource/IOUringProvider.java +++ b/src/main/java/io/lettuce/core/resource/IOUringProvider.java @@ -120,6 +120,13 @@ public static void applyKeepAlive(Bootstrap bootstrap, int count, Duration idle, bootstrap.option(IOUringChannelOption.TCP_KEEPINTVL, Math.toIntExact(interval.getSeconds())); } + /** + * Apply TcpUserTimeout options. + */ + public static void applyTcpUserTimeout(Bootstrap bootstrap, Duration timeout) { + bootstrap.option(IOUringChannelOption.TCP_USER_TIMEOUT, Math.toIntExact(timeout.toMillis())); + } + /** * {@link EventLoopResources} for available io_uring. */ diff --git a/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java b/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java index 892a2f6c9b..1819343cbe 100644 --- a/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import io.lettuce.core.SocketOptions.TcpUserTimeoutOptions; import org.junit.jupiter.api.Test; /** @@ -88,4 +89,18 @@ void checkAssertions(SocketOptions sut) { assertThat(sut.isTcpNoDelay()).isTrue(); assertThat(sut.getConnectTimeout()).isEqualTo(Duration.ofSeconds(10)); } + + @Test + void testDefaultTcpUserTimeoutOption() { + SocketOptions sut = SocketOptions.builder().build(); + assertThat(sut.isEnableTcpUserTimeout()).isFalse(); + } + + @Test + void testConfigTcpUserTimeoutOption() { + SocketOptions sut = SocketOptions.builder().tcpUserTimeout(TcpUserTimeoutOptions + .builder().enable().tcpUserTimeout(Duration.ofSeconds(60)).build()).build(); + assertThat(sut.isEnableTcpUserTimeout()).isTrue(); + assertThat(sut.getTcpUserTimeout().getTcpUserTimeout()).isEqualTo(Duration.ofSeconds(60)); + } }