|
|
|||||||||||||||||||||||||||||
|
Планировщик задач в ASP.NET (исходники)Источник: ASPNET Mania Дмитрий Руденко
ВведениеИногда перед веб разработчиками возникают задачи, на первый взгляд в принципе неразрешимые если исходить из общего строения веб приложений и общения "запрос-ответ". Одной из таких задач является задача реализации системы повторяющихся заданий, т.е. заданий, выполняемых через определенные промежутки времени или в указанное время, например регулярная очистка таблиц в базе данных или отсылка email сообщений из очереди. В этом случае счастливые обладатели выделенных серверов пишут windows сервисы или запускают консольные приложения с помощью встроенного в Windows планировщика задач. Чуть менее счастливым приходится договариваться о подобных сервисах с хостером. Ну а основная масса решает такую задачу путем дергания с помощью того же планировщика страниц, в логике которых забиваются необходимые задания. Но на самом деле подобные задачи в ASP.NET можно реализовать намного проще, не прибегая к использованию сторонних средств. Краткое описаниеВкратце алгоритм решения подобной задачи крайне прост: при старте веб приложения запускается таймер System.Threading.Timer, в колбек методе которого и пишется необходимая логика задания. И тогда коллбек метод таймера будет вызываться через указанный промежуток времени все время работы веб приложения. На всякий случай я приведу пример описанного выше кода, запускающего с интервалом в одну минуту метод, пишущий сообщение о своем вызове в лог файл (код расположен в файле global.asax).
Данное решение, безусловно, имеет право на существование, но оно не отличается ни особым изяществом, ни удобством применения - ведь каждый раз для изменения логики работы задач придется пересобирать все веб приложение. Поэтому, взяв за основу вышенаписанное я постараюсь реализовать более удобоваримую систему для планировщика задач, одинаково пригодную для использования как в веб, так и в windows приложениях. Сама система состоит из 2-х модулей - общего модуля управления заданиями и модуля управления отдельным заданием. Задания же представляют собой классы, реализующие интерфейс ITask с единственным методом Run(), в котором и реализуется логика задания. Ну а конфигурация всей системы реализована в виде секции конфигурационного файла. Программная реализация системы повторяющихся заданийКраткое отступление. Весь код этой статьи написан на C# 2.0 и .NET 2.0. Но в архиве, содержащем исходный код статьи, находятся проекты и для .NET 2, и для .NET 1.1. ЗаданияНачнем мы, пожалуй, с простейшего - описания интерфейса ITask для заданий. Как я уже упомянул выше, этот интерфейс содержит единственный метод Run с параметром типа XmlNodeList для передачи параметров заданию. Вы спросите "а почему тип XmlNodeList?" Тут все достаточно просто - так как настройка заданий производится в .config файле, мы можем просто передать часть этой настройки, относящуюся к данному заданию. Честно говоря, я думал о применении чего-нибудь более удобного (например, того же Hashtable), но при реализации задания Scheduler, о котором я расскажу в конце статьи, я столкнулся с тем, что для большего удобства лучше не ограничивать возможность задания параметров. Итак, весь код интерфейса ITask будет таким
Классы конфигурацииТеперь создадим конфигурационные классы и реализуем класс секции конфигурационного файла. Фактически конфигурация системы будут состоять из трех уровней - уровня настроек самой системы, уровня настроек отдельных задач и уровня параметров задач. Про параметры задач я уже писал выше - они будут задаваться в свободном виде и работа с ними целиком и полностью будет выполняться в самой задаче. Настройки отдельной задачи содержат имя и тип задачи (имя задачи должно быть уникально среди всех задач системы), метку о том, включена ли задача, метку о том, использует ли задача общий таймер системы или свой собственный таймер и интервал (в секундах) вызова задачи. Ну а в настройках самой системы есть метка о том, включена ли система, метка об использовании общего таймера и интервал срабатывания общего таймера в секундах. И, кроме того, настройки верхнего уровня содержат в себе настройки более низкого уровня, т.е. в настройки задачи включаются и параметры задачи, а в настройках системы содержатся настройки всех задач. И раз уж зашел разговор об общем таймере, то придется рассказать, что это такое. В общем случае для каждой задачи запускается свой собственный таймер с уникальными настройками интервала срабатывания. Но так как немалое количество рутинных задач частенько вызывается с одним и тем же интервалом - имеет смысл объединить их вызовы в одном таймере. Но, в отличие от собственного таймера задачи этот таймер запускается в главном управляющем классе системы. В принципе по настройке все и теперь мы можем взглянуть на получившийся код
В приведенном коде класс TasksSettings - это настройки системы, TaskSettings - настройки отдельного класса, ну а XmlParameters - параметры задачи. Класс секции конфигурационного файла тоже не представляет собой ничего сложного и про принципы его создания я уже недавно писал в статье о создании системы статистики сайта. Поэтому просто приведу этот класс без дополнительных объяснений.
Ну и напоследок, пожалуй, нужно привести кусок конфигурационного файла с описанием какой-то задачи
Теперь, когда с конфигурацией покончено, настала пора перейти к классу управления задачей. Классы управления заданиямиДанный класс, по сути, является оберткой над классом задачи и служит для создания экземпляра класса задачи и вызова метода Run этого экземпляра. Кроме того, именно этот класс содержит таймер для задач, работающих с собственным таймером. Ну и вдобавок ко всему этот класс выставляет наружу свойства запуска задачи для управляющего класса. Но прежде чем глядеть на класс управления заданием, давайте рассмотрим общий принцип работы системы. Как я уже упоминал ранее, основную работу по управлению планировщиком задач выполняет отдельный класс - TaskEngine. Этот синглтон (singleton) класс имеет 2 метода Start() и Stop() соотв. для запуска системы и для ее остановки. Метод Start() читает конфигурацию планировщика задач и на основе данных из конфигурации формирует список экземпляров класса управления заданием TaskLauncher и запускает таймеры задач (если система не работает с единым таймером или если данное задание использует собственный таймер). Кроме того, в методе Start(), если это необходимо, создается и запускается общий таймер системы. Метод Stop(), соответственно, уничтожает все классы управления задачами в списке и общий таймер пданировщика (если он используется). Ну и, естественно, класс TaskEngine содержит коллбек метод общего таймера, в котором происходит вызов активных задач, использующих общий таймер. Класс управления задачами TaskLauncher в свою очередь, используя настройки задачи, создает экземпляр класса указанного в настройках типа и в своих свойствах выставляет наружу некоторые свойства задачи, как то имя и тип задачи, метку использования задачей общего таймера, метку, активна ли задача и признак того, что задача выполняется. Ну и, кроме того, этот класс содержит методы для создания экземпляра класса задачи, запуска задачи (вызова метода Run экземпляра класса) и работу с таймером (в случае, если задача работает с собственным таймером). Вот, пожалуй, и все про общее описание работы планировщика задач и мы можем, наконец, перейти к рассмотрению кода классов. Для начала я приведу код класса управления заданием TaskLauncher. Описание свойств данного класса я уже дал, поэтому приведу этот код без комментариев
Более интересен метод GetInstance(), возвращающий экземпляр класса задачи. В случае, если по каким-то причинам экземпляр класса задачи создать не получается - этот метод кроме всего прочего выключает задачу
Метод Run() запуска задачи также не отличается особенной сложностью - он вызывает метод GetInstance() для получения экземпляра класса задачи и в случае возврата экземпляра класса просто вызывает его метод ITask.Run()
Кроме этого в классе есть пара методов для работы с собственным таймером - метод инициализации таймера и коллбек метод самого таймера, вызывающий только что рассмотренный нами метод Run()
Ну и последний метод этого класса, Dispose(), зачищает таймер в случае его наличия
Теперь можно перейти к рассмотрению кода класса управления системой TaskEngine. Для работы этот singleton класс использует список классов управления задачами и таймер. Кроме того, в нем есть свойства для доступа к текущим задачам системы. Вообщем это проще показать кодом, нежели описать ;)
Более интересен метод Start(), запускающий систему. В этом методе загружаются настройки планировщика задач, на основе этих настроек создается список классов управления задачами и запускаются все необходимые для работы таймеры (внутренние таймеры для задач и общий таймер для всей системы).
Метод Stop() же делает обратные действия - уничтожает все классы управления задачами и останавливает и уничтожает общий таймер.
Ну и последний метод - коллбек метод таймера - запускает активные задачи, использующие общий таймер
Все, планировщик задач готов и теперь можно приступать к тестам. Тестируем планировщик задачКак я уже упоминал ранее, данная система одинаково хорошо будет работать как в веб, так и в windows приложениях. И дабы не усложнять себе жизнь тесты работоспособности планировщика задач мы будем делать в консольном приложении - и наглядней получится, и с правами проблем избежим. Первым делом для тестов нужно написать класс задачи, реализующий интерфейс ITask. Это будет простенький класс, просто выводящий дату начала своей работы и все передаваемые ему параметры. Вообщем не буду развозить кашу по столу, а покажу код этого класса
Как вы, я надеюсь, помните, параметры задачи передаются в метод Run в виде XmlNodeList и вся логика для получения значений этих параметров должна быть написана в этом методе. В данном случае параметры будут передаваться в виде элемента <param name="имя параметра" value="значение параметра" type="тип параметра" /> и для получения их значений я воспользовался методом Convert.ChangeType(). Код конфигурирования планировщика задач в .config файле приложения будет точно таким же, как я приводил выше. На всякий случай повторюсь
В данном случае планировщик настроен на выполнение задачи раз в 60 секунд и для этого используется общий таймер. Осталось написать код для запуска планировщика задач в приложении
И можно запустить получившееся приложение и наслаждаться результатом - теперь раз в минуту выполняемая в планировщике задач задача будет выводить сообщение в консоль. Вот, пожалуй, и все, что можно было написать о создании планировщика задач для .NET приложения. Все, да не совсем - данная система все таки не позволяет задавать вызов задач по расписанию, а только лишь по таймеру. И задавать таймер для, например, задачи, выполняющейся раз в день или, и того хуже, раз в неделю как-то не совсем правильно (да и не факт, что это задание вообще будет выполняться исходя из того, что таймер привязан к приложению, которое имеет особенность перегружаться). Посему описанный выше планировщик задач необходимо расширить. Задача "Задачи по расписанию"Можно было бы добавить необходимый функционал в саму систему планировщика задач, но мне показалось более правильным поступить иначе - реализовать задачу "Задачи по расписанию". Идея реализации этой задачи очень проста - параметрами задачи задаются задачи с расписанием, а при срабатывании таймера задачи вычисляется необходимость запуска той или иной заданной задачи. Фактически получается реализация написанного выше планировщика задач в миниатюре с учетом дополнительных параметров в виде задания расписания запуска задач. Я не буду слишком сильно углубляться в задание расписания для задач, дабы не усложнять код. Дополнительно к стандартным параметрам задач, описанным ранее в классе TaskSettings, каждая задача может запускаться один раз в какой-то промежуток времени - час, день, неделю или месяц в указанный день (если нужно) и время. Для задания этих значений в конфигурационном файле служат атрибуты duration и startAt. При этом duration принимает значения из перечисления
А значение параметра startAt задается по следующему принципу:
Соответственно, класс настроек задач, задаваемых в этой задаче, будет выглядеть так:
Аналогично нужно также сделать класс управления заданиями - наследник класса TaskLauncher. Так как в данной задаче задания кроме всего прочего еще и имеют параметры, указывающие время их запуска - работу с ними необходимо добавить в этот класс. Эти изменения коснутся только конструктора класса и метода Run(), в которых будет производиться вычисление времени следующего запуска задачи. Код этого класса достаточно прост, посему приведу его без комментариев
Предварительная работа завершена, осталось реализовать метод Run() задачи Scheduler. И этот код нуждается в некоторых комментариях. Во первых так как сама задача Scheduler работает по таймеру, то необходимо определить каким образом вычислять момент запуска той или иной задачи в Scheduler-е. Код этот достаточно простой - если разница между текущим временем и временем запуска задачи больше или равна 0 и меньше интервала запуска задачи Scheduler, то эта задача должна быть запущена на выполнение. Но для этого самой задаче Scheduler необходимо передать в параметре interval значение, равное значению управляющего параметра задачи seconds (или значение управляющего параметра seconds всей системы, если задача Scheduler использует общий таймер). Во вторых же так как параметры передаются в задачу при вызове метода Run(), то в нем (при необходимости) нужно проинициализировать параметры задачи и закешировать их в полях класса. В итоге код задачи Scheduler у меня лично получился вот таким
Все, задача Scheduler для запуска задач по расписанию готова. Осталось привести пример конфигурационного файла этой задачи
В этом примере задача Dimon.Tasks.Tests.TestTask будет вызываться в 30 минут каждого часа. ЗаключениеВсе, теперь уже точно все :). Планировщик задач написан, Scheduler к нему приделан - что еще нужно для нормальной работы? Конечно же данная реализация не может претендовать на лавры самой-самой - кому-то не понравится урезанная возможность задания расписания в задаче Scheduler, кто-то захочет в саму систему ввести уровень таймеров для объединения задач. Ну так дерзайте - сделать подобные расширения не так уж и сложно :). При переписывании планировщика задач в современный вид множество идей было почерпнуто из реализации подобного функционала в Community Server 2.0.
|
|