Repository navigation
[sdk] Fix suppression state isolation across async flows - #7803
Conversation
Pull request dashboard statusMerged · refreshed 2026-09-24 21:16 UTC Status above doesn't look right?
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
A core SDK behavioural bug fix requires an entry in src/OpenTelemetry/CHANGELOG.md under ## Unreleased, which is currently missing.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Fixes a correctness issue in the SDK’s suppression mechanism where a mutable suppression state stored in runtime context could be shared by reference across inherited ExecutionContext/async flows, causing suppression depth mutations in one flow to affect others.
Changes:
- Reworks
SuppressInstrumentationScopeto store immutable suppression state objects in the runtime context slot, with a bounded cache for common reference counts. - Adds regression tests to validate isolation across overlapping parent/child async flows and correct nested-scope restoration semantics.
- Extends test coverage for deep reference-count nesting beyond the cached boundary.
| File | Description |
|---|---|
| src/OpenTelemetry/SuppressInstrumentationScope.cs | Switches suppression tracking to immutable, cached state tokens to prevent cross-async-flow leakage while retaining performance benefits. |
| test/OpenTelemetry.Tests/SuppressInstrumentationTests.cs | Adds regression tests covering overlapping async flow isolation, explicit-scope restoration, and deep reference count behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7803 +/- ##
=======================================
Coverage 91.90% 91.91%
=======================================
Files 341 341
Lines 18615 18610 -5
=======================================
- Hits 17109 17106 -3
+ Misses 1506 1504 -2
Flags with carried forward coverage won't be shown. Click here to find out more.
|
martincostello
left a comment
There was a problem hiding this comment.
Just some comments on the tests.

Both PR and description AI assisted. I have reviewed both carefully.
Changes
Fix
SuppressInstrumentationScopereference-count state leaking between inheritedExecutionContext/async flows.The fix stores immutable suppression states in the runtime context slot. Common
reference counts are cached, preserving most of the allocation benefit of the
current implementation without sharing mutable state between async flows.
This change also adds regression coverage for:
Enter,IncrementIfTriggered, andDecrementIfTriggeredisolation whileparent and child async flows overlap.
Why this is needed
#4304
changed the slot from an
intto a mutableSuppressInstrumentationScopeinstance as a performance optimization. This avoided boxing an integer and
updating
AsyncLocal/ExecutionContextstate for every reference-count change.However, an
AsyncLocalreference value flows into a child execution context asthe same object reference. Mutating
Depthin place therefore mutates the valueobserved by the parent and sibling flows as well. Suppression may consequently
end too early or remain active too long, causing telemetry to be incorrectly
recorded or suppressed.
A test that lets the child increment and then decrement before checking the
parent does not expose the bug: the shared counter has already returned to its
starting value. The new tests coordinate the flows so the parent observes its
own state while the child's mutation is still active. These tests fail against
the mutable implementation.
The mutable implementation also changes nested-scope semantics. When a
reference count reaches zero inside
Begin(false), it restores the precedingscope immediately instead of waiting for the explicit scope to be disposed.
The immutable-state implementation retains the original value-slot behavior.
Implementation
slot.
state for normal nesting depths.
Slot.Setwhenever the reference count changes so each execution contextreceives its own value.
Performance
The original #4304 results show the cost that a direct return to
AsyncLocal<int>would reintroduce:
int)BeginscopesEnterreference-count cycleNew comparison benchmarks were run with BenchmarkDotNet 0.15.8 on .NET
10.0.12, Windows 11, and an Intel Core i9-13950HX. Runs were pinned to one
performance core, with 5 warmup and 15 measurement iterations.
intvalueEnter+ increment + two decrementsBeginscopesFor the active reference-count cycle, the immutable implementation costs about
25.1 ns and 104 B more than the unsafe mutable holder. A direct integer fix
would cost about 48.5 ns and 272 B more. The immutable cache therefore recovers
roughly half the time penalty and most of the additional allocation.
On .NET 10, nested scopes are faster than both alternatives because they reuse
the same immutable always-suppressed token. Redundant
AsyncLocalwrites becomeno-ops and the JIT can eliminate the short-lived scope allocations.
These are focused microbenchmarks. Normal non-triggered suppression checks do
not update the runtime context, and exporter work will usually dominate the
absolute scope-management cost.
Testing
CI
Merge requirement checklist
CHANGELOG.mdfiles updated for non-trivial changes (requires the new PR number)