Search code examples
c#.net-coreasp.net-core-mvcasp.net-core-hosted-services

Is it possible to run my jobs (that are based on Scoped Background Services) one by one?


Some time ago I had self-created scheduler that was created with the help of 'System.Timers'. I will show you this code:

public class Scheduler
{
    private const int MSecond = 1000;
    private readonly int _seconds = MSecond * 10;
    private Timer _aTimer;

    public void Start()
    {
        Console.WriteLine("Sending is started ...");

        _aTimer = new Timer();
        _aTimer.Interval = _seconds;

        _aTimer.Elapsed += OnTimedEvent;

        _aTimer.AutoReset = true;

        _aTimer.Enabled = true;
    }

    public bool IsWorking()
    {
        return _aTimer != null;
    }

    private async void OnTimedEvent(object sender, ElapsedEventArgs e)
    {
        await JustDoIt();
    }

    private async Task JustDoIt()
    {
        _aTimer.Stop();

        // big and difficult work
        await Task.Delay(1000 * 12);
        Console.WriteLine("Done !!");

        _aTimer.Start();
    }

    public void Stop()
    {
        _aTimer.Stop();
        _aTimer = null;
    }
}

So, to be sure that I have finished one job before starting new one I just cancel my timer directly after starting a job. Then, when the job is finished, I switch on my timer. This helps my to avoid errors when several parallels job are trying to get access to resources or are writing in the database together. And my business rules directly tells me: jobs mast go one by one, not together.

All was ok but I decided to rewrite all on Scoped Hosted Background services. Here is Microsoft documentation. I will show you the result:

IScopedProcessingService

internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

ConsumeScopedServiceHostedService

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        _logger.LogInformation("Consume Scoped Service Hosted Service is working.");

        while (!stoppingToken.IsCancellationRequested) {
            using (var scope = Services.CreateScope()) {
                IServiceProvider serviceProvider = scope.ServiceProvider;
                var service = serviceProvider.GetRequiredService<IScopedProcessingService>();    
                await service.DoWork(stoppingToken);
            }
            //Add a delay between executions.
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await Task.CompletedTask;
    }
}

and ScopedProcessingService

internal class ScopedProcessingService : IScopedProcessingService
{
   // here is injections and constructor

    public async Task DoWork(CancellationToken stoppingToken)
    {
      // a job with no fixed time. Sometimes it's minute, sometimes it's more, sometimes it's less           
        await Task.Delay(TimeSpan.FromSeconds(40));

    }
}

And in the Startup.cs

        services.AddHostedService<ConsumeScopedServiceHostedService>();
        services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

So how I can protect my application from parallels jobs? I need exactly one by one. On the example above the next task will start after 10 seconds. But, if I right, the previous task will go on in this time! It means that I may have mess in my DB as result.


Solution

  • This is a misunderstanding of how the async-await is working.

    Each task will be invoked sequentially, because the tasks are being awaited.

    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        _logger.LogInformation("Consume Scoped Service Hosted Service is working.");
    
        while (!stoppingToken.IsCancellationRequested) {
            using (var scope = Services.CreateScope()) {
                IServiceProvider serviceProvider = scope.ServiceProvider;
                var service = serviceProvider.GetRequiredService<IScopedProcessingService>();    
                await service.DoWork(stoppingToken);
            }
            //Add a delay between executions.
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
    

    There should be no overlap as the tasks will be awaited one after the other.