Error with Hangfire Nested Batches

We’ve been successfully using Hangfire for a while on several of our systems, and have successfully used Hangfire batch functionality to execute tasks in parallel, however we’ve hit a bit of a problem trying to use it on a sequential job.

Using a simple batch as follows works fine:

    BatchJob.Attach(masterBatch, batch =>
    {
        var lockJobId = batch.Enqueue<IProcessJob>(job => job.ObtainLock(businessUnit));
    
        var preparationJobId = batch.ContinueWith<IPrepareProcessJob>(lockJobId,
            job => job.PrepareData(businessUnit, workingJobData, JobCancellationToken.Null));
    
        var statisticsJobId = batch.ContinueWith<IPrepareProcessJob>(preparationJobId,
            job => job.AddStatistics(businessUnit, workingJobData, JobCancellationToken.Null));
    
        var processFileId = batch.ContinueWith<IProcessJob>(statisticsJobId,
            job => job.ProcessFile(workingJobData, notifierInstructions, businessUnit,
                            JobCancellationToken.Null));
    
        batch.ContinueWith<IProcessJob>(processFileId, job => job.ReleaseAPTLock(businessUnit));
    });

However we want to always run the final unlock job whatever happens on the three processing jobs, so we tried introducing a nested batch around the middle three jobs as follows:

    BatchJob.Attach(masterBatch, batch =>
    {
        var lockJobId = batch.Enqueue<IProcessJob>(job => job.ObtainLock(businessUnit));
    
        var mainBatchId = batch.AwaitJob(lockJobId, mainBatch =>
        {
            var preparationJobId = mainBatch.Enqueue<IPrepareProcessJob>(
                job => job.PrepareData(businessUnit, jobData, JobCancellationToken.Null));
    
            var statisticsJobId = mainBatch.ContinueWith<IPrepareProcessJob>(preparationJobId,
                job => job.AddStatistics(businessUnit, jobData, JobCancellationToken.Null));
    
            mainBatch.ContinueWith<IProcessJob>(statisticsJobId,
                job => job.ProcessFile(jobData, notifierInstructions, businessUnit,
                    JobCancellationToken.Null));
        });

        batch.AwaitBatch<IProcessJob>(mainBatchId, job => job.ReleaseLock(businessUnit));
    });

This produces the error:

Can't create a continuation for batch 'a5955434-294e-4568-9b64-c167feeb95da' because it doesn't exist.

Investigating the error is being produced when Hangfire tries to attach the final Release Lock. Has anybody got any suggestions as to what we might be doing wrong?

We’ve spent several more hours trying to fathom out this problem, we still can’t get a version with nested batches to work without throwing errors, however with a little bit of rearrangement we can get the desired functionality without nesting:

BatchJob.Attach(mainBatchId, batch =>
{
    var lockJobId = batch.Enqueue<IProcessJob>(job => job.ObtainLock(businessUnit));

    var preparationJobId = batch.ContinueWith<IPrepareProcessJob>(lockJobId,
        job => job.PrepareData(businessUnit, workingJobData, JobCancellationToken.Null));

    var statisticsJobId = batch.ContinueWith<IPrepareProcessJob>(preparationJobId,
        job => job.AddStatistics(businessUnit, workingJobData, JobCancellationToken.Null));

    batch.ContinueWith<IProcessJob>(statisticsJobId,
        job => job.ProcessFile(workingJobData, notifierInstructions, businessUnit,
            JobCancellationToken.Null));
});

// Catch-all unlock
BatchJob.AwaitBatch(mainBatchId,
    batch => batch.Enqueue<IProcessJob>(job => job.ReleaseLock(businessUnit)),
    $"Unlock for {reportName}", BatchContinuationOptions.OnAnyFinishedState);