An Event Based Cron (Scheduled) Job In C#
“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.
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:
- 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.
The ‘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.
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.
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.
The ‘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:
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:
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.
Here 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.
Waited for years to find an article. Thanks for posting this. Will try to implement it.
Thank you for this valuable article.
* CronObject.cs throws an error in line
if(!_wh.WaitOne(sleepSpan))
when the timespan.Miliseconds is larger than Int32.MaxValue(=2147483647, which is about 25 days).
when I search on google someone(the link is below) , he uses
if (!_wh.WaitOne(sleepSpan.TotalMilliseconds < int.MaxValue ? (int)sleepSpan.TotalMilliseconds : int.MaxValue))
is this the right way? I don't know 🙂
http://code.google.com/p/sambapos/source/browse/Samba.Infrastructure/Cron/CronObject.cs?spec=svn8dccd23a09a00b4fee2c681b209050d05e80fe9e&r=8dccd23a09a00b4fee2c681b209050d05e80fe9e
Apparently my test cases didn’t cover this size of a delay. The solution provided looks like a good solution. It should be noted that the solution caps the maximum cron to int32.maxvalue.
Bob
Which is evident of that Test Coverage can only ever tell you when you lack tests for sure, It can never tell you when you have enough…
This is because it only tests that each line in your code is passed in a test, but not if it’s passed with edge case values etc… 😉
Hi Bob, first of all thanks for the great software!! I also run into the same Problem as yucel. But on my System the exception is triggered whenever the timespan is longer than about two minutes. Actually I’m not 100% sure why, but I’m using some Special ARM Computers so I guess they even use less than 32bit for an int.
Anyway, I found a solution to fix that, maybe you want to add it in your software. In CronObject.cs in the function ThreadRoutine I added the following try-catch Statement:
try {
// 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;
}
} catch(ArgumentOutOfRangeException e) {
//If timespan is too big sleep for one minute and retry
_wh.WaitOne(new TimeSpan(0, 0, 1, 0, 0));
}
Regards
Thomas
First of all nice article.
I need to know if their is a way to get the last time the schedule had triggered before some datetime value. Is it possible?
Actually the problem is i need to manage an event which has a starttime cron expression and an endtime cron expression. Now when ever the application is started, it first should check that if the current time is greater than the lastendtime (this is what i need) and also the current time is greater than the laststarttime AND the lastendtime should be greater than the laststarttime. If it is then it should be in the event action phase. It should do what it should because the current time is valid between the two time window. But if it is not then it wont do anything but wait for the next starttime.
Hi Shariq,
This will require some custom code. I would start by looking at the ‘DetermineNextCronTrigger’ method in the ‘CronObject’ class. You can probably extract this out of this class and provide a delegate to allow a custom implementation. Any way, that is where I would start.
Hope this helps.
Bob
OK i found that i can use Quartz.NET for this. I just wanted to parse Cron Expressions and determine the schedule times that is why i found your library more intrusive. But then i found that the expressions you are using are not matching the Quartz.NET implementation, actually i am working on another open source project of mine, So i dont want to confuse the configurator user to use another type of expression. Your library just differed from Quratz.NET in the way it is parsing Cron expressions. Any ways i like the simplicity of your lightweight library.
Glad you found something that works for you. After all, that is what it is all about!
Hi, Is there any way to have a GUI for the schedule details and generate the Cron expression dynamically.
Sure. That is not in this code, you would have to develop that or find one.
Thanks for the cron expression syntax explanation. I find myself back to this page every time I need to create a new cron syntax.
Hi Bob,
Good job there.
You have a small bug:
CronSchedule.cs
public bool GetNext(DateTime start, DateTime end, out DateTime next)
The last line should be
return GetNext(new DateTime(baseYear, month, day, 23, 59, 0, 0), end, out next);
Heres some test code for you:
var schedule = new CronSchedule(“1”, “1”, “1”, “1”, “1”);
var next = new DateTime();
var success = schedule.GetNext(DateTime.Now, DateTime.Now.AddMonths(3), out next);
next will be in 2018 and success wil be true
On more thing, everytime GetNext is called recursively, a minute is skipped on this line
DateTime baseSearch = start.AddMinutes(1.0);
this can if you’re very unlucky, cause you to miss a hit
Hello,
I have cron syntaks “0 18 * * 5”, but I don’t want it to trigger first time when I declare CronObject. How can I solve this?
I added a variable bool ExecuteNow = false; to my Class that I use to call the CronObject. Then I added a variable DateTime BaseDateTime. The return value of this variable is dependent upon the ExecuteNow value. If the value of ExecuteNow is false, then the BaseDateTime is DateTime.Now. If it is true, then BaseDateTime is DateTime.MinVal. Then I set the CronObjectDataContext.LastTrigger to BaseDateTime before calling new CronObject(CronObjectDataContext). This will allow you to control the initial firing of the event.
So far love the class.
I would make one comment. When you are creating your CronObjectDataContext…setting LastTrigger = DateTime.MinValue forces the object to be called immediately. I prefer that everything follow a schedule so I have set my LastTrigger = DateTime.Now,
-Greg
See my response to Selan
I’m missing something, what is “myObject” in
CronObjectDataContext dc = new CronObjectDataContext
{
Object = myObject,
CronSchedules = cronSchedules,
LastTrigger = DateTime.MinValue
};
Hi,
I tried Quartz but the process stops when there is no traffic for 20 minutes. Does cron runs like windows service irrespective of IIS time outs ?
This article is provides a .net class that you can use to create a windows service. I sometimes have what I call ‘service URLs’ that I configure the crontab daemon (equivalent to windows task/schedule manager) to call. Otherwise, if you have an active site you can use site visits to trigger the background work. This is what WordPress does for example.
IIS will shut down your process after a period of time, as it is not expecting you to have stuff running in the background, but there are times when this makes a lot of sense. There is a good article by Rick Strahl that covers how to do this: http://weblog.west-wind.com/posts/2013/Oct/02/Use-IIS-Application-Initialization-for-keeping-ASPNET-Apps-alive
HTH…
Hi,
great tool. But how can I schedule a trigger for ‘every last day in month’? Is this possible?
Cheers.
As far as I understand, when a task is started, the CronObject starts a thread that continually checks the time and if the time matches the trigger then it runs the OnTrigger event?
Doesn’t this mean that each task has its own thread for checking the time whereas it would be more efficient to have just one thread checking the time and running each task when the time is right? This way additional threads are only started when the task is scheduled to run, rather than taking up resources all the time.
Excellent stuff! Thanks.
This saved me a lot of time when converting an iCal RRULE, which I parsed using DDay.iCal, to a crontab schedule. After a good few hours searching and getting frustrated I happened apon your library and love it. More people should know about your cronbuilder class.
Cheers
Hi thanks fro the code
I’m finding that WaitOne is accurate on some computers over 24hour periods
This causes the job to fire a few seconds late (fine), but also i’ve seen it fire up to 30 seconds early. This causes the job to run twice on the day, as the next trigger is set and it is all but 30 seconds away.
I’m guessing some kind of clock issue on the computer – Any thoughts on a more stable approach?
I have a job that checks to see if certain files are in a folder, and if they are then they get processed. I need the job to check if they are their every couple of hours, but once they have been processed, I do not need it to run again for that day. Is there a way to accomplish this?