Repository navigation
Conversation
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
4ccc924 to
0c7bb96
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
More details
The completed static review raised no reportable defect in coroutine stack ownership or trace-continuation lifetimes. Runtime handoff and concurrency behavior remain unverified.
🤖 Bits Code Review · Commit 0c7bb96
|
I think I was able to follow what the code is doing, but the flow is hard to follow. To a large extent, I think that's inherent to the problem, but I also find that we don't have agreed upon terms to describe what we're doing. I had Claude map seed, owner, trace kept open into my own terms, so I could understand what's happening here. Since we're doing similar changes in a number of locations, I think it might be good to add some extra conveniences into the Context system to fit these situations. Claude was able to reproduce an issue around The scenario... And to be clear, adding the "macros" doesn't have to be part of this PR, I just think reviewing will remain hard until we have some common terms for these things. |
|
Reproducer for the scoped-coroutine issue above. With an empty Over four runs:
/**
* A child launched by its parent after a scoped coroutine in the parent has completed. The
* parent is started under a root span that finishes before the child runs.
*/
open fun structuredChildAfterScopedCoroutine(withScoped: Boolean): Int {
runTest {
childSpan("root").activateAndUse {
detached = launch(Job() + jobName("detached")) {
childSpan("seed").activateAndUse {
if (withScoped) {
withContext(jobName("scoped")) {}
}
launch(coroutineContext + jobName("child")) {
gate.await()
childSpan("child").finish()
}
}
}
}
}
return 3
}
def "child launched after a scoped coroutine keeps the trace open withScoped=#withScoped #dispatcherName"() {
setup:
CoreKotlinCoroutineTests kotlinTest = getCoreKotlinCoroutineTestsInstance(dispatcher)
int expectedNumberOfSpans = kotlinTest.structuredChildAfterScopedCoroutine(withScoped)
kotlinTest.finishDetached()
TEST_WRITER.waitForTraces(1)
List<DDSpan> trace = TEST_WRITER.get(0)
expect:
TEST_WRITER.size() == 1
trace.size() == expectedNumberOfSpans
findSpan(trace, "child").spanContext().getParentId() == findSpan(trace, "seed").spanContext().getSpanId()
findSpan(trace, "seed").spanContext().getParentId() == findSpan(trace, "root").spanContext().getSpanId()
where:
[dispatcherName, dispatcher, withScoped] << [dispatchersToTest, [true, false]].combinations().collect { it[0] + [it[1]] }
}Written by Claude, reviewed by @dougqh |
|
@dougqh actually the majority of changes here have nothing to do with the |
|
@dougqh @amarziali one option could be to declare that we cannot support Kotlin coroutines/suspensions properly with the legacy scope manager and instead pivot to replacing that as soon as we can with the new context manager. |
|
@dougqh @amarziali regarding the scoped-coroutine issue - I confirmed the issue and fixed it in f3f8dc8. Scoped coroutines share their caller’s Datadog element, so their completion must not release the owning coroutine’s continuation. Release now checks ownership, and owner completion also clears traceKeptOpen. Note: this issue predates this PR: running the regression tests with the previous element implementation also reproduces premature trace splitting. The PR’s continuation optimization relied on that existing lifetime accounting being correct. I also added coverage for a child launched after withContext, coroutineScope, or withTimeout completes, plus a control without a scoped coroutine. Both full suites pass on kotlinx 1.3 and 1.8. |
|
I split off the general fixes (the ones that don't rely on AgentTracer / ContinuableScopeManager changes) into #12914 |
…2914) Fix Kotlin coroutine context isolation and continuation ownership Extract the Kotlin fixes and independent regression tests from PR #12864. Give builder-created coroutines fresh context elements while preserving context element order. Only release an element's continuation when its owning coroutine completes, including children of already-cancelled jobs. Skip redundant trace continuations for eligible structured children. Keep the existing context capture and swap/restore behavior. Parent live stack seeding, shared scope stacks, AgentTracer/CoreTracer/scope-manager changes, and the handoff tests requiring them are deliberately excluded. Include context replacement and trace-lifetime regressions, run the latest dependency suite on kotlinx-coroutines 1.8, and regenerate the module lockfile. Source: #12864 Validation: test and latestDepTest on JDK 17 (276 tests per run), with both legacy and non-legacy context managers; module spotlessCheck. Test Kotlin coroutine instrumentation across supported runtime versions Add coroutines16Test and coroutines18Test suites pinned to 1.6.4 and 1.8.1, reusing the existing latest-dependency test sources and shared fixtures. Keep the minimum-version suite on 1.3.0 and advance latestDepTest to 1.+, currently locked to 1.11.0. Give the flow builders explicit Int element types so the shared tests compile with the newer dependency using the existing Kotlin compiler. Regenerate the module dependency lockfile for all suites. Validation: all four suites on JDK 17 with both legacy and non-legacy context managers (562 tests per matrix); module spotlessCheck. Use local root spans to track coroutine trace ownership Identify the trace held open by a coroutine or its parent using the local root AgentSpan rather than AgentTraceCollector. Compare local root span identity when deciding whether a structured child needs its own trace continuation, and rename the ownership fields and helper accordingly. Validation: all four coroutine version suites on JDK 17 with legacy and non-legacy context managers (562 tests per matrix); module spotlessCheck. Release completed coroutine owners with a sentinel Align coroutine test dependencies with their core modules Preserve active span changes when a scoped coroutine reinstalls the Datadog element Mark this lazy coroutine test as flaky when using the legacy context manager Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
dougqh
left a comment
There was a problem hiding this comment.
Confirmed the scoped-coroutine trace-splitting issue from my earlier reproducer is fixed: KotlinCoroutineInstrumentationTest passed 5/5 runs locally (127 tests each), including all child launched after a scoped coroutine cases on Unconfined, which used to fail reliably.
A few further findings inline. The preload one matters most to me since it can affect non-Kotlin virtual-thread users.
These findings came from a Claude Code review that I've read through; they haven't all been reproduced.
dougqh
left a comment
There was a problem hiding this comment.
Thanks for fixing that issue - Claude has several other findings which I had it post inline
edad8de to
a4269e2
Compare
A coroutine can resume on another worker before its previous worker has finished restoring thread context. Add a legacy scope stack that follows the coroutine so every worker observes scope changes across that handoff. Publish the current scope for lock-free reads, serialize stack mutations and shared reference-count updates, and prevent closed or overdue scopes from being retained again. Run profiler hooks, scope listeners, and span finishing outside the stack lock, refreshing the top after profiler hooks. Expose shared stack creation through the tracer API, reuse shared handles and same-thread restore tokens, and preserve saved snapshots for ordinary cross-thread restoration. Update virtual-thread preloading for the saved context rename without eagerly loading shared stacks. Cover cross-thread closure, reference-count races, overdue scopes, context views, token reuse, reentrant callbacks, and notification ordering in core tests; add a virtual-thread class-loading regression. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use a shared legacy scope stack for each coroutine so scopes opened before suspension remain visible when execution resumes on another worker. Seed child elements from the inherited element's current context, then release the seed after capture to avoid retaining the parent's stack. Restore each worker's original stack without rebinding the coroutine's shared stack, including overlapping callbacks and replayed restore tokens. Retain context refresh and swap-based saving for the non-legacy manager, and preserve context saving when the tracer cannot create shared stacks. Keep the existing continuation ownership and local-root tracking. Add regressions for suspension APIs, dispatcher changes, structured child waits, child isolation, nested contexts, cancellation, overlapping context elements, and restore-token replay. Bound worker shutdown and exclude the replay test on coroutines 1.6.1, which lacks the required replay behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a4269e2 to
33d9f9e
Compare
What Does This Do
Preserves Datadog scopes when Kotlin coroutines suspend on one worker and resume on another with the legacy context manager, keeping nested scopes and span parenting intact across dispatcher changes and structured coroutine boundaries.
Motivation
Kotlin can resume a coroutine on worker B before worker A finishes
restoreThreadContext. Saving the coroutine's context during restoration can therefore publish it too late for resumption. The legacy manager also restores only a shallow snapshot when a saved context crosses threads, losing deeper scopes.Each coroutine with its own Datadog context element now keeps a persistent
SharedScopeStack. Workers swap that same live stack in while executing the coroutine and restore their own stack afterward. This uses the existingThreadContextElementlifecycle without instrumenting individual suspension primitives.Additional Notes
Context and restoration
AgentTracer.sharedContextStack(initial)operation. Independent child elements get their own stack seeded from the inherited stack's current context; scoped coroutines that borrow their caller's element continue sharing its stack. Clear the temporary seed after initialization so children do not retain the parent's stack.ScopeContext→SavedScopeContextrename, with a forked regression checking that virtual-thread handoffs do not load the coroutine shared-stack classes.Overlapping callbacks
Coroutine execution passes between workers in sequence, but the previous worker's restoration callbacks can overlap the next worker's execution. Other context elements may open or close Datadog scopes during those callbacks.
Shared-stack mutations use the stack monitor; frequent active-scope reads use a volatile top. Shared scopes synchronize reference-count changes on the scope itself, including closes from a thread with another stack installed. Closed or cleared scopes cannot be retained again, and overdue iteration scopes are retired before removal.
Profiler hooks, scope listeners, and span finishing run outside the stack lock so callbacks can reenter tracing or wait for another worker. Activation notifications reread the top after profiler hooks, and cleanup preserves scopes opened by close listeners. SpotBugs suppressions document the publication ordering and concurrency assumptions.
Legacy listeners receive no context argument, so another worker can still change the top during a notification. Scopes opened by other context elements during restoration can briefly remain visible until closed. Cleanup of scopes closed from another stack is best effort and retried on later operations or swaps. The shared-stack implementation and tracer API hook are specific to the legacy manager.
Regression coverage
yield,delay,select, nested scopes, dispatcher changes with delayed restoration, child isolation, suspension afterflowOncollection, and trace retention until completion.coroutineScope,supervisorScope,withContext, andwithTimeout, including scopes closed inside the block, exceptions, and cancellation.Validation: GitHub PR checks pass; the build and DDCI checks are still pending at the time of this update. No additional tests were run for this description-only update.
Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issue