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 2 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
22 changes: 16 additions & 6 deletions src/Polly/Timeout/AsyncTimeoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal static async Task<TResult> ImplementationAsync<TResult>(
TimeSpan timeout = timeoutProvider(context);

using var timeoutCancellationTokenSource = new CancellationTokenSource();
// 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
using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token);

Task<TResult> actionTask = null;
Expand All @@ -37,7 +39,6 @@ internal static async Task<TResult> ImplementationAsync<TResult>(
actionTask = action(context, combinedToken);

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

}
catch (Exception ex)
{
Expand All @@ -51,6 +52,15 @@ internal static async Task<TResult> ImplementationAsync<TResult>(

throw;
}
finally
{
// If the timeoutCancellation was canceled & our combined token hasn't been signaled, signal it.
// This avoids the exception propagating before the linked token can signal the downstream to cancel.
martincostello marked this conversation as resolved.
Show resolved Hide resolved
if (!combinedTokenSource.IsCancellationRequested && timeoutCancellationTokenSource.IsCancellationRequested)
{
combinedTokenSource.Cancel();
}
}
}

private static Task<TResult> AsTask<TResult>(this CancellationToken cancellationToken)
Expand All @@ -60,11 +70,11 @@ private static Task<TResult> AsTask<TResult>(this CancellationToken cancellation
// A generalised version of this method would include a hotpath returning a canceled task (rather than setting up a registration) if (cancellationToken.IsCancellationRequested) on entry. This is omitted, since we only start the timeout countdown in the token _after calling this method.

IDisposable registration = null;
registration = cancellationToken.Register(() =>
{
tcs.TrySetCanceled();
registration?.Dispose();
}, useSynchronizationContext: false);
registration = cancellationToken.Register(() =>
{
tcs.TrySetCanceled();
registration?.Dispose();
}, useSynchronizationContext: false);

return tcs.Task;
}
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