Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C#
Article

.NET Scheduled Timer

Rate me:
Please Sign up or sign in to vote.
4.89/5 (139 votes)
16 Sep 2005CPOL15 min read 1.1M   38.4K   360   317
A timer that easily supports absolute schedules like run at 4:00 AM every day or at 5:00 PM on Fridays..

Introduction

In most of the projects I've worked on, there has usually been a need for events to be triggered at various absolute times. For example, every Friday at 5:00 PM, or every hour at 15 after. The .NET native timers only support a relative timing interface. You can specify how long you need to wait from the current time. It's easy to calculate for simple events, but the code can become convoluted once you begin dealing with more complicated schedules. This is an attempt to write a simple set of scheduling primitives to simplify building more complicated schedules.

Handling human oriented schedules is just one of the goals for this timer. Automatic recovery, event logging, and resolving concurrency issues are also goals.

Background

Scheduling batch operations is a common yet often overlooked programming task. Many applications have the need to send out batches of emails, or generate reports at fixed times. The native timers that come with .NET are designed to operate like a hardware timer, going off at a fixed rate from the time they were started. This is fine for many applications, but can be inconvenient when you have to schedule events at a fixed time each day, or at alternating intervals, let alone trying to manage something which only occurs on weekdays. After having to write custom logic to handle these operations, I figured a more general solution was in order.

Creating a timer and scheduling events is just one part of the problem that has to be solved. Every automatic process needs someone to maintain it and restart it when it stops, rerun it when events are skipped, and debug it when it just won't do what it is supposed to. The great thing about these processes is that they remove the human element from making sure that these things are taken care of. The downside is that when they fail they can go unnoticed for untold periods of time. So a good timer not only needs to be able to handle the craziest schedule that you can throw at it, it also needs to be extremely failure resistant, and provide a means to notify its operators when things go wrong.

Error handling comes in two forms, first event handlers throwing exceptions shouldn't be able to shut down the process. Second the timer needs a way to recover from system down time like power outages and similar failures. Both of these operations should be managed separately from the events themselves.

Some of the .NET native timers have properties and complications that are not clearly documented. For example, the System.Threading timer uses threads from the thread pool to run events on. This means that if an event handler runs too long, then other handlers can start up while the first one is running on a separate thread. If you haven't explicitly made sure your process is thread safe, then you can have some really difficult to track down errors. Our timer should allow this to be controlled easily from the timer, rather than forcing the event handler to deal with this, or creating a new object to deal with synchronization issues.

When I originally wrote this timer, I used the System.Threading timer as a model. This had the limit of having a single event associated with it. Since then, I have had many requests to add support for scheduling multiple events off the same timer. At first I didn't really like the idea because it made the whole process more complicated, and I really prefer simple operations. However, after writing a few consumers of this timer, I realized how much easier it is to code against. You only have one object to start and stop, so I've warmed up to the idea since it makes the clients simpler.

One of my favorite features of the .NET framework is the BeginInvoke/EndInvoke operations on each delegate. I thought it would be extremely cool to just provide a method similar to those on a delegate with a schedule, which just ran a function on a specific schedule. Or in general something like delegate.EventInvoke(EventSource, function parameters here);. Unfortunately the delegate operation is too closely integrated into the various language compilers to handle things like this. So, one of the things I want to do with this timer, is mimic this operation as closely as I possibly can.

So now we have the requirements:

  • Handle various human oriented schedules along with basic timer operations like the Windows timers.
  • Operate similarly to the native System.Threading Windows timer.
  • Events should be signaled as accurately as possible, ideally within milliseconds of their scheduled time.
  • Be able to operate in headless service processes like the System.Threading Windows timers.
  • Be more robust under error conditions. An unhandled exception from an event should not trigger the AppDomain unhandled exception handler.
  • Provide a means for any errors to be reported.
  • Provide an optional automatic recovery method for cases when the timer was shut down and events were missed.
  • Provide more explicit options to prevent concurrent event operations.
  • Each timer will handle multiple events, each with its own separate schedule.
  • Timers will not be limited to events, but provide a means to execute arbitrary functions without creating wrapper functions.

