fix: Create a single TaskBlockingListener per process for CaptureBlockingCalls - #5381
Merged
Merged
Conversation
…kingCalls SentryMiddleware is registered as a transient IMiddleware, so DI constructs a new instance per request. With CaptureBlockingCalls enabled, its constructor created a new TaskBlockingListener (an EventListener) every request and never disposed it. Each listener registered itself in the runtime's process-global listener chain and enabled TplEventSource, so the chain grew unboundedly and EventSource.DispatchToAllListeners walked it on every task-wait of every await in the process - degrading latency, CPU, and managed heap linearly with the total number of requests served. Register BlockingMonitor and TaskBlockingListener as singletons and have the transient middleware resolve (rather than construct) them, so exactly one listener exists per process. The per-request DetectBlockingSynchronizationContext swap is unchanged, since it carries per-request suppression state. Also fix TaskBlockingListener.DefaultState, which was an expression-bodied property allocating a fresh Lazy on every access instead of a single shared instance. Fixes #5378 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5381 +/- ##
==========================================
+ Coverage 74.25% 74.28% +0.03%
==========================================
Files 509 509
Lines 18420 18427 +7
Branches 3606 3606
==========================================
+ Hits 13677 13688 +11
+ Misses 3869 3866 -3
+ Partials 874 873 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jamescrosswell
commented
Jul 13, 2026
jamescrosswell
commented
Jul 13, 2026
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
jamescrosswell
commented
Jul 14, 2026
Address review feedback: instead of manual factory lambdas, register the services by type and let the container resolve their constructors. BlockingMonitor takes (Func<IHub>, SentryOptions) - both already registered - and TaskBlocking listener takes IBlockingMonitor, so an IBlockingMonitor -> BlockingMonitor forward keeps them the same singleton instance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-leak' into fix/issue-5378-blocking-listener-leak # Conflicts: # src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs
jamescrosswell
commented
Jul 14, 2026
Address review feedback: register the monitor as AddSingleton<IBlockingMonitor, BlockingMonitor>() and have the middleware resolve IBlockingMonitor (which is all DetectBlockingSynchronizationContext needs) instead of the concrete type, removing the separate interface forward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jamescrosswell
marked this pull request as ready for review
July 15, 2026 01:28
bruno-garcia
approved these changes
Jul 15, 2026
bruno-garcia
left a comment
Member
There was a problem hiding this comment.
LGTM, I'd add some integration tests too as a follow up
Collaborator
Author
Follow up issue: |
3 tasks
This was referenced Jul 27, 2026
This was referenced Jul 27, 2026
Open
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.
Summary
Fixes a per-request resource leak in
CaptureBlockingCalls(ASP.NET Core), reported in #5378.SentryMiddlewareimplementsIMiddlewareand is registered withAddTransient<SentryMiddleware>(), so DI constructs a new instance per request. WithCaptureBlockingCalls = true, its constructor created a newTaskBlockingListener(aSystem.Diagnostics.Tracing.EventListener) on every request and never disposed it:Every
EventListenerregisters itself in the runtime's process-global listener chain and enablesTplEventSource(Verbose). So the chain grew by one per request andEventSource.DispatchToAllListenerswalked the whole (ever-growing) chain on every task-wait event of everyawaitin the process — not just inside requests.The reporter observed, on effectively-idle services (only a 10s healthcheck): p95 of a trivial endpoint growing ~5 ms → ~150 ms over 5 days (~45k requests → ~45k leaked listeners), 96–97% of CPU inside
EventSource.DispatchToAllListeners, and steadily growing background CPU and managed heap — all reset by a restart. Full credit to @gerardfontmora for the diagnosis.Fix
BlockingMonitorandTaskBlockingListeneras singletons, and have the transient middleware resolve (rather than construct) them, so exactly oneTaskBlockingListenerexists per process with constant overhead. They are only resolved whenCaptureBlockingCallsis enabled, so disabling the feature has no cost.DetectBlockingSynchronizationContextswap inInvokeAsyncis unchanged — it carries per-request suppression state and is correctly created per request.TaskBlockingListener.DefaultStatewas an expression-bodied property (=> new()) that allocated a freshLazy<>on every access instead of returning a single shared instance; changed to a static auto-property.Regression test
Constructor_CaptureBlockingCalls_SharesSingleListenerAcrossInstancesbuilds two middleware instances (simulating two requests) and asserts they share oneListener/Monitor. Verified it fails against the previous per-request construction and passes with this change.Fixes #5378