1

I have a SignalR app in DotNet 3.1, kind-of a large chat app, and I am trying to add two BackgroundServices.

The BackgroundServices are setup to run for as long as the ASP.NET app runs.

The first BackgroundService has a very fast main loop (50 ms) and seems to work well.

The second BackgroundService has a much longer main loop (1000 ms) and seems to start randomly, stop executing randomly, and then re-starts executing again ... randomly. It is almost like the second bot is going to sleep, for a long period of time (30 to 90 seconds) and then wakes up again with the object state preserved.

Both BackgroundServices have the same base code with different Delays.

Is it possible to have multiple, independent, non-ending, BackgroundServices? If so, then what am I doing wrong?

I have the services registered like this ...

_services.AddSimpleInjector(_simpleInjectorContainer, options =>
{
    options.AddHostedService<SecondaryBackgroundService>();
    options.AddHostedService<PrimaryBackgroundService>();

    // AddAspNetCore() wraps web requests in a Simple Injector scope.
    options.AddAspNetCore()
        // Ensure activation of a specific framework type to be created by
        // Simple Injector instead of the built-in configuration system.
        .AddControllerActivation()
        .AddViewComponentActivation()
        .AddPageModelActivation()
        .AddTagHelperActivation();
});

And I have two classes (PrimaryBackgroundService/SecondaryBackgroundService) that have this ...

public class SecondaryBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await Task.Factory.StartNew(async () =>
        {
            // loop until a cancalation is requested
            while (!cancellationToken.IsCancellationRequested)
            {
                //await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
                await Task.Delay(TimeSpan.FromMilliseconds(1000), cancellationToken);
    
                try
                {
                    await _doWorkDelegate();
                }
                catch (Exception ex)
                {
                }
            }
    
        }, cancellationToken);
    }
}

Should I setup a single BackgroundService that spins off two different Tasks; in their own threads? Should I be using IHostedService instead?

I need to make sure that the second BackgroundService runs every second. Also, I need to make sure that the second BackgroundService never impacts the faster running primary BackgroundService.

UPDATE:

I changed the code to use a Timer, as suggested, but now I am struggling with calling an async Task from a Timer event.

Here is the class I created with the different options that work and do not work.

// used this as the base: https://github.com/aspnet/Hosting/blob/master/src/Microsoft.Extensions.Hosting.Abstractions/BackgroundService.cs
public abstract class RecurringBackgroundService : IHostedService, IDisposable
{
    private Timer _timer;

    protected int TimerIntervalInMilliseconds { get; set; } = 250;

    // OPTION 1. This causes strange behavior; random starts and stops
    /*
    protected abstract Task DoRecurringWork();

    private async void OnTimerCallback(object notUsedTimerState)  // use "async void" for event handlers
    {
        try
        {
            await DoRecurringWork();
        }
        finally
        {
            // do a single call timer pulse
            _timer.Change(this.TimerIntervalInMilliseconds, Timeout.Infinite);
        }
    }
    */

    // OPTION 2. This causes strange behavior; random starts and stops
    /*
    protected abstract Task DoRecurringWork();

    private void OnTimerCallback(object notUsedTimerState)
    {
        try
        {
            var tf = new TaskFactory(System.Threading.CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

            tf.StartNew(async () =>
            {
                await DoRecurringWork();
            })
            .Unwrap()
            .GetAwaiter()
            .GetResult();
        }
        finally
        {
            // do a single call timer pulse
            _timer.Change(this.TimerIntervalInMilliseconds, Timeout.Infinite);
        }
    }
    */

    // OPTION 3. This works but requires the drived to have "async void"
    /*
    protected abstract void DoRecurringWork();

    private void OnTimerCallback(object notUsedTimerState)
    {
        try
        {
            DoRecurringWork(); // use "async void" in the derived class
        }
        finally
        {
            // do a single call timer pulse
            _timer.Change(this.TimerIntervalInMilliseconds, Timeout.Infinite);
        }
    }
    */

    // OPTION 4. This works just like OPTION 3 and allows the drived class to use a Task
    protected abstract Task DoRecurringWork();

    protected async void DoRecurringWorkInternal() // use "async void"
    {
        await DoRecurringWork();
    }

    private void OnTimerCallback(object notUsedTimerState)
    {
        try
        {
            DoRecurringWork();
        }
        finally
        {
            // do a single call timer pulse
            _timer.Change(this.TimerIntervalInMilliseconds, Timeout.Infinite);
        }
    }

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // https://stackoverflow.com/questions/684200/synchronizing-a-timer-to-prevent-overlap
        // do a single call timer pulse
        _timer = new Timer(OnTimerCallback, null, this.TimerIntervalInMilliseconds, Timeout.Infinite);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        try { _timer.Change(Timeout.Infinite, 0); } catch {; }

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        try { _timer.Change(Timeout.Infinite, 0); } catch {; }
        try { _timer.Dispose(); } catch {; }
    }
}

Is OPTION 3 and/or OPTION 4 correct?

I have confirmed that OPTION 3 and OPTION 4 are overlapping. How can I stop them from overlapping? (UPDATE: use OPTION 1)

UPDATE

Looks like OPTION 1 was correct after all.

Stephen Cleary was correct. After digging and digging into the code I did find a Task that was stalling the execution under the _doWorkDelegate() method. The random starts and stops was caused by an HTTP call that was failing. Once I fixed that (with a fire-and-forget) OPTION 1 started working as expected.

R C
  • 55
  • 1
  • 5
  • 1
    I suspect the problem may be in `_doWorkDelegate`, which may impact your timing. Note that the code is not currently running it every second; it's running it *and then waiting a second in-between each run*. Side note: [Don't use `Task.Factory.StartNew`](https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html); use `Task.Run` instead. – Stephen Cleary Jun 16 '21 at 13:34
  • Yes, you are correct, I would like the code to run and then wait so it does not overlap – R C Jun 16 '21 at 14:44

1 Answers1

2

I would recommend writing two timed background tasks as shown in the documentation

Timed background tasks documentation

then they are independent and isolated.

public class PrimaryBackgroundService : IHostedService, IDisposable
{
    private readonly ILogger<PrimaryBackgroundService> _logger;
    private Timer _timer;

    public PrimaryBackgroundService(ILogger<PrimaryBackgroundService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("PrimaryBackgroundService StartAsync");
        
        TimeSpan waitTillStart = TimeSpan.Zero;
        TimeSpan intervalBetweenWork = TimeSpan.FromMilliseconds(50);

        _timer = new Timer(DoWork, null, waitTillStart, intervalBetweenWork);

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("PrimaryBackgroundService DoWork");

        // ... do work
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("PrimaryBackgroundService is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

create the SecondaryBackgroundService using similar code and register them as you did before

options.AddHostedService<SecondaryBackgroundService>();
options.AddHostedService<PrimaryBackgroundService>();

Note that if you want to use any dependency injection then you have to inject IServiceScopeFactory into the background service constructor and call scopeFactory.CreateScope()

Rosco
  • 2,108
  • 17
  • 17
  • I switched to a Timer (please see my update above). I am not sure how to correctly call an async Task from a Timer that does not overlap. As in, I need the async Task to complete before resetting the Timer to pulse again. – R C Jun 16 '21 at 14:48