fix(otel): Avoid deadlock in SentryContextStorage.root() with virtual threads#5234
Merged
fix(otel): Avoid deadlock in SentryContextStorage.root() with virtual threads#5234
Conversation
… threads SentryContextStorage.root() called SentryContextWrapper.wrap() which triggers scope.clone() and acquires locks. Under virtual threads, ReentrantLock.unlock() can re-enter root() via OpenTelemetry executor instrumentation on ForkJoinPool.execute(), causing a deadlock. Return the default OTel root context without wrapping. Scopes are resolved later via attach() or Sentry.getCurrentScopes(). Fixes GH-5226 Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Bug Fixes 🐛
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
Sentry Build Distribution
|
Member
Author
|
cursor review |
Member
Author
|
@sentry review |
alexander-alderman-webb
approved these changes
Mar 26, 2026
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6405ec5 | 310.88 ms | 354.56 ms | 43.69 ms |
| d5a29b6 | 298.62 ms | 391.78 ms | 93.16 ms |
| f064536 | 329.00 ms | 395.62 ms | 66.62 ms |
| fcec2f2 | 357.47 ms | 447.32 ms | 89.85 ms |
| bbc35bb | 324.88 ms | 425.73 ms | 100.85 ms |
| 694d587 | 312.37 ms | 402.77 ms | 90.41 ms |
| b8bd880 | 314.56 ms | 336.50 ms | 21.94 ms |
| 889ecea | 367.58 ms | 437.52 ms | 69.94 ms |
| e2dce0b | 308.96 ms | 360.10 ms | 51.14 ms |
| 83884a0 | 334.46 ms | 400.92 ms | 66.46 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6405ec5 | 1.58 MiB | 2.12 MiB | 552.23 KiB |
| d5a29b6 | 1.58 MiB | 2.12 MiB | 549.37 KiB |
| f064536 | 1.58 MiB | 2.20 MiB | 633.90 KiB |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.50 KiB |
| bbc35bb | 1.58 MiB | 2.12 MiB | 553.01 KiB |
| 694d587 | 1.58 MiB | 2.19 MiB | 620.06 KiB |
| b8bd880 | 1.58 MiB | 2.29 MiB | 722.92 KiB |
| 889ecea | 1.58 MiB | 2.11 MiB | 539.75 KiB |
| e2dce0b | 0 B | 0 B | 0 B |
| 83884a0 | 1.58 MiB | 2.29 MiB | 722.97 KiB |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
SentryContextStorage.root()no longer callsSentryContextWrapper.wrap(). Instead it returns the default OTel root context directly.wrap()triggersforkCurrentScope()→scope.clone()which acquires locks onSynchronizedQueue(breadcrumbs). Under virtual threads,ReentrantLock.unlock()can schedule a virtual thread continuation viaForkJoinPool.execute(), which OpenTelemetry's executor instrumentation intercepts. That instrumentation callsContext.current()which falls back toContext.root()→SentryContextStorage.root(), re-entering scope cloning and attempting to acquire the same lock. This causes a deadlock.Sentry scopes are resolved later via
attach()(when a context is actually used) orSentry.getCurrentScopes().💡 Motivation and Context
Fixes #5226 — customer-reported deadlock with virtual threads + OTel agent (Java 25, Spring Boot, Redisson/Netty).
The OTel
ContextStorage.root()contract requires it to be trivially cheap, idempotent, and lock-free. Our implementation violated that contract by triggering scope cloning.💚 How did you test it?
spotlessApply,apiDump)📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
attach()for similar re-entrancy risk from auto-instrumented executor pathsSynchronizedQueuewith lock-freeConcurrentLinkedQueue+AtomicInteger(defense-in-depth)