Cancel & Dispose CancellationTokenSource in reconfigurable periodic BackgroundService (IHostedService)

67 Views Asked by At

I am trying to create a BackgroundService (IHostedService) that calls some async business logic periodically. I am also trying to make it reconfigurable: When the settings change, the background service should react.

I read somewhere that when creating linked CancellationTokenSource instances we have to make sure to dispose them, otherwise it might result in a memory leak. This is a big concern for me since I am creating a new linked CancellationTokenSource after every configuration change. So I decided to create the CancellationTokenSource in a using declaration.

I also have to be able to signal cancellation when the configuration changes so I have registered a callback in IOptionsMonitor<MyOptions>.OnChange(Action<MyOptions> listener). This registration is also called in a using declaration so that the listener is unregistered at the end of the iteration. (I am hoping that it would not be triggered after the CancellationTokenSource is disposed.)

VS is warning me that the captured variable (the token source) is disposed in the outer scope.

What I would like to know:

  • Is there a chance for a memory leak?
  • Is there a chance the OnChange callback calls Cancel() on a disposed CancellationTokenSource? Should I be worried about the warning?
  • Did I overcomplicate it? Is there a more elegant solution?

This is what I have so far:

public sealed class MyBackgroundService : BackgroundService
{
  private static readonly TimeSpan _checkIsEnabledDelay = TimeSpan.FromMinutes(1d);
  private readonly IOptionsMonitor<MyOptions> _options;
  private readonly IServiceProvider _serviceProvider;
  private readonly ILogger<MyBackgroundService> _logger;

  public MyBackgroundService(
    IOptionsMonitor<MyOptions> options,
    IServiceProvider serviceProvider,
    ILogger<MyBackgroundService> logger = null)
  {
    ArgumentNullException.ThrowIfNull(options);
    ArgumentNullException.ThrowIfNull(serviceProvider);

    _options = options;
    _serviceProvider = serviceProvider;
    _logger = logger;
  }

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

    while (!stoppingToken.IsCancellationRequested)
    {
      var options = _options.CurrentValue;

      if (!options.IsEnabled)
      {
        await Task.Delay(_checkIsEnabledDelay, stoppingToken);
        continue;
      }
      
      using var iterationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
      using var optionsChangedRegistration = _options.OnChange(updatedOptions =>
      {
        if (updatedOptions.IsEnabled != options.IsEnabled
          || updatedOptions.Period != options.Period)
        {
          iterationTokenSource.Cancel(); // VS warns: Captured variable is disposed in the outer scope.
        }
      });

      while (!iterationTokenSource.IsCancellationRequested)
      {
        using var scope = _serviceProvider.CreateScope();

        var businessLogic = scope.ServiceProvider.GetRequiredService<SomeBusinessLogic>();

        try
        {
          await businessLogic.PerformSomeBusinessLogic(stoppingToken);
        }
        catch (Exception exception)
        {
          _logger?.LogError(exception, "Failure message.");
        }

        try
        {
          await Task.Delay(options.Period, iterationTokenSource.Token);
        }
        catch (OperationCanceledException) { }
      }
    }
  }
}
0

There are 0 best solutions below