|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionIn 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. BackgroundScheduling 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 When I originally wrote this timer, I used the One of my favorite features of the .NET framework is the So now we have the requirements:
First AttemptsThis 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 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 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 timerSchedulesIScheduledItemEach 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 public interface IScheduledItem
{
void AddEventsInInterval(DateTime Begin,
DateTime End, ArrayList List);
DateTime NextRunTime(DateTime time);
}
SimpleIntervalThe ScheduledTimeThe SingleEventThe EventQueue
BlockWrapper
A Single EventI've tried to keep the interface as close to the native .NET ScheduleTimer TickTimer = new ScheduleTimer();
TickTimer.Events.Add(new Schedule.ScheduledTime("Daily", "5:00 PM"));
TickTimer.Elapsed += new ScheduledEventHandler(TickTimer_Elapsed);
TickTimer.Start();
Multiple EventsThe If you need more control, then you can create your own The regular Error HandlingThe timer provides an RecoveryRecovery 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: public interface IEventStorage
{
void RecordLastTime(DateTime Time);
DateTime ReadLastTime();
}
I've provided three implementations of this. The default is 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. IMethodCallThe .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. 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 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
Clock Sample ApplicationFor 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
| ||||||||||||||||||||