dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.96k stars 4.65k forks source link

Awaiting the cancellation token in a BackgroundService throws an exception. #60122

Closed james-world closed 2 years ago

james-world commented 2 years ago

Description

If I do this in my BackgroundService (.NET 5.0)

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.FromCanceled(stoppingToken);
    }

I get this:

[13:44:52 INF] Starting web host
[13:44:54 FTL] Host terminated unexpectedly
System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'cancellationToken')
   at System.Threading.Tasks.Task.FromCanceled(CancellationToken cancellationToken)
   at Test.MyService.ExecuteAsync(CancellationToken stoppingToken) in C:\Code\Imburse\akka-cosmos-persistence\src\Test\MyService.cs:line 24
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at Test.Program.Main(String[] args) in C:\Code\src\Test\Program.cs:line 23

But if I do this:

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Yield();
        await Task.FromCanceled(stoppingToken);
    }

All is well. Which seems plain wrong to me.

It's not an uncommon pattern either where you want to do something like:

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Call a synchronous method that starts some background job
        await Task.FromCanceled(stoppingToken);
        // Call a synchronous method that shuts down that background job.
    }

Configuration

.NET 5.0, SDK 5.0.401

Related to https://github.com/dotnet/runtime/issues/36063 possibly.

ghost commented 2 years ago

Tagging subscribers to this area: @eerhardt, @maryamariyan See info in area-owners.md if you want to be subscribed.

Issue Details
### Description If I do this in my BackgroundService (.NET 5.0) protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.FromCanceled(stoppingToken); } I get this: ``` [13:44:52 INF] Starting web host [13:44:54 FTL] Host terminated unexpectedly System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'cancellationToken') at System.Threading.Tasks.Task.FromCanceled(CancellationToken cancellationToken) at Test.MyService.ExecuteAsync(CancellationToken stoppingToken) in C:\Code\Imburse\akka-cosmos-persistence\src\Test\MyService.cs:line 24 at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken) at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token) at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token) at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host) at Test.Program.Main(String[] args) in C:\Code\src\Test\Program.cs:line 23 ``` But if I do this: protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.Yield(); await Task.FromCanceled(stoppingToken); } All is well. Which seems plain wrong to me. It's not an uncommon pattern either where you want to do something like: protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Call a synchronous method that starts some background job await Task.FromCanceled(stoppingToken); // Call a synchronous method that shuts down that background job. } ### Configuration .NET 5.0, SDK 5.0.401 Related to https://github.com/dotnet/runtime/issues/36063 possibly.
Author: james-world
Assignees: -
Labels: `untriaged`, `area-Extensions-Hosting`
Milestone: -
stephentoub commented 2 years ago

Task.FromCanceled requires that the CancellationToken passed to it has had cancellation requested, i.e. its IsCancellationRequested returns true. I assume in your test, the code calling ExecuteAsync cancels the token immediately after ExecuteAsync synchronously returns, which would explain why the original snippet always fails (it's calling FromCanceled with a non-canceled token) and why the latter non-deterministically but almost always succeeds (because the token has had cancellation requested by the time you call FromCanceled).

james-world commented 2 years ago

OK - I misunderstood what Task.FromCanceled is for (note to self, read the intellisense!). Adding the Task.Yield just caused the exception to occur on a background thread, so I missed it. Doh! It would be nice to have an easier wait to await cancellation though. It seems you have to do a lot of boiler plate to do this.

stephentoub commented 2 years ago

It would be nice to have an easier wait to await cancellation though

Do you mean you want to await for a cancellation request? If nothing else, you can do:

await Task.Delay(-1, cancellationToken);