Skip to content

fix(otel): Avoid deadlock in SentryContextStorage.root() with virtual threads#5234

Merged
adinauer merged 2 commits intomainfrom
fix/otel-virtual-thread-deadlock
Mar 26, 2026
Merged

fix(otel): Avoid deadlock in SentryContextStorage.root() with virtual threads#5234
adinauer merged 2 commits intomainfrom
fix/otel-virtual-thread-deadlock

Conversation

@adinauer
Copy link
Copy Markdown
Member

📜 Description

SentryContextStorage.root() no longer calls SentryContextWrapper.wrap(). Instead it returns the default OTel root context directly.

wrap() triggers forkCurrentScope()scope.clone() which acquires locks on SynchronizedQueue (breadcrumbs). Under virtual threads, ReentrantLock.unlock() can schedule a virtual thread continuation via ForkJoinPool.execute(), which OpenTelemetry's executor instrumentation intercepts. That instrumentation calls Context.current() which falls back to Context.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) or Sentry.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?

  • Verified the change compiles and passes formatting (spotlessApply, apiDump)

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

  • Audit attach() for similar re-entrancy risk from auto-instrumented executor paths
  • Replace SynchronizedQueue with lock-free ConcurrentLinkedQueue + AtomicInteger (defense-in-depth)
  • Fix Scope copy constructor thread-safety issues (fingerprint, session, propagationContext races)

adinauer and others added 2 commits March 26, 2026 10:48
… 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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Bug Fixes 🐛

  • (otel) Avoid deadlock in SentryContextStorage.root() with virtual threads by adinauer in #5234

Internal Changes 🔧

  • Pin GitHub Actions to full-length commit SHAs by joshuarli in #5229

🤖 This preview updates automatically when you update the PR.

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 26, 2026

Sentry Build Distribution

App Name App ID Version Configuration Install Page
SDK Size io.sentry.tests.size 8.37.0 (1) release Install Build

@adinauer
Copy link
Copy Markdown
Member Author

cursor review

@adinauer
Copy link
Copy Markdown
Member Author

@sentry review

@adinauer adinauer marked this pull request as ready for review March 26, 2026 09:55
@github-actions
Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 340.96 ms 416.90 ms 75.94 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@adinauer adinauer merged commit f2c2e7d into main Mar 26, 2026
67 of 68 checks passed
@adinauer adinauer deleted the fix/otel-virtual-thread-deadlock branch March 26, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deadlock occurs in Sentry OpenTelemetry Agent under virtual thread environment

2 participants