Skip to content

Add AbandonOnRepeatCancellation extension method #1227

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

fredrikhr
Copy link
Contributor

TL;DR

  • Adds a extension and middleware AbandonOnRepeatCancellation to CommandLineBuidler.

Description

When using the CancelOnProcessTermination middleware (or similar mechanism) a CancellationToken is added to the InvocationContext. The CancelOnProcessTermination middleware monitors the Console.CancelKeyPress event and cancels the token when a console cancellation key press is intercepted. After that is waits for the command-handler to return.

Since the CancelOnProcessTermination middleware suppresses the termination of the process to allow for a graceful shutdown and return, this behaviour can lead to the application not being terminable if the command-handler does not monitor the invocation cancellation token or is stuck in a critical section where monitoring the token is not viable.

This proposal adds another middleware that will wait for the first cancellation to occur and then registers an additional event handler for the Console.CancelKeyPress event. When a repeated cancellation signal is intercepted, this middleware abandons the invocation and immediately returns, disregarding the state of the invocation.

Note that the invocation cannot really be forcibly terminated, the name abandon really means that the original invocation will continue to run in the background. However, the pipeline will terminate returning directly back to main. Most likely, main will either just return terminating the application immediately anyways, or else main may contain som critical clean-up code that will now have the chance to run.

This will enable the following application pattern:

  • First-time CTRL+C press: Attempt graceful shutdown
  • Graceful shutdown takes a long time, user gets impatient
  • Repeated CTRL+C press: Application terminates ungracefully

@fredrikhr
Copy link
Contributor Author

fredrikhr commented Mar 21, 2021

@jonsequitur Adding unit-tests for this functionality is kinda hard, since unit-tests and Ctrl+C signaling don't really work well together. Instead I created an additional Console application project for manual testing of AbandonOnRepeatCancellation in 5e03ff4.

Sample output from that test application: (shortened for brevity)

Invocation started. Press Ctrl+C to request cancellation
Time since start: 00:00:06.1178291, Is cancellation requested: False
Cancellation requested. Press Ctrl+C again, to abandon invocation.
Time since start: 00:00:06.8887164, Is cancellation requested: True
Invocation was cancelled or abandoned.
Back in Main, waiting 5 seconds to exit.
Time since start: 00:00:11.4440263, Is cancellation requested: True

Here we can see a Command handler that does not return from the invocation when the cancellation token is cancelled (in this case the cancelToken is actually observed, but the handler purposefully does not return).

After the initial Ctrl+C press (line 3), cancellation of the invocation is requested, but the handler does not return, and so the program continues running. At line 5 Ctrl+C was pressed a second time, causing the invocation to be abandoned and control flow immediately returns back to Main. Note in line 7 that the invocation is still running in the background while Main is about to finish, terminating the application.

@@ -127,6 +129,51 @@ public static CommandLineBuilder CancelOnProcessTermination(this CommandLineBuil
return builder;
}

public static CommandLineBuilder AbandonOnRepeatCancellation(
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if enabling this functionality would make sense as a parameter to CancelOnProcessTermination rather than a separate method.

Thoughts, @tmds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe as an optional parameter with a struct/class containing behaviour options. Among them bool abandon and int threshold?

Copy link
Member

@tmds tmds Apr 1, 2021

Choose a reason for hiding this comment

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

Yes, I think we should add it to CancelOnProcessTermination to avoid weird interactions like https://github.com/dotnet/command-line-api/pull/1227/files#r605602596.

CancelOnProcessTermination gets included by UseDefaults. There is no pattern for passing patterns to the default middlewares that are used by UseDefaults.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are there tools that you've seen where the force quit behavior would involve more than two CTRL-C presses? I'm wondering if the threshold needs to be configurable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, okay, we don't need a threshold. But @tmds has a good point. Should UseDefault include this abandoning behaviour? How would someone use UseDefault and abandon on secondary cancel?
Should we maybe also inject a state object into the Invocation that keeps a count of cancellations, so that people could react to a secondary cancel? In hosting the StopAsync method accepts a cancellation token to abort graceful shutdown and just force shutdown the host. We could use such an injected status object to control cancellation.

Copy link
Contributor

Choose a reason for hiding this comment

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

My intuition is that this behavior should be enabled by default so that the path of least resistance for the developer leads to the most consistent experience for end users.

@KathleenDollard?

{
builder.AddMiddleware(async (context, next) =>
{
var initialCancelToken = context.GetCancellationToken();
Copy link
Member

Choose a reason for hiding this comment

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

We can't call GetCancellationToken here. When the CancellationToken gets requested (which is assumed to be done by the user), the context assumes that CancellationToken will be used to implement Ctrl+C handling, and the app no longer terminates by default.

So this has the unintended side-effect that command handlers which don't use a CancellationToken no longer terminate on first Ctrl+C.

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.

3 participants