From 2c80db166a3689f1c7904b93530decacb6334af7 Mon Sep 17 00:00:00 2001
From: "slav.babanin"
Date: Tue, 3 Mar 2026 23:14:43 -0800
Subject: [PATCH 1/5] Add stack-safe async loop support with trampoline pattern
- Add AsyncTrampoline class to prevent stack overflow in loops by converting callback recursion into iterative execution
- Add thenRunWhileLoop method to AsyncRunnable to support while-loop semantics where condition is checked before body execution
- Integrate trampoline into AsyncCallbackLoop by making LoopingCallback implement Runnable to avoid per-iteration lambda allocation
JAVA-6120
---
.../mongodb/internal/async/AsyncRunnable.java | 29 +++
.../internal/async/AsyncTrampoline.java | 106 +++++++++++
.../async/function/AsyncCallbackLoop.java | 11 +-
.../async/AsyncFunctionsAbstractTest.java | 174 ++++++++++++++++++
.../async/AsyncFunctionsTestBase.java | 9 +-
5 files changed, 324 insertions(+), 5 deletions(-)
create mode 100644 driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
index e404e2b8152..d9bbb56be55 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
@@ -243,6 +243,35 @@ default AsyncRunnable thenRunRetryingWhile(
});
}
+ /**
+ * This method is equivalent to a while loop, where the condition is checked before each iteration.
+ * If the condition returns {@code false} on the first check, the body is never executed.
+ *
+ * @param loopBodyRunnable the asynchronous task to be executed in each iteration of the loop
+ * @param whileCheck a condition to check before each iteration; the loop continues as long as this condition returns true
+ * @return the composition of this and the looping branch
+ * @see AsyncCallbackLoop
+ */
+ default AsyncRunnable thenRunWhileLoop(final BooleanSupplier whileCheck, final AsyncRunnable loopBodyRunnable) {
+ return thenRun(finalCallback -> {
+ LoopState loopState = new LoopState();
+ new AsyncCallbackLoop(loopState, iterationCallback -> {
+
+ if (loopState.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) {
+ return;
+ }
+ loopBodyRunnable.finish((result, t) -> {
+ if (t != null) {
+ iterationCallback.completeExceptionally(t);
+ return;
+ }
+ iterationCallback.complete(iterationCallback);
+ });
+
+ }).run(finalCallback);
+ });
+ }
+
/**
* This method is equivalent to a do-while loop, where the loop body is executed first and
* then the condition is checked to determine whether the loop should continue.
diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
new file mode 100644
index 00000000000..c7fc27e602b
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb.internal.async;
+
+import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.assertions.Assertions;
+import com.mongodb.lang.Nullable;
+
+/**
+ * A trampoline that converts recursive callback invocations into an iterative loop,
+ * preventing stack overflow in async loops.
+ *
+ * When async loop iterations complete synchronously on the same thread, callback
+ * recursion occurs: each iteration's {@code callback.onResult()} immediately triggers
+ * the next iteration, causing unbounded stack growth. For example, a 1000-iteration
+ * loop would create > 1000 stack frames and cause {@code StackOverflowError}.
+ *
+ * The trampoline intercepts this recursion: instead of executing the next iteration
+ * immediately (which would deepen the stack), it enqueues the work and returns, allowing
+ * the stack to unwind. A flat loop at the top then processes enqueued work iteratively,
+ * maintaining constant stack depth regardless of iteration count.
+ *
+ * Since async chains are sequential, at most one task is pending at any time.
+ * The trampoline uses a single slot rather than a queue.
+ *
+ * The first call on a thread becomes the "trampoline owner" and runs the drain loop.
+ * Subsequent (re-entrant) calls on the same thread enqueue their work and return immediately.
+ *
+ * This class is not part of the public API and may be removed or changed at any time
+ */
+@NotThreadSafe
+public final class AsyncTrampoline {
+
+ private static final ThreadLocal TRAMPOLINE = new ThreadLocal<>();
+
+ private AsyncTrampoline() {
+ }
+
+ /**
+ * Execute work through the trampoline. If no trampoline is active, become the owner
+ * and drain all enqueued work. If a trampoline is already active, enqueue and return.
+ */
+ public static void run(final Runnable work) {
+ Bounce bounce = TRAMPOLINE.get();
+ if (bounce != null) {
+ // Re-entrant, enqueue and return
+ bounce.enqueue(work);
+ } else {
+ // Become the trampoline owner.
+ bounce = new Bounce();
+ TRAMPOLINE.set(bounce);
+ try {
+ work.run();
+ // drain any re-entrant work iteratively
+ while (bounce.hasWork()) {
+ bounce.runNext();
+ }
+ } finally {
+ TRAMPOLINE.remove();
+ }
+ }
+ }
+
+ /**
+ * A single-slot container for deferred work.
+ * At most one task is pending at any time in a sequential async chain.
+ */
+ @NotThreadSafe
+ private static final class Bounce {
+ @Nullable
+ private Runnable work;
+
+ void enqueue(final Runnable task) {
+ if (this.work != null) {
+ throw new AssertionError("Trampoline slot already occupied. "
+ + "It could happen if there are multiple concurrent operations in a sequential async chain.");
+ }
+ this.work = task;
+ }
+
+ boolean hasWork() {
+ return work != null;
+ }
+
+ void runNext() {
+ Runnable task = this.work;
+ this.work = null;
+ Assertions.assertNotNull(task);
+ task.run();
+ }
+ }
+}
\ No newline at end of file
diff --git a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
index a347a2a7e47..e9bd0c18a46 100644
--- a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
+++ b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.async.function;
import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.internal.async.AsyncTrampoline;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.lang.Nullable;
@@ -58,15 +59,21 @@ public void run(final SingleResultCallback callback) {
/**
* This callback is allowed to be completed more than once.
+ * Also implements {@linkplain Runnable} to avoid lambda allocation per iteration when using trampoline.
*/
@NotThreadSafe
- private class LoopingCallback implements SingleResultCallback {
+ private class LoopingCallback implements SingleResultCallback, Runnable {
private final SingleResultCallback wrapped;
LoopingCallback(final SingleResultCallback callback) {
wrapped = callback;
}
+ @Override
+ public void run() {
+ body.run(this);
+ }
+
@Override
public void onResult(@Nullable final Void result, @Nullable final Throwable t) {
if (t != null) {
@@ -80,7 +87,7 @@ public void onResult(@Nullable final Void result, @Nullable final Throwable t) {
return;
}
if (continueLooping) {
- body.run(this);
+ AsyncTrampoline.run(this);
} else {
wrapped.onResult(result, null);
}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
index 9a9b7552d3e..05072a49925 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
@@ -26,6 +26,7 @@
import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.internal.async.AsyncRunnable.beginAsync;
+import static org.junit.jupiter.api.Assertions.assertEquals;
abstract class AsyncFunctionsAbstractTest extends AsyncFunctionsTestBase {
private static final TimeoutContext TIMEOUT_CONTEXT = new TimeoutContext(new TimeoutSettings(0, 0, 0, 0L, 0));
@@ -723,6 +724,120 @@ void testTryCatchTestAndRethrow() {
});
}
+ @Test
+ void testWhile() {
+ // last iteration: 3 < 3 = 1
+ // 1(plainTest exception) + 1(plainTest false) + 1(sync exception) + 1(sync success) * 1(transition to next iteration) = 4
+ // 1(plainTest exception) + 1(plainTest false) + 1(sync exception) + 1(sync success) * 4(transition to next iteration) = 7
+ // 1(plainTest exception) + 1(plainTest false) + 1(sync exception) + 1(sync success) * 7(transition to next iteration) = 10
+ assertBehavesSameVariations(10,
+ () -> {
+ int counter = 0;
+ while (counter < 3 && plainTest(counter)) {
+ counter++;
+ sync(counter);
+ }
+ },
+ (callback) -> {
+ MutableValue counter = new MutableValue<>(0);
+ beginAsync().thenRunWhileLoop(() -> counter.get() < 3 && plainTest(counter.get()), c2 -> {
+ counter.set(counter.get() + 1);
+ async(counter.get(), c2);
+ }).finish(callback);
+ });
+ }
+
+ @Test
+ void testWhileWithThenRun() {
+ // while: last iteration: 3 < 3 = 1
+ // 1(plainTest exception) + 1(plainTest false) + 1(sync exception) + 1(sync success) * 1(transition to next iteration) = 4
+ // 1(plainTest exception) + 1(plainTest false) + 1(sync exception) + 1(sync success) * 4(transition to next iteration) = 7
+ // 1(plainTest exception) + 1(plainTest false) + 1(sync exception) + 1(sync success) * 7(transition to next iteration) = 10
+ // trailing sync: 1(exception) + 1(success) = 2
+ // 6(while exception) + 4(while success) * 2(trailing sync) = 14
+ assertBehavesSameVariations(14,
+ () -> {
+ int counter = 0;
+ while (counter < 3 && plainTest(counter)) {
+ counter++;
+ sync(counter);
+ }
+ sync(counter + 1);
+ },
+ (callback) -> {
+ MutableValue counter = new MutableValue<>(0);
+ beginAsync().thenRun(c -> {
+ beginAsync().thenRunWhileLoop(() -> counter.get() < 3 && plainTest(counter.get()), c2 -> {
+ counter.set(counter.get() + 1);
+ async(counter.get(), c2);
+ }).finish(c);
+ }).thenRun(c -> {
+ async(counter.get() + 1, c);
+ }).finish(callback);
+ });
+ }
+
+ @Test
+ void testNestedWhileLoops() {
+ // inner while: 4 success + 6 exception = 10
+ // last inner iteration: 3 < 3 = 1
+ // 1(outer plainTest exception) + 1(outer plainTest false) + (inner while) * 1(transition to next iteration) = 12
+ // 1(outer plainTest exception) + 1(outer plainTest false) + (inner while) * 12(transition to next iteration) = 56
+ // 1(outer plainTest exception) + 1(outer plainTest false) + (inner while) * 56(transition to next iteration) = 232
+ assertBehavesSameVariations(232,
+ () -> {
+ int outer = 0;
+ while (outer < 3 && plainTest(outer)) {
+ int inner = 0;
+ while (inner < 3 && plainTest(inner)) {
+ sync(outer + inner);
+ inner++;
+ }
+ outer++;
+ }
+ },
+ (callback) -> {
+ MutableValue outer = new MutableValue<>(0);
+ beginAsync().thenRunWhileLoop(() -> outer.get() < 3 && plainTest(outer.get()), c -> {
+ MutableValue inner = new MutableValue<>(0);
+ beginAsync().thenRunWhileLoop(
+ () -> inner.get() < 3 && plainTest(inner.get()),
+ c2 -> {
+ beginAsync().thenRun(c3 -> {
+ async(outer.get() + inner.get(), c3);
+ }).thenRun(c3 -> {
+ inner.set(inner.get() + 1);
+ c3.complete(c3);
+ }).finish(c2);
+ }
+ ).thenRun(c2 -> {
+ outer.set(outer.get() + 1);
+ c2.complete(c2);
+ }).finish(c);
+ }).finish(callback);
+ });
+ }
+
+ @Test
+ void testWhileLoopStackConstant() {
+ int depthWith100 = maxStackDepthForIterations(100);
+ int depthWith10000 = maxStackDepthForIterations(10_000);
+ assertEquals(depthWith100, depthWith10000, "Stack depth should be constant regardless of iteration count (trampoline)");
+ }
+
+ private int maxStackDepthForIterations(final int iterations) {
+ MutableValue counter = new MutableValue<>(0);
+ MutableValue maxDepth = new MutableValue<>(0);
+ beginAsync().thenRunWhileLoop(() -> counter.get() < iterations, c -> {
+ maxDepth.set(Math.max(maxDepth.get(), Thread.currentThread().getStackTrace().length));
+ counter.set(counter.get() + 1);
+ c.complete(c);
+ }).finish((v, t) -> {});
+
+ assertEquals(iterations, counter.get());
+ return maxDepth.get();
+ }
+
@Test
void testRetryLoop() {
assertBehavesSameVariations(InvocationTracker.DEPTH_LIMIT * 2 + 1,
@@ -768,6 +883,65 @@ void testDoWhileLoop() {
});
}
+ @Test
+ void testNestedDoWhileLoops() {
+ // inner do-while: 3 success + 5 exception = 8
+ // last outer iteration: 3 < 3 = 1
+ // 5(inner exception) + 3(inner success) * 1(transition to next iteration) = 8
+ // 5(inner exception) + 3(inner success) * 1(outer plainTest exception) + 1(outer plainTest false) + 8(transition to next iteration) = 35
+ // 5(inner exception) + 3(inner success) * 1(outer plainTest exception) + 1(outer plainTest false) + 35(transition to next iteration) = 116
+ assertBehavesSameVariations(116,
+ () -> {
+ int outer = 0;
+ do {
+ int inner = 0;
+ do {
+ sync(outer + inner);
+ inner++;
+ } while (inner < 3 && plainTest(inner));
+ outer++;
+ } while (outer < 3 && plainTest(outer));
+ },
+ (callback) -> {
+ MutableValue outer = new MutableValue<>(0);
+ beginAsync().thenRunDoWhileLoop(c -> {
+ MutableValue inner = new MutableValue<>(0);
+ beginAsync().thenRunDoWhileLoop(c2 -> {
+ beginAsync().thenRun(c3 -> {
+ async(outer.get() + inner.get(), c3);
+ }).thenRun(c3 -> {
+ inner.set(inner.get() + 1);
+ c3.complete(c3);
+ }).finish(c2);
+ }, () -> inner.get() < 3 && plainTest(inner.get())
+ ).thenRun(c2 -> {
+ outer.set(outer.get() + 1);
+ c2.complete(c2);
+ }).finish(c);
+ }, () -> outer.get() < 3 && plainTest(outer.get())).finish(callback);
+ });
+ }
+
+ @Test
+ void testDoWhileLoopStackConstant() {
+ int depthWith100 = maxDoWhileStackDepthForIterations(100);
+ int depthWith10000 = maxDoWhileStackDepthForIterations(10_000);
+ assertEquals(depthWith100, depthWith10000,
+ "Stack depth should be constant regardless of iteration count");
+ }
+
+ private int maxDoWhileStackDepthForIterations(final int iterations) {
+ MutableValue counter = new MutableValue<>(0);
+ MutableValue maxDepth = new MutableValue<>(0);
+ beginAsync().thenRunDoWhileLoop(c -> {
+ maxDepth.set(Math.max(maxDepth.get(), Thread.currentThread().getStackTrace().length));
+ counter.set(counter.get() + 1);
+ c.complete(c);
+ }, () -> counter.get() < iterations).finish((v, t) -> {});
+ assertEquals(iterations, counter.get());
+ return maxDepth.get();
+ }
+
@Test
void testFinallyWithPlainInsideTry() {
// (in try: normal flow + exception + exception) * (in finally: normal + exception) = 6
diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsTestBase.java b/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsTestBase.java
index 10a58152d9f..73d9d59b4dc 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsTestBase.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsTestBase.java
@@ -32,6 +32,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
+import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -272,14 +273,16 @@ private void assertBehavesSame(final Supplier sync, final Runnable betwee
}
assertTrue(wasCalledFuture.isDone(), "callback should have been called");
- assertEquals(expectedEvents, listener.getEventStrings(), "steps should have matched");
- assertEquals(expectedValue, actualValue.get());
assertEquals(expectedException == null, actualException.get() == null,
- "both or neither should have produced an exception");
+ format("both or neither should have produced an exception. Expected exception: %s, actual exception: %s",
+ expectedException,
+ actualException));
if (expectedException != null) {
assertEquals(expectedException.getMessage(), actualException.get().getMessage());
assertEquals(expectedException.getClass(), actualException.get().getClass());
}
+ assertEquals(expectedEvents, listener.getEventStrings(), "steps should have matched");
+ assertEquals(expectedValue, actualValue.get());
listener.clear();
}
From cfef056364e76a9e978411a76577df35eabbd580 Mon Sep 17 00:00:00 2001
From: "slav.babanin"
Date: Wed, 4 Mar 2026 13:28:06 -0800
Subject: [PATCH 2/5] Fis SpotBugs concern.
---
.../internal/async/AsyncTrampoline.java | 21 +++++++------------
1 file changed, 7 insertions(+), 14 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
index c7fc27e602b..44e5be12f65 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
@@ -20,6 +20,8 @@
import com.mongodb.assertions.Assertions;
import com.mongodb.lang.Nullable;
+import static com.mongodb.assertions.Assertions.assertNotNull;
+
/**
* A trampoline that converts recursive callback invocations into an iterative loop,
* preventing stack overflow in async loops.
@@ -66,8 +68,10 @@ public static void run(final Runnable work) {
try {
work.run();
// drain any re-entrant work iteratively
- while (bounce.hasWork()) {
- bounce.runNext();
+ while (bounce.work != null) {
+ Runnable workToRun = bounce.work;
+ bounce.work = null;
+ workToRun.run();
}
} finally {
TRAMPOLINE.remove();
@@ -91,16 +95,5 @@ void enqueue(final Runnable task) {
}
this.work = task;
}
-
- boolean hasWork() {
- return work != null;
- }
-
- void runNext() {
- Runnable task = this.work;
- this.work = null;
- Assertions.assertNotNull(task);
- task.run();
- }
}
-}
\ No newline at end of file
+}
From 9d38e5f47b023394fd143fee7fbd2e914093cf18 Mon Sep 17 00:00:00 2001
From: "slav.babanin"
Date: Wed, 4 Mar 2026 13:29:06 -0800
Subject: [PATCH 3/5] Remove unused imports.
---
.../src/main/com/mongodb/internal/async/AsyncTrampoline.java | 3 ---
1 file changed, 3 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
index 44e5be12f65..7a399e98b71 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
@@ -17,11 +17,8 @@
package com.mongodb.internal.async;
import com.mongodb.annotations.NotThreadSafe;
-import com.mongodb.assertions.Assertions;
import com.mongodb.lang.Nullable;
-import static com.mongodb.assertions.Assertions.assertNotNull;
-
/**
* A trampoline that converts recursive callback invocations into an iterative loop,
* preventing stack overflow in async loops.
From 62f4bb37b362c49c840211b5dfd27213ca6bdea3 Mon Sep 17 00:00:00 2001
From: "slav.babanin"
Date: Fri, 6 Mar 2026 15:40:35 -0800
Subject: [PATCH 4/5] Remove redundant comments.
---
.../com/mongodb/internal/async/AsyncTrampoline.java | 9 ++-------
.../internal/async/function/AsyncCallbackLoop.java | 12 ++++--------
2 files changed, 6 insertions(+), 15 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
index 7a399e98b71..d5d09996122 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
@@ -46,8 +46,7 @@ public final class AsyncTrampoline {
private static final ThreadLocal TRAMPOLINE = new ThreadLocal<>();
- private AsyncTrampoline() {
- }
+ private AsyncTrampoline() {}
/**
* Execute work through the trampoline. If no trampoline is active, become the owner
@@ -56,15 +55,12 @@ private AsyncTrampoline() {
public static void run(final Runnable work) {
Bounce bounce = TRAMPOLINE.get();
if (bounce != null) {
- // Re-entrant, enqueue and return
bounce.enqueue(work);
} else {
- // Become the trampoline owner.
bounce = new Bounce();
TRAMPOLINE.set(bounce);
try {
work.run();
- // drain any re-entrant work iteratively
while (bounce.work != null) {
Runnable workToRun = bounce.work;
bounce.work = null;
@@ -87,8 +83,7 @@ private static final class Bounce {
void enqueue(final Runnable task) {
if (this.work != null) {
- throw new AssertionError("Trampoline slot already occupied. "
- + "It could happen if there are multiple concurrent operations in a sequential async chain.");
+ throw new AssertionError("Trampoline slot already occupied");
}
this.work = task;
}
diff --git a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
index e9bd0c18a46..311892874b0 100644
--- a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
+++ b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
@@ -59,19 +59,15 @@ public void run(final SingleResultCallback callback) {
/**
* This callback is allowed to be completed more than once.
- * Also implements {@linkplain Runnable} to avoid lambda allocation per iteration when using trampoline.
*/
@NotThreadSafe
- private class LoopingCallback implements SingleResultCallback, Runnable {
+ private class LoopingCallback implements SingleResultCallback {
private final SingleResultCallback wrapped;
+ private final Runnable nextIteration;
LoopingCallback(final SingleResultCallback callback) {
wrapped = callback;
- }
-
- @Override
- public void run() {
- body.run(this);
+ nextIteration = () -> body.run(this);
}
@Override
@@ -87,7 +83,7 @@ public void onResult(@Nullable final Void result, @Nullable final Throwable t) {
return;
}
if (continueLooping) {
- AsyncTrampoline.run(this);
+ AsyncTrampoline.run(nextIteration);
} else {
wrapped.onResult(result, null);
}
From 7d9bf06309558c28ffbcf2899326f8a5d21693ec Mon Sep 17 00:00:00 2001
From: "slav.babanin"
Date: Fri, 6 Mar 2026 16:12:56 -0800
Subject: [PATCH 5/5] Rename classes.
---
.../internal/async/AsyncTrampoline.java | 48 +++++++++----------
.../async/AsyncFunctionsAbstractTest.java | 4 +-
2 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
index d5d09996122..5fc074b7008 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java
@@ -29,42 +29,42 @@
* loop would create > 1000 stack frames and cause {@code StackOverflowError}.
*
* The trampoline intercepts this recursion: instead of executing the next iteration
- * immediately (which would deepen the stack), it enqueues the work and returns, allowing
- * the stack to unwind. A flat loop at the top then processes enqueued work iteratively,
+ * immediately (which would deepen the stack), it enqueues the continuation and returns, allowing
+ * the stack to unwind. A flat loop at the top then processes enqueued continuation iteratively,
* maintaining constant stack depth regardless of iteration count.
*
* Since async chains are sequential, at most one task is pending at any time.
* The trampoline uses a single slot rather than a queue.
*
* The first call on a thread becomes the "trampoline owner" and runs the drain loop.
- * Subsequent (re-entrant) calls on the same thread enqueue their work and return immediately.
+ * Subsequent (re-entrant) calls on the same thread enqueue their continuation and return immediately.
*
* This class is not part of the public API and may be removed or changed at any time
*/
@NotThreadSafe
public final class AsyncTrampoline {
- private static final ThreadLocal TRAMPOLINE = new ThreadLocal<>();
+ private static final ThreadLocal TRAMPOLINE = new ThreadLocal<>();
private AsyncTrampoline() {}
/**
- * Execute work through the trampoline. If no trampoline is active, become the owner
- * and drain all enqueued work. If a trampoline is already active, enqueue and return.
+ * Execute continuation through the trampoline. If no trampoline is active, become the owner
+ * and drain all enqueued continuations. If a trampoline is already active, enqueue and return.
*/
- public static void run(final Runnable work) {
- Bounce bounce = TRAMPOLINE.get();
- if (bounce != null) {
- bounce.enqueue(work);
+ public static void run(final Runnable continuation) {
+ ContinuationHolder continuationHolder = TRAMPOLINE.get();
+ if (continuationHolder != null) {
+ continuationHolder.enqueue(continuation);
} else {
- bounce = new Bounce();
- TRAMPOLINE.set(bounce);
+ continuationHolder = new ContinuationHolder();
+ TRAMPOLINE.set(continuationHolder);
try {
- work.run();
- while (bounce.work != null) {
- Runnable workToRun = bounce.work;
- bounce.work = null;
- workToRun.run();
+ continuation.run();
+ while (continuationHolder.continuation != null) {
+ Runnable continuationToRun = continuationHolder.continuation;
+ continuationHolder.continuation = null;
+ continuationToRun.run();
}
} finally {
TRAMPOLINE.remove();
@@ -73,19 +73,19 @@ public static void run(final Runnable work) {
}
/**
- * A single-slot container for deferred work.
- * At most one task is pending at any time in a sequential async chain.
+ * A single-slot container for continuation.
+ * At most one continuation is pending at any time in a sequential async chain.
*/
@NotThreadSafe
- private static final class Bounce {
+ private static final class ContinuationHolder {
@Nullable
- private Runnable work;
+ private Runnable continuation;
- void enqueue(final Runnable task) {
- if (this.work != null) {
+ void enqueue(final Runnable continuation) {
+ if (this.continuation != null) {
throw new AssertionError("Trampoline slot already occupied");
}
- this.work = task;
+ this.continuation = continuation;
}
}
}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
index 05072a49925..8f6bc7046a2 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
@@ -888,8 +888,8 @@ void testNestedDoWhileLoops() {
// inner do-while: 3 success + 5 exception = 8
// last outer iteration: 3 < 3 = 1
// 5(inner exception) + 3(inner success) * 1(transition to next iteration) = 8
- // 5(inner exception) + 3(inner success) * 1(outer plainTest exception) + 1(outer plainTest false) + 8(transition to next iteration) = 35
- // 5(inner exception) + 3(inner success) * 1(outer plainTest exception) + 1(outer plainTest false) + 35(transition to next iteration) = 116
+ // 5(inner exception) + 3(inner success) * (1(outer plainTest exception) + 1(outer plainTest false) + 8(transition to next iteration)) = 35
+ // 5(inner exception) + 3(inner success) * (1(outer plainTest exception) + 1(outer plainTest false) + 35(transition to next iteration)) = 116
assertBehavesSameVariations(116,
() -> {
int outer = 0;