Recently, one of my jobs demanded me to create a windows service for scheduling some tasks without human intervention, all we need to mention those services XML file.
Configure the scheduled tasks in an XML file (
Tasks.xml)
On Service Start, load the tasks configuration from the
Tasks.xml file into a
DataSet
.
Get a reference to the assembly that contains the tasks.
Use a
Systems.Timer
to run a method (
RunTasks
) periodically that checks the tasks that need to run and run the tasks.
This is how the
Tasks.xml file looks like:
<appSchedule>
<task name="Task1" time="06/07/2009 12:00" repeat="H" />
<task name="Task2" time="06/15/2009 12:00" repeat="W" />
<task name="Task3" time="06/29/2009 12:00" repeat="D" />
<task name="Task5" time="06/10/2009 19:00" repeat="M" />
</appSchedule>
name is the class name of the task to run, time is date and time (MM/dd/yyyy HH:mm format) when the task should run, and repeat is how often the task should run (H- hourly, W-Weekly, M-Monthly, D-Daily)
Using the Code
We used ThreadPooling to ease the burden of managing the threads. Since each task has to run in its own thread and we don't know how many threads to create, we zeroed in on
ThreadPooling
to manage the threads.
namespace MailTasks
{
public interface ITask
{
void RunTask();
}
}
We have
System.Timer
object that periodically calls the
RunTasks
method. We use the global boolean variable
workInProgress
to track if the
RunTasks
method is running or idling. If the
workInProgress
is
true
, we just return to wait for the completion of the earlier call to
RunTasks
method. If the
workInProgress
is
false
, we proceed further to run the scheduled tasks.
We get the list of tasks to run by calling a method
GetTasksToRun()
. Inside the
GetTasksToRun
method, we go through the
DataSet
with the tasks schedule information, for each task scheduled, if the current time is greater than the scheduled time, using reflection we create the
Task
Object that needs to run and then add it to the list of tasks to run.
Once we get the list of tasks to run, we update a global variable
numBusy
with the count of tasks to run. This
numBusy
variable will be used to track the number of busy threads at any given time. We loop through the scheduled tasks list, and queue each task in the
ThreadPool
by passing reference to a method (
DoTask
) and the task object itself to the
ThreadPool
's
QueueUserWorkItem
method.
Inside the
DoTask
method, we call the
RunTask()
method on the task object passed in as an argument. We update the next run time for the task in the
DataSet
by calling the method
UpdateNextRunTime
and decrement the count of busy threads (
numBusy
) in the
finally
.
Back in the
RunTasks
method, we wait for all the threads to complete by calling
WaitOne()
method on the
ManualResetEvent
object
doneEvent
.
After all the queued tasks are complete, we persist the tasks data in the
DataSet
back to the disk and set
workInProgress
to
false
to mark the completion of all the tasks queued.
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;
using System.Timers;
using System.Xml;
using System.Configuration;
using MailTasks;
namespace SchedulerService
{
public partial class Scheduler : ServiceBase
{
private static ManualResetEvent doneEvent;
private static string configPath = string.Empty;
private static int numBusy;
private static DataSet dsTasks;
private const string TASKS_NAME_SPACE = "MailTasks.";
private const string DATE_FORMAT_STRING = "MM/dd/yyyy HH:mm";
private static Assembly tasksAssembly;
private static EventLog eventLog1;
readonly System.Timers.Timer _timer = new System.Timers.Timer();
private static bool workInProgress;
public Scheduler()
{
InitializeComponent();
if (!EventLog.SourceExists("MailScheduler"))
EventLog.CreateEventSource("MailScheduler", "Application" );
eventLog1 = new EventLog("Application", Environment.MachineName, "MailScheduler");
}
protected override void OnStart(string[] args)
{
try
{
eventLog1.WriteEntry("Mail Scheduler Service Started");
LoadTasksIntoDataSet();
LoadTasksAssembly();
_timer.Interval = 60000;
_timer.Elapsed += RunTasks;
_timer.Start();
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred OnStart "+ex.Message);
}
}
protected override void OnStop()
{
eventLog1.WriteEntry("MailScheduler service stopped");
try
{
UpdateTasksConfigonDisk();
}
catch(Exception ex)
{
eventLog1.WriteEntry("Error occurred onStop "+ex.Message);
}
}
private static void RunTasks(object sender, ElapsedEventArgs args)
{
if (workInProgress) return;
numBusy = 0;
doneEvent = new ManualResetEvent(false);
List<itask> tasksList = GetTasksToRun();
numBusy = tasksList.Count;
if (numBusy > 0)
{
workInProgress = true;
foreach (ITask task in tasksList)
{
ThreadPool.QueueUserWorkItem(DoTask, task);
}
doneEvent.WaitOne();
}
if (numBusy == 0 && tasksList.Count > 0)
{
workInProgress = false;
UpdateTasksConfigonDisk();
}
}
private static void DoTask(object o)
{
ITask task = o as ITask;
if (task == null) return;
string scheduleName = task.GetType().ToString();
try
{
task.RunTask();
int lastIndexOfPeriod = scheduleName.LastIndexOf(".");
UpdateNextRunTime(scheduleName.Substring(lastIndexOfPeriod + 1));
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred while executing task: " + scheduleName);
eventLog1.WriteEntry("Stack Trace is: " + ex.Message);
}
finally
{
if (Interlocked.Decrement(ref numBusy) == 0)
{
doneEvent.Set();
}
}
}
private static void LoadTasksIntoDataSet()
{
try
{
eventLog1.WriteEntry("Trying to Load Tasks into DataSet");
configPath = ConfigurationManager.AppSettings["tasksConfigPath"];
XmlTextReader xmlTextReader = new XmlTextReader(configPath);
XmlDataDocument xdoc1 = new XmlDataDocument();
xdoc1.DataSet.ReadXml(xmlTextReader, XmlReadMode.InferSchema);
dsTasks = xdoc1.DataSet;
xmlTextReader.Close();
eventLog1.WriteEntry("Finished Loading Tasks into DataSet");
}
catch(Exception ex)
{
eventLog1.WriteEntry("Error occurred while loading tasks into DataSet " + ex.Message);
throw;
}
}
private static void UpdateTasksConfigonDisk()
{
try
{
eventLog1.WriteEntry("Attempting to save tasks information to disk ");
StreamWriter sWrite = new StreamWriter(configPath);
XmlTextWriter xWrite = new XmlTextWriter(sWrite);
dsTasks.WriteXml(xWrite, XmlWriteMode.WriteSchema);
xWrite.Close();
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred while savings tasks information to disk "+ex.Message);
throw;
}
}
private static void UpdateNextRunTime(string taskName)
{
if (dsTasks == null) return;
foreach (DataRow row in dsTasks.Tables[0].Rows)
{
if (taskName.ToLower() != row[0].ToString().ToLower()) continue;
DateTime scheduledTime = DateTime.Parse(row[1].ToString());
string repeat = row["repeat"].ToString().ToUpper();
switch (repeat)
{
case "H":
scheduledTime = scheduledTime.AddHours(1);
if (scheduledTime < DateTime.Now)
scheduledTime = DateTime.Now.AddHours(1);
break;
case "D":
while (scheduledTime < DateTime.Now)
{
scheduledTime = scheduledTime.AddDays(1);
}
break;
case "W":
while (scheduledTime < DateTime.Now)
{
scheduledTime = scheduledTime.AddDays(7);
}
break;
case "M":
while (scheduledTime < DateTime.Now)
{
scheduledTime = scheduledTime.AddMonths(1);
}
break;
}
row[1] = scheduledTime.ToString(DATE_FORMAT_STRING);
dsTasks.AcceptChanges();
}
}
private static List<itask> GetTasksToRun()
{
if (dsTasks == null) return null;
List<itask> tasks = new List<itask>();
foreach (DataRow row in dsTasks.Tables[0].Rows)
{
DateTime scheduledTime = DateTime.Parse(row[1].ToString());
if (DateTime.Now < scheduledTime) continue;
ITask task = CreateTaskInstance(row[0].ToString());
if (task != null)
tasks.Add(task);
}
return tasks;
}
private static ITask CreateTaskInstance(string taskName)
{
string taskFullName = TASKS_NAME_SPACE + taskName;
try
{
if(tasksAssembly==null)
throw new Exception("Tasks Assembly is null, cannot proceed further..");
ITask task = (ITask)tasksAssembly.CreateInstance(taskFullName, true);
return task;
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred while creating Task Instance " + ex.Message);
}
return null;
}
private static void LoadTasksAssembly()
{
try
{
if (tasksAssembly == null)
tasksAssembly = Assembly.GetAssembly(typeof(MailTasks.ITask));
}
catch(Exception ex)
{
eventLog1.WriteEntry("Error occurred while loading tasks Assembly " + ex.Message);
throw;
}
}
}
}
Points of Interest
To recap, the requirements for the windows service are:
- The tasks should be loaded from a class library
- The schedule information for the tasks should be configurable in an XML file.