As part of my REST API I have a BackgroundService that subscribes to a message queue and waits for particular messages to be received and performs certain actions based on the type of message.
In ExecuteAsync I'm currently returning Task.CompletedTask since I don't have anything to 'await', however I'm thinking this is actually incorrect and could be potentially problematic although I'm not sure what exactly how or what problems could arise as a result.
Would it be acceptable to simply 'await' Task.Delay with an infinite delay? Something like:
await Task.Delay(Timeout.infinite, cancellationToken);
Essentially it boils down to: Return Task.CompleteTask.
I'd even recommend just using IHostedService directly instead of BackgroundService because you aren't really gaining anything from what I can see.
The first thing to look into is IHostedService. This interface is what BackgroundService implements, and is what enables running code on startup of the host. The interface only adds the StartAsync and StopAsync methods (no ExecuteAsync method).
If we look at the source code for Microsoft.Extensions.Hosting we can see that when we call AddHostedService all it's doing is adding the IHostedService to the IServiceProvider.
public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services)
where THostedService : class, IHostedService
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>());
return services;
}
Then, inside Host.StartAsync it does some startup stuff, but the main thing is how it starts the IHostedService's.
public async Task StartAsync(CancellationToken cancellationToken = default)
{
// Omitted for brevity
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
foreach (IHostedService hostedService in _hostedServices)
{
// Fire IHostedService.Start
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
if (hostedService is BackgroundService backgroundService)
{
_ = HandleBackgroundException(backgroundService);
}
}
// Omitted for brevity
}
Why show all this? I think it's important to know there is no real magic happening here. Whenever StartAsync on the host is called, all it does is call StartAsync on the IHostedServices. Conversely StopAsync on the IHostedService is only called when Host.StopAsync is called.
So what makes BackgroundService special?
You might have noticed there is some special code when starting the IHostedService's checking if it's a BackgroundService. You have already posted a link to the source for BackgroundService, but I'll post the main parts here for visibility.
public virtual Task ExecuteTask => _executeTask;
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Create linked token to allow cancelling executing task from provided token
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Store the task we're executing
_executeTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executeTask.IsCompleted)
{
return _executeTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
As you can see, it's never actually awaiting on the Task returned from ExecuteAsync. This is what allows us to have ongoing tasks inside a BackgroundService. If you were to try doing a Task.Delay with a huge delay inside a normal IHostedService, your Host would never start properly.
In the situation that you return Task.CompleteTask from ExecuteAsync, it'll just return that complete task back to the StartAsync from Host.
So what's the point of doing all this with BackgroundTask's? Someone correct me if I'm wrong, but from looking at it, it seems like it only really exists for exception handling on long running Tasks. If we look at the HandleBackgroundException inside Host.StartAsync, we see this:
private async Task HandleBackgroundException(BackgroundService backgroundService)
{
try
{
await backgroundService.ExecuteTask.ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.BackgroundServiceFaulted(ex);
}
}
All it does it takes the Task from ExecuteAsync and waits it, which allows us to see any exceptions thrown by sending it to the Logger.
Based on what I saw in the source code, I didn't see anything about your case that warrants a BackgroundService.
You've said 'It isn't an asynchronous API and as such doesn't need to awaited', which to me means you don't gain anything from BackgroundService.
You should just use a IHostedService and setup your callbacks in StartAsync, then remove them in StopAsync. There is no reason from what I can see this wouldn't work. It's not like your IHostedService is being stopped because it's not 'running' anymore.
If you wanted to stick with a BackgroundService, it's smarter to return Task.CompleteTask instead of awaiting a delay. The delay will still use some resources to keep the timer, even if that's small.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With