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 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
21 changes: 15 additions & 6 deletions src/Polly/Timeout/AsyncTimeoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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 +50,16 @@ internal static async Task<TResult> ImplementationAsync<TResult>(

throw;
}
finally
{
// If timeoutCancellationTokenSource was canceled & our combined token hasn't been signaled, cancel it.
// This avoids the exception propagating before the linked token can signal the downstream to cancel.
// See https://github.com/App-vNext/Polly/issues/722.
if (!combinedTokenSource.IsCancellationRequested && timeoutCancellationTokenSource.IsCancellationRequested)
{
combinedTokenSource.Cancel();
}
}
}

private static Task<TResult> AsTask<TResult>(this CancellationToken cancellationToken)
Expand All @@ -60,11 +69,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
27 changes: 26 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,29 @@ 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);
bool isCancelled = false;

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

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

isCancelled.Should().BeTrue();
}

#endregion

#region Timeout operation - optimistic
Expand Down