Introduction
I'm currently working on VFS, a virtual file system. For running transfers, VFS internally maintains locks that do have an expiration time. Accordingly, I found myself in need for a job scheduling mechanism in order to properly release expired locks. I looked around for a few alternatives, but eventually ended up writing my own lightweight version. The code presented here can be found within VFS, but also has its own repository at CodePlex.
Quick Feature Overview
- Simple scheduling and callback mechanisms
- Silverlight compatible
- Lightweight
- Fluent API
- Detection of system time changes with optional rescheduling
- Optional forwarding of exceptions during job execution
What Does it Do?
Basically, the library allows you to create a job, and submit that job to a scheduler, along with a callback action. This callback action is invoked as soon as (or every time) the job is due.
Before going into the details, here’s a first code snippet that creates a simple Job
that is supposed to run repeatedly (every 1.5 seconds) for a minute. Once the job is created, it is submitted to a Scheduler
instance which processes the job and makes sure the submitted callback action is being invoked every time the job is due:
private Scheduler scheduler = new Scheduler();
public void RunJob()
{
DateTime startTime = DateTime.Now.AddSeconds(5);
DateTime endTime = startTime.AddSeconds(60);
Job consoleJob = new Job();
consoleJob.Run.From(startTime)
.Every.Seconds(1.5)
.Until(endTime);
scheduler.SubmitJob(consoleJob, j => Console.Out.WriteLine("hello world"));
}
Silverlight
The project provides class libraries for .NET 3.5 and Silverlight 3, along with a Silverlight sample application that shows how to add scheduling functionality to your SL application with just a few lines of code. (Note that the Silverlight projects come as an individual solution, which can be found in the Silverlight sub folder of the download.)
While long-term scheduling isn't probably something you need to do in a Silverlight application, the scheduler simplifies the management of periodic jobs, such as polling a server for updates. Below is a snippet from the Silverlight sample application. This job starts immediately, and runs indefinitely with an interval of 2 seconds:
private void CreateJob2()
{
Job<int> job = new Job<int>("Job 2");
job.Data = 0;
job.Run.Every.Seconds(2);
scheduler.SubmitJob(job, LogJobExecution);
}
private void LogJobExecution(Job<int> job, int data)
{
}
Table Of Contents
Jobs
A job is a simple configuration item for the scheduler. There’s two built-in job types: Job
and Job<T>
. The only difference between the two is that the latter provides a Data
property which allows you to attach state information directly to the job and have it delivered back to you when the job runs.
Creating Jobs
Creating a job is pretty easy, and can be configured through a fluent interface (optional, you can also set the properties the traditional way). Jobs may run once, several times, or indefinitely – you can configure intervals, an optional expiration time, or a maximum number of executions. Here’s a few configuration samples:
DateTime startTime = DateTime.Now.AddSeconds(5);
Job job = new Job();
job.Run.From(startTime)
.Once();
Job job = new Job();
job.Run.Every.Hours(1.5);
Job<FileInfo> job = new Job<FileInfo>();
job.Data = new FileInfo(@"C:\readme.txt");
TimeSpan interval = TimeSpan.FromSeconds(1.5);
job.Run.Every.TimeSpan(interval).Times(5);
Job job = new Job();
DateTime expiration = DateTime.Now.AddMinutes(5);
job.Run.Every.Seconds(1.5).Until(expiration);
The Scheduler Class
Jobs are being submitted to the Scheduler
class, which provides a very simple API. Most methods should be pretty self-explanatory, and they are well documented.
Callback Actions
If you add a Job
to the Scheduler
through one of the SubmitJob
overloads, you have to submit a callback action that is being invoked whenever the job runs. Depending on whether you use Job
or Job<T>
, a different callback delegate may be used:
Job
invokes a callback action of type Action<Job>
Job<T>
invokes a callback action of type Action<Job<T>, T>
Here’s a sample that uses two different callback methods:
class Program
{
private static Scheduler scheduler = new Scheduler();
static void Main(string[] args)
{
Job simple = new Job("simple");
simple.Run.From(DateTime.Now.AddSeconds(3)).Once();
Job<int> generic = new Job<int>("generic") {Data = 123};
generic.StartAt(DateTime.Now.AddSeconds(1)).Run.Once();
scheduler.SubmitJob(simple, HandleSimpleJob);
scheduler.SubmitJob(generic, HandleGenericJob);
Console.ReadLine();
}
private static void HandleGenericJob(Job job, int data)
{
Console.Out.WriteLine("Job '{0}' submitted data: {1}", job.Id, data);
}
private static void HandleSimpleJob(Job job)
{
Console.Out.WriteLine("Simple job '{0}' was executed.", job.Id);
}
}
The sample above produces the following output on the console:
Cancelling or Pausing a Job
If you want to cancel or pause a job, you can do this either via the Scheduler
class, or directly on the Job
instance that is submitted to you whenever the job runs. Note that cancelling the job is a terminal operation – a cancelled job cannot be resumed.
private void MyJobCallbackAction(Job job)
{
job.Pause();
job.Resume();
job.Cancel();
}
Actually, doing these operations directly via the Job
class is the preferred mechanism – if you use the scheduler’s methods, the scheduler needs to lock its internal list and search for the job itself, which costs you processing power.
Exception Handling
An exception that occurs during the execution of a job does not affect the scheduler – all jobs are being executed on worker threads taken from the .NET thread pool. However, in order not to miss any exceptions, you can instruct the Scheduler
class to supervise executing jobs, and forward any unhandled exceptions to a single exception handling routine. All you need to do is registering a callback action to the JobExceptionHandler
of the Scheduler
class:
private Scheduler scheduler;
private void InitScheduler()
{
scheduler = new Scheduler();
scheduler.JobExceptionHandler = LogJobException;
}
private void LogJobException(Job job, Exception exception)
{
string msg = "Exception occurred while executing job {0}: {1}";
msg = String.Format(msg, job.JobId, exception.ToString());
MyLogger.LogError(msg);
}
Performance
The library could surely be tweaked, but it performs pretty well. Running 10000 jobs, each with an interval of 100 ms (that’s around 100 jobs per millisecond) keeps the CPU busy (at around 25% on my machine), but neither eats away your memory nor freezes the PC. Bigger intervals aren’t a problem at all because the scheduler is sleeping most of the time.
Reacting to System Time Change Events
Assume the following:
- The current time is 21:00, your job is scheduled to run at 22:00
- The user changes the computer’s time to 21:30
Now, depending on your needs, you might want the scheduler to adjust itself based on two strategies:
- A: If you want the job to run at a fixed time (22:00), you expect the job to run in 30 minutes. This is the scheduler’s default behavior.
- B: If you want the job to run based on relative times, you still expect the job to run in an hour, so the execution time would have to be change to 22:30.
In order not to miss such an event, the scheduler performs a quick self test with a fixed configurable interval from time to time - even if no jobs are up for execution. Per default, the scheduler’s SelfTestInterval
property is set to two minutes, but this can be configured should you need more (or less) accurate re-scheduling.
If you want the scheduler to just readjust its internal schedule and keep fixed times (A), you don’t have to do anything. However, if you want the scheduler to reschedule its jobs if a system time change was detected (B), you can do by setting the SystemTimeChangeRescheduling
property of the Scheduler
class, which takes an enum
value of type ReschedulingStrategy
:
As you can see, you can choose not to reschedule at all (default), only reschedule the next execution time (the next time the job runs), or even shift the expiration time of your jobs.
Fixed vs. Relative Rescheduling Depending on Jobs
What if you have a few jobs that should run on a fixed time, while others should be rescheduled?
I decided against making the API more complicated by allowing jobs to be individually configured. As an alternative, I’d suggest to just use two scheduler classes with individual configurations. You could even write a wrapper class that just maintains two schedulers, and forwards all job submissions to the correct one. Here’s a quick but working implementation:
public class MyScheduler
{
private readonly Scheduler fixedSchedules = new Scheduler();
private readonly Scheduler relativeSchedules = new Scheduler();
public MyScheduler()
{
var strategy = ReschedulingStrategy.RescheduleNextExecution;
relativeSchedules.SystemTimeChangeRescheduling = strategy;
}
public void SubmitJob(Job job, Action<Job> callback, bool hasFixedSchedule)
{
if(hasFixedSchedule)
{
fixedSchedules.SubmitJob(job, callback);
}
else
{
relativeSchedules.SubmitJob(job, callback);
}
}
}
Persisting Jobs
Persisting jobs is not part of the library, but could be done by subclassing the Scheduler
class. The class gives you protected access to all its internals, including its job list. Job
is a very simple component, so it should be fairly easy to store and reload all jobs.
However, from an architectural point of view, I’d probably not persist any jobs at all, but rather recreate them during initialization based on persisted business data. A job does execute in a given context, because of “something”. Let’s assume this “something” is a reminder flag in a calendar application:
- User sets a reminder on a calendar entry.
- Calendar application updates database record of the calendar entry, then creates a job with the scheduler.
- User turns off application – the scheduler and running jobs are being disposed.
- User restarts application.
- The application retrieves all reminders from the database and schedules jobs for them.
This approach is simple, and clearly defines the responsibilities of the components in your application. It also minimizes dependencies on the scheduling system.
Implementation Notes
Creating a Fluent Interface
The library provides a fluent API that allows you to chain configuration settings:
Job myJob = new Job();
myJob.Run.From(startTime)
.Every.Seconds(1.5)
.Until(expiration);
The fluent API is optional – a job can also be configured via traditional properties setters. However, the corresponding declaration is more verbose:
Job myJob = new Job
{
Loops = null,
StartTime = startTime,
Interval = TimeSpan.FromSeconds(1.5),
ExpirationTime = expiration
};
Basically, the implementation of the fluent API is very simple:
- The
Run
property returns an instance of a helper class called JobSchedule
. This helper class provides methods such as From
, Until
, Every
, or Once
. - All methods of the
JobSchedule
class have a return value of JobSchedule
again, which allows the developer to chain the operations as in the sample above.
Below is a part of JobSchedule
’s implementation. You can see that the class receives an instance of type Job
, and just sets the properties on this class.
public class JobSchedule
{
private readonly Job job;
public JobSchedule(Job job)
{
this.job = job;
}
public JobSchedule From(DateTimeOffset startTime)
{
job.StartTime = startTime;
return this;
}
...
}
DateTime, DateTimeOffset, and SystemTime
SystemTime Pattern
With a scheduler, everything is about time. However, time-related code is very hard to test, which is why the scheduler does not directly access the current system time, but makes use of Ayende’s SystemTime. This is an awesome pattern, and I highly recommend using it whenever timestamps play an important role in code.
DateTimeOffset vs. DateTime
You might have noticed that usually, the code works with DateTimeOffset
rather then the commonly known DateTime
struct. DateTimeOffset
is basically an alternative to DateTime
, but operates on UTC which makes it an ideal candidate for most time-related scenarios. You can read more about it at MSDN or at the BCL team blog.
However, you can even still use DateTime
in your code if you feel more comfortable with it:
Job job = new Job();
DateTimeOffset expDate1 = DateTimeOffset.Now.AddHours(2);
DateTime expDate2 = DateTime.Now.AddHours(2);
job.ExpirationTime = expDate1;
job.ExpirationTime = expDate2;
Job List, Timer and Pooled Execution
Job List
Internally, the Scheduler
class uses a very simple mechanism: All jobs are being cached in a sorted list, which allows the scheduler to easily determine the next execution time. If a new job is being submitted, the scheduler only has to check whether the new job runs before the next scheduled job or not. If yes, the scheduler adjusts its next execution. If no, the job is just added to the end of the list, and a flag is set that reminds the scheduler to reorder the list once the next job was done. This approach has a big advantage: Even if many jobs are being submitted, reordering only takes place once the next job runs.
public void SubmitJob(Job job, Action<Job> callback)
{
JobContext context = new JobContext(job, callback);
lock(syncRoot)
{
if (NextExecution == null || context.NextExecution <= NextExecution.Value)
{
jobs.Insert(0, context);
if (NextExecution == null || NextExecution.Value.Subtract(SystemTime.Now())
.TotalMilliseconds > MinJobInterval)
{
Reschedule();
}
}
else
{
jobs.Add(context);
isSorted = false;
}
}
}
Timer and Rescheduling
In order to trigger the execution, a single Timer
is used to wake up the scheduler once jobs are due. The timer is set to one of those values:
- If the scheduler has no jobs at all, the timer is disabled.
- If jobs are pending, the next interval is the execution time of the next pending job.
- If the self test interval is lower than the execution time of the next job, the scheduler will run an evaluation at this time (which ensures that a changed system time does not cause the scheduler to oversleep).
The whole timer logic is encapsulated in the Reschedule
method:
private void Reschedule()
{
if(jobs.Count == 0)
{
NextExecution = null;
timer.Change(Timeout.Infinite, Timeout.Infinite);
}
else
{
var executionTime = jobs[0].NextExecution;
DateTimeOffset now = SystemTime.Now();
TimeSpan delay = executionTime.Value.Subtract(now);
long dueTime = Math.Max(MinJobInterval, (long)delay.TotalMilliseconds);
dueTime = Math.Min(dueTime, SelfTestInterval);
NextExecution = SystemTime.Now().AddMilliseconds(dueTime);
timer.Change(dueTime, Timeout.Infinite);
}
}
Job Execution
“Executing” a job basically means invoking the callback action that was submitted along with the job. This always happens asynchronously through the .NET thread pool:
public void ExecuteAsync()
{
if (...)
{
ThreadPool.QueueUserWorkItem(s => CallbackAction(ManagedJob));
}
UpdateState();
}
Conclusion
This is a neat little helper library, and its small footprint makes it a viable alternative to handling timers yourself even for small applications. Happy coding! :)