Martin Andersen
Martin Andersen

Reputation: 2730

ASP.NET BackgroundService - Only run sequential

I have a BackgroundService that I start from an API Controller. There should never be more than one BackgroundService running. How can I check if a job is already running? So I don't start a new?

API to start a new job and related code

[HttpPost]
public async Task<IActionResult> RunJob(JobMessage msg)
{
    if (_queue.Count > 0)
    {
        return StatusCode(429, "DocumentDistributor are running. Try again later");
    }
    await _queue.Queue(msg);
    return Ok("DocumentDistributor will start in about one minute.");
}

public interface IBackgroundTaskQueue
{
    Task Queue(JobMessage message);
    Task<JobMessage> Dequeue();
    public int Count { get; }
}


public sealed class QueuedHostedService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public QueuedHostedService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _serviceProvider.CreateScope();
                var calculator = scope.ServiceProvider.GetRequiredService<QueueDocumentDistributor>();
                await calculator.RunService();
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }
            catch (Exception e)
            {
                Log.Error(e, "Error in QueuedHostedService");
            }
            
            // check queue every 1 minute
            await Task.Delay(1000 * 60, stoppingToken);
        }
    }
}


public class QueueDocumentDistributor
{
    private readonly IBackgroundTaskQueue _queue;
    private readonly ReportService _service;

    public QueueDocumentDistributor(IBackgroundTaskQueue queue, ReportService service)
    {
        _queue = queue;
        _service = service;
    }

    public async Task RunService()
    {
        var message = await _queue.Dequeue();
        if (message == null) return;
        await _service.CreateReports(message);
    }
}

Upvotes: 1

Views: 1086

Answers (2)

Christian Del Bianco
Christian Del Bianco

Reputation: 1043

In my case I have a BackgroundService (hosted in an IIS Web API app) with internally has a loop that checks my Rabbit MQ.

Then I have a Web API controller that return last status execution data time of the BackgroundService.

In order to share the last loop execution in BackgroundService, I created the following static class:

public static class QueueMessageConsumerBackgroundServiceExecutionStatus
{
    private static SemaphoreSlim _semaphore = new SemaphoreSlim(1);
    private static BackgroundServiceReportOutputModel _state = new BackgroundServiceReportOutputModel { Created = DateTime.Now, ProcessName = nameof(QueueMessageConsumerBackgroundService) };

    public static void SetExecuting()
    {
        try
        {
            _semaphore.Wait();

            _state.InExecution = true;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public static BackgroundServiceReportOutputModel Get()
    {
        try
        {
            _semaphore.Wait();

            return new BackgroundServiceReportOutputModel 
            { 
                ProcessName = _state.ProcessName, 
                Created = _state.Created,
                InExecution = _state.InExecution, 
                ExecutionCancelled = _state.ExecutionCancelled, 
                NextElaboration = _state.NextElaboration 
            };
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public static void SetNextElaborationTime(DateTime nextElaborationTime)
    {
        try
        {
            _semaphore.Wait();

            _state.NextElaboration = nextElaborationTime;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public static void SetExecutionCancelled()
    {
        try
        {
            _semaphore.Wait();

            _state.InExecution = false;
            _state.NextElaboration = null;
            _state.ExecutionCancelled = DateTime.Now;
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

The BackgroundService set the status as:

public class QueueMessageConsumerBackgroundService : BackgroundService
{
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
             // Some code
             QueueMessageConsumerBackgroundServiceExecutionStatus.SetExecuting();
             // Some code        
             QueueMessageConsumerBackgroundServiceExecutionStatus.SetExecutionCancelled();
        }        
}

And the Web API controller reads the status as:

public IActionResult GetBackgroundTasksReport()
{
    var outputModels = new List<BackgroundServiceReportOutputModel>
    {
        QueueMessageConsumerBackgroundServiceExecutionStatus.Get()
    };

    return base.Ok(outputModels);
}

Upvotes: -1

Guru Stron
Guru Stron

Reputation: 142008

AddHostedService adds singleton instance of IHostedService, so if there is no parallel processing in the implementation framework guarantees the single job execution.

There should never be more than one BackgroundService running.

There will be only single instance of background service per type running.

How can I check if a job is already running?

Depends on what do you mean by "job". If BackgroundService - then it is started by the framework. If your some custom payload in queue - then you will need to implement some monitoring manually.

So I don't start a new?

You don't start (usually) background service manually. If QueueDocumentDistributor.RunService gurantees single execution of your logic at a time - your are fine.

Based on provided implementation looks like a single queue element is processed at a time.

Read more:

Upvotes: 3

Related Questions