Skip to content

Adding in cancellation support for InvokeAsync #1502

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

Merged
merged 5 commits into from
Jul 30, 2022
Merged

Conversation

Keboo
Copy link
Member

@Keboo Keboo commented Dec 2, 2021

Fixes #1469

There is a slight behavior change in this. Previously, if you did not request a CancellationToken in a command handler, it would not register up on the Console.CancelKeyPress and AppDomain.CurrentDomain.ProcessExit events.
With these changes it now always registers for those events if you call CommandLineBuilder.CancelOnProcessTermination (which is part of the defaults; so likely often).

Key areas to review are CommandLineBuilderExtensions and InvocationContext changes.

@Keboo Keboo marked this pull request as ready for review December 3, 2021 19:04
@jonsequitur jonsequitur requested a review from tmds December 3, 2021 19:09
{
if (_lazyCancellationToken.IsValueCreated)
{
throw new InvalidOperationException($"Cannot add additional linked cancellation tokens once {nameof(InvocationContext)}.{nameof(CancellationToken)} has been invoked");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw new InvalidOperationException($"Cannot add additional linked cancellation tokens once {nameof(InvocationContext)}.{nameof(CancellationToken)} has been invoked");
throw new InvalidOperationException($"Cannot add additional linked cancellation tokens once {nameof(InvocationContext)}.{nameof(CancellationToken)} has been cancelled");

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think this is correct. The failure occurs if the Lazy has already been evaluated, meaning something requested the CancellationToken from the context. It may or may not be cancelled at this point.

Copy link
Contributor

Choose a reason for hiding this comment

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

This initially caught my eye because I wasn't sure what it means to "invoke" a CancellationToken. If I saw this exception message, I'm not sure I'd understand what I should do differently. What can we do to make this error clearly actionable for developers?

@tmds
Copy link
Member

tmds commented Dec 3, 2021

Previously, if you did not request a CancellationToken in a command handler, it would not register up on the Console.CancelKeyPress and AppDomain.CurrentDomain.ProcessExit events. With these changes it now always registers for those events if you call CommandLineBuilder.CancelOnProcessTermination (which is part of the defaults; so likely often).

I haven't looked at the code yet, but it's important to know this:

Requesting the CancellationToken means you 'promise' to use that token for terminating the application.
If it is unconditionally enabled then applications that are not actually using the token will no longer terminate on SIGTERM or Ctrl+C.

@tmds
Copy link
Member

tmds commented Dec 20, 2021

Requesting the CancellationToken means you 'promise' to use that token for terminating the application.
If it is unconditionally enabled then applications that are not actually using the token will no longer terminate on SIGTERM or Ctrl+C.

@Keboo is this handled somehow?

@Keboo
Copy link
Member Author

Keboo commented Dec 21, 2021

Hi @tmds sorry for the long delay. I just got back to this and addressed the merge issues. After digging a bit more I did refactor the code so that it will not register for Console.CancelKeyPress or AppDomain.CurrentDomain.ProcessExit unless the command's handler requests a CancellationToken. I believe this addresses your concern above. Thank you very much for your feedback.

{
if (_cts is null)
if (_lazyCancellationToken.IsValueCreated)
Copy link

Choose a reason for hiding this comment

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

GetCancellationToken forces creation of final cancellation token. As a result, any subsequent calls to AddLinkedCancellationToken will not work. For instance, I'm using GetCancellationToken to inject token to the binding context through middleware to make the token available for all asynchronous command handlers via custom binder. If I put the middleware too early in the chain, nobody else will be able to add the linked token in the chain.

@sakno
Copy link

sakno commented Jul 2, 2022

I would like to propose a different approach to maintain linked token sources within InvocationContext:

public sealed class InvocationContext : IDisposable
{
    private readonly CancellationToken _token; // cached token to avoid ObjectDisposedException due to concurrency between Dispose() and Cancel()
    private readonly LinkedList<CancellationTokenRegistration> _registrations = new();
    private volatile CancellationTokenSource? _source;
    private volatile int _canceled;

    public InvocationContext(...)
    {
        _source = new();
        _token = _source.Token;
    }

    public CancellationToken GetCancellationToken() => _token;

    // internal visible for easy access from program termination callbacks
    internal void Cancel()
    {
       using (var source = Interlocked.Exchange(ref _source, null))
       {
           source?.Cancel();
       }
    }

    public void LinkToken(CancellationToken token)
    {
        _registrations.AddLast(token.Register(this.Cancel));
    }

    // explicit to prevent invocation from middleware or any other parts of user code
    void IDisposable.Dispose()
    {
        Interlocked.Exchange(ref _source, null)?.Dispose();
        foreach (var registration in _registrations)
           registration.Dispose();
    }
}

@@ -357,8 +357,8 @@ System.CommandLine.Help
System.CommandLine.Invocation
public interface IInvocationResult
public System.Void Apply(InvocationContext context)
public class InvocationContext
.ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
public class InvocationContext, System.IDisposable
Copy link
Member Author

Choose a reason for hiding this comment

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

@jonsequitur not sure if this is important to you or not, but explicit interface members are not shown.

Keboo added 4 commits July 8, 2022 00:08
Moving the storage of the cancellation token to the InvocationContext


Adding InvocationContext tests


Updating the approved public API


Fixes after rebase


Reworking the code so it doesn't register for events without a cancellation token
@jonsequitur jonsequitur merged commit f1f43df into dotnet:main Jul 30, 2022
@tmds tmds mentioned this pull request Jan 13, 2023
@Keboo Keboo deleted the fix1469 branch January 31, 2024 23:53
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.

add CancellationToken parameter to command handler InvokeAsync methods
4 participants