Skip to content

Commit 813d10a

Browse files
authored
Prevent circuit breakers from capturing ambient ExecutionContext (#3284)
Fixes #3275.
1 parent 62e7a8c commit 813d10a

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

‎src/Polly.Core/CircuitBreaker/Controller/ScheduledTaskExecutor.cs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ internal sealed class ScheduledTaskExecutor : IDisposable
1111
private readonly SemaphoreSlim _semaphore = new(0);
1212
private bool _disposed;
1313

14-
public ScheduledTaskExecutor() => ProcessingTask = Task.Run(StartProcessingAsync);
14+
public ScheduledTaskExecutor()
15+
{
16+
// The processing loop is infrastructure that outlives the code creating the executor,
17+
// so it must not capture (and keep alive) the ambient ExecutionContext.
18+
using (ExecutionContext.SuppressFlow())
19+
{
20+
ProcessingTask = Task.Run(StartProcessingAsync);
21+
}
22+
}
1523

1624
public Task ProcessingTask { get; }
1725

‎test/Polly.Core.Tests/CircuitBreaker/Controller/ScheduledTaskExecutorTests.cs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.CompilerServices;
12
using Polly.CircuitBreaker;
23

34
namespace Polly.Core.Tests.CircuitBreaker.Controller;
@@ -167,4 +168,38 @@ public async Task Dispose_EnsureNoBackgroundProcessing()
167168

168169
scheduler.ProcessingTask.IsCompleted.ShouldBeTrue();
169170
}
171+
172+
[Fact]
173+
public void Constructor_DoesNotCaptureAmbientExecutionContext()
174+
{
175+
// The background processing loop outlives the code that creates the executor,
176+
// so it must not capture (and keep alive) the ambient ExecutionContext.
177+
var (executor, reference) = CreateExecutorWithAmbientValue();
178+
179+
for (var i = 0; i < 3; i++)
180+
{
181+
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true);
182+
GC.WaitForPendingFinalizers();
183+
}
184+
185+
// Keep the executor (and its background processing task) alive for the check:
186+
// without the fix, the task's captured ExecutionContext keeps the ambient value alive.
187+
GC.KeepAlive(executor);
188+
reference.IsAlive.ShouldBeFalse();
189+
190+
executor.Dispose();
191+
}
192+
193+
[MethodImpl(MethodImplOptions.NoInlining)]
194+
private static (ScheduledTaskExecutor Executor, WeakReference Reference) CreateExecutorWithAmbientValue()
195+
{
196+
var asyncLocal = new AsyncLocal<object?>();
197+
var value = new object();
198+
asyncLocal.Value = value;
199+
200+
var executor = new ScheduledTaskExecutor();
201+
202+
asyncLocal.Value = null;
203+
return (executor, new WeakReference(value));
204+
}
170205
}

0 commit comments

Comments
 (0)