First Attempts

This section details some of the timer methods I have seen and some of my early attempts along with the motivations for the current design. I've seen many processes running under the NT task scheduler. In many cases this has been either a script or a regular executable running as a task. This has the advantage of being integrated into the operating system and easy to setup and configure, with a built in UI. Management is more difficult, error handling and recovery is only as good as the process being executed. Further each separate schedule is tied to a specific process. In many cases there were VB and DCOM applications scheduled this way, which required an interactive user logged into the system for anything to run correctly, even though this was due more to a lack of configuration know-how than anything else.

For those of us with access to a SQL Server, the SQL Server agent and DTS provides an excellent platform for scheduling operations. It has both a UI and a programmatic interface for scheduling operations. It can run processes or execute SQL statements, and it includes logging, error recovery workflow, and notification management. If you have access to this, then there are a lot of strong reasons to use these tools. The one real downside is that your process will be competing for resources with the SQL Server. SQL Server runs best when it has an entire system dedicated to itself, so it should really be limited to operations that run much more efficiently on the same system, or those that don't consume many resources.

The third common scheduling method is to create a Windows service. This provides the most flexibility with the fewest built in features. All the framework provides you with is an installer, and the ability to start and stop your service. All the error handling, reporting, configuration and other details are left up to you. Services which shut down as soon as an exception is thrown, or hang when someone tries to stop the service occur all too often. Another common problem is that the batch actions are not restartable, and after a failure, manual actions need to be taken to get systems back into the correct state before the service can be restarted.

On top of the minimal support from the service infrastructure, the only timers available are the .NET pulse type timers. I have typically seen them used to create a series of events at a fixed rate, say every 5 minutes. Then a switch statement, or a lookup is done on every event comparing the batch execution time to the current time and if it is within the execution window, then the batch process is run. Some of the problems with this approach is that it only guarantees that the batch will fire sometime within the required time. So if your pulse rate is every 15 minutes and you schedule something for 12:00, then it will run sometime between 12:00 and 12:15 depending on when the service was started. You can compensate by making the pulse frequency faster. However, this increases the risk that the event can be missed, if a higher priority thread is running for the entire pulse period for example.

To handle these possibilities, we need a timer that knows when it misses a beat. So even if it fires late it won't drop any scheduled events. The timer can do this by maintaining an event history. It records the last time it fired and finds all the events that occurred between then and now. It fires all of them before it waits until the next event needs to fire. Not only does this prevent missing events while the timer is running, it can help the timer recover from outages if the state is persisted somewhere.

This timer should make the minimal possible assumption about each of the handlers that is hooked up to it. This means that every event should be wrapped in an exception handler. An error event is provided for clients to hook into and handle as they need to.

Preventing concurrent event operations: if you use the System.Timers.Timer class, each of your events will occur on a thread from the thread pool. Many times, I've seen applications use this timer, and everything works fine in development, but the reports and data processed are all screwed up in production. This is because the default settings for this timer allow events to occur concurrently on different threads. What ends up happening is that the batch process takes longer in production and you end up having multiple batch processes running at the same time. The timer allows you to provide a SynchronizingObject to prevent events from occurring concurrently. This keeps them from executing at the same time, but doesn't let us control how these duplicate events are handled. This depends on the event we are dealing with. The correct solution might be to let them run concurrently, skip the overlapping event completely, or queue the overlapping event to run as soon as the current event finishes running.

When I originally wrote this timer, I provided a simple event driven interface to set a single event with a single schedule per timer since that is how the .NET timers were setup. After several requests, I've added methods to schedule multiple independent events with different schedules on the same timer. This allows the start and stop methods on a single timer to control all the events.

Using the timer

Schedules

IScheduledItem

