Search code examples
c#asp.net-coredependency-injectionasp.net-core-hosted-services

Is it safe putting a Task, with scoped service dependencies, in a Singleton queue?


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..


Solution

  • 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.