Skip to content

Commit dae1cb4

Browse files
committed
HTTPCLIENT-2398: break recursive pool callback chain
Guard callback drain in Strict/Lax pools; avoid fireCallbacks()->release() re-entry. Prevents StackOverflowError under fast-fail connect/retry; no semantic change.
1 parent c16c887 commit dae1cb4

2 files changed

Lines changed: 130 additions & 16 deletions

File tree

httpcore5/src/main/java/org/apache/hc/core5/pool/StrictConnPool.java

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public class StrictConnPool<T, C extends ModalCloseable> implements ManagedConnP
8080
private final ReentrantLock lock;
8181
private final AtomicBoolean isShutDown;
8282

83+
private final AtomicBoolean dispatching = new AtomicBoolean(false);
84+
8385
private volatile int defaultMaxPerRoute;
8486
private volatile int maxTotal;
8587

@@ -225,6 +227,11 @@ public Future<PoolEntry<T, C>> lease(final T route, final Object state) {
225227

226228
@Override
227229
public void release(final PoolEntry<T, C> entry, final boolean reusable) {
230+
releaseInternal(entry, reusable);
231+
fireCallbacks();
232+
}
233+
234+
private void releaseInternal(final PoolEntry<T, C> entry, final boolean reusable) {
228235
if (entry == null) {
229236
return;
230237
}
@@ -264,7 +271,6 @@ public void release(final PoolEntry<T, C> entry, final boolean reusable) {
264271
} finally {
265272
this.lock.unlock();
266273
}
267-
fireCallbacks();
268274
}
269275

270276
private void processPendingRequests() {
@@ -384,23 +390,35 @@ private boolean processPendingRequest(final LeaseRequest<T, C> request) {
384390
}
385391

386392
private void fireCallbacks() {
387-
LeaseRequest<T, C> request;
388-
while ((request = this.completedRequests.poll()) != null) {
389-
final BasicFuture<PoolEntry<T, C>> future = request.getFuture();
390-
final Exception ex = request.getException();
391-
final PoolEntry<T, C> result = request.getResult();
392-
boolean successfullyCompleted = false;
393-
if (ex != null) {
394-
future.failed(ex);
395-
} else if (result != null) {
396-
if (future.completed(result)) {
397-
successfullyCompleted = true;
393+
for (;;) {
394+
if (!dispatching.compareAndSet(false, true)) {
395+
return;
396+
}
397+
try {
398+
LeaseRequest<T, C> request;
399+
while ((request = this.completedRequests.poll()) != null) {
400+
final BasicFuture<PoolEntry<T, C>> future = request.getFuture();
401+
final Exception ex = request.getException();
402+
final PoolEntry<T, C> result = request.getResult();
403+
boolean delivered = false;
404+
if (ex != null) {
405+
future.failed(ex);
406+
} else if (result != null) {
407+
delivered = future.completed(result);
408+
} else {
409+
future.cancel();
410+
}
411+
if (!delivered && result != null) {
412+
// return quietly to pool without triggering another drain
413+
releaseInternal(result, true);
414+
}
398415
}
399-
} else {
400-
future.cancel();
416+
} finally {
417+
dispatching.set(false);
401418
}
402-
if (!successfullyCompleted) {
403-
release(result, true);
419+
// if something arrived while we were draining, loop once more
420+
if (this.completedRequests.isEmpty()) {
421+
break;
404422
}
405423
}
406424
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.core5.pool;
28+
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertTrue;
31+
32+
import java.io.IOException;
33+
import java.util.concurrent.CountDownLatch;
34+
import java.util.concurrent.TimeUnit;
35+
import java.util.concurrent.atomic.AtomicLong;
36+
37+
import org.apache.hc.core5.concurrent.FutureCallback;
38+
import org.apache.hc.core5.io.CloseMode;
39+
import org.apache.hc.core5.io.ModalCloseable;
40+
import org.apache.hc.core5.util.Timeout;
41+
import org.junit.jupiter.api.Test;
42+
43+
final class TestStrictConnPoolRecursion {
44+
45+
static final class Dummy implements ModalCloseable {
46+
@Override
47+
public void close(final CloseMode closeMode) {
48+
}
49+
50+
@Override
51+
public void close() throws IOException {
52+
}
53+
}
54+
55+
/**
56+
* Regression test for HTTPCLIENT-2398:
57+
* Previously this pattern caused fireCallbacks()->release()->fireCallbacks() recursion
58+
* and a StackOverflowError. With the fix, we should iterate many times without error.
59+
*/
60+
@Test
61+
@org.junit.jupiter.api.Timeout(10)
62+
void leaseFromCallback_no_recursion_after_fix() throws Exception {
63+
final StrictConnPool<String, Dummy> pool = new StrictConnPool<>(1, 1);
64+
final String route = "r";
65+
66+
final int LIMIT = 10_000; // high enough to catch recursion, low enough to be fast
67+
final AtomicLong seen = new AtomicLong();
68+
final CountDownLatch done = new CountDownLatch(1);
69+
70+
final FutureCallback<PoolEntry<String, Dummy>> cb = new FutureCallback<PoolEntry<String, Dummy>>() {
71+
@Override
72+
public void completed(final PoolEntry<String, Dummy> entry) {
73+
pool.release(entry, true);
74+
final long v = seen.incrementAndGet();
75+
if (v < LIMIT) {
76+
pool.lease(route, null, Timeout.ZERO_MILLISECONDS, this);
77+
} else {
78+
done.countDown();
79+
}
80+
}
81+
82+
@Override
83+
public void failed(final Exception ex) { /* not used */ }
84+
85+
@Override
86+
public void cancelled() { /* not used */ }
87+
};
88+
89+
// Seed the first lease; the callback keeps re-leasing synchronously.
90+
pool.lease(route, null, Timeout.ZERO_MILLISECONDS, cb);
91+
92+
// We should finish LIMIT iterations within the timeout and without StackOverflowError.
93+
assertTrue(done.await(5, TimeUnit.SECONDS), "Did not complete iterations in time");
94+
assertEquals(LIMIT, seen.get(), "Unexpected number of lease iterations");
95+
}
96+
}

0 commit comments

Comments
 (0)