Each schedule needs to provide two similar operations in order to be scheduled. First, return the next time they will fire after a particular time. This is used by the timer to figure out how long to wait before the next event. Second, find all the events that are fired in a particular time interval. This is used to call all the proper events when the timer goes off. This is represented in the IScheduledItem interface.

C#
public  interface  IScheduledItem
{
  void  AddEventsInInterval(DateTime  Begin,
       DateTime  End,  ArrayList  List);
  DateTime  NextRunTime(DateTime  time);
}

SimpleInterval

The SimpleInterval class models a simple pulse timer. Its constructor takes two parameters, an absolute start time and a TimeSpan for the interval between events. It is more general than the ScheduledTime object because any interval can be scheduled.

ScheduledTime

The ScheduledTime class models a timer that goes off at one of several fixed rates like monthly, daily, weekly or hourly. It makes it easier to schedule things on a more human oriented rate, like at 6:00 AM every Thursday.

SingleEvent

The SingleEvent class models a timer which fires once at a fixed time and then is inactive.

EventQueue

EventQueue takes several schedules and provides the union of them. So if you need to execute an event every day at 5:00 AM and 7:00 PM, you could create schedules for the two events and add them both to an EventQueue object.

BlockWrapper

BlockWrapper is a scheduler for a very specific operation. It limits another schedule to only fire within a repeating range of time. This is used primarily to manage something that will only run on weekdays, weekends or only during business hours.

A Single Event

I've tried to keep the interface as close to the native .NET System.Timers.Timer object as possible. However, the native event args are sealed and not publicly creatable so I had to create a separate delegate and event argument definition. Here is a simple example of using the timer to run once a day at 5:00 PM:

C#
ScheduleTimer  TickTimer  =  new  ScheduleTimer();
TickTimer.Events.Add(new  Schedule.ScheduledTime("Daily",  "5:00  PM"));
TickTimer.Elapsed  +=  new  ScheduledEventHandler(TickTimer_Elapsed);
TickTimer.Start();

Multiple Events

The AddJob method on the timer is used to add multiple events or jobs to the timer. The first overload is the easiest to use, it takes three parameters, the schedule, a delegate, and an optional array of the parameters to pass to the method. In order to give you a little more flexibility, you don't have to specify all the parameters of your method. If there are unspecified DateTime or object parameters, the object firing the event and the time this event should have run are passed in. This preserves the .NET EventArgs calling convention, while giving you the freedom of passing additional parameters in to your events.

If you need more control, then you can create your own TimerJob specifying the exact type of MethodCall you need for your events.

The regular AddJob method synchronizes the jobs so that only one job is executed at once. If your jobs can run concurrently then you can add them with the AddAsyncJob method.

Error Handling

The timer provides an Error event handler. If you don't add a handler to this event you won't be notified of any exceptions thrown by your event handlers.

Recovery

Recovery or state persistence is the ability to automatically run jobs that were missed because of a service outage. This is disabled by default because it requires storing the last execution time in an application specific manner. To add application specific storage, you just need to implement the following interface:

C#
public  interface  IEventStorage
{
  void  RecordLastTime(DateTime  Time);
  DateTime  ReadLastTime();
}

I've provided three implementations of this. The default is LocalEventStorage which stores the last event time in memory, so as long as the timer stays in memory it will make sure every event fires. If you don't want any recovery then you can assign the NullEventStorage class like so:

C#
timer.EventStorage = new NullEventStorage();

I've also provided a simple XML file based event storage class which can be used for things like services, but if you are really concerned about recovery you should implement your own.

IMethodCall

The .NET delegates can be really useful for providing callbacks to objects because they let you store an object and call a specific method on that object as if it was a simple static method. This allows generic operations which only depend on a particular method signature. The downside of this is that if your method doesn't match the signature, you need to write a wrapper method, or write a wrapper class if you don't have the source to the object. C# 2.0 gets around this with anonymous delegates, but in the mean time I've written a few classes to simplify building a delegate by partially passing parameters to a method. Let's say we want to schedule a method that takes a report ID as a parameter.

