Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ internal sealed class ScheduledTaskExecutor : IDisposable
private readonly SemaphoreSlim _semaphore = new(0);
private bool _disposed;

public ScheduledTaskExecutor() => ProcessingTask = Task.Run(StartProcessingAsync);
public ScheduledTaskExecutor()
{
// The processing loop is infrastructure that outlives the code creating the executor,
// so it must not capture (and keep alive) the ambient ExecutionContext.
using (ExecutionContext.SuppressFlow())
{
ProcessingTask = Task.Run(StartProcessingAsync);
}
}

public Task ProcessingTask { get; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using Paramore.Fences.CircuitBreaker;

namespace Paramore.Fences.Core.Tests.CircuitBreaker.Controller;
Expand Down Expand Up @@ -167,4 +168,42 @@ public async Task Dispose_EnsureNoBackgroundProcessing()

scheduler.ProcessingTask.IsCompleted.ShouldBeTrue();
}

[Fact]
public void Constructor_AmbientExecutionContext_IsNotCaptured()
{
// The background processing loop outlives the code that creates the executor,
// so it must not capture (and keep alive) the ambient ExecutionContext.

// Arrange
var (executor, reference) = CreateExecutorWithAmbientValue();

// Act
for (var i = 0; i < 3; i++)
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers();
}

// Assert
// Keep the executor (and its background processing task) alive for the check:
// without the fix, the task's captured ExecutionContext keeps the ambient value alive.
GC.KeepAlive(executor);
reference.IsAlive.ShouldBeFalse();

executor.Dispose();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static (ScheduledTaskExecutor Executor, WeakReference Reference) CreateExecutorWithAmbientValue()
{
var asyncLocal = new AsyncLocal<object?>();
var value = new object();
asyncLocal.Value = value;

var executor = new ScheduledTaskExecutor();

asyncLocal.Value = null;
return (executor, new WeakReference(value));
}
}
Loading