-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Delay pipeline disposal when still in use (#1579)
- Loading branch information
Showing
7 changed files
with
260 additions
and
15 deletions.
There are no files selected for viewing
This file contains 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
59 changes: 59 additions & 0 deletions
59
src/Polly.Core/Utils/Pipeline/ExecutionTrackingComponent.cs
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
namespace Polly.Utils.Pipeline; | ||
|
||
internal sealed class ExecutionTrackingComponent : PipelineComponent | ||
{ | ||
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30); | ||
|
||
public static readonly TimeSpan SleepDelay = TimeSpan.FromSeconds(1); | ||
|
||
private readonly TimeProvider _timeProvider; | ||
private int _pendingExecutions; | ||
|
||
public ExecutionTrackingComponent(PipelineComponent component, TimeProvider timeProvider) | ||
{ | ||
Component = component; | ||
_timeProvider = timeProvider; | ||
} | ||
|
||
public PipelineComponent Component { get; } | ||
|
||
public bool HasPendingExecutions => Interlocked.CompareExchange(ref _pendingExecutions, 0, 0) > 0; | ||
|
||
internal override async ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>( | ||
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, | ||
ResilienceContext context, | ||
TState state) | ||
{ | ||
Interlocked.Increment(ref _pendingExecutions); | ||
|
||
try | ||
{ | ||
return await Component.ExecuteCore(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); | ||
} | ||
finally | ||
{ | ||
Interlocked.Decrement(ref _pendingExecutions); | ||
} | ||
} | ||
|
||
public override async ValueTask DisposeAsync() | ||
{ | ||
var start = _timeProvider.GetTimestamp(); | ||
var stopwatch = Stopwatch.StartNew(); | ||
|
||
// We don't want to introduce locks or any synchronization primitives to main execution path | ||
// so we will do "dummy" retries until there are no more executions. | ||
while (HasPendingExecutions) | ||
{ | ||
await _timeProvider.Delay(SleepDelay).ConfigureAwait(false); | ||
|
||
// stryker disable once equality : no means to test this | ||
if (_timeProvider.GetElapsedTime(start) > Timeout) | ||
{ | ||
break; | ||
} | ||
} | ||
|
||
await Component.DisposeAsync().ConfigureAwait(false); | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
168 changes: 168 additions & 0 deletions
168
test/Polly.Core.Tests/Utils/Pipeline/ExecutionTrackingComponentTests.cs
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Time.Testing; | ||
using Polly.Utils.Pipeline; | ||
|
||
namespace Polly.Core.Tests.Utils.Pipeline; | ||
|
||
public class ExecutionTrackingComponentTests | ||
{ | ||
private readonly FakeTimeProvider _timeProvider = new(); | ||
|
||
[Fact] | ||
public async Task DisposeAsync_PendingOperations_Delayed() | ||
{ | ||
using var assert = new ManualResetEvent(false); | ||
using var executing = new ManualResetEvent(false); | ||
|
||
await using var inner = new Inner | ||
{ | ||
OnExecute = () => | ||
{ | ||
executing.Set(); | ||
assert.WaitOne(); | ||
} | ||
}; | ||
|
||
var component = new ExecutionTrackingComponent(inner, _timeProvider); | ||
var execution = Task.Run(() => new ResiliencePipeline(component, Polly.Utils.DisposeBehavior.Allow).Execute(() => { })); | ||
executing.WaitOne(); | ||
|
||
var disposeTask = component.DisposeAsync().AsTask(); | ||
_timeProvider.Advance(ExecutionTrackingComponent.SleepDelay); | ||
inner.Disposed.Should().BeFalse(); | ||
assert.Set(); | ||
|
||
_timeProvider.Advance(ExecutionTrackingComponent.SleepDelay); | ||
await execution; | ||
|
||
_timeProvider.Advance(ExecutionTrackingComponent.SleepDelay); | ||
await disposeTask; | ||
|
||
inner.Disposed.Should().BeTrue(); | ||
} | ||
|
||
[Fact] | ||
public async Task HasPendingExecutions_Ok() | ||
{ | ||
using var assert = new ManualResetEvent(false); | ||
using var executing = new ManualResetEvent(false); | ||
|
||
await using var inner = new Inner | ||
{ | ||
OnExecute = () => | ||
{ | ||
executing.Set(); | ||
assert.WaitOne(); | ||
} | ||
}; | ||
|
||
await using var component = new ExecutionTrackingComponent(inner, _timeProvider); | ||
var execution = Task.Run(() => new ResiliencePipeline(component, Polly.Utils.DisposeBehavior.Allow).Execute(() => { })); | ||
executing.WaitOne(); | ||
|
||
component.HasPendingExecutions.Should().BeTrue(); | ||
assert.Set(); | ||
await execution; | ||
|
||
component.HasPendingExecutions.Should().BeFalse(); | ||
} | ||
|
||
[Fact] | ||
public async Task DisposeAsync_Timeout_Ok() | ||
{ | ||
using var assert = new ManualResetEvent(false); | ||
using var executing = new ManualResetEvent(false); | ||
|
||
await using var inner = new Inner | ||
{ | ||
OnExecute = () => | ||
{ | ||
executing.Set(); | ||
assert.WaitOne(); | ||
} | ||
}; | ||
|
||
var component = new ExecutionTrackingComponent(inner, _timeProvider); | ||
var execution = Task.Run(() => new ResiliencePipeline(component, Polly.Utils.DisposeBehavior.Allow).Execute(() => { })); | ||
executing.WaitOne(); | ||
|
||
var disposeTask = component.DisposeAsync().AsTask(); | ||
inner.Disposed.Should().BeFalse(); | ||
_timeProvider.Advance(ExecutionTrackingComponent.Timeout - TimeSpan.FromSeconds(1)); | ||
inner.Disposed.Should().BeFalse(); | ||
_timeProvider.Advance(TimeSpan.FromSeconds(1)); | ||
_timeProvider.Advance(TimeSpan.FromSeconds(1)); | ||
await disposeTask; | ||
inner.Disposed.Should().BeTrue(); | ||
|
||
assert.Set(); | ||
await execution; | ||
} | ||
|
||
[Fact] | ||
public async Task DisposeAsync_WhenRunningMultipleTasks_Ok() | ||
{ | ||
var tasks = new ConcurrentQueue<ManualResetEvent>(); | ||
await using var inner = new Inner | ||
{ | ||
OnExecute = () => | ||
{ | ||
var ev = new ManualResetEvent(false); | ||
tasks.Enqueue(ev); | ||
ev.WaitOne(); | ||
} | ||
}; | ||
|
||
var component = new ExecutionTrackingComponent(inner, TimeProvider.System); | ||
var pipeline = new ResiliencePipeline(component, Polly.Utils.DisposeBehavior.Allow); | ||
|
||
for (int i = 0; i < 10; i++) | ||
{ | ||
_ = Task.Run(() => pipeline.Execute(() => { })); | ||
} | ||
|
||
while (tasks.Count != 10) | ||
{ | ||
await Task.Delay(1); | ||
} | ||
|
||
var disposeTask = component.DisposeAsync().AsTask(); | ||
|
||
while (tasks.Count > 1) | ||
{ | ||
tasks.TryDequeue(out var ev).Should().BeTrue(); | ||
ev!.Set(); | ||
ev.Dispose(); | ||
disposeTask.Wait(1).Should().BeFalse(); | ||
inner.Disposed.Should().BeFalse(); | ||
} | ||
|
||
// last one | ||
tasks.TryDequeue(out var last).Should().BeTrue(); | ||
last!.Set(); | ||
last.Dispose(); | ||
await disposeTask; | ||
inner.Disposed.Should().BeTrue(); | ||
} | ||
|
||
private class Inner : PipelineComponent | ||
{ | ||
public bool Disposed { get; private set; } | ||
|
||
public override ValueTask DisposeAsync() | ||
{ | ||
Disposed = true; | ||
return default; | ||
} | ||
|
||
public Action OnExecute { get; set; } = () => { }; | ||
|
||
internal override async ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, ResilienceContext context, TState state) | ||
{ | ||
OnExecute(); | ||
|
||
return await callback(context, state); | ||
} | ||
} | ||
} |