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

feature: add cancellation token to variety of ...WaitFor... APIs #2597

Closed
wants to merge 21 commits into from

Conversation

maw136
Copy link
Contributor

@maw136 maw136 commented May 30, 2023

fixes: microsoft/playwright#24083
adds CancellationToken where feasible and extends Waiter class to provide CancellationToken as a source to cancel wait

Comment on lines 122 to 123
var cancelledTask = Task.FromCanceled(cancellationToken);
RejectOn(cancelledTask, () => cancelledTask.Dispose());
Copy link
Contributor

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:

Suggested change
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());

Copy link
Contributor Author

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.

dispose?.Invoke();
_error = ex.ToString();
Dispose();
return default;
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -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)
Copy link
Contributor

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)

Copy link
Contributor

@campersau campersau May 31, 2023

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

Copy link
Contributor Author

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

}

var cts = new TaskCompletionSource<bool>();
var registration = cancellationToken.Register(cts.SetCanceled, false);
Copy link
Contributor

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.

Copy link
Contributor Author

@maw136 maw136 May 31, 2023

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;
}

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used that

Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Member

@mxschmitt mxschmitt Jun 27, 2023

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.

Copy link
Contributor Author

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; }
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maw136
Copy link
Contributor Author

maw136 commented Jun 29, 2023

Added PR for upstream:
microsoft/playwright#23953

@maw136 maw136 force-pushed the feature/add-cancellation-feature branch from c57d901 to 5772c32 Compare July 18, 2023 18:00
@maw136
Copy link
Contributor Author

maw136 commented Jul 18, 2023

Used this generator for generated members:
microsoft/playwright#23953

@mxschmitt
Copy link
Member

Closing as per microsoft/playwright#24083 (comment).

@mxschmitt mxschmitt closed this Jul 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] External cancellation of ...WaitFor... methods where feasible
3 participants