From e9767eda9349d0eeb4a2351d58c401e95684d515 Mon Sep 17 00:00:00 2001 From: shinseongsu Date: Thu, 5 Nov 2026 01:10:47 +0900 Subject: [PATCH] HTTPCLIENT-2437: LaxConnPool fails expired lease requests queued behind a live one PerRoutePool#servicePendingRequests stops at the first request it cannot serve. When that request has not expired yet, expired requests queued behind it are not failed by closeExpired() or closeIdle() and stay pending until they reach the head of the queue. enumAvailable() now also runs validatePendingRequests(), which checks every pending request, as StrictConnPool already does. --- .../org/apache/hc/core5/pool/LaxConnPool.java | 3 ++ .../pool/TestPoolPendingRequestExpiry.java | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java b/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java index efc263c16..4b44db4ce 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java @@ -642,6 +642,9 @@ public void enumAvailable(final Callback> callback) { } releaseSeqNum.incrementAndGet(); servicePendingRequests(RequestServiceStrategy.ALL); + // servicePendingRequests stops at the first request it cannot serve, + // so expired requests queued behind it would stay pending. + validatePendingRequests(); } public void enumLeased(final Callback> callback) { diff --git a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestPoolPendingRequestExpiry.java b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestPoolPendingRequestExpiry.java index ef4927e2b..8c40596e4 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestPoolPendingRequestExpiry.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestPoolPendingRequestExpiry.java @@ -26,6 +26,7 @@ */ package org.apache.hc.core5.pool; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -92,6 +93,33 @@ void closeIdleFailsExpiredPendingRequest(final PoolConcurrencyPolicy policy) thr } } + @ParameterizedTest + @EnumSource(PoolConcurrencyPolicy.class) + void closeExpiredFailsExpiredRequestQueuedBehindLiveRequest(final PoolConcurrencyPolicy policy) throws Exception { + final TestingClock clock = new TestingClock(0L); + final ManagedConnPool pool = + PoolTestSupport.createPool(policy, 1, 1, clock); + try { + final PoolEntry leased = + pool.lease("r1", null, Timeout.ofSeconds(1), null).get(1, TimeUnit.SECONDS); + assertNotNull(leased); + final Future> live = + pool.lease("r1", null, Timeout.ofMinutes(1), null); + final Future> expired = + pool.lease("r1", null, Timeout.ofMilliseconds(100), null); + + clock.advanceMillis(10_000L); + pool.closeExpired(); + + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> expired.get(5, TimeUnit.SECONDS)); + assertInstanceOf(TimeoutException.class, ex.getCause()); + assertFalse(live.isDone(), "Request that has not expired must stay pending"); + } finally { + pool.close(CloseMode.IMMEDIATE); + } + } + @ParameterizedTest @EnumSource(PoolConcurrencyPolicy.class) void expiredPendingRequestIsDoneAfterEviction(final PoolConcurrencyPolicy policy) throws Exception {