Пишем свой обработчик тасков на .NET

Всем привет.
В вашей компании не любят open-source?
Вы любите велосипеды?
Всегда было интересно, как устроены планировщики задач?
Под катом история о том, как мне пришлось сделать свой аналог известного open source планировщика quartz.net.

Предыстория


Все началось где-то года полтора назад, когда я перебрался на новую работу. Проект был интересный, сейчас он запущен и даже держит неплохую нагрузку, в веб части несколько сотен хитов в секунду, но это, полагаю, станет темой отдельной статьи. На определенном этапе разработки появилось требование выполнить “пару” задач асинхронно. Естественно, сразу стало ясно, что парой задач дело не обойдется и вылезет куча новых требований. Так и вышло, сейчас количество параллельных тасков перевалило за 20 и компонент развернули на нескольких серверах. В этой статье я расскажу о самой первой реализации, quick and dirty решении, которое позволило быстро стартовать.

У меня уже был опыт использования quartz.net — это порт известного java компонента quartz на .net платформу и, честно говоря, единственная известная мне более менее серьезная реализация планировщика на .net. Еще я слышал про Castle.Scheduling, но руки не дошли повозиться. Буду рад услышать о других решениях в комментариях. Я, как ни в чем не бывало, пришел к своему тимлиду с предложением использовать это решение, а тут меня и поджидал неприятный сюрприз.

Оказалось, что мы не можем вот так вот взять и использовать какой-то сторонний компонент. Ведь он не прошел одобрения совета архитекторов! Возможно, вы слышали об этих людях, проводящих половину дня на совещаниях, а в остальное время рисующих футуристичные картинки, в которые никто кроме них не заглядывает. Также необходимо одобрение отдела безопасности, который должен проверить, нет ли в коде закладок. А вот еще юристам неплохо бы посмотреть лицензионное соглашение, мало ли там что. Я ощутил себя Донкихотом, который выходит на сражение с ветряными мельницами и решил пойти своим путем.

Конечно, я понимал, что сроки не резиновые, поэтому полез в корпоративный FishEye и быстро понял, что не одинок. Я нашел две реализации этого функционала и даже захотел использовать одну из них. Пообщавшись с автором, я понял, что он не против, но мне придется заточиться на несколько библиотек созданных их командой и самостоятельно бороться со всеми багами, т.к. времени (читай — желания ) поддержать меня у них нет.

Как всяких уважающий себя программист, я не люблю ковыряться в чужих багах, я люблю создавать свои. Поэтому я засучил рукава и приступил к делу.

История


Время поджимало, надо было релизить прототип. Сразу скажу, что очень не хотелось связываться с Task Scheduler, встроенный в Windows, хотя рассматривали и такой вариант. Можно было сделать сходным с GAE образом и использовать интервалы для вызова методов вебсервиса. Можно было расковырять quartz, но дело это не благородное.
Хотелось сделать по-простому, чтобы в течении рабочего дня все заработало.
А как? Ну, давайте зададим наш таск самым простым образом

  1. public interface ITask
  2. {
  3.  void Execute();
  4. }
* This source code was highlighted with Source Code Highlighter.


Теперь определим простейший триггер — класс, который будет сохранять информацию об интервале выполнения таска

public interface ITrigger
{
 Guid Id { get; set; }

 DateTime? NextProcessTime { get; set; }

 TimeSpan Interval { get; set;}

 ITask Task { get; set;}
}


* This source code was highlighted with Source Code Highlighter.


После этого нужно задать в конфиге что-то в духе.

  1. <objects xmlns="http://www.springframework.net" xmlns:aop="http://www.springframework.net/aop">
  2.  <object id="SampleTrigger1" type="TaskHandler.BusinessLogic.Impl.GenericTrigger, TaskHandler.BusinessLogic">
  3.   <property name="Interval" value="10s"></property>
  4.   <property name="Task" ref="SampleTask1"></property>
  5.  </object>
  6.  <object id="SampleTask1" type="TaskHandler.BusinessLogic.Impl.SampleTask1, TaskHandler.BusinessLogic"></object>
  7.  <object id="ITriggerLoader" type="TaskHandler.BusinessLogic.Impl.TriggerLoaderImpl, TaskHandler.BusinessLogic"></object>
  8. </objects>
* This source code was highlighted with Source Code Highlighter.


И пусть оно выполняется каждые 10 секунд. Давайте посмотрим, как нам этого добиться.

Разбираем код


Чтобы наши таски выполнялись параллельно, нам понадобится custom thread pool.
Сразу оговорюсь, что фишки .net 4.0 не использовались, т.к. код писался до выхода оной в широкие массы.
Написание своего пула — дело не из легких, потребует много времени и, вероятнее всего, приведет к провалу, поэтому я взял готовый от признанного гуру в этих делах — Jon Skeet (тут найдется и множество других полезностей)

