Планировщик задач в ASP.NET (исходники)

Дмитрий Руденко

Введение

Иногда перед веб разработчиками возникают задачи, на первый взгляд в принципе неразрешимые если исходить из общего строения веб приложений и общения "запрос-ответ". Одной из таких задач является задача реализации системы повторяющихся заданий, т.е. заданий, выполняемых через определенные промежутки времени или в указанное время, например регулярная очистка таблиц в базе данных или отсылка 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.


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=7163