Skip to content

Commit a953ba8

Browse files
authored
Fix for retry cancellation (#2456)
Fixes #2375.
1 parent 4f207a1 commit a953ba8

File tree

2 files changed

+122
-28
lines changed

2 files changed

+122
-28
lines changed

src/Polly.Core/Retry/RetryResilienceStrategy.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func
6767
TelemetryUtil.ReportExecutionAttempt(_telemetry, context, outcome, attempt, executionTime, handle);
6868
}
6969

70-
if (context.CancellationToken.IsCancellationRequested || isLastAttempt || !handle)
70+
if (isLastAttempt || !handle)
7171
{
7272
return outcome;
7373
}
@@ -100,17 +100,20 @@ protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func
100100
await DisposeHelper.TryDisposeSafeAsync(resultValue, context.IsSynchronous).ConfigureAwait(context.ContinueOnCapturedContext);
101101
}
102102

103-
// stryker disable once all : no means to test this
104-
if (delay > TimeSpan.Zero)
103+
try
105104
{
106-
try
105+
context.CancellationToken.ThrowIfCancellationRequested();
106+
107+
// stryker disable once all : no means to test this
108+
if (delay > TimeSpan.Zero)
107109
{
108110
await _timeProvider.DelayAsync(delay, context).ConfigureAwait(context.ContinueOnCapturedContext);
109111
}
110-
catch (OperationCanceledException e)
111-
{
112-
return Outcome.FromException<T>(e);
113-
}
112+
113+
}
114+
catch (OperationCanceledException e)
115+
{
116+
return Outcome.FromException<T>(e);
114117
}
115118

116119
if (incrementAttempts)

test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,130 @@ public void ExecuteAsync_EnsureResultNotDisposed()
3131
}
3232

3333
[Fact]
34-
public async Task ExecuteAsync_CancellationRequested_EnsureNotRetried()
34+
public async Task ExecuteAsync_CanceledBeforeExecution_EnsureNotExecuted()
3535
{
36-
SetupNoDelay();
3736
var sut = CreateSut();
38-
using var cts = new CancellationTokenSource();
39-
cts.Cancel();
40-
var context = ResilienceContextPool.Shared.Get(cts.Token);
4137
var executed = false;
4238

43-
var result = await sut.ExecuteOutcomeAsync((_, _) => { executed = true; return Outcome.FromResultAsValueTask("dummy"); }, context, "state");
44-
result.Exception.ShouldBeOfType<OperationCanceledException>();
39+
var result = await sut.ExecuteOutcomeAsync(
40+
(_, _) =>
41+
{
42+
executed = true;
43+
return Outcome.FromResultAsValueTask(new object());
44+
},
45+
ResilienceContextPool.Shared.Get(new CancellationToken(canceled: true)),
46+
default(object));
47+
48+
result.Exception.ShouldBeAssignableTo<OperationCanceledException>();
4549
executed.ShouldBeFalse();
4650
}
4751

4852
[Fact]
49-
public async Task ExecuteAsync_CancellationRequestedAfterCallback_EnsureNotRetried()
53+
public async Task ExecuteAsync_CanceledDuringExecution_EnsureResultReturned()
5054
{
51-
using var cts = new CancellationTokenSource();
55+
var sut = CreateSut();
56+
using var cancellation = new CancellationTokenSource();
57+
var executions = 0;
58+
59+
var result = await sut.ExecuteOutcomeAsync(
60+
(_, _) =>
61+
{
62+
executions++;
63+
cancellation.Cancel();
64+
return Outcome.FromResultAsValueTask(new object());
65+
},
66+
ResilienceContextPool.Shared.Get(cancellation.Token),
67+
default(object));
68+
69+
result.Exception.ShouldBeNull();
70+
executions.ShouldBe(1);
71+
}
72+
73+
[Fact]
74+
public async Task ExecuteAsync_CanceledDuringExecution_EnsureNotExecutedAgain()
75+
{
76+
var reported = false;
5277

5378
_options.ShouldHandle = _ => PredicateResult.True();
54-
_options.OnRetry = _ =>
55-
{
56-
cts.Cancel();
57-
return default;
58-
};
79+
_options.OnRetry =
80+
args =>
81+
{
82+
reported = true;
83+
return default;
84+
};
5985

60-
var sut = CreateSut(TimeProvider.System);
61-
var context = ResilienceContextPool.Shared.Get(cts.Token);
62-
var executed = false;
86+
var sut = CreateSut();
87+
using var cancellation = new CancellationTokenSource();
88+
var executions = 0;
89+
90+
var result = await sut.ExecuteOutcomeAsync(
91+
(_, _) =>
92+
{
93+
executions++;
94+
cancellation.Cancel();
95+
return Outcome.FromResultAsValueTask(new object());
96+
},
97+
ResilienceContextPool.Shared.Get(cancellation.Token),
98+
default(object));
99+
100+
result.Exception.ShouldBeAssignableTo<OperationCanceledException>();
101+
executions.ShouldBe(1);
102+
reported.ShouldBeTrue();
103+
}
104+
105+
[Fact]
106+
public async Task ExecuteAsync_CanceledAfterExecution_EnsureNotExecutedAgain()
107+
{
108+
using var cancellation = new CancellationTokenSource();
109+
110+
_options.ShouldHandle = _ => PredicateResult.True();
111+
_options.OnRetry =
112+
args =>
113+
{
114+
cancellation.Cancel();
115+
return default;
116+
};
117+
118+
var sut = CreateSut();
119+
var executions = 0;
120+
121+
var result = await sut.ExecuteOutcomeAsync(
122+
(_, _) =>
123+
{
124+
executions++;
125+
return Outcome.FromResultAsValueTask(new object());
126+
},
127+
ResilienceContextPool.Shared.Get(cancellation.Token),
128+
default(object));
129+
130+
result.Exception.ShouldBeAssignableTo<OperationCanceledException>();
131+
executions.ShouldBe(1);
132+
}
133+
134+
[Fact]
135+
public async Task ExecuteAsync_CanceledDuringDelay_EnsureNotExecutedAgain()
136+
{
137+
_options.ShouldHandle = _ => PredicateResult.True();
138+
139+
using var cancellation = _timeProvider.CreateCancellationTokenSource(_options.Delay);
140+
141+
var sut = CreateSut();
142+
var executions = 0;
143+
144+
var resultTask = sut.ExecuteOutcomeAsync(
145+
(_, _) =>
146+
{
147+
executions++;
148+
return Outcome.FromResultAsValueTask(new object());
149+
},
150+
ResilienceContextPool.Shared.Get(cancellation.Token),
151+
default(object));
152+
153+
_timeProvider.Advance(_options.Delay);
154+
var result = await resultTask;
63155

64-
var result = await sut.ExecuteOutcomeAsync((_, _) => { executed = true; return Outcome.FromResultAsValueTask("dummy"); }, context, "state");
65-
result.Exception.ShouldBeOfType<OperationCanceledException>();
66-
executed.ShouldBeTrue();
156+
result.Exception.ShouldBeAssignableTo<OperationCanceledException>();
157+
executions.ShouldBe(1);
67158
}
68159

69160
[Fact]

0 commit comments

Comments
 (0)