I’m very excited to write about all of these changes, because they take batches and Hangfire itself to a whole new level by unlocking a lot of usage scenarios. This pre-release adds support for nested batches, modifications of existing batches, batch continuations for jobs and vice versa, batch cancellation as well as support for .NET Standard and configurable batch expiration time.
Nested Batches
Batches now can consist of other batches, not only of background jobs. Outer batch is called as parent, inner batch is a child one (for continuations, it’s an antecedent/continuation relationship now). You can mix both batches and background jobs together in a single batch.
BatchJob.StartNew(parent =>
{
parent.Enqueue(() => Console.WriteLine("First"));
batch.StartNew(child => child.Enqueue(() => Console.WriteLine("Second")));
});
Multiple nesting levels are supported, so each child batch can, in turn, become a parent for another batch, allowing you to create very complex batch hierarchies.
BatchJob.StartNew(batch1 =>
{
batch1.StartNew(batch2 =>
{
batch2.StartNew(batch3 => batch3.Enqueue(() => Console.WriteLine("Nested")));
});
});
The whole hierarchy, including parent batch, all of its child batches and background jobs are created in a single transaction. So this feature not only allows you to see a group of related batches on a single dashboard page, but also create multiple batches atomically.
var antecedentId = BatchJob.StartNew(batch =>
{
batch.StartNew(inner => inner.Enqueue(() => Console.WriteLine("First")));
batch.StartNew(inner => inner.Enqueue(() => Console.WriteLine("Second")));
});
Parent batch is succeeded, if all of its background jobs and batches are succeeded. Parent batch is finished, if all of its batches and background jobs are in a final state. So you can create continuation for multiple batches, not just for a single one. Batch continuations also support the nesting feature.
BatchJob.AwaitBatch(antecedentId, continuation =>
{
continuation.StartNew(inner => inner.Enqueue(() => Console.WriteLine("First")));
continuation.StartNew(inner => inner.Enqueue(() => Console.WriteLine("Second")));
});
Batch Modification
This is another interesting feature that allows you to modify existing batches by attaching new background jobs and child batches to them. You can add background jobs in any states, as well as nested batches. If a modified batch has already been finished, it will be move to the started state back.
var batchId = BatchJob.StartNew(batch => batch.Enqueue(() => Console.WriteLine("First")));
BatchJob.Attach(batchId, batch => batch.Enqueue(() => Console.WriteLine("Second")));
This feature helps, if you want a list of records you want to process in parallel, and then execute a continuation. Previously you had to generate a very long chain of continuations, and it was very hard to debug them. Now you can create the structure, and modify a batch later.
var batchId = BatchJob.StartNew(batch => batch.Enqueue(() => ProcessHugeList(batch.Id, ListId)));
BatchJob.AwaitBatch(batchId, batch => batch.Enqueue(() => SendNotification(ListId)));
// ProcessHugeList
BatchJob.Attach(batchId, batch =>
{
foreach (var record in records)
{
batch.Enqueue(() => ProcessRecord(ListId, record.Id)));
}
});
Batches now can be created without any background jobs. Initially such an empty batches are considered as completed, and once some background jobs or child batches are added, they move a batch to the started state (or to another, depending on their state).
var batchId = BatchJob.StartNew(batch => {});
BatchJob.Attach(batchId, batch => batch.Enqueue(() => Console.WriteLine("Hello, world!")));
More Continuations
Did you try to continue batch by a background job? Now it’s possible without creating a batch that consist only of a single background job. Unfortunately we can’t add extension methods for static classes, so let’s create a client first.
var backgroundJob = new BackgroundJobClient();
var batchId = BatchJob.StartNew(/* ... */);
backgroundJob.AwaitBatch(batchId, () => Console.WriteLine("Continuation"));
You can use the new feature in other way, and create batch continuations for regular background jobs. So you are free to define workflows, where synchronous actions are continued by a group of parallel work, and then continue back to a synchronous method.
var jobId = BackgroundJob.Enqueue(() => Console.WriteLine("Antecedent"));
BatchJob.AwaitJob(jobId, batch => batch.Enqueue(() => Console.WriteLine("Continuation")));
Cancellation of a Batch
If you want to stop a batch with millions of background jobs from being executed, not a problem, you can now call the Cancel
method, or click the corresponding button in dashboard.
var batchId = BatchJob.StartNew(/* a lot of jobs */);
BatchJob.Cancel(batchId);
This method does not iterate through all the jobs, it simply sets a property of a batch. When a background job is about to execute, job filter checks for a batch status, and move a job to the Deleted state, if a batch has cancelled.
.NET Standard
.NET Standard is supported since the Hangfire.Pro 1.5.0-alpha2 version. It’s a pity that I didn’t release it as 1.5.0 (with configurable expiration time), because wanted to add other features as well. So here they are, can’t wait for the March 7th!
Configurable Expiration Time
No more methods that use the reflection. if you want to change the default batch expiration time (7 days by default), just pass a parameter to the UseBatches
method. Please note that this setting affects all the batches. If you have an interesting use case for different expiration times, just tell me.
GlobalConfiguration.Configuration
.UseXXX()
.UseBatches(expirationTime: TimeSpan.FromHours(1));
Upgrading
Run the following code in the Package Manager Console window, or use the Manage NuGet Packages window in Visual Studio to upgrade to the newest version. Don’t forget to include the pre-release modifier.
PM> Update-Package Hangfire.Pro -Pre
The only thing changed in Hangfire.Pro 2.0.0 is the type of some arguments in the IBatchJobClient
interface and BatchJob
class. ContinueWith
methods became obsolete to be replaced with AwaitBatch
for consistency with the new features. So, recompilation is required, but storage schema is the same.