From 7bf40d3abf4a14cd899990d248c1b8d34b680c15 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 26 Aug 2026 19:01:33 +0000 Subject: [PATCH] Make TimeoutsHolder.start idempotent --- .../org/asynchttpclient/netty/timeout/TimeoutsHolder.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutsHolder.java b/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutsHolder.java index ba7693995..0930e85a7 100755 --- a/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutsHolder.java +++ b/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutsHolder.java @@ -43,6 +43,7 @@ public class TimeoutsHolder { private static final Logger LOGGER = LoggerFactory.getLogger(TimeoutsHolder.class); private final AtomicBoolean cancelled = new AtomicBoolean(); + private final AtomicBoolean started = new AtomicBoolean(); private final Timer nettyTimer; private volatile @Nullable EventExecutor eventExecutor; private final NettyRequestSender requestSender; @@ -105,8 +106,14 @@ public TimeoutsHolder(Timer nettyTimer, @Nullable EventExecutor eventExecutor, N *

* Called by {@link org.asynchttpclient.netty.NettyResponseFuture#setTimeoutsHolder}, so that installing a * holder is what arms it and neither can be done without the other. + *

+ * Only the first call arms. A second would overwrite the first handle, leaving an entry nobody can + * cancel to sit in its scheduler until the full deadline, pinning the task, the future and the channel. */ public void start() { + if (!started.compareAndSet(false, true)) { + return; + } if (requestTimeoutTask != null) { // The configured duration rather than the remaining time: this runs within microseconds of the // constructor, and reading the clock again would only expose the deadline to a step between the two.