Skip to content

Commit 1865484

Browse files
committed
HTTPCLIENT-2416 - Fix pool entry leak on late lease completion
1 parent 08f3fdc commit 1865484

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestConnectionManagement.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929

3030
import java.io.IOException;
3131
import java.net.InetSocketAddress;
32+
import java.util.concurrent.CountDownLatch;
33+
import java.util.concurrent.ExecutionException;
34+
import java.util.concurrent.ExecutorService;
35+
import java.util.concurrent.Executors;
3236
import java.util.concurrent.TimeoutException;
37+
import java.util.concurrent.atomic.AtomicLong;
3338
import java.util.function.Consumer;
3439

3540
import org.apache.hc.client5.http.HttpRoute;
@@ -63,6 +68,7 @@
6368
import org.apache.hc.core5.io.CloseMode;
6469
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
6570
import org.apache.hc.core5.pool.PoolReusePolicy;
71+
import org.apache.hc.core5.pool.PoolStats;
6672
import org.apache.hc.core5.util.TimeValue;
6773
import org.apache.hc.core5.util.Timeout;
6874
import org.junit.jupiter.api.AfterEach;
@@ -391,4 +397,49 @@ void testConnectionTimeoutSetting() throws Exception {
391397
connManager.close();
392398
}
393399

400+
@Test
401+
void testConnectionRequestTimeout() throws Exception {
402+
configureServer(bootstrap -> bootstrap
403+
.register("/random/*", new RandomHandler()));
404+
final HttpHost target = startServer();
405+
406+
connManager.setMaxTotal(1);
407+
408+
final HttpRoute route = new HttpRoute(target, null, false);
409+
final Timeout connRequestTimeout = Timeout.ofMicroseconds(1);
410+
411+
final int concurrentThreads = 10;
412+
final CountDownLatch countDownLatch = new CountDownLatch(concurrentThreads);
413+
final AtomicLong n = new AtomicLong(concurrentThreads * 100);
414+
415+
final ExecutorService executorService = Executors.newFixedThreadPool(concurrentThreads);
416+
for (int i = 0; i < concurrentThreads; i++) {
417+
executorService.execute(() -> {
418+
try {
419+
while (n.decrementAndGet() > 0) {
420+
try {
421+
final LeaseRequest request = connManager.lease("id1", route, connRequestTimeout, null);
422+
final ConnectionEndpoint connectionEndpoint = request.get(connRequestTimeout);
423+
connManager.release(connectionEndpoint, null, null);
424+
} catch (final InterruptedException ex) {
425+
Thread.currentThread().interrupt();
426+
Assertions.fail("Unexpected exception", ex);
427+
} catch (final TimeoutException | ExecutionException ignored) {
428+
}
429+
}
430+
} finally {
431+
countDownLatch.countDown();
432+
}
433+
});
434+
}
435+
436+
Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
437+
Assertions.assertTrue(n.get() <= 0);
438+
439+
final PoolStats stats = connManager.getStats(route);
440+
Assertions.assertEquals(0, stats.getLeased());
441+
442+
connManager.close();
443+
}
444+
394445
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.io.IOException;
3030
import java.nio.file.Path;
3131
import java.util.Set;
32+
import java.util.concurrent.CancellationException;
3233
import java.util.concurrent.ExecutionException;
3334
import java.util.concurrent.Future;
3435
import java.util.concurrent.TimeoutException;
@@ -388,8 +389,20 @@ public ConnectionEndpoint get(
388389
final PoolEntry<HttpRoute, ManagedHttpClientConnection> poolEntry;
389390
try {
390391
poolEntry = leaseFuture.get(timeout.getDuration(), timeout.getTimeUnit());
391-
} catch (final TimeoutException ex) {
392-
leaseFuture.cancel(true);
392+
} catch (final TimeoutException | InterruptedException ex) {
393+
if (!leaseFuture.cancel(true)) {
394+
try {
395+
final PoolEntry<HttpRoute, ManagedHttpClientConnection> latePoolEntry = leaseFuture.get(
396+
Timeout.ZERO_MILLISECONDS.getDuration(),
397+
Timeout.ZERO_MILLISECONDS.getTimeUnit());
398+
if (latePoolEntry != null) {
399+
pool.release(latePoolEntry, false);
400+
}
401+
} catch (final TimeoutException | ExecutionException | CancellationException ignore) {
402+
} catch (final InterruptedException interrupted) {
403+
Thread.currentThread().interrupt();
404+
}
405+
}
393406
throw ex;
394407
}
395408
if (LOG.isDebugEnabled()) {

httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,56 @@ void testLeaseFutureTimeout() throws Exception {
171171
connRequest1.get(Timeout.ofSeconds(1)));
172172
}
173173

174+
@Test
175+
void testLeaseFutureTimeoutLateLeaseReleased() throws Exception {
176+
final HttpHost target = new HttpHost("localhost", 80);
177+
final HttpRoute route = new HttpRoute(target);
178+
final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
179+
180+
Mockito.when(future.get(1, TimeUnit.SECONDS)).thenThrow(new TimeoutException());
181+
Mockito.when(future.cancel(true)).thenReturn(false);
182+
Mockito.when(future.get(0L, TimeUnit.MILLISECONDS)).thenReturn(entry);
183+
Mockito.when(pool.lease(
184+
Mockito.eq(route),
185+
Mockito.eq(null),
186+
Mockito.any(),
187+
Mockito.eq(null)))
188+
.thenReturn(future);
189+
190+
final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
191+
Assertions.assertThrows(TimeoutException.class, () ->
192+
connRequest1.get(Timeout.ofSeconds(1)));
193+
194+
Mockito.verify(future).cancel(true);
195+
Mockito.verify(future).get(0L, TimeUnit.MILLISECONDS);
196+
Mockito.verify(pool).release(entry, false);
197+
}
198+
199+
@Test
200+
void testLeaseFutureInterruptedLateLeaseReleased() throws Exception {
201+
final HttpHost target = new HttpHost("localhost", 80);
202+
final HttpRoute route = new HttpRoute(target);
203+
final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
204+
205+
Mockito.when(future.get(1, TimeUnit.SECONDS)).thenThrow(new InterruptedException());
206+
Mockito.when(future.cancel(true)).thenReturn(false);
207+
Mockito.when(future.get(0L, TimeUnit.MILLISECONDS)).thenReturn(entry);
208+
Mockito.when(pool.lease(
209+
Mockito.eq(route),
210+
Mockito.eq(null),
211+
Mockito.any(),
212+
Mockito.eq(null)))
213+
.thenReturn(future);
214+
215+
final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
216+
Assertions.assertThrows(InterruptedException.class, () ->
217+
connRequest1.get(Timeout.ofSeconds(1)));
218+
219+
Mockito.verify(future).cancel(true);
220+
Mockito.verify(future).get(0L, TimeUnit.MILLISECONDS);
221+
Mockito.verify(pool).release(entry, false);
222+
}
223+
174224
@Test
175225
void testReleaseReusable() throws Exception {
176226
final HttpHost target = new HttpHost("localhost", 80);

0 commit comments

Comments
 (0)