Skip to content

Commit e67eb03

Browse files
z0rimojamescrosswellclaude
authored
test: add ASP.NET Core blocking detection integration tests (#5578)
* test: add ASP.NET Core blocking detection integration tests * test: make the sync-context blocking test deterministic ManualResetEventSlim.Wait spins before it blocks, and only the blocking phase goes through SynchronizationContext.Wait. On a loaded machine the spin phase can consume the whole 25ms timeout, so no wait notification fires and no blocking event is captured — which is why this test failed on six of the seven CI jobs while passing locally. Measured with a standalone probe counting SynchronizationContext.Wait invocations on a thread-pool thread: idle, both primitives hit 50/50; under CPU load ManualResetEventSlim dropped to 44/50 and 41/50 across trials, while ManualResetEvent.WaitOne stayed at 50/50 (250/250 overall). WaitHandle.WaitOne checks IsWaitNotificationRequired and routes through the sync context in shared managed code, with no spin phase, so it exercises the intended path on every platform. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent baa2be6 commit e67eb03

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

‎test/Sentry.AspNetCore.TestUtils/SentrySdkTestFixture.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ protected virtual void ConfigureBuilder(WebHostBuilder builder)
7676

7777
public void Dispose()
7878
{
79+
HttpClient?.Dispose();
80+
TestServer?.Dispose();
7981
SentrySdk.Close();
8082
}
8183
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.DependencyInjection.Extensions;
3+
using Sentry.AspNetCore.TestUtils;
4+
using Sentry.Ben.BlockingDetector;
5+
6+
namespace Sentry.AspNetCore.Tests;
7+
8+
public partial class IntegrationsTests
9+
{
10+
private const string BlockingCallDetectorMechanism = "BlockingCallDetector";
11+
12+
[Fact]
13+
public async Task InvokeAsync_CaptureBlockingCallsEnabled_ReusesListenerAcrossRequests()
14+
{
15+
var middlewareInstances = new ConcurrentQueue<SentryMiddleware>();
16+
17+
Configure = options => options.CaptureBlockingCalls = true;
18+
AfterConfigureBuilder = builder => builder.ConfigureServices(services =>
19+
{
20+
services.RemoveAll<SentryMiddleware>();
21+
services.AddTransient(serviceProvider =>
22+
{
23+
var middleware = ActivatorUtilities.CreateInstance<SentryMiddleware>(serviceProvider);
24+
middlewareInstances.Enqueue(middleware);
25+
return middleware;
26+
});
27+
});
28+
29+
Build();
30+
31+
_ = await HttpClient.GetAsync("/");
32+
_ = await HttpClient.GetAsync("/");
33+
34+
var instances = middlewareInstances.ToArray();
35+
Assert.Equal(2, instances.Length);
36+
Assert.NotSame(instances[0], instances[1]);
37+
Assert.NotNull(instances[0].Monitor);
38+
Assert.NotNull(instances[0].Listener);
39+
Assert.Same(instances[0].Monitor, instances[1].Monitor);
40+
Assert.Same(instances[0].Listener, instances[1].Listener);
41+
Assert.Same(ServiceProvider.GetRequiredService<IBlockingMonitor>(), instances[0].Monitor);
42+
Assert.Same(ServiceProvider.GetRequiredService<TaskBlockingListener>(), instances[0].Listener);
43+
}
44+
45+
[Fact]
46+
public async Task InvokeAsync_BlockingCallDetectionEnabled_CapturesBlockingCallEvent()
47+
{
48+
var events = new ConcurrentQueue<SentryEvent>();
49+
Configure = options =>
50+
{
51+
options.CaptureBlockingCalls = true;
52+
options.SetBeforeSend(@event =>
53+
{
54+
events.Enqueue(@event);
55+
return @event;
56+
});
57+
};
58+
Handlers =
59+
[
60+
new RequestHandler
61+
{
62+
Path = "/blocking",
63+
Handler = _ =>
64+
{
65+
Task.Delay(25).Wait();
66+
return Task.CompletedTask;
67+
}
68+
}
69+
];
70+
71+
Build();
72+
73+
_ = await HttpClient.GetAsync("/blocking");
74+
75+
var blockingEvent = Assert.Single(events, IsBlockingCallDetectorEvent);
76+
var mechanism = blockingEvent.SentryExceptions!.Single(exception => exception.Mechanism?.Type == BlockingCallDetectorMechanism).Mechanism;
77+
Assert.Equal("EventListener", mechanism!.Source);
78+
}
79+
80+
[Fact]
81+
public async Task InvokeAsync_BlockingCallDetectionEnabled_SynchronizationContextWait_CapturesBlockingCallEvent()
82+
{
83+
var events = new ConcurrentQueue<SentryEvent>();
84+
Configure = options =>
85+
{
86+
options.CaptureBlockingCalls = true;
87+
options.SetBeforeSend(@event =>
88+
{
89+
events.Enqueue(@event);
90+
return @event;
91+
});
92+
};
93+
Handlers =
94+
[
95+
new RequestHandler
96+
{
97+
Path = "/blocking",
98+
Handler = _ =>
99+
{
100+
using var manualResetEvent = new ManualResetEvent(false);
101+
manualResetEvent.WaitOne(25);
102+
return Task.CompletedTask;
103+
}
104+
}
105+
];
106+
107+
Build();
108+
109+
_ = await HttpClient.GetAsync("/blocking");
110+
111+
var blockingEvent = Assert.Single(events, IsBlockingCallDetectorEvent);
112+
var mechanism = blockingEvent.SentryExceptions!.Single(exception => exception.Mechanism?.Type == BlockingCallDetectorMechanism).Mechanism;
113+
Assert.Equal("SynchronizationContext", mechanism!.Source);
114+
}
115+
116+
[Fact]
117+
public async Task InvokeAsync_BlockingCallDetectionDisabled_DoesNotCaptureOrInstantiateDetector()
118+
{
119+
var events = new ConcurrentQueue<SentryEvent>();
120+
var middlewareCount = 0;
121+
var monitorCount = 0;
122+
var listenerCount = 0;
123+
Configure = options =>
124+
{
125+
options.CaptureBlockingCalls = false;
126+
options.SetBeforeSend(@event =>
127+
{
128+
events.Enqueue(@event);
129+
return @event;
130+
});
131+
};
132+
ConfigureCountingBlockingDetectionServices(
133+
() => Interlocked.Increment(ref middlewareCount),
134+
() => Interlocked.Increment(ref monitorCount),
135+
() => Interlocked.Increment(ref listenerCount));
136+
Handlers =
137+
[
138+
new RequestHandler
139+
{
140+
Path = "/blocking",
141+
Handler = _ =>
142+
{
143+
Task.Delay(25).Wait();
144+
return Task.CompletedTask;
145+
}
146+
}
147+
];
148+
149+
Build();
150+
151+
_ = await HttpClient.GetAsync("/blocking");
152+
153+
Assert.Equal(1, middlewareCount);
154+
Assert.Equal(0, monitorCount);
155+
Assert.Equal(0, listenerCount);
156+
Assert.DoesNotContain(events, IsBlockingCallDetectorEvent);
157+
}
158+
159+
private void ConfigureCountingBlockingDetectionServices(
160+
Action middlewareCreated,
161+
Action monitorCreated,
162+
Action listenerCreated)
163+
{
164+
AfterConfigureBuilder = builder => builder.ConfigureServices(services =>
165+
{
166+
services.RemoveAll<SentryMiddleware>();
167+
services.AddTransient(serviceProvider =>
168+
{
169+
middlewareCreated();
170+
return ActivatorUtilities.CreateInstance<SentryMiddleware>(serviceProvider);
171+
});
172+
173+
services.RemoveAll<IBlockingMonitor>();
174+
services.AddSingleton<IBlockingMonitor>(serviceProvider =>
175+
{
176+
monitorCreated();
177+
return ActivatorUtilities.CreateInstance<BlockingMonitor>(serviceProvider);
178+
});
179+
180+
services.RemoveAll<TaskBlockingListener>();
181+
services.AddSingleton(serviceProvider =>
182+
{
183+
listenerCreated();
184+
return ActivatorUtilities.CreateInstance<TaskBlockingListener>(serviceProvider);
185+
});
186+
});
187+
}
188+
189+
private static bool IsBlockingCallDetectorEvent(SentryEvent @event) =>
190+
@event.SentryExceptions?.Any(exception => exception.Mechanism?.Type == BlockingCallDetectorMechanism) == true;
191+
}

‎test/Sentry.AspNetCore.Tests/Sentry.AspNetCore.Tests.csproj‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
<Compile Update="IntegrationsTests.EventProcessor.cs">
2121
<DependentUpon>IntegrationsTests.cs</DependentUpon>
2222
</Compile>
23+
<Compile Update="IntegrationsTests.BlockingDetection.cs">
24+
<DependentUpon>IntegrationsTests.cs</DependentUpon>
25+
</Compile>
2326
</ItemGroup>
2427

2528
</Project>

0 commit comments

Comments
 (0)