Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions src/Sentry.AspNetCore/SentryMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Sentry.AspNetCore.Extensions;
Expand Down Expand Up @@ -34,10 +35,13 @@ internal static readonly SdkVersion NameAndVersion
private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name;

// Ben.BlockingDetector
private readonly BlockingMonitor? _monitor;
private readonly DetectBlockingSynchronizationContext? _detectBlockingSyncCtx;
private readonly IBlockingMonitor? _monitor;
private readonly TaskBlockingListener? _listener;

// Internal for testing
internal IBlockingMonitor? Monitor => _monitor;
internal TaskBlockingListener? Listener => _listener;

/// <summary>
/// Initializes a new instance of the <see cref="SentryMiddleware"/> class.
/// </summary>
Expand All @@ -48,6 +52,7 @@ internal static readonly SdkVersion NameAndVersion
/// <param name="eventExceptionProcessors">Custom Event Exception Processors</param>
/// <param name="eventProcessors">Custom Event Processors</param>
/// <param name="transactionProcessors">Custom Transaction Processors</param>
/// <param name="serviceProvider">The service provider, used to resolve dependencies.</param>
/// <exception cref="ArgumentNullException">
/// next
/// or
Expand All @@ -60,7 +65,8 @@ public SentryMiddleware(
ILogger<SentryMiddleware> logger,
IEnumerable<ISentryEventExceptionProcessor> eventExceptionProcessors,
IEnumerable<ISentryEventProcessor> eventProcessors,
IEnumerable<ISentryTransactionProcessor> transactionProcessors)
IEnumerable<ISentryTransactionProcessor> transactionProcessors,
IServiceProvider serviceProvider)
{
ArgumentNullException.ThrowIfNull(getHub);

Expand All @@ -74,9 +80,9 @@ public SentryMiddleware(

if (_options.CaptureBlockingCalls)
{
_monitor = new BlockingMonitor(_getHub, _options);
_detectBlockingSyncCtx = new DetectBlockingSynchronizationContext(_monitor);
_listener = new TaskBlockingListener(_monitor);
// Resolve shared singletons to keep overhead constant - See #5378.
_monitor = serviceProvider.GetRequiredService<IBlockingMonitor>();
_listener = serviceProvider.GetRequiredService<TaskBlockingListener>();
}
}

Expand Down Expand Up @@ -150,7 +156,11 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
if (_options.CaptureBlockingCalls && _monitor is not null)
{
var syncCtx = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(syncCtx == null ? _detectBlockingSyncCtx : new DetectBlockingSynchronizationContext(_monitor, syncCtx));
// Created per request as it carries per-request suppression state.
var detectingSyncCtx = syncCtx is null
? new DetectBlockingSynchronizationContext(_monitor)
: new DetectBlockingSynchronizationContext(_monitor, syncCtx);
SynchronizationContext.SetSynchronizationContext(detectingSyncCtx);
try
{
// For detection to work we need ConfigureAwait=true
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Options;
using Sentry.AspNetCore;
using Sentry.Ben.BlockingDetector;

// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Hosting;
Expand Down Expand Up @@ -112,6 +113,9 @@ public static IWebHostBuilder UseSentry(
_ = builder.ConfigureServices(c => _ =
c.AddTransient<IStartupFilter, SentryStartupFilter>()
.AddTransient<IStartupFilter, SentryTracingStartupFilter>()
// Single listener/monitor per process (the listener is a global EventListener) - See #5378.
.AddSingleton<IBlockingMonitor, BlockingMonitor>()
.AddSingleton<TaskBlockingListener>()
.AddTransient<SentryMiddleware>()
);

Expand Down
3 changes: 1 addition & 2 deletions src/Sentry/Ben.BlockingDetector/TaskBlockingListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ internal class TaskBlockingListener : EventListener
private readonly IBlockingMonitor _monitor;
private readonly ITaskBlockingListenerState _state;

private static Lazy<StaticTaskBlockingListenerState> LazyDefaultState => new();
internal static StaticTaskBlockingListenerState DefaultState => LazyDefaultState.Value;
internal static StaticTaskBlockingListenerState DefaultState { get; } = new();

public TaskBlockingListener(IBlockingMonitor monitor)
: this(monitor, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public SentryMiddleware GetSut()
MiddlewareLogger,
EventExceptionProcessors,
EventProcessors,
TransactionProcessors);
TransactionProcessors,
Substitute.For<IServiceProvider>());

public void Dispose() => _disposable.Dispose();
}
Expand Down
37 changes: 36 additions & 1 deletion test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sentry.Ben.BlockingDetector;

#if NETCOREAPP3_1_OR_GREATER
using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
Expand All @@ -27,6 +29,7 @@ private class Fixture
public IEnumerable<ISentryTransactionProcessor> TransactionProcessors { get; set; } = Substitute.For<IEnumerable<ISentryTransactionProcessor>>();
public IFeatureCollection FeatureCollection { get; set; } = Substitute.For<IFeatureCollection>();
public Scope Scope { get; set; }
public IServiceProvider ServiceProvider { get; }

public Fixture()
{
Expand All @@ -40,6 +43,15 @@ public Fixture()
_ = Hub.IsEnabled.Returns(true);
_ = Hub.StartTransaction("", "").ReturnsForAnyArgs(new TransactionTracer(Hub, "test", "test"));
_ = HttpContext.Features.Returns(FeatureCollection);

// Mirrors the singleton registrations in SentryWebHostBuilderExtensions so the
// (transient) middleware can resolve the shared blocking monitor/listener.
ServiceProvider = new ServiceCollection()
.AddSingleton(HubAccessor)
.AddSingleton<SentryOptions>(Options)
.AddSingleton<IBlockingMonitor, BlockingMonitor>()
.AddSingleton<TaskBlockingListener>()
.BuildServiceProvider();
}

public SentryMiddleware GetSut()
Expand All @@ -50,11 +62,34 @@ public SentryMiddleware GetSut()
Logger,
EventExceptionProcessors,
EventProcessors,
TransactionProcessors);
TransactionProcessors,
ServiceProvider);
}

private readonly Fixture _fixture = new();

[Fact]
public void Constructor_CaptureBlockingCalls_SharesSingleListenerAcrossInstances()
{
_fixture.Options.CaptureBlockingCalls = true;

// The middleware is registered as transient, so DI creates a new instance per request.
// Simulate two requests: distinct instances must share the one process-wide monitor and
// listener rather than each allocating (and leaking) their own EventListener. See #5378.
var first = _fixture.GetSut();
var second = _fixture.GetSut();

Assert.NotSame(first, second);
Assert.NotNull(first.Listener);
Assert.NotNull(first.Monitor);
Assert.Same(first.Listener, second.Listener);
Assert.Same(first.Monitor, second.Monitor);

// Dispose the shared listener so it doesn't remain registered as a global EventListener
// (and keep TplEventSource enabled) for the rest of the test run.
(_fixture.ServiceProvider as IDisposable)?.Dispose();
}

[Fact]
public async Task InvokeAsync_DisabledSdk_InvokesNextHandlers()
{
Expand Down
Loading