C#
public Delegate void GenerateReport(int reportID);

public class Report
{
    public static void Generate(int reportID) {}
}

public class EventWrapper
{
    EventWrapper(int reportID, GenerateReport report)
    {
        mReportID = reportID;
        mReport = report;
    }
    public void EventHandler(object src, EventArgs e)
    {
        mReport(mReportID);
    }
    int mReportID;
    GenerateReport mReport;
}

Using the MethodCall objects, we can just write something like this:

C#
IMethodCall call = new DelegateMethodCall(new GenerateReport(Report.Generate), 10);
obj.Event = new EventType(call.EventHandler);

Also, using some of the parameter setter objects, we can bind parameters to the method based on the name instead of order and type.

Code Examples

  • Run every second on the second.
    C#
    TickTimer.Events.Add(new  Schedule.ScheduledTime("BySecond",  "0"));
  • Run every minute 15 seconds after the second.
    C#
    TickTimer.Events.Add(new  Schedule.ScheduledTime("ByMinute",  "15,0"));
  • Run at 6:00 AM on Mondays.
    C#
    TickTimer.Events.Add(new  Schedule.ScheduledTime("Weekly",  "1,6:00AM"));
  • Run once at 6:00 AM on 6/27/08.
    C#
    TickTimer.Events.Add(new  Schedule.SingleEvent(new DateTime("6/27/2008 6:00")));
  • Run every 12 minutes starting on midnight 1/1/2003.
    C#
    TickTimer.Events.Add(new Schedule.SimpleInterval(new 
               DateTime("1/1/2003"), TimeSpan.FromMinutes(12)));
  • Run every 15 minutes from 6:00 AM to 5:00 PM.
    C#
    TickTimer.Events.Add(
        new Schedule.BlockWrapper(
            new Schedule.SimpleInterval(new DateTime("1/1/2003"),
                                               TimeSpan.FromMinutes(15)),
            "Daily",
            "6:00 AM",
            "5:00 PM"
        )
    );
    
Clock Sample Application

For the sample project I wrote a simple alarm clock. Just to jazz it up a little bit, I made it transparent and always visible. It was remarkably easy to add this functionality to a .NET Forms application. I just had to set the form opacity and hide the normal Windows frame. It was just as easy to override the mouse down and up handlers to make the entire window dragable, as well as add a context menu to close down and set the schedule.

Since I mostly code ASP.NET applications, I was surprised that there wasn't an easy way to store dynamic state information in a form application. It's very simple to just read the data out of the app.config file, but there isn't a simple API to update that data. It was just as easy to hack together a simple class for this application to store data in an XML file.

History

  • Mar 24, 2004 - Release.
  • Jun 14, 2004 - Bug fixes and updated timer object model.
  • August 20, 2005 - Updates to the article text, better exception handing in the timer, tests updated to be locale independent, error in monthly timer parsing fixed.
  • August 29, 2005 - Updates to fix stoppage issue, added no recovery mode, and added a single event scheduled item.
  • September 12, 2005 - Update to fix missing Dispose method on the timer object.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Standard Beagle Studios
United States United States
I co-founded Standard Beagle Studio, a software development consulting service in Austin Texas with my wife Cindy Brummer. We focus mostly on web projects, but have built some react native mobile apps, and even a windows screen saver or two.

I started my career back when ASP pages were state of the art, and IE3 was considered a web browser. I've worked with Microsoft technologies for most of that time, and have recently branched out into node, wordpress, and react native applications.

I'm a web developer, math and physics enthusiast, father of 2, and all around great guy. I live in Austin TX and love using technology to change people's lives for the better. When I manage scrape together some spare time, I build generative art at curvature of the mind.

