Create Simple Scheduled Tasks in the Cloud with Azure’s WebJobs and TimerTriggers

In two of my recent projects (Procuments and Tempng), I needed some way to utilize scheduled tasks. For Procuments I needed to send daily reminder emails and with Tempng I needed to send timecard reminders and automate payments. For both of these I found that the easiest way to accomplish this is with Azure WebJobs and TimerTriggers.
I've consolidated what I've learned from multiple sources into this blog post.

WebJob TimerTriggers

TimerTrigger is an extension to the WebJobs SDK that allows for scheduling functions within the WebJob. You could also use Azure's Scheduler feature, but I've found the TimerTriggers to be easier to both create and manage. Plus, TimerTriggers doesn't use an additional Azure resource like Scheduler does, so no extra charges.

When first working on Procuments, I used the Azure Scheduler feature. At the time that's all I could find online about how to create scheduled tasks in the cloud. I didn't like working with Scheduler though, so when working on Tempng I did a little more research and found the TimerTriggers method.

Benefits of TimerTriggers

After implementing TimerTriggers on my Tempng WebJobs, I went back and redid my Procuments WebJobs to remove the Scheduler. I did this for multiple reasons:

  • Scheduler is not in the "New" Azure portal
  • Scheduler has a 1 hour minimum interval when using the free tier
    • TimerTriggers can go down to once a second
  • TimerTriggers use a cron expression to create the schedule
  • TimerTriggers are easier to manage, as they don't require another Azure resource (the Scheduler)

TimerTriggers are also multi-instance safe if your WebJob is spread across multiple instances. They use the WebJobs SDK Singleton that gets a lock to make sure that each timer event is only processed once. More on this later.
Note: Scheduler provides this as well.

Setup

Before you add the code, you'll need to add the WebJob Extensions NuGet package to your project: Microsoft.Azure.WebJobs.Extensions.

You'll also need to make sure that the Web App your WebJob runs on is set to "always on" in the Azure portal. This will make sure that the Azure does not stop your site if it has been idle for too long. Stopping the site means your WebJobs won't run. You also need to set the WebJob to run continuously under "How To Run" in the WebJob settings.

Adding the Code

It requires all of 4 lines to successfully get a WebJob function to run on a schedule.
In the Main() method of program.cs add the following lines

var config = new JobHostConfiguration();  
config.useTimers();  
var host = new JobHost(config);  

You'll see that we simply added 2 new lines and overwrote the declaration of the host variable to add the newly created config.
Now update the functions themselves. Add the following to the method declaration to pass in a TimerInfo object with the cron expression dictating the desired schedule

public static void RemindersFunction([TimerTrigger("0 0 8 * * *", RunOnStartup = false)] TimerInfo timerInfo, TextWriter log)  
{
    //Do stuff every day at 8AM
}

And you're done.
Once published, this function will run every day at 8AM. You'll also notice I told it not to run on startup.

It's important to note that this will by default run at 8AM in UTC time (the exception is if you're debugging locally). To change this, add an app setting to your WebJob's App.config:

<appSettings>  
  <add key="WEBSITE_TIME_ZONE" value="Central Standard Time" />
</appSettings>  

Make sure to also add this app setting to your web app in the Azure portal.

Setting the Schedule

TimerTriggers are usually set using cron expressions, but more advanced schedules can be set using a custom schedule. We'll discuss cron expressions first as they will be used most of the time.

Cron Expressions

If you've never used cron expressions before, they are pretty easy to learn and can be a powerful tool. It may take a bit to wrap your head around it, but once you do it will be easy to create some advanced schedules.

TimerTrigger cron expressions use 6 fields instead of the usual 5:

{second} {minute} {hour} {day} {month} {day-of-week}

If there's a * then it means to "run for all the x". So in our example above ( 0 0 8 * * * ) the function runs every day of the week, every month, every day of the month, the 8th hour, the 0th minute, and the 0th second. Or, 8AM every day.

