Add stack-safe async loop support with trampoline pattern#1905
Add stack-safe async loop support with trampoline pattern#1905vbabanin wants to merge 11 commits intomongodb:mainfrom
Conversation
- 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
|
|
||
| LoopingCallback(final SingleResultCallback<Void> callback) { | ||
| wrapped = callback; | ||
| nextIteration = () -> body.run(this); |
There was a problem hiding this comment.
The nextIteration is reused to avoid creation of extra objects via LambdaMetafactory as we have the capturing lambda.
bounce.work = task is a write to a heap object's field, which can be considered an automatic escape in the JIT's analysis. Even if the Bounce object is short-lived, the JIT sees "object written to another object's field" and should give up.
The AsyncCallbackLoop JMH GC profiling (OpenJDK 17.0.10 LTS, 64-bit Server VM, mixed mode with compressed oops).
| Metric | Runnable (this) | Lambda |
|---|---|---|
| Alloc rate | 0.039 MB/sec | 96.924 MB/sec |
| Alloc per op | 64 B/op | 160,048 B/op |
| GC count | ~ 0 | 10 |
| GC time | 0 ms | 9 ms |
For Lambda case:
Per iteration: 1 lambda * 16 bytes = 16 B
- Per op (10,000 iterations): 10,000 * 16 = 160,000 B
- Plus one-time objects ~ 48 B
There was a problem hiding this comment.
Pull request overview
This PR introduces stack-safe async looping by adding a trampoline mechanism and extending the internal async chaining API with pre-check while-loop semantics.
Changes:
- Added
AsyncTrampolineto convert re-entrant callback recursion into iterative execution (stack-safe loop continuations). - Added
AsyncRunnable.thenRunWhileLoop(...)forwhile (condition) { body }semantics in async chains. - Updated
AsyncCallbackLoopto use the trampoline and avoid per-iteration lambda allocation; expanded unit tests to cover while/do-while behavior and nested loops.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| driver-core/src/main/com/mongodb/internal/async/AsyncTrampoline.java | New trampoline utility to flatten re-entrant async loop continuations and keep stack depth constant. |
| driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java | Routes loop continuation through AsyncTrampoline.run(...) and caches the “next iteration” runnable. |
| driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java | Adds thenRunWhileLoop API to express while-loops in async chains. |
| driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java | Adds tests for while-loop semantics, nested loops, and stack-depth invariance checks. |
| driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsTestBase.java | Enhances assertion messaging when sync/async exception presence diverges. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| format("both or neither should have produced an exception. Expected exception: %s, actual exception: %s", | ||
| expectedException, | ||
| actualException)); |
There was a problem hiding this comment.
The assertion message formats actualException (the AtomicReference) instead of the stored throwable (actualException.get()), so failures will print something like AtomicReference@... rather than the actual exception. Pass actualException.get() to format (and consider also including expectedException.getClass()/actualException.get().getClass() if helpful).
| format("both or neither should have produced an exception. Expected exception: %s, actual exception: %s", | |
| expectedException, | |
| actualException)); | |
| format("both or neither should have produced an exception. " | |
| + "Expected exception: %s (%s), actual exception: %s (%s)", | |
| expectedException, | |
| expectedException == null ? null : expectedException.getClass(), | |
| actualException.get(), | |
| actualException.get() == null ? null : actualException.get().getClass())); |
There was a problem hiding this comment.
agree with copilot here , actualException is atomic reference that gives throwable, I think we should replace it with actualException.get
There was a problem hiding this comment.
I have changed it to use actualException.get(). However, what Copilot suggested with ternary logic is not correct, because expectedException.getClass() will provide less details than using toString on the exception.
driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
Outdated
Show resolved
Hide resolved
driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
Show resolved
Hide resolved
driver-core/src/test/unit/com/mongodb/internal/async/AsyncFunctionsAbstractTest.java
Show resolved
Hide resolved
…java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java
Outdated
Show resolved
Hide resolved
| assertEquals(depthWith100, depthWith10000, "Stack depth should be constant regardless of iteration count (trampoline)"); | ||
| } | ||
|
|
||
| private int maxStackDepthForIterations(final int iterations) { |
There was a problem hiding this comment.
can we move this method below all other test cases
so that we preserve the order
- tests first
- utility + private method second
There was a problem hiding this comment.
I agree that separating utilities from tests is useful in general. However, these methods each have exactly one caller (the test directly above). I kept them inline for locality so the reader sees the test and its implementation detail together without scrolling. The codebase doesn't enforce a strict ordering convention - CrudProseTest.java#L357 and several other test classes use the same inline approach.
| format("both or neither should have produced an exception. Expected exception: %s, actual exception: %s", | ||
| expectedException, | ||
| actualException)); |
There was a problem hiding this comment.
agree with copilot here , actualException is atomic reference that gives throwable, I think we should replace it with actualException.get
…CallbackLoop.java Co-authored-by: Almas Abdrazak <almas337519@gmail.com>
JAVA-6120
JAVA-6120