Skip to content

Commit c48f2af

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

3 files changed

Lines changed: 241 additions & 35 deletions

File tree

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@
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;
36+
import java.util.concurrent.TimeUnit;
3237
import java.util.concurrent.TimeoutException;
38+
import java.util.concurrent.atomic.AtomicLong;
3339
import java.util.function.Consumer;
3440

3541
import org.apache.hc.client5.http.HttpRoute;
@@ -63,6 +69,7 @@
6369
import org.apache.hc.core5.io.CloseMode;
6470
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
6571
import org.apache.hc.core5.pool.PoolReusePolicy;
72+
import org.apache.hc.core5.pool.PoolStats;
6673
import org.apache.hc.core5.util.TimeValue;
6774
import org.apache.hc.core5.util.Timeout;
6875
import org.junit.jupiter.api.AfterEach;
@@ -391,4 +398,98 @@ void testConnectionTimeoutSetting() throws Exception {
391398
connManager.close();
392399
}
393400

401+
@Test
402+
void testConnectionRequestTimeout() throws Exception {
403+
configureServer(bootstrap -> bootstrap
404+
.register("/random/*", new RandomHandler()));
405+
final HttpHost target = startServer();
406+
407+
connManager.setMaxTotal(1);
408+
409+
final HttpRoute route = new HttpRoute(target, null, false);
410+
final Timeout connRequestTimeout = Timeout.ofMicroseconds(1);
411+
412+
final int concurrentThreads = 10;
413+
final CountDownLatch countDownLatch = new CountDownLatch(concurrentThreads);
414+
final AtomicLong n = new AtomicLong(concurrentThreads * 100);
415+
416+
final ExecutorService executorService = Executors.newFixedThreadPool(concurrentThreads);
417+
for (int i = 0; i < concurrentThreads; i++) {
418+
executorService.execute(() -> {
419+
try {
420+
while (n.decrementAndGet() > 0) {
421+
try {
422+
final LeaseRequest request = connManager.lease("id1", route, connRequestTimeout, null);
423+
final ConnectionEndpoint connectionEndpoint = request.get(connRequestTimeout);
424+
connManager.release(connectionEndpoint, null, null);
425+
} catch (final InterruptedException ex) {
426+
Thread.currentThread().interrupt();
427+
Assertions.fail("Unexpected exception", ex);
428+
} catch (final TimeoutException | ExecutionException ignored) {
429+
}
430+
}
431+
} finally {
432+
countDownLatch.countDown();
433+
}
434+
});
435+
}
436+
437+
Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
438+
Assertions.assertTrue(n.get() <= 0);
439+
440+
final PoolStats stats = connManager.getStats(route);
441+
Assertions.assertEquals(0, stats.getLeased());
442+
443+
connManager.close();
444+
}
445+
446+
@Test
447+
void testConnectionRequestCancelLateLeaseReleased() throws Exception {
448+
configureServer(bootstrap -> bootstrap
449+
.register("/random/*", new RandomHandler()));
450+
final HttpHost target = startServer();
451+
452+
connManager.setMaxTotal(1);
453+
454+
final HttpRoute route = new HttpRoute(target, null, false);
455+
final Timeout t = Timeout.ofSeconds(5);
456+
457+
final LeaseRequest holdRequest = connManager.lease("hold", route, t, null);
458+
final ConnectionEndpoint heldEndpoint = holdRequest.get(t);
459+
460+
final LeaseRequest pendingRequest = connManager.lease("pending", route, t, null);
461+
462+
connManager.release(heldEndpoint, null, null);
463+
464+
PoolStats stats;
465+
final long deadline1 = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
466+
for (;;) {
467+
stats = connManager.getStats(route);
468+
if (stats.getLeased() == 1) {
469+
break;
470+
}
471+
if (System.nanoTime() > deadline1) {
472+
break;
473+
}
474+
Thread.yield();
475+
}
476+
Assertions.assertEquals(1, stats.getLeased(), "Expected pending lease to complete and become leased");
477+
Assertions.assertFalse(pendingRequest.cancel(), "Expected cancel() to lose the race once lease is completed");
478+
479+
final long deadline2 = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
480+
for (;;) {
481+
stats = connManager.getStats(route);
482+
if (stats.getLeased() == 0) {
483+
break;
484+
}
485+
if (System.nanoTime() > deadline2) {
486+
break;
487+
}
488+
Thread.yield();
489+
}
490+
Assertions.assertEquals(0, stats.getLeased(), "Late-completed lease must not remain stranded after cancel()");
491+
492+
connManager.close();
493+
}
494+
394495
}

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.apache.hc.core5.annotation.Contract;
5757
import org.apache.hc.core5.annotation.Internal;
5858
import org.apache.hc.core5.annotation.ThreadingBehavior;
59+
import org.apache.hc.core5.concurrent.FutureCallback;
5960
import org.apache.hc.core5.function.Resolver;
6061
import org.apache.hc.core5.http.ClassicHttpRequest;
6162
import org.apache.hc.core5.http.ClassicHttpResponse;
@@ -369,7 +370,36 @@ public LeaseRequest lease(
369370
if (LOG.isDebugEnabled()) {
370371
LOG.debug("{} endpoint lease request ({}) {}", id, requestTimeout, ConnPoolSupport.formatStats(route, state, pool));
371372
}
372-
final Future<PoolEntry<HttpRoute, ManagedHttpClientConnection>> leaseFuture = this.pool.lease(route, state, requestTimeout, null);
373+
final AtomicBoolean aborted = new AtomicBoolean(false);
374+
final AtomicBoolean resultObtained = new AtomicBoolean(false);
375+
final AtomicReference<PoolEntry<HttpRoute, ManagedHttpClientConnection>> lateEntryRef = new AtomicReference<>();
376+
final Future<PoolEntry<HttpRoute, ManagedHttpClientConnection>> leaseFuture = this.pool.lease(
377+
route,
378+
state,
379+
requestTimeout,
380+
new FutureCallback<PoolEntry<HttpRoute, ManagedHttpClientConnection>>() {
381+
382+
@Override
383+
public void completed(final PoolEntry<HttpRoute, ManagedHttpClientConnection> poolEntry) {
384+
lateEntryRef.set(poolEntry);
385+
if (aborted.get()) {
386+
if (lateEntryRef.compareAndSet(poolEntry, null)) {
387+
pool.release(poolEntry, false);
388+
}
389+
} else if (resultObtained.get()) {
390+
lateEntryRef.compareAndSet(poolEntry, null);
391+
}
392+
}
393+
394+
@Override
395+
public void failed(final Exception ex) {
396+
}
397+
398+
@Override
399+
public void cancelled() {
400+
}
401+
402+
});
373403
return new LeaseRequest() {
374404
// Using a ReentrantLock specific to each LeaseRequest instance to maintain the original
375405
// synchronization semantics. This ensures that each LeaseRequest has its own unique lock.
@@ -388,8 +418,15 @@ public ConnectionEndpoint get(
388418
final PoolEntry<HttpRoute, ManagedHttpClientConnection> poolEntry;
389419
try {
390420
poolEntry = leaseFuture.get(timeout.getDuration(), timeout.getTimeUnit());
391-
} catch (final TimeoutException ex) {
421+
resultObtained.set(true);
422+
lateEntryRef.compareAndSet(poolEntry, null);
423+
} catch (final TimeoutException | InterruptedException ex) {
424+
aborted.set(true);
392425
leaseFuture.cancel(true);
426+
final PoolEntry<HttpRoute, ManagedHttpClientConnection> latePoolEntry = lateEntryRef.getAndSet(null);
427+
if (latePoolEntry != null) {
428+
pool.release(latePoolEntry, false);
429+
}
393430
throw ex;
394431
}
395432
if (LOG.isDebugEnabled()) {
@@ -467,7 +504,18 @@ public ConnectionEndpoint get(
467504

468505
@Override
469506
public boolean cancel() {
470-
return leaseFuture.cancel(true);
507+
lock.lock();
508+
try {
509+
aborted.set(true);
510+
final boolean cancelled = leaseFuture.cancel(true);
511+
final PoolEntry<HttpRoute, ManagedHttpClientConnection> latePoolEntry = lateEntryRef.getAndSet(null);
512+
if (latePoolEntry != null) {
513+
pool.release(latePoolEntry, false);
514+
}
515+
return cancelled;
516+
} finally {
517+
lock.unlock();
518+
}
471519
}
472520

473521
};

0 commit comments

Comments
 (0)