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 @@
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);

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / macos-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check warning on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check warning on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check warning on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check warning on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

Check failure on line 20 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

Use recommended dispose pattern to ensure that object created by 'CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000)

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

try
{

Check failure on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Check failure on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Check failure on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Check failure on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Check failure on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / windows-latest

Check warning on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Check warning on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Check warning on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)

Check warning on line 26 in src/Polly/Timeout/AsyncTimeoutEngine.cs

View workflow job for this annotation

GitHub Actions / code-ql (csharp)


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 @@

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();
bool isCancelled = false;
lor1mp marked this conversation as resolved.
Show resolved Hide resolved

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);
isCancelled.Should().BeTrue();
lor1mp marked this conversation as resolved.
Show resolved Hide resolved
}

#endregion

#region Timeout operation - optimistic
Expand Down
Loading