-
Notifications
You must be signed in to change notification settings - Fork 401
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
Conversation
{ | ||
if (_lazyCancellationToken.IsValueCreated) | ||
{ | ||
throw new InvalidOperationException($"Cannot add additional linked cancellation tokens once {nameof(InvocationContext)}.{nameof(CancellationToken)} has been invoked"); |
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.
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"); |
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 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.
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 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?
I haven't looked at the code yet, but it's important to know this: Requesting the |
@Keboo is this handled somehow? |
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 |
{ | ||
if (_cts is null) | ||
if (_lazyCancellationToken.IsValueCreated) |
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.
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.
I would like to propose a different approach to maintain linked token sources within 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 |
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.
@jonsequitur not sure if this is important to you or not, but explicit interface members are not shown.
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
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 theConsole.CancelKeyPress
andAppDomain.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
andInvocationContext
changes.