ProgrammingLlama
ProgrammingLlama

Reputation: 38737

How to cancel executing Hangfire batch child jobs?

Problem

Simply put, BatchJob.Cancel doesn't cancel already-running child jobs, and they instead run to completion, but cancelling the child job by id (when it's running) using BackgroundJob.Delete results in the child job being cancelled. Injecting either JobCancellationToken or CancellationToken into the method yields the same problem.

Minimal Example

public class BatchTest
{
    public async Task Method1(CancellationToken ct)
    {
        Console.WriteLine("Started 1");
        try
        {
            // meaningless work to simulate some long-running task
            await Task.Delay(TimeSpan.FromSeconds(30), ct);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Cancelled 1");
            return;
        }
        Console.WriteLine("Completed 1");
    }
}

Client code:

string? childId = null;
var batchJobId = BatchJob.StartNew(act =>
{
    childId = act.Enqueue<BatchTest>(t => t.Method1(default));
});

Console.WriteLine("Scheduled");

// Uncomment to immediately cancel (batch children will correctly never start)
// BatchJob.Cancel(batchJobId);

// For the sake of this example, we'll use this code to
// detect when the child job actually starts. In the real
// code, I don't do/need to do this, but I do need the batch
// to be cancellable.
while (true)
{
    var details = JobStorage.Current.GetMonitoringApi().JobDetails(childId);
    if (details.History.Any(h => h.StateName == ProcessingState.StateName))
    {
        Console.WriteLine("Started");
        break;
    }
}

// Allow some time for the job to do some "work" and then cancel it
await Task.Delay(TimeSpan.FromSeconds(10));
BatchJob.Cancel(batchJobId);
//BackgroundJob.Delete(childId!);
Console.WriteLine("Cancelled batch");

If I cancel the batch job as above, Method1 will first output "Started 1" and after 30s will output "Completed 1". It doesn't register that the cancellation has even occurred.

However, if I comment out BatchJob.Cancel(batchJobId); and uncomment BackgroundJob.Delete(childId!);, the method correctly prints "Started 1" followed by "Cancelled 1".

And, of course, if I cancel the batch before it gets picked up for execution, it simply never runs. That part works correctly, at least.

Question

From D.A. I feel that I might be misunderstanding what batch cancellation is for, though D.A. even suggests that it sets the batch to deleted, so surely that should also handle the child jobs?

I can't find any way of getting the child job IDs by batch ID, so it's not like I have a means to manually cancel them all without first saving them all in my database.

How can I make batch cancellation work as I expected it to? I'm open to creating an extension method on IBackgroundJob if it allows me to cancel all of the child jobs.

Upvotes: 2

Views: 426

Answers (1)

D A
D A

Reputation: 2054

The Hangfire Server uses multiple threads to perform background jobs. Meaning it can process a background job per thread within the Hangfire server. This allows you to execute background jobs concurrently. By default, the number of threads it uses is 5 per Processor Count. For the BatchJob it uses thread pool. In a thread pool you can't cancel a worker thread. Once put in there you will have to wait until its completion or implement a custom CancellationToken mechanism to stop them. The BatchJob.Cancel(batchId) just set the Deleted state to prevent the other scheduled working threads to start.

Upvotes: 0

Related Questions