Job scheduler via Azure Service Bus

There are many kinds of jobs that applications can run regularly. For instance:

  • renewing subscriptions for paying users on the first of every month
  • downloading new data from partner systems every X minutes
  • sending reminder or close subscriptions for high-debt users
  • jobs related to well-known events like opening/closing stock exchange, closing fiscal year, public holidays, black Friday and so on.

What tools can be used? Usually, cron, Windows Task Scheduler, Quartz.NET are used or Task.Delay after all.

Problem

Mostly, job schedulers are doing their job fine unless you run multiple instances of the same application for scalability and/or availability purpose. Just imagine you have 5 instances of the application. cron, Windows Task Scheduler and Task.Delay will fail in such setup. However, Quartz.NET will work if database backplane is enabled.

Solution

I would like to show how a cloud solution can help, specifically Azure Service Bus.

Azure Service Bus is a fully managed enterprise integration message broker. Service Bus is most commonly used to decouple applications and services from each other, and is a reliable and secure platform for asynchronous data and state transfer — Microsoft

It has a really useful feature - scheduled messages at a low price. $0,05 per million operations should be enough. Azure Service Bus should work well - Stack Overflow proof. So our jobs will be distributed evenly across multiple instances and if some instance dies the others will get their jobs to run.

To implement the scheduler I will use C# and Microsoft.Azure.ServiceBus package. However, it is easy to implement in your programming language since .NET, Java, Node.js, PHP, Python, Ruby are supported and REST API fills that gap for other languages.

public class ServiceBusJobScheduler
{
    private readonly string _connectionString;

    public ServiceBusJobScheduler(string connectionString)
    {
        _connectionString = connectionString;
    }

    public async Task Run(
        string queueName,
        Func<Message> init,
        Func<Message, Task<Message>> job,
        CancellationToken cancellation
    )
    {
        var queueClient = new QueueClient(_connectionString, queueName);
        var created = await EnsureQueueCreated(queueName);
        if (created)
            await queueClient.SendAsync(init());
        queueClient.RegisterMessageHandler(
            handler: async (message, _) =>
            {
                var nextMessage = await job(message);
                if (nextMessage != null)
                    await queueClient.SendAsync(nextMessage);
            },
            exceptionReceivedHandler: _ => Task.CompletedTask
        );
        await Wait(cancellation);
        await queueClient.CloseAsync();
    }

    ...
}

A new queue queueName must be created per a job. init callback is called the first time only to schedule very first run. job function is our job itself, it receives a scheduled message and returns the next scheduled message. In the scheduled message you can store a state for the job between runs. cancellation is the way to stop the job. EnsureQueueCreated creates queueName queue if it is not there. Wait just waits until cancellation is requested. The complete source code is on Gaev.Blog.AzureServiceBusTaskScheduler.

That’s how a job looks like.

var scheduler = new ServiceBusJobScheduler(ConnectionString);
await scheduler.Run(
    queueName: "TakeABreak",
    init: () => new Message
    {
        ScheduledEnqueueTimeUtc = DateTime.UtcNow
    },
    job: async (message) =>
    {
        var scheduledFor = message.ScheduledEnqueueTimeUtc.ToLocalTime();
        Console.WriteLine($"Take a break! It is {scheduledFor:T}.");
        await Task.Delay(100);
        return new Message
        {
            ScheduledEnqueueTimeUtc = CronExpression
                .Parse("*/30 8-18 * * MON-FRI")
                .GetNextOccurrence(DateTime.UtcNow, TimeZoneInfo.Local)
                .Value
        };
    },
    cancellation.Token
);

Let me introduce you TakeABreak job. It will remind me to take a break from the computer at */30 8-18 * * MON-FRI. I’m using cron expression to compute the next date via Cronos. That’s it and we are done.

Azure Service Bus queue

Take a look at how tasks are distributed across multiple instances.

Running multiple instances of the job

By the way, you can implement almost the same via Azure Queues but it has a limit for scheduling time. Messages cannot be scheduled more than 7 days ahead.

Pitfalls

Watch out for exceptions! By default, Service Bus retries 10 times then moves the message into the dead-letter queue.

Watch out for long-running tasks! By default, Service Bus waits 5 minutes then returns the message to the queue so others can process is again.

What job scheduler works well for you? Let me and readers know in comments :)

Chat with blog beta
  • Assistant: Hello, I'm a blog assistant powered by GPT-3.5 Turbo. Ask me about the article.