Cron expressions can also use the symbols -, /, or , to add additional functionality:

  • - gives a range, so 0 0 10 * * 1-5 means to run every weekday at 10 AM
  • / makes it an interval, so 0 */15 * * * * means to run every 15 minutes. The function will run at the 0, 15, 30, and 45 minute mark every hour
  • , lets you list out specific values, so 0 0 9 1,15,31 * * means to run on the 1st, 15th, and 31st day of every month at 9AM

Here are a few more examples:

  • 0 0 * * * * - run every hour
  • 0 0 12-17 * * * - run once an hour from noon to 5PM
  • 0 0 */8 1-15 * * - run every 8 hours for the first 15 days of the month
  • 0 30/5 21 * * 0,6 - run once every 5 minutes for minutes 30-59 after 9PM on Sunday and Saturday

As you can see the schedules made using cron expressions can be very versatile. If you need help creating an expression there are websites that can help you figure out what to do. Note that many of them will not include the seconds parameter so you'll have to adjust for that.

Custom Schedules

Some schedules cannot be created using cron expressions. That's where custom schedules come into play. They can be used to make more "unusual" schedules. For instance, say I want to schedule a function for Tuesdays at 9AM and 5PM, and Thursdays at noon. Cron expressions don't allow for this, but custom schedules do. To do so we'll have to add a new class to our code:

public class MyWeeklySchedule : WeeklySchedule  
{
    public MyWeeklySchedule()
    {
        // Every Tuesday at 9AM and 5PM
        Add(DayOfWeek.Tuesday, new TimeSpan(9, 0, 0));
        Add(DayOfWeek.Tuesday, new TimeSpan(17, 0, 0));

        // Every Thursday at 12PM
        Add(DayOfWeek.Thursday, new TimeSpan(12, 0, 0));
    }
}

The WeeklySchedule class is a part of the Microsoft.Azure.WebJobs.Extensions.Timers namespace.

Then we add the schedule in the function's method:

public static void RemindersFunction([TimerTrigger(typeof(MyWeeklySchedule))] TimerInfo timerInfo, TextWriter log)  
{
    //Do stuff
}

There is also a DailySchedule class that allows for multiple times per day:

public class MyDailySchedule : DailySchedule  
{
    public MyDailySchedule() :
        base("7:30:00", "12:00:00", "17:30:00")
    { }
}

 

Testing

If you want to test your cron jobs locally, there are a few things you should do to help facilitate this. After creating the config in your program.cs, add the following:

config.UseDevelopmentSettings();  
//If you want to set it manually. 15 seconds is the lowest it can go
//config.Singleton.ListenerLockPeriod = new TimeSpan(0, 0, 15);

The ListenerLockPeriod setting will tell the Singleton how long it can hold on to the lock. This is part of the multi-instance safety that WebJobs provides. The lock is a Storage Blob created automatically in the attached storage container of your WebJob. If an instance is already holding onto the lock then a new instance of your function will not be started.

This can cause problems when testing locally since you will be stopping and starting the solution frequently. Once stopped ungracefully (closing the console window, etc), the lock is still held on to for the ListenerLockPeriod (default is 60 seconds). This means that your function will not be able to get a lock, and will not start. Telling the config to UseDevelopmentSettings() will set this to 15 seconds and set a number of magic settings that will help while testing locally. This allows you to restart the app more quickly while testing.

This also means that if your function is running in the cloud on Azure you won't be able to run locally, since the instance in the cloud still has the blob locked. You'll have to either use a different storage account or temporarily stop the job on Azure.

Takeaway

You can find some more in-depth reading on TimerTriggers here and here. I've covered the important parts, but more reading can never hurt.

I've found working with TimerTriggers to be exceptionally easy and have yet to run into any issues with them. The ease of implementation makes them a no-brainer when looking for a method to create scheduled tasks in the cloud.