Введение
Иногда перед веб разработчиками возникают задачи, на первый взгляд в принципе неразрешимые если исходить из общего строения веб приложений и общения "запрос-ответ". Одной из таких задач является задача реализации системы повторяющихся заданий, т.е. заданий, выполняемых через определенные промежутки времени или в указанное время, например регулярная очистка таблиц в базе данных или отсылка email сообщений из очереди. В этом случае счастливые обладатели выделенных серверов пишут windows сервисы или запускают консольные приложения с помощью встроенного в Windows планировщика задач. Чуть менее счастливым приходится договариваться о подобных сервисах с хостером. Ну а основная масса решает такую задачу путем дергания с помощью того же планировщика страниц, в логике которых забиваются необходимые задания. Но на самом деле подобные задачи в ASP.NET можно реализовать намного проще, не прибегая к использованию сторонних средств.
Краткое описание
Вкратце алгоритм решения подобной задачи крайне прост: при старте веб приложения запускается таймер System.Threading.Timer, в колбек методе которого и пишется необходимая логика задания. И тогда коллбек метод таймера будет вызываться через указанный промежуток времени все время работы веб приложения. На всякий случай я приведу пример описанного выше кода, запускающего с интервалом в одну минуту метод, пишущий сообщение о своем вызове в лог файл (код расположен в файле global.asax).
void Application_Start(object sender, EventArgs e)
{
System.Threading.Timer t = new System.Threading.Timer(new System.Threading.TimerCallback(DoRun), null, 0, 60000);
}
private void DoRun(object state)
{
System.IO.StreamWriter sw = new System.IO.StreamWriter(new System.IO.FileStream(Server.MapPath("test.log"), System.IO.FileMode.Append));
sw.WriteLine("Job started at {0}", DateTime.Now);
sw.WriteLine();
sw.Close();
}
Данное решение, безусловно, имеет право на существование, но оно не отличается ни особым изяществом, ни удобством применения – ведь каждый раз для изменения логики работы задач придется пересобирать все веб приложение. Поэтому, взяв за основу вышенаписанное я постараюсь реализовать более удобоваримую систему для планировщика задач, одинаково пригодную для использования как в веб, так и в windows приложениях.
Сама система состоит из 2-х модулей – общего модуля управления заданиями и модуля управления отдельным заданием. Задания же представляют собой классы, реализующие интерфейс ITask с единственным методом Run(), в котором и реализуется логика задания. Ну а конфигурация всей системы реализована в виде секции конфигурационного файла.
Программная реализация системы повторяющихся заданий
Краткое отступление. Весь код этой статьи написан на C# 2.0 и .NET 2.0. Но в архиве, содержащем исходный код статьи, находятся проекты и для .NET 2, и для .NET 1.1.
Задания
Начнем мы, пожалуй, с простейшего – описания интерфейса ITask для заданий. Как я уже упомянул выше, этот интерфейс содержит единственный метод Run с параметром типа XmlNodeList для передачи параметров заданию. Вы спросите "а почему тип XmlNodeList?" Тут все достаточно просто – так как настройка заданий производится в .config файле, мы можем просто передать часть этой настройки, относящуюся к данному заданию. Честно говоря, я думал о применении чего-нибудь более удобного (например, того же Hashtable), но при реализации задания Scheduler, о котором я расскажу в конце статьи, я столкнулся с тем, что для большего удобства лучше не ограничивать возможность задания параметров. Итак, весь код интерфейса ITask будет таким
public interface ITask
{
void Run(XmlNodeList parameters);
}
Классы конфигурации
Теперь создадим конфигурационные классы и реализуем класс секции конфигурационного файла. Фактически конфигурация системы будут состоять из трех уровней – уровня настроек самой системы, уровня настроек отдельных задач и уровня параметров задач. Про параметры задач я уже писал выше – они будут задаваться в свободном виде и работа с ними целиком и полностью будет выполняться в самой задаче. Настройки отдельной задачи содержат имя и тип задачи (имя задачи должно быть уникально среди всех задач системы), метку о том, включена ли задача, метку о том, использует ли задача общий таймер системы или свой собственный таймер и интервал (в секундах) вызова задачи. Ну а в настройках самой системы есть метка о том, включена ли система, метка об использовании общего таймера и интервал срабатывания общего таймера в секундах. И, кроме того, настройки верхнего уровня содержат в себе настройки более низкого уровня, т.е. в настройки задачи включаются и параметры задачи, а в настройках системы содержатся настройки всех задач.
И раз уж зашел разговор об общем таймере, то придется рассказать, что это такое. В общем случае для каждой задачи запускается свой собственный таймер с уникальными настройками интервала срабатывания. Но так как немалое количество рутинных задач частенько вызывается с одним и тем же интервалом – имеет смысл объединить их вызовы в одном таймере. Но, в отличие от собственного таймера задачи этот таймер запускается в главном управляющем классе системы.
В принципе по настройке все и теперь мы можем взглянуть на получившийся код
public class TasksSettings
{
public bool IsSingleThreaded;
internal int Seconds;
internal bool Enabled;
public Collection<TaskSettings> Tasks;
public TasksSettings()
{
IsSingleThreaded = true;
Enabled = true;
Seconds = 60;
Tasks = new Collection<TaskSettings>();
}
}
public class TaskSettings
{
public string Name;
public string Type;
public bool IsSingleThreaded;
public bool Enabled;
public int Seconds;
public XmlNodeList XmlParameters;
public TaskSettings()
{
Name = "";
Type = "";
Enabled = true;
IsSingleThreaded = true;
Seconds = 0;
XmlParameters = null;
}
}
В приведенном коде класс TasksSettings – это настройки системы, TaskSettings – настройки отдельного класса, ну а XmlParameters – параметры задачи.
Класс секции конфигурационного файла тоже не представляет собой ничего сложного и про принципы его создания я уже недавно писал в статье о создании системы статистики сайта. Поэтому просто приведу этот класс без дополнительных объяснений.
class TasksSectionHandler : IConfigurationSectionHandler
{
object IConfigurationSectionHandler.Create(object parent, object configContext, XmlNode section)
{
TasksSettings ret = new TasksSettings();
ret.IsSingleThreaded = bool.Parse(section.Attributes["isSingleThreaded"].Value);
if(ret.IsSingleThreaded)
ret.Seconds = Int32.Parse(section.Attributes["seconds"].Value);
ret.Enabled = bool.Parse(section.Attributes["enabled"].Value);
foreach (XmlNode node in section.ChildNodes)
{
TaskSettings task = new TaskSettings();
task.Name = node.Attributes["name"].Value;
task.Type = node.Attributes["type"].Value;
task.IsSingleThreaded = bool.Parse(node.Attributes["isSingleThreaded"].Value);
task.Enabled = bool.Parse(node.Attributes["enabled"].Value);
if (!task.IsSingleThreaded)
task.Seconds = Int32.Parse(node.Attributes["seconds"].Value);
task.XmlParameters = node.ChildNodes;
ret.Tasks.Add(task);
}
return ret;
}
}
Ну и напоследок, пожалуй, нужно привести кусок конфигурационного файла с описанием какой-то задачи
<configuration>
<configSections>
<section name="tasks" type="Dimon.Tasks.TasksSectionHandler, Dimon.Tasks"/>
</configSections>
<tasks seconds="60" isSingleThreaded="true" enabled="true">
<task name="test" type="Dimon.Tasks.Tests.TestTask, Dimon.Tasks.Tests" isSingleThreaded="true" enabled="true" seconds="60">
<param name="StringParam" value="value" type="System.String" />
<param name="IntParam" value="150" type="System.Int32" />
<param name="DateTimeParam" value="2005-11-11" type="System.DateTime" />
</task>
</tasks>
</configuration>
Теперь, когда с конфигурацией покончено, настала пора перейти к классу управления задачей.
Классы управления заданиями
Данный класс, по сути, является оберткой над классом задачи и служит для создания экземпляра класса задачи и вызова метода Run этого экземпляра. Кроме того, именно этот класс содержит таймер для задач, работающих с собственным таймером. Ну и вдобавок ко всему этот класс выставляет наружу свойства запуска задачи для управляющего класса. Но прежде чем глядеть на класс управления заданием, давайте рассмотрим общий принцип работы системы.
Как я уже упоминал ранее, основную работу по управлению планировщиком задач выполняет отдельный класс – TaskEngine. Этот синглтон (singleton) класс имеет 2 метода Start() и Stop() соотв. для запуска системы и для ее остановки. Метод Start() читает конфигурацию планировщика задач и на основе данных из конфигурации формирует список экземпляров класса управления заданием TaskLauncher и запускает таймеры задач (если система не работает с единым таймером или если данное задание использует собственный таймер). Кроме того, в методе Start(), если это необходимо, создается и запускается общий таймер системы. Метод Stop(), соответственно, уничтожает все классы управления задачами в списке и общий таймер пданировщика (если он используется). Ну и, естественно, класс TaskEngine содержит коллбек метод общего таймера, в котором происходит вызов активных задач, использующих общий таймер.
Класс управления задачами TaskLauncher в свою очередь, используя настройки задачи, создает экземпляр класса указанного в настройках типа и в своих свойствах выставляет наружу некоторые свойства задачи, как то имя и тип задачи, метку использования задачей общего таймера, метку, активна ли задача и признак того, что задача выполняется. Ну и, кроме того, этот класс содержит методы для создания экземпляра класса задачи, запуска задачи (вызова метода Run экземпляра класса) и работу с таймером (в случае, если задача работает с собственным таймером).
Вот, пожалуй, и все про общее описание работы планировщика задач и мы можем, наконец, перейти к рассмотрению кода классов. Для начала я приведу код класса управления заданием TaskLauncher.
Описание свойств данного класса я уже дал, поэтому приведу этот код без комментариев
public class TaskLauncher : IDisposable
{
private ITask task;
private bool isRunning;
private Type taskType;
private Timer timer;
private bool disposed;
public TaskSettings settings;
public bool Enabled
{
get { return settings.Enabled; }
}
protected int Interval
{
get { return settings.Seconds * 1000; }
}
public bool IsRunning
{
get { return isRunning; }
}
public Type TaskType
{
get { return taskType; }
}
public string Name
{
get { return settings.Name; }
}
public bool SingleThreaded
{
get { return settings.IsSingleThreaded; }
}
public TaskLauncher(TaskSettings task)
{
taskType = Type.GetType(task.Type);
settings = task;
}
Более интересен метод GetInstance(), возвращающий экземпляр класса задачи. В случае, если по каким-то причинам экземпляр класса задачи создать не получается – этот метод кроме всего прочего выключает задачу
private ITask GetInstance()
{
if (Enabled && task == null)
{
if (taskType != null)
task = Activator.CreateInstance(taskType) as ITask;
settings.Enabled = task != null;
if (!Enabled)
{
this.Dispose();
}
}
return task;
}
Метод Run() запуска задачи также не отличается особенной сложностью – он вызывает метод GetInstance() для получения экземпляра класса задачи и в случае возврата экземпляра класса просто вызывает его метод ITask.Run()
public virtual void RunTask()
{
isRunning = true;
ITask task = this.GetInstance();
if (task != null)
{
try
{
task.Run(settings.XmlParameters);
}
catch
{
}
}
isRunning = false;
}
Кроме этого в классе есть пара методов для работы с собственным таймером – метод инициализации таймера и коллбек метод самого таймера, вызывающий только что рассмотренный нами метод Run()
public void InitializeTimer()
{
if (timer == null && Enabled)
{
timer = new Timer(new TimerCallback(timer_Callback), null, this.Interval, this.Interval);
}
}
private void timer_Callback(object state)
{
if (Enabled)
{
timer.Change(-1, -1);
RunTask();
if (Enabled)
{
timer.Change(this.Interval, this.Interval);
}
else
{
this.Dispose();
}
}
}
Ну и последний метод этого класса, Dispose(), зачищает таймер в случае его наличия
public void Dispose()
{
if ((timer != null) && !disposed)
{
lock (this)
{
timer.Dispose();
timer = null;
disposed = true;
}
}
}
Теперь можно перейти к рассмотрению кода класса управления системой TaskEngine. Для работы этот singleton класс использует список классов управления задачами и таймер. Кроме того, в нем есть свойства для доступа к текущим задачам системы. Вообщем это проще показать кодом, нежели описать ;)
public sealed class TaskEngine
{
static TaskEngine taskengine = null;
static readonly object padlock = new object();
private bool isStarted;
private bool _isRunning;
private int Interval;
private Dictionary<string, TaskLauncher> taskList;
private Timer singleTimer;
private TaskEngine()
{
taskList = new Dictionary<string, TaskLauncher>();
Interval = 60000;
}
public static TaskEngine Instance
{
get
{
lock (padlock)
{
if (taskengine == null)
{
taskengine = new TaskEngine();
}
return taskengine;
}
}
}
public Dictionary<string, TaskLauncher> CurrentJobs
{
get
{
return this.taskList;
}
}
public bool IsTaskEnabled(string taskName)
{
if (!this.taskList.ContainsKey(taskName))
{
return false;
}
return this.taskList[taskName].Enabled;
}
Более интересен метод Start(), запускающий систему. В этом методе загружаются настройки планировщика задач, на основе этих настроек создается список классов управления задачами и запускаются все необходимые для работы таймеры (внутренние таймеры для задач и общий таймер для всей системы).
public void Start()
{
if (isStarted)
return;
isStarted = true;
lock (padlock)
{
if (taskList.Count != 0)
{
return;
}
TasksSettings settings = (TasksSettings) WebConfigurationManager.GetSection("tasks");
if (!settings.Enabled)
return;
if (settings.IsSingleThreaded)
this.Interval = settings.Seconds * 1000;
foreach (TaskSettings t in settings.Tasks)
{
if (!taskList.ContainsKey(t.Name))
{
TaskLauncher task = new TaskLauncher(t);
taskList.Add(t.Name, task);
if (!task.SingleThreaded || !settings.IsSingleThreaded)
{
task.InitializeTimer();
}
}
}
if(settings.IsSingleThreaded)
this.singleTimer = new Timer(new TimerCallback(this.call_back), null, this.Interval, this.Interval);
}
}
Метод Stop() же делает обратные действия – уничтожает все классы управления задачами и останавливает и уничтожает общий таймер.
public void Stop()
{
if (isStarted)
{
lock (padlock)
{
foreach (TaskLauncher task in this.taskList.Values)
{
task.Dispose();
}
this.taskList.Clear();
if (this.singleTimer != null)
{
this.singleTimer.Dispose();
this.singleTimer = null;
}
}
}
}
Ну и последний метод – коллбек метод таймера – запускает активные задачи, использующие общий таймер
private void call_back(object state)
{
if (_isRunning)
return;
_isRunning = true;
this.singleTimer.Change(-1, -1);
foreach (TaskLauncher task in this.taskList.Values)
{
if (task.Enabled && task.SingleThreaded)
{
task.RunTask();
}
}
this.singleTimer.Change(this.Interval, this.Interval);
_isRunning = false;
}
Все, планировщик задач готов и теперь можно приступать к тестам.
Тестируем планировщик задач
Как я уже упоминал ранее, данная система одинаково хорошо будет работать как в веб, так и в windows приложениях. И дабы не усложнять себе жизнь тесты работоспособности планировщика задач мы будем делать в консольном приложении – и наглядней получится, и с правами проблем избежим.
Первым делом для тестов нужно написать класс задачи, реализующий интерфейс ITask. Это будет простенький класс, просто выводящий дату начала своей работы и все передаваемые ему параметры. Вообщем не буду развозить кашу по столу, а покажу код этого класса
class TestTask : Dimon.Tasks.ITask
{
void Dimon.Tasks.ITask.Run(XmlNodeList parameters)
{
Console.WriteLine("Task started at {0}", DateTime.Now);
Console.WriteLine("Parameters:");
foreach (XmlNode param in parameters)
{
Console.WriteLine("{0}\t{1}", param.Attributes["name"].Value, Convert.ChangeType(param.Attributes["value"].Value, Type.GetType(param.Attributes["type"].Value)));
}
Console.WriteLine();
Console.WriteLine();
}
}
Как вы, я надеюсь, помните, параметры задачи передаются в метод Run в виде XmlNodeList и вся логика для получения значений этих параметров должна быть написана в этом методе. В данном случае параметры будут передаваться в виде элемента <param name="имя параметра" value="значение параметра" type="тип параметра" /> и для получения их значений я воспользовался методом Convert.ChangeType().
Код конфигурирования планировщика задач в .config файле приложения будет точно таким же, как я приводил выше. На всякий случай повторюсь
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="tasks" type="Dimon.Tasks.TasksSectionHandler, Dimon.Tasks"/>
</configSections>
<tasks seconds="60" isSingleThreaded="true" enabled="true">
<task name="test" type=" Dimon.Tasks.Tests.TestTask, Dimon.Tasks.Tests" isSingleThreaded="true" enabled="true" seconds="60">
<param name="StringParam" value="value" type="System.String" />
<param name="IntParam" value="150" type="System.Int32" />
<param name="DateTimeParam" value="2005-11-11" type="System.DateTime" />
</task>
</tasks>
</configuration>
В данном случае планировщик настроен на выполнение задачи раз в 60 секунд и для этого используется общий таймер.
Осталось написать код для запуска планировщика задач в приложении
static void Main(string[] args)
{
TaskEngine.Instance.Start();
Console.ReadLine();
TaskEngine.Instance.Stop();
}
И можно запустить получившееся приложение и наслаждаться результатом – теперь раз в минуту выполняемая в планировщике задач задача будет выводить сообщение в консоль.
Вот, пожалуй, и все, что можно было написать о создании планировщика задач для .NET приложения. Все, да не совсем – данная система все таки не позволяет задавать вызов задач по расписанию, а только лишь по таймеру. И задавать таймер для, например, задачи, выполняющейся раз в день или, и того хуже, раз в неделю как-то не совсем правильно (да и не факт, что это задание вообще будет выполняться исходя из того, что таймер привязан к приложению, которое имеет особенность перегружаться). Посему описанный выше планировщик задач необходимо расширить.
Задача "Задачи по расписанию"
Можно было бы добавить необходимый функционал в саму систему планировщика задач, но мне показалось более правильным поступить иначе – реализовать задачу "Задачи по расписанию".
Идея реализации этой задачи очень проста – параметрами задачи задаются задачи с расписанием, а при срабатывании таймера задачи вычисляется необходимость запуска той или иной заданной задачи. Фактически получается реализация написанного выше планировщика задач в миниатюре с учетом дополнительных параметров в виде задания расписания запуска задач.
Я не буду слишком сильно углубляться в задание расписания для задач, дабы не усложнять код. Дополнительно к стандартным параметрам задач, описанным ранее в классе TaskSettings, каждая задача может запускаться один раз в какой-то промежуток времени – час, день, неделю или месяц в указанный день (если нужно) и время. Для задания этих значений в конфигурационном файле служат атрибуты duration и startAt. При этом duration принимает значения из перечисления
internal enum Duration
{
Hour,
Day,
Week,
Month
}
А значение параметра startAt задается по следующему принципу:
- для частоты Hour в параметре startAt задается минута запуска задачи. То есть при конфигурации duration="Hour" startAt="15" задание будет выполняться в 15 минут каждого часа.
- для частоты Day в параметре startAt задается время запуска задачи. Например, в случае duration="Day" startAt="15:30" задание будет выполняться в 15:30 каждый день.
- Для частоты Week и Month в параметре startAt задаются день и время запуска задачи, разделенные запятой. При этом для Week день запуска задачи является значением перечисления System.DayOfWeek, а для Month – номер дня в месяце. Например, в случае duration="Week" startAt="Sunday, 2:00" задание будет запускаться в 2 часа ночи каждое воскресенье
Соответственно, класс настроек задач, задаваемых в этой задаче, будет выглядеть так:
public class SchedulerTaskSettings : TaskSettings
{
internal Duration duration;
public string startAt;
public SchedulerTaskSettings()
{
}
}
Аналогично нужно также сделать класс управления заданиями – наследник класса TaskLauncher. Так как в данной задаче задания кроме всего прочего еще и имеют параметры, указывающие время их запуска – работу с ними необходимо добавить в этот класс. Эти изменения коснутся только конструктора класса и метода Run(), в которых будет производиться вычисление времени следующего запуска задачи. Код этого класса достаточно прост, посему приведу его без комментариев
public class SchedulerTaskLauncher : TaskLauncher
{
private DateTime startTime;
private Duration duration;
public DateTime StartTime
{
get { return startTime; }
}
public SchedulerTaskLauncher(SchedulerTaskSettings task)
: base(task)
{
string[] starttime = task.startAt.Split(',');
TimeSpan time = TimeSpan.MinValue;
DateTime date = DateTime.MinValue;
switch (task.duration)
{
case Duration.Hour:
date = DateTime.Now;
int min = int.Parse(starttime[0]);
if (min < date.Minute)
time = new TimeSpan(date.Hour + 1, min, 0);
else
time = new TimeSpan(date.Hour, min, 0);
date = DateTime.Today;
break;
case Duration.Day:
time = TimeSpan.Parse(starttime[0]);
date = DateTime.Today;
break;
case Duration.Week:
time = TimeSpan.Parse(starttime[1]);
date = DateTime.Today.AddDays(-((int)DateTime.Today.DayOfWeek)).AddDays((int)(DayOfWeek)Enum.Parse(typeof(DayOfWeek), starttime[0]));
break;
case Duration.Month:
time = TimeSpan.Parse(starttime[1]);
date = DateTime.Today.AddDays(-DateTime.Today.Day).AddDays(int.Parse(starttime[0]));
break;
}
startTime = date.Add(time);
duration = task.duration;
}
public override void RunTask()
{
base.RunTask();
switch (duration)
{
case Duration.Hour:
startTime = startTime.AddHours(1);
break;
case Duration.Day:
startTime = startTime.AddDays(1);
break;
case Duration.Week:
startTime = startTime.AddDays(7);
break;
case Duration.Month:
startTime = startTime.AddMonths(1);
break;
}
}
}
Предварительная работа завершена, осталось реализовать метод Run() задачи Scheduler. И этот код нуждается в некоторых комментариях.
Во первых так как сама задача Scheduler работает по таймеру, то необходимо определить каким образом вычислять момент запуска той или иной задачи в Scheduler-е. Код этот достаточно простой – если разница между текущим временем и временем запуска задачи больше или равна 0 и меньше интервала запуска задачи Scheduler, то эта задача должна быть запущена на выполнение. Но для этого самой задаче Scheduler необходимо передать в параметре interval значение, равное значению управляющего параметра задачи seconds (или значение управляющего параметра seconds всей системы, если задача Scheduler использует общий таймер).
Во вторых же так как параметры передаются в задачу при вызове метода Run(), то в нем (при необходимости) нужно проинициализировать параметры задачи и закешировать их в полях класса.
В итоге код задачи Scheduler у меня лично получился вот таким
class Scheduler : ITask
{
private bool isInitialized = false;
private int interval;
private Dictionary<string, SchedulerTaskLauncher> taskList;
public void Run(XmlNodeList parameters)
{
if (!isInitialized)
{
taskList = new Dictionary<string, SchedulerTaskLauncher>();
foreach (XmlNode param in parameters)
{
if (param.Name == "param" && param.Attributes["name"].Value == "interval")
interval = int.Parse(param.Attributes["value"].Value);
else if (param.Name == "task")
{
SchedulerTaskSettings task = new SchedulerTaskSettings();
task.Name = param.Attributes["name"].Value;
task.Type = param.Attributes["type"].Value;
task.duration = (Duration)Enum.Parse(typeof(Duration), param.Attributes["duration"].Value);
task.startAt = param.Attributes["startAt"].Value;
task.XmlParameters = param.ChildNodes;
if (!taskList.ContainsKey(task.Name))
{
SchedulerTaskLauncher t = new SchedulerTaskLauncher(task);
taskList.Add(t.Name, t);
}
}
else
throw new ArgumentException("Unknown parameter: " + param.Name);
}
isInitialized = true;
}
foreach (SchedulerTaskLauncher task in taskList.Values)
{
TimeSpan t = DateTime.Now - task.StartTime;
if (t.TotalSeconds >= 0 && t.TotalSeconds <= interval)
{
task.RunTask();
}
}
}
}
Все, задача Scheduler для запуска задач по расписанию готова. Осталось привести пример конфигурационного файла этой задачи
<tasks seconds="60" isSingleThreaded="true" enabled="true">
<task name="scheduler" type="Dimon.Tasks.Scheduler, Dimon.Tasks" isSingleThreaded="false" enabled="true" seconds="60">
<param name="interval" value="60" />
<task name="test" type="Dimon.Tasks.Tests.TestTask, Dimon.Tasks.Tests" duration="Hour" startAt="30">
<param name="StringParam" value="value" type="System.String" />
<param name="IntParam" value="150" type="System.Int32" />
<param name="DateTimeParam" value="2005-11-11" type="System.DateTime" />
</task>
</task>
</tasks>
В этом примере задача Dimon.Tasks.Tests.TestTask будет вызываться в 30 минут каждого часа.
Заключение
Все, теперь уже точно все :). Планировщик задач написан, Scheduler к нему приделан – что еще нужно для нормальной работы? Конечно же данная реализация не может претендовать на лавры самой-самой – кому-то не понравится урезанная возможность задания расписания в задаче Scheduler, кто-то захочет в саму систему ввести уровень таймеров для объединения задач. Ну так дерзайте – сделать подобные расширения не так уж и сложно :).
При переписывании планировщика задач в современный вид множество идей было почерпнуто из реализации подобного функционала в Community Server 2.0.
Исходный код для статьи (реализация системы на .NET 1.1 и .NET 2 и тестовое приложение) можно скачать здесь. |