-
Notifications
You must be signed in to change notification settings - Fork 235
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
feature: add cancellation token to variety of ...WaitFor... APIs #2597
Conversation
src/Playwright/Core/Waiter.cs
Outdated
var cancelledTask = Task.FromCanceled(cancellationToken); | ||
RejectOn(cancelledTask, () => cancelledTask.Dispose()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably look something like this:
var cancelledTask = Task.FromCanceled(cancellationToken); | |
RejectOn(cancelledTask, () => cancelledTask.Dispose()); | |
if (!cancellationToken.CanBeCanceled) return; | |
var cts = new TaskCompletionSource<bool>(); | |
var registration = cancellationToken.Register(state => cts.SetCanceled((CancellationToken)state), cancellationToken); | |
RejectOn(cts.Task, () => registration.Dispose()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much. I was fighting with this part. I found some approaches but nothing worked properly.
I am adding tests to cover cancellation scenarios.
src/Playwright/Core/Waiter.cs
Outdated
dispose?.Invoke(); | ||
_error = ex.ToString(); | ||
Dispose(); | ||
return default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it would be strange to just get the default value back instead of a canceled exception.
In .NET it is standard that the user needs to handle canceled exceptions as soon as they are passing cancellation tokens around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was looking for an accepted general rule for that. Do you suggest having that specific exception bubble up or converting it (as by default) to PlaywrightException?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my experience cancellation exceptions escpecially ones triggered by a user provided CancellationToken aren't wrapped / converted in other exceptions. So I would vote for letting the cancellation exception bubble up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've refactored into rethrow.
src/Playwright/Core/Waiter.cs
Outdated
@@ -218,6 +230,13 @@ internal async Task<T> WaitForPromiseAsync<T>(Task<T> task, Action dispose = nul | |||
Dispose(); | |||
throw new TimeoutException(ex.Message + FormatLogRecording(_logs), ex); | |||
} | |||
catch (TaskCanceledException ex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catch OperationCanceledException
instead or both.
See dotnet/runtime#83406 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both exception handlers for TaskCanceledException
and OperationCanceledException
do exactly the same thing then you just need the OperationCanceledException
one because TaskCanceledException
inherits from it.
(That is what I meant with instead
;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, missed that inheritance hierarchy. Fixed
src/Playwright/Core/Waiter.cs
Outdated
} | ||
|
||
var cts = new TaskCompletionSource<bool>(); | ||
var registration = cancellationToken.Register(cts.SetCanceled, false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you let the cancellation throw instead (#2597 (comment)) this should use the Set Canceled(cancellationToken)
instead so users can inspect it if the catch it higher up in the stack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used TrySetCanceled
as it is available in 3.1 as well as 6.0, otherwise I would have to add compile-time conditional
{ | ||
return; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an idea: maybe it would be good to call cancellationToken.ThrowIfCancellationRequested()
(here or maybe higher up in the call stack) so that if it is already cancelled we don't do anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
used that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking more of something like this:
if (!cancellationToken.CanBeCanceled)
{
return; // don't add a cancellation registration if the cancellationToken can never be cancelled (e.g. CancellationToken.None / default)
}
cancellationToken.ThrowIfCancellationRequested(); // if the cancellation token is already cancelled then throw immediately
If the cancellation token can't be cancelled then ThrowIfCancellationRequested
will always noop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was my understanding of your comment as well, alas I have added the throw in the wrong place, fixed now :)
@@ -243,7 +267,11 @@ private static async Task WrapActionAsync(Func<Task> action, CancellationTokenSo | |||
} | |||
catch | |||
{ | |||
cts.Cancel(); | |||
if (!cts.IsCancellationRequested) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change? What breaks if we always cancel here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need to cancel 'harder'. I have replicated the solution from the other place in this PR where this check is made, and I remembered that I have added this cancellation here some time ago. This is the correct solution for the overall problem. Any code should be written in the most proper/correct way. Simply working is not ever good enough.
dispose?.Invoke(); | ||
_error = ex.ToString(); | ||
Dispose(); | ||
throw; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be nice to throw the same exception but with formatted logs like in line 250. We can potentially also have a single catch for both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With OperationCanceledException
the suggested flow is to throw the original exception to have better understanding of the place of cancelation. https://devblogs.microsoft.com/premier-developer/recommended-patterns-for-cancellationtoken/
/// </para> | ||
/// </summary> | ||
[JsonPropertyName("cancellationToken")] | ||
public CancellationToken CancellationToken { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you add these manually? All these files in that folder are generated automatically. We should add csharp specific properties upstream like here so that the generator will pick it up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, manual addition. I will move it to the source for the generator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See here for some notes on how it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5f1be00
to
5f06aff
Compare
Added PR for upstream: |
c57d901
to
5772c32
Compare
Used this generator for generated members: |
Closing as per microsoft/playwright#24083 (comment). |
fixes: microsoft/playwright#24083
adds CancellationToken where feasible and extends Waiter class to provide CancellationToken as a source to cancel wait