Comments and Discussions

 
GeneralRe: Missed Events #2 Pin
Edwin Vis16-Apr-13 4:02
Edwin Vis16-Apr-13 4:02 
GeneralRe: Missed Events #2 Pin
James Hutchison28-Apr-16 0:22
James Hutchison28-Apr-16 0:22 
GeneralMy vote of 5 Pin
Capajj19-Sep-12 8:23
Capajj19-Sep-12 8:23 
QuestionStill Looks good 8 years later Pin
DVFaulkner7-Apr-12 21:30
DVFaulkner7-Apr-12 21:30 
SuggestionProject hosting website Pin
Dmitry Fisenko23-Dec-11 23:41
Dmitry Fisenko23-Dec-11 23:41 
QuestionNeed more sample Pin
stani050212-Dec-11 23:19
stani050212-Dec-11 23:19 
QuestionSome events are missed ... Pin
Daniel GARRIVIER11-Oct-11 22:14
Daniel GARRIVIER11-Oct-11 22:14 
QuestionIn this scheduled timer, How to add job using vb.net Pin
John M Mukesh24-Aug-11 2:37
John M Mukesh24-Aug-11 2:37 
In this scheduled timer, How to add job using vb.net .

I have tried with TickHandler.but, it is not supported.


How to i do?


I need to raise a function (Perform backup) at 10:00 PM daily.Can any one help me.

Advance thanks.
AnswerRe: In this scheduled timer, How to add job using vb.net Pin
Atomsk258-Sep-11 4:31
Atomsk258-Sep-11 4:31 
QuestionSystem.ObjectDisposed Pin
pdqrose210-Apr-11 5:03
pdqrose210-Apr-11 5:03 
GeneralRun every N weeks on DayOfWeek at Time Pin
Travis Thelen31-Mar-11 7:37
Travis Thelen31-Mar-11 7:37 
GeneralPerfect! Pin
Pascal Pinchauret-Lamothe23-Feb-11 2:33
Pascal Pinchauret-Lamothe23-Feb-11 2:33 
GeneralMy vote of 1 Pin
BR_Richo1-Dec-10 14:45
BR_Richo1-Dec-10 14:45 
GeneralIautomatic recovery method when events were missed Pin
Orcun Iyigun11-Nov-10 7:19
Orcun Iyigun11-Nov-10 7:19 
GeneralRe: Iautomatic recovery method when events were missed Pin
Andy Brummer11-Nov-10 9:36
sitebuilderAndy Brummer11-Nov-10 9:36 
GeneralRe: Iautomatic recovery method when events were missed Pin
Orcun Iyigun11-Nov-10 9:50
Orcun Iyigun11-Nov-10 9:50 
General.NET Scheduled Timer in Compact Framework 2.0 Pin
Tom Netland10-Nov-10 6:19
Tom Netland10-Nov-10 6:19 
GeneralRe: .NET Scheduled Timer in Compact Framework 2.0 Pin
Andy Brummer11-Nov-10 6:37
sitebuilderAndy Brummer11-Nov-10 6:37 
GeneralMy vote of 5 Pin
Member 380872820-Sep-10 21:24
Member 380872820-Sep-10 21:24 
Generallike it Pin
Pranay Rana13-Jul-10 23:45
professionalPranay Rana13-Jul-10 23:45 
GeneralWeekday wrapper Pin
flipdoubt13-Oct-09 5:48
flipdoubt13-Oct-09 5:48 
GeneralRun every 15 minutes from 6:00 AM to 5:00 PM. Pin
remerg20-Sep-09 22:18
remerg20-Sep-09 22:18 
GeneralError using Daily Mode Pin
Juan Pablo Hurtado3-Aug-09 13:38
Juan Pablo Hurtado3-Aug-09 13:38 
GeneralRe: Error using Daily Mode Pin
Juan Pablo Hurtado4-Aug-09 6:35
Juan Pablo Hurtado4-Aug-09 6:35 
GeneralRe: Error using Daily Mode Pin
shivangyvyas16-Nov-09 11:00
shivangyvyas16-Nov-09 11:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.