Сам по себе обработчик обернем в windows сервис, это тривиально, поэтому не будем на этом останавливаться. Посмотрим на картинку, которая пояснит работу планировщика.



Давайте разбираться, что здесь происходит.
Вначале, мы инициализируем наш пул каким-либо количеством потоков, которое напрямую зависит от количества процессорных ядер на нашем сервере. Не забывайте, что кроме планировщика процессорное время потребляет и ОС, и другие сервисы, поэтому лучше ограничиться разумным количеством.
Таски мы положим в словарь, чтобы в случае необходимости легко отслеживать их по идентификатору. В дальнейшем это может пригодиться нам для персистентности. Общая схема выполнения выглядит так:
  • Загрузить все наши таски
  • Запустить основной цикл
  • В цикле мы пробегаем по всем таскам и кладем их в очередь нашего пула.
  • Когда в очереди появляется задача, пул сразу передает ее в свободный поток на выполнение.
  • Поток выполнит задачу и снова вернется в состояние “готов”.


Для остановки выполнения мы используем Monitor и какую-нибудь булеву переменную aka Halted, которая просигналит основному циклу, что надо выйти.

Опытный глаз наверняка уже высмотрел в коде использование spring.net. Действительно, таски задаются как poco, управляемыемые spring. Это упростит нам динамическую загрузку и даст ряд преимуществ. Например, через interceptors мы сможем логгировать время выполнения тасков и использовать другие плюшки АОП.

Давайте поговорим немного о недостатках реализации.
Во-первых, мы не ограничиваем время выполнения каждого таска, что может привести к зависаниям, таймауты надо выставлять всегда.
Во-вторых, наш код весьма прямолинеен в том, что касается определения триггеров. Он не позволит нам выполнять таски в заданное время, например, каждый первый день месяца или каждое воскресенье.
Не могу не заметить, что в .net 4.0 появилось много вкусного для многопоточной работы, что также поможет улучшить и упростить наш код.

В заключении, пару слов о параллелизме. Практика показывает, что надо проектировать таски с учетом развертывания на нескольких серверах. Для этого таски не должны иметь состояния и элементы вашего workflow также должны быть максимально независимы. Старайтесь также избегать продолжительных тасков, лучше сделать много шустрых.

Если читателям будет интересно я планирую написать о реализации фич, которые считаю интересными. Среди них GUI, который позволит рулить фермой из таких планировщиков, видеть, какие таски выполняются, среднее время выполнения задач и проблемы, которые возникают. А также останавливать/возобновлять выполнение тасков на любом планировщике в ферме опять же через GUI.

Исходный код к статье можно брать тут.
Искренне рекомендую использовать в работе quartz.net и функционал из .net 4.0 вместо самописных решений.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 8

    0
    интересная тема, ждем продолжения
      0
      я планирую сделать цикл статей на эту тему и в итоге превратить этот велосипед в полноценное open-source решение на .net 4.0, как альтернативу quartz.net, который все-таки слишком близок к java и не использует родных для .net фич. Если есть желающие присоединиться — буду только рад, пишите в личку.
        0
        В Windows уже есть Task Scheduler 2.0 — чем он хуже самодельного?
          0
          Постараюсь ответить по пунктам
          1. нативного для C# API у него нет, нужно использовать COM обертки
          2. для запуска таска надо либо делать консольное (.exe) приложения для каждого таска, либо делать каждый таск, как COM объект, что согласитесь усложняет жизнь
          3. это внутренний компонент Windows, значит, на Mono его не используешь
          4. у него нет внешних интерфейсов, поэтому использовать его, как компонент кластера, не получится.
          Поверьте, я сам весьма ленив и не стал бы браться за что-то подобное для фана, но так уж вышло. Поэтому я и решил — почему бы не поделиться с людьми полученным продуктом и опытом?

            0
            1. COM Interop это довольно просто
            2. Ну это еще проще
            3. Можно было бы сделать обертку для него и использовать либо его, либо свою собственную реализацию
            4. Опять-же wcf сервис проксирующий запросы не такая большая проблема.

            За 2-3 дня можно написать вполне рабочее решение. потом еще потратили-бы пару дней на правильную генерацию interop и все :)
              0
              Ага, ага. И заставить все это использовать не шибко продвинутых китайцев и индусов, коих все больше в компании, тем самым обеспечив себе безбедную жизнь на поддержке на долгие времена. :)
                0
                Ну так за чем-же дело встало :) + Чужие баги править не надо :) и уволить никто и никогда не уволит :) знаете все это надо написать на F# с использованием T4 :)
                  0
                  Да, не, лучше сразу на Nemerle, его вообще юзают только пара преданных энтузиастов с rsdn.
                  Кстати, у меня на полном серьезе была мысль сделать свой DSL для задания триггеров.
                  Например, согласитесь — читать «every 5 minutes», намного приятнее, чем «0 0/5 * * * ?»

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое