An Event Based Cron (Scheduled) Job In C#

Wikipedia defines ‘cron’ as

“Cron is a time-based job scheduler in Unix-like computer operating systems. Cron is short for Chronograph. Cron enables users to schedule jobs (commands or shell scripts) to run automatically at a certain time or date.”

At times you will want to schedule an operation to periodically repeat. For instance you may want one of the following periods:

  • every 15 minutes
  • every 0,15,30,45 minutes
  • once per day at 12 PM
  • 4 times per day at 12AM, 6AM, 12PM, 6PM
  • on Saturdays at 12PM
  • the first of every month
  • the first of every other month

In this post, I will explore a C# implementation of a cron enabled object.

Download

Credit Where Credit Is Due

A lot of other people have worked on implementing cron objects in C#. When I initially started this project, I looked for an already developed solution. I found a number of solutions on the web. As I looked over what was available, I found that either features were missing, the API was not straightforward, the code base had maintainability issues, or the library contained too many features. For these reasons, I developed this C# cron library. The following two are the C# cron libraries that were close to what I wanted:

  • Quartz.NET – This is a very complete cron job library. By far it has the most features. In fact I wanted something less complex.
  • NCrontab – This is a simpler library that implements a cron scheduler (no cron job). I almost used this library, however decided to add the extra burden of rolling my own scheduler. Mostly to put myself in a better position to maintain and extend this code going forward.

In the end, my implementation of the cron scheduler is loosely based off of the NCrontab library. I give credit to that group for inspiring this design. I have added original work in adapting the the cron objects & API to how I want to use the cron, adding cron expression tooling that allows schedules to be built programmatically, and developing an event based cron object.

How Do I Use The Code?

This was a key factor in the decision to not leverage already developed software. I felt the workflow with existing software was awkward and/or complex and could be simplified. Here is an example of setting up a cron job using this code:

// Create the cron schedule.
//
CronExpression cronExpression = CronBuilder.CreateHourlyTrigger(new int[] {0, 30});
CronSchedule cronSchedule = CronSchedule.Parse(cronExpression.ToString());
List<CronSchedule> cronSchedules = new List<CronSchedule> { cronSchedule };

// Create the data context for the cron object.
//
CronObjectDataContext dc = new CronObjectDataContext
{
    Object = myObject,
    CronSchedules = cronSchedules,
    LastTrigger = DateTime.MinValue
};

// Create the cron object.
//
CronObject cron = new CronObject(dc);

// Register for events.
//
cron.OnCronTrigger += Cron_OnCronTrigger;

// Start the cron job.
//
cron.Start();

First, a cron schedule is created. The cron schedule in the above example will trigger at the top and bottom of every hour (every 30 minutes). Multiple cron schedules can be attached to a job by adding them to the list. The ‘CronBuilder’ tooling allows cron expression to be created without the underlying knowledge of the cron expression format. For example, in the above case the equivalent cron expression is “0,30 * * * *” or “*/30 * * * *”. I find it much easier to let the tooling create the expressions. There is more info below on cron expressions.

Next the data context is instantiated and properties are set. The ‘Object’ property holds a reference to the job object. As you will see, this object is provided back to you when the cron triggers. In addition the ‘LastTrigger’ date constrains the search space for the next cron trigger. In an application, this is date will usually be persisted to allow for restarts.

The cron object is then created and events are registered. In this case, only the ‘OnCronTrigger’ event is subscribed to. There are other events (more later) that can be subscribed to by the application.

Finally, the cron job is started. The ‘OnCronTrigger’ event will be raised the any time one of the configured cron schedules becomes due. The following code demonstrates an example ‘OnCronTrigger’ event handler:

private void Cron_OnCronTrigger(CronObject cronObject)
{
    IRunnable myObject = cronObject.Object as IRunnable;
    myObject.Run();
}

Here we see that the event provides us with full access to the original CronObject. As a result, I simply cast the object I passed into the data context back into the original type. Then I can call methods (in this case call the ‘Run’ method) on that object.

Cron Expression Format

The cron expression is a string with 5 space-separated ‘entries’ as shown graphically below:

image 

  • Entry #1 – Filters the minutes (0-59) that are allowed to generate a trigger.
  • Entry #2 – Filters the hours (0-23).
  • Entry #3 – Filters the days (1-31).
  • Entry #4 – Filters the months (1-12).
  • Entry #5 – Filters the days of the week (0-6, 0=Sunday).

Each entry can hold a string (no spaces allowed) of characters. Some examples of valid entries are the following:

  • “*” – Any valid value can cause a trigger. The validity of the values is checked to ensure that the resulting date / time exists. For example the day of the month is checked against the current month & year to ensure it is a valid day (including leap years).
  • “3” – Only a single digit (in this case a three) can cause a trigger.
  • “0,30” – A list of digits (in this case a zero and a thirty) can cause a trigger.
  • “1-10” – A range of digits (in this case the numbers 1 through 10).
  • “*/2” – Only even digits in the valid range.
  • “0-30/10” – Only 0,10,20,30 will cause triggers.

This provides quite a bit of flexibility when you combine all five entries to create a cron expression. For example:

  • “0 * * * *” – Triggered at the top of every hour.
  • “0 0 * * *” – Triggered at 12 AM every day.
  • “0 0 1 * *” – Triggered at 12 AM the 1st of every month.
  • “0 0 1 1 *” – Triggered at 12 AM the 1st of January of every year.
  • “0 0 1 1 0” – Triggered at 12 AM the 1st of January when it falls on a Sunday.
  • “*/30 * * * *” – Triggered at the top and bottom of the hour.
  • “0 0 */2 * *” – Triggered every other day at 12 AM.
  • “0 3 * * 6” – Triggered at 3 AM every Saturday.

As you can see these expressions are powerful. With a bit of tooling (see below) they become easier (IMO) to write.

The Cron Scheduler

An instance of the ‘CronSchedule’ encapsulates the details of evaluating the cron expression and determines the next trigger date/time. This object has a private constructor and is instantiated by using overloaded ‘Parse’ methods. The ‘Parse’ methods simply take a cron expression as a parameter.

imageThe ‘GetNext’ and ‘GetAll’ methods generate the next date / time that is a trigger for the cron expression.  The private members contain instances of entry objects that help determine the next value based upon an entry filter.

image I am not going to go into the details of the code to determine the next cron trigger. The algorithm is similar to the algorithms used in other cron libraries. The source code is provided so the interested reader can dig in.

It helps me to visualize the algorithm as a date/time odometer with a spinning wheels for year, month, day, hour and minute (shown left to right below). Each wheel has the allowed values which are filtered from all possible values by applying the cron expression.

image

The algorithm starts at one minute beyond the last triggered date/time. It then ‘spins’ the minute until the next valid minute is found. If it wraps around to the beginning of the list, the hour element is incremented. Then the hour element is ‘spun’ to find the next valid minute. This process continues. Validation is embedded in the code to ensure that the month, day, year combination results in a valid day (accounting for months having different number of days…including leap year).

Cron Expression Tooling

Although the cron expressions are powerful, I find using them directly first starts with me finding my cheat sheet reminding me of the format. I created some tooling to help with this effort.

imageThe  ‘CronBuilder’ object is a static class with methods for creating a number of standard cron expressions. This builder class can continue to be expanded. Here is an example of one of the builder methods:

public static CronExpression CreateDailyTrigger(
            int[] triggerHours,
            DayOfWeek[] daysOfWeekFilter)
{
    CronExpression cronExpression = new CronExpression
        {
            Minutes = "0",
            Hours = triggerHours.ConvertArrayToString(),
            Days = "*",
            Months = "*",
            DaysOfWeek = daysOfWeekFilter.ConvertArrayToString()
        };

    return cronExpression;
}

This method creates a cron expression for a trigger that occurs daily at the provided list of hours and only on the days of the week provided.

Event Based Cron Object

The purpose of the cron is to have a job scheduled and run periodically. I wanted to create a cron that was job agnostic. This allows the code base to be reused in different situations. When you create an instance of the ‘CronObject’ class, you inject in the following data context:

image This object is simply a class with a bunch of properties that define the inputs that are important to the ‘CronObject’ class. This allows the CTOR to remain simple (one parameter) even as the injected dependency list may continue to grow. The data context above has three properties.

  • CronSchedules – This is a list of ‘CronSchedule’ objects. Each job can have multiple triggers. For instance you can have a hourly trigger during the week days and a twice a day trigger for week end days.
  • LastTrigger – This is the last time the job was triggered. This allows you to restart a cron job without past cron dates being re-triggered.
  • Object – This object is represents your job. It can be an instance of any class. This object is accessible at as a parameter from any event that is raised by the ‘CronObject’ class. This could be an object that implements an IRunnable interface for example.

The public elements for the ‘CronObject’ are shown in the following class diagram:

image

As shown above, the ‘CronObject’ communicates to consumers via events. Probably the most important event is the ‘OnCronTrigger’ event that is raised when ever the current date/time matches the next cron trigger date/time.

The ‘CronObject’ class itself has only two public properties: Start and Stop. This is a very simple interface. These methods obviously start and stop the cron job object. The code for these methods is shown below:

