Go to content

Bulletcode.NET

Background Jobs

Bulletcode.NET includes a mechanism for executing long-running jobs in the background. They can be used to prevent timeout errors while performing potentially long running requests. This is especially useful in case of Azure app services, which have a hard-coded timeout of 230 seconds.

In order to enable background jobs, the application should call the AddBackgroundJobExecutor() extension method of the IServiceCollection.

The IBackgroundJobExecutor service can be used to start a job and check its status. When a job is started by calling AddJob(), a GUID is returned which identifies that job. The GetJobStatus() method returns a structure which describes the status of the given job and makes it possible to retrieve its result.

Jobs are executed sequentially, i.e. only one job can be executed at any time, and other jobs are queued until the current job finishes.

Implementing jobs

A job is an asynchronous function which returns a Task. It should not be a method of a controller or a scoped service, because the scope of the HTTP request in which the job was started may be destroyed before the job is executed.

The job can be executed in the root scope of the service provider. In order to do this, create a singleton service which implements the job, for example:

[Singleton]
public class JobService
{
    public async Task ExecuteAsync()
    {
        // TODO
    }
}

Then use the IBackgroundJobExecutor to start the job:

var jobId = _backgroundJobExecutor.AddJob( _jobService.ExecuteAsync );

The job can also be executed in a separate service provider scope. Create a scoped service which implements the job, for example:

[Scoped]
public class ScopedJobService
{
    public async Task ExecuteAsync()
    {
        // TODO
    }
}

Then create a factory service which inherits BackgroundJobFactory and is registered as a singleton. It should call the CreateJob method with the scoped service passed as a template parameter, and a callback function which calls the appropriate method of the service, for example:

[Singleton]
public class ScopedJobFactory : BackgroundJobFactory
{
    public ScopedJobFactory( IServiceProvider serviceProvider )
        : base( serviceProvider )
    {
    }

    public IBackgroundJob CreateJob()
    {
        return CreateJob<ScopedJobService>( s => s.ExecuteAsync() );
    }
}

Use the IBackgroundJobExecutor to start the job:

var jobId = _backgroundJobExecutor.AddJob( _scopedJobFactory.CreateJob() );

The BackgroundJobFactory creates the scoped service when the job is started, and destroys the scope when it finishes executing.

The job can also take parameters and return a result, for example:

[Scoped]
public class ScopedJobService
{
    public async Task<string> ExecuteAsync( string input )
    {
        // TODO

        return result;
    }
}

The factory should pass the parameters to the scoped service:

[Singleton]
public class ScopedJobFactory : BackgroundJobFactory
{
    public ScopedJobFactory( IServiceProvider serviceProvider )
        : base( serviceProvider )
    {
    }

    public IBackgroundJob Execute( string input )
    {
        return CreateJob<ScopedJobService>( s => s.ExecuteAsync( input ) );
    }
}

The parameters can then be passed to the factory when scheduling the job:

var jobId = _backgroundJobExecutor.AddJob( _scopedJobFactory.CreateJob( "abc" ) );

The GetResult() method of the BackgroundJobStatus can be called to get the result of the job if it has been completed:

var status = _backgroundJobExecutor.GetJobStatus( jobId );

if ( status.IsCompleted )
{
    string result = status.GetResult<string>();

    // TODO
}

If the job throws an exception, it is re-thrown by the GetJobStatus() method, preserving the original stack trace.

Status polling

After scheduling a job, the controller should immediately return an Accepted status code with an URL which makes it possible to periodically check the status of the job:

[HttpGet]
public IActionResult Start()
{
    var jobId = _backgroundJobExecutor.AddJob( _jobService.ExecuteAsync );

    return AcceptedAtAction( "Status", new { jobId }, null );
}

The controller can use the GetJobStatus() method to check if the job has been completed or is still pending:

[HttpGet( "{jobId}" )]
public IActionResult Status( Guid jobId )
{
    var status = _backgroundJobExecutor.GetJobStatus( jobId );

    if ( status.IsCompleted )
        return Ok();

    if ( status.IsPending )
        return AcceptedAtAction( "Status", new { jobId }, null );

    return NotFound();
}

The API Client can handle requests which return an Accepted status code and poll the returned URL at the configured interval.