I am pretty much using the example provided in Microsoft docs for queuing background tasks.
In this queue I am adding a Func<Task>
, to be executed later by QueuedHostedService
.
Controller - HTTP POST
...
Func<Task> workItem = () => _mockService.DoWorkAsync(guid);
_queue.QueueBackgroundWorkItem(workItem);
return Ok();
MockService.DoWorkAsync
var data = await _insideMockService.GetAsync();
await _anotherService.Notify(data);
BackgroundTaskQueue.QueueBackgroundWorkItem
private ConcurrentQueue<Func<Task>> _workItems
private SemaphoreSlim _signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(Func<Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
_signal.Release();
}
QueuedHostedService.ExecuteAsync
_currentTask = await _queue.DequeueAsync(cancellationToken);
try
{
await _currentTask();
...
BackgroundTaskQueue.DequeueAsync
public async Task<Func<Task>> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItemsById.TryDequeue(out var workItem);
return workItem;
}
Startup.ConfigureServices
services.AddScoped<IMockService, MockService>(); // Implements DoWorkAsync
services.AddScoped<IInsideMockService, InsideMockService>(); // DoWorkAsync requires this dependency
services.AddScoped<IAnotherService, AnotherService>(); // DoWorkAsync requires this dependency
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
IMockService.DoWorkAsync
method use scoped
services. Ref to this method is added to a queue which resides in a singleton
service. This queue is later read by a HostedService
which is also a singleton
.
Is there any chance that the service-references in DoWorkAsync
will be disposed before being handled by HostedService
? If we exclude the scenarios that the application is gracefully(or ungracefully) shut down.
Some local testruns with maybe a hundred requests (and some added Task.Delay
in DoWorkAsync
) seems to be working fine but I'm not sure if I'm missing something..
The main problem here is not that reference to Func will be garbade-collected.
Problem is that any scoped service it use can be disposed.
When requests finishes, scope is disposed with all his disposable content. If your services will implement IDisposable - they will be disposed too. And your queued task will try to call disposed service(s) and fail.
And even if your service will not implement IDisposable itself - it may use, directly or indirectly, some other IDisposable object that will be disposed (for example, DbContext).
To make sure this stuff will not fail at some day - you should carefully control all objects/services (and their sub-objects/sub-services) used during DoWorkAsync
. In this case - why not you register MockService istelf as singleton? :)
Correct way for background task is to capture IServiceScopeFactory
instance, and call CreateScope() before any real job, and obtain any scoped services from it.