public bool Start()
{
    lock (_startStopLock)
    {
        // Can't start if already started.
        //
        if (_isStarted)
        {
            return false;
        }
        _isStarted = true;
        _isStopRequested = false;

        // This is a long running process. Need to run on a thread
        //    outside the thread pool.
        //
        _thread = new Thread(ThreadRoutine);
        _thread.Start();
    }

    // Raise the started event.
    //
    if(OnStarted != null)
    {
        OnStarted(this);
    }

    return true;
}


public bool Stop()
{
    lock (_startStopLock)
    {
        // Can't stop if not started.
        //
        if (!_isStarted)
        {
            return false;
        }
        _isStarted = false;
        _isStopRequested = true;

        // Signal the thread to wake up early
        //
        _wh.Set();

        // Wait for the thread to join.
        //
        if(!_thread.Join(5000))
        {
            _thread.Abort();

            // Raise the thread abort event.
            //
            if(OnThreadAbort != null)
            {
                OnThreadAbort(this);
            }
        }
    }

    // Raise the stopped event.
    //
    if(OnStopped != null)
    {
        OnStopped(this);
    }
    return true;
}

The code is pretty simple. Even though these methods are called in the same thread, synchronization is used to prevent a new call from occurring before the other has finished. That could lead to some issues where the object state is half baked. Notice that the ‘Start’ method spins up a new ‘System.Thread’ to perform the cron work.

The thread routine that does the cron job work of raising the ‘OnCronTrigger’ event is shown below. It uses an ‘EventWaitHandle’ object to allow the thread to pause until the next cron trigger. This object is also used in the above ‘Stop’ method to wake up the thread so it can be stopped.

private readonly EventWaitHandle _wh = new AutoResetEvent(false);
private void ThreadRoutine()
{
    // Continue until stop is requested.
    //
    while(!_isStopRequested)
    {
        // Determine the next cron trigger
        //
        DetermineNextCronTrigger(out _nextCronTrigger);

        TimeSpan sleepSpan = _nextCronTrigger - DateTime.Now;
        if(sleepSpan.TotalMilliseconds < 0)
        {
            // Next trigger is in the past. Trigger the right away.
            //
            sleepSpan = new TimeSpan(0, 0, 0, 0, 50);
        }

        // Wait here for the timespan or until I am triggered
        //    to wake up.
        //
        if(!_wh.WaitOne(sleepSpan))
        {
            // Timespan is up...raise the trigger event
            //
            if(OnCronTrigger != null)
            {
                OnCronTrigger(this);
            }

            // Update the last trigger time.
            //
            _cronObjectDataContext.LastTrigger = DateTime.Now;
        }
    }
}

First the next trigger date/time is determined. This may occur in the past and in this case the ‘OnCronTrigger’ event is raised immediately. Otherwise, the thread sleeps until the next trigger. After the ‘OnCronTrigger’ event is raised, the ‘LastTrigger’ property is updated. This can be used in the ‘OnStopped’ event to serialize out this date for restarts.

The following method determines the next trigger date/time from the cron schedules that were supplied in the constructor:

private void DetermineNextCronTrigger(out DateTime nextTrigger)
{
    nextTrigger = DateTime.MaxValue;
    foreach (CronSchedule cronSchedule in _cronObjectDataContext.CronSchedules)
    {
        DateTime thisTrigger;
        if(cronSchedule.GetNext(LastTigger, out thisTrigger))
        {
            if (thisTrigger < nextTrigger)
            {
                nextTrigger = thisTrigger;
            }
        }
    }
}

This simply loops over all the cron schedules to determine the next trigger.

Testing

There is a suite of automated unit tests in the project. Test coverage is 100% (see NCoverExplorer screen shot below) at this point and I believe test quality is high.

imageHere again, a big nod to the NCrontab group. I had a number of tests that I had generated for the cron expression cases and this was supplemented by adding the NCrontab test cases.

Summary

Being able to schedule a process or job to occur on a periodic basis is a very common development task. This library provides a clean and easy way to accomplish this behavior. Please feel free to download and use. If you have suggestions for improvements I will attempt to maintain.

Tags:,
Comments
  1. Chendur
  2. yucel
    • rcravens
      • JEME
      • Thomas
  3. Shariq
    • rcravens
  4. Shariq
    • rcravens
  5. Anamika
    • rcravens
  6. Ted Krumbach
  7. Lars
    • Lars
  8. Selan
    • Don Perrett
  9. Greg Jessup
    • Don Perrett
  10. larrybud
  11. Sowjanya
    • rcravens
    • Robb Sadler
  12. Michael Kempe
  13. joe
  14. Mark
  15. Michael
  16. Jacob Alley

Leave a Reply to JEME Cancel reply

Your email address will not be published. Required fields are marked *

*