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

KernelInvocationContext.Current.CancellationToken is unreliable in VS Code #2987

Open
gtbuchanan opened this issue May 17, 2023 · 6 comments

Comments

@gtbuchanan
Copy link

Describe the bug

I've tried using KernelInvocationContext.Current.CancellationToken to cancel long-running tasks as described here but it rarely ever works.

Please complete the following:

Which version of .NET Interactive are you using? (In a notebook, run the #!about magic command. ):

  • Version: 1.0.425803+1db2979099d0272660e1497cae9b9af1238db42f
  • Library version: 1.0.0-beta.23258.3+1db2979099d0272660e1497cae9b9af1238db42f
  • Build date: 2023-05-17T14:00:58.1639536Z
  • OS: Windows 11
  • Frontend: Visual Studio Code 1.78.2

Reproduction

#r "nuget:System.Reactive"
using System.Reactive.Linq;
using System.Threading;
using Microsoft.DotNet.Interactive;

var subscription = Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(
    value => Console.WriteLine($"OnNext: {value}"),
    ex => Console.WriteLine($"OnError: {ex}"),
    () => Console.WriteLine("OnCompleted"));
Console.WriteLine("Subscribed");

var cts = CancellationTokenSource.CreateLinkedTokenSource(
    KernelInvocationContext.Current.CancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(15)); // Prevent infinite wait due to bug
await Task.Delay(-1, cts.Token).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
subscription.Dispose();
Console.WriteLine("Disposed");
cts.Dispose();

Just keep running the above reproduction, clicking the "Cancel" button before 15s, and you're bound to run into the problem.

@jonsequitur
Copy link
Contributor

Recent improvements in #2982 might have improved this.

@gtbuchanan
Copy link
Author

This seems to have been fixed (kind of). My code still doesn't get notified of the cancellation through the token, but the program is actually terminated now. Is the expectation that canceling is effectively the same as "force stop" where there is no opportunity for cleanup?

@jonsequitur
Copy link
Contributor

Cancellation is best effort. Any code path that's not checking the CancellationToken will simply continue to run.

An API to actually force a stop (a kind of replacement for the .NET Framework-only Thread.Abort) was introduced in .NET 7 and we plan to adopt it in .NET Interactive but there's some additional work needed.

@alienghub
Copy link

alienghub commented Nov 20, 2024

Still have the same issue not being able to dispose of resources

@jonsequitur
Copy link
Contributor

Looking at this again, I realized that I misunderstood what's happening here. This is subtle and deserves a detailed explanation. TL;DR: It works as intended. 😮

Every line of the code above should run because the CancellationToken is not being used in a way where it would cause an exception to be thrown in your code. (@alienghub, in your linked example, the behavior will be different because await Task.Delay(500, KernelInvocationContext.Current.CancellationToken); is most likely the currently executing line of code when you hit the stop button, which will throw and prevent the rest of the code from executing, but the behavior is equivalent to the example above if you don't pass theKernelInvocationContext.Current.CancellationToken to Task.Delay.)

Ok, so I realized that the code in the example here is not doing what folks probably think it's doing. I added a simple variable assignment (finished = true) after the last Console.WriteLine to illustrate:

Image

Note the value for finished shown in the variable viewer is True, indicating that the final line of code in the cell did in fact run.

So what's going on? What happened to Console.WriteLine("Disposed")?

Console output is only intercepted and written to the output cell during the lifetime of the KernelInvocationContext. Once the context is completed (which is what happens when the stop button is pressed and KernelInvocationContext.Current.CancellationToken is canceled), then .NET Interactive stops intercepting console output and publishing further events (including StandardOutputValueProduced) for the current code submission. So Console.WriteLine("Disposed") ran, but the KernelInvocationContext was no longer listening and publishing events.

This behavior is unintuitive enough that it could be considered a bug, and I'm going to take a look at whether it's possible to stop listening to those events just a little bit later so that console output isn't lost this way. (#3772)

@jonsequitur
Copy link
Contributor

Still have the same issue not being able to dispose of resources

If you need to dispose of resources at the end of the current cell execution, and you're doing something like await Task.Delay(500, KernelInvocationContext.Current.CancellationToken);, then putting resource disposal into a try..finally block is probably the safest bet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants