Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to cancel downstream operation in TimeoutStrategy.Pessimistic #1697

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 14 additions & 3 deletions src/Polly/Timeout/AsyncTimeoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,28 @@ internal static async Task<TResult> ImplementationAsync<TResult>(
TimeSpan timeout = timeoutProvider(context);

using var timeoutCancellationTokenSource = new CancellationTokenSource();
using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token);
// Do not use a using here, the exception will exit the scope before we have time to
// notify the downstream of the cancellation.
martincostello marked this conversation as resolved.
Show resolved Hide resolved
var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token);

Task<TResult> actionTask = null;
CancellationToken combinedToken = combinedTokenSource.Token;

try
{

var actionTaskFn = async () =>
{
var result = await action(context, combinedToken).ConfigureAwait(continueOnCapturedContext);
// dispose of the token source after we've waited on the task to finish.
combinedTokenSource.Dispose();
return result;
};
martincostello marked this conversation as resolved.
Show resolved Hide resolved

if (timeoutStrategy == TimeoutStrategy.Optimistic)
{
SystemClock.CancelTokenAfter(timeoutCancellationTokenSource, timeout);
return await action(context, combinedToken).ConfigureAwait(continueOnCapturedContext);
return await actionTaskFn().ConfigureAwait(continueOnCapturedContext);
}

// else: timeoutStrategy == TimeoutStrategy.Pessimistic
Expand All @@ -34,7 +45,7 @@ internal static async Task<TResult> ImplementationAsync<TResult>(

SystemClock.CancelTokenAfter(timeoutCancellationTokenSource, timeout);

actionTask = action(context, combinedToken);
actionTask = actionTaskFn();

return await (await Task.WhenAny(actionTask, timeoutTask).ConfigureAwait(continueOnCapturedContext)).ConfigureAwait(continueOnCapturedContext);

Expand Down
29 changes: 28 additions & 1 deletion test/Polly.Specs/Timeout/TimeoutAsyncSpecs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Polly.Specs.Timeout;
using System;

namespace Polly.Specs.Timeout;

[Collection(Constants.SystemClockDependentTestCollection)]
public class TimeoutAsyncSpecs : TimeoutSpecsBase
Expand Down Expand Up @@ -248,6 +250,31 @@ public async Task Should_rethrow_exception_from_inside_delegate__pessimistic()
await policy.Awaiting(p => p.ExecuteAsync(() => throw new NotImplementedException())).Should().ThrowAsync<NotImplementedException>();
}

[Fact]
public async Task Should_cancel_downstream_token_on_timeout__pessimistic()
{
// It seems that there's a difference in the mocked clock vs. the real time clock.
// This test does not fail when using the mocked timer.
// In the TimeoutSpecsBase we actually cancel the combined token. Which hides
// the fact that it doesn't actually cancel irl.
SystemClock.Reset();

var policy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(200), TimeoutStrategy.Pessimistic);
var upstreamToken = new CancellationToken();
lor1mp marked this conversation as resolved.
Show resolved Hide resolved
bool isCancelled = false;

var act = () => policy.ExecuteAsync(async (combinedToken) =>
{
combinedToken.Register(() => isCancelled = true);
await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(300), combinedToken);
}, upstreamToken);

await act.Should().ThrowAsync<TimeoutRejectedException>();

await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(400), default);
lor1mp marked this conversation as resolved.
Show resolved Hide resolved
isCancelled.Should().BeTrue();
}

#endregion

#region Timeout operation - optimistic
Expand Down