Урок 24. Планировщик задач
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
В прошлом уроке мы узнали про планировщик задач <Scheduler>
, который по расписанию или при старте сервера выполняет задачи. И создали задачу на отправку писем с поздравлениями при старте сервера:
<Scheduler>
<Task Name="HappyBirthdayEmailSendTask">
<Condition Type="OnStart" />
<Commands>
<Command Name="HappyBirthdayEmailSendCommand" />
</Commands>
</Task>
</Scheduler>
Когда мы первый раз запустили сервер с задачей о рассылки писем, движок создал в базе данных таблицу public.schedule. В эту таблицу сервер будет писать последнее время выполнения каждой задачи. После выполнения нашей задачи в таблице появилась запись:

Расписание запусков
Для задач можно настроить расписание, по которому они будут выполнятся. Есть два способа задать расписание:
прописать его в серверном xml-файле, тогда разработчик будут сам его редактировать при необходимости;
вынести настройки расписания в базу данных и через интерфейс приложения позволить пользователю самому управлять расписанием.
Статическое расписание
Для того чтобы задать статического расписания для выполнения задачи, изменим код следующим образом:
<Task Name="HappyBirthdayEmailSendTask">
<Condition Type="Values">
<Hour>10-15</Hour>
<Minute>/20</Minute>
</Condition>
<Commands>
<Command Name="HappyBirthdayEmailSendCommand" />
</Commands>
</Task>
У атрибута Type
тэга <Condition>
сменили значение на Values и добавили вложенные тэги с настройкой времени. Доступны тэги: <Month>
, <Day>
, <Hour>
, <Minute>
и <Second>
.
В тэге <Hour>
мы задали период, в пределах которого будет выполняться задача, а в тэге <Minute>
указали интервал, через который задача будет повторяться. Т.е. планировщик будет запускать нашу задачу каждый день в период с 10 до 15 часов с повтором каждые 20 минут.
У тэга <Task>
есть необязательный вложенный тэг <ExecutionStrategy>
, который задает правило обработки пропущенных плановых запусков. Если тэг не указан, то будет использоваться значение ExecuteMissed, которое означает, что будут выполняться все пропущенные запуски.
В нашем случае нет необходимости выполнять все пропущенный запуски. Достаточно, чтобы выполнился последний пропущенный. Поэтому будем использовать значение ExecuteLastMissed для тэга <ExecutionStrategy>
:
<Task Name="HappyBirthdayEmailSendTask">
<Condition Type="Values">
<Hour>10-15</Hour>
<Minute>/20</Minute>
</Condition>
<Commands>
<Command Name="HappyBirthdayEmailSendCommand" />
</Commands>
<ExecutionStrategy>ExecuteLastMissed</ExecutionStrategy>
</Task>
Динамическое расписание
Для динамических расписаний у атрибута Type
тэга <Condition>
используется значение Query. В этом случае необходимо прописывать sql-запрос, с помощью которого будем получать настройки интервала выполнения задачи. Sql-запрос должен возвращать хотя бы один столбец с именем из списка: Month, Day, Hour, Minute, Second.
Давайте в таблицу template.settings добавим колонку, которую будем использовать для хранения времени отправки писем:
ALTER TABLE template.settings
ADD COLUMN auto_send_bday_email_time time without time zone;
UPDATE template.settings
SET
auto_send_bday_email_time = '05:00:00'::time;
ALTER TABLE template.settings
ALTER COLUMN auto_send_bday_email_time SET NOT NULL;
Переделаем синтаксис задачи:
<Task Name="HappyBirthdayEmailSendTask">
<Condition Type="Query">
<Text>
SELECT
extract(hour FROM auto_send_bday_email_time) AS "Hour",
extract(minute FROM auto_send_bday_email_time) AS "Minute"
FROM template.settings;
</Text>
</Condition>
<Commands>
<Command Name="HappyBirthdayEmailSendCommand" />
</Commands>
<ExecutionStrategy>ExecuteLastMissed</ExecutionStrategy>
</Task>
Теперь наша задача будет выполняться при старте сервера, если был пропущен предыдущий запуск, и каждый день один раз в определенное время, которое указано в настройках.
Рестарт задачи
Скорректируйте форму настроек, добавив на вкладку "Email" поле для редактирования времени отправки писем и CheckBox для включения/отключения автоматической рассылки:

Добавим в таблицу template.settings признак автоматической отправки:
ALTER TABLE template.settings
ADD COLUMN auto_send_bday_email boolean NOT NULL DEFAULT false;
Ранее в уроках упоминалось, что платформа только дату и только время из DateTimePicker передает на сервер без приведения к UTC. Таким образом, время запуска рассылки будет сохранено в базе данных так, как его указали на форме. И сервер будет воспринимать это время в своем часовом поясе.
Чтобы этого не происходило, необходимо вручную скорректировать время при сохранении в базу данных (SettingsUpdateSqlQuery), используя для этого функцию:
CREATE OR REPLACE FUNCTION public.convert_time_to_server_timezone(time without time zone)
RETURNS time without time zone AS
$BODY$
-- используется для преобразования времени из часового пояса КЛИЕНТА к часовому поясу СЕРВЕРА
DECLARE
_time_zone_name text;
BEGIN
_time_zone_name = time_zone_info.name
FROM
"user" pu
LEFT JOIN time_zone_info USING (time_zone_info_id)
WHERE
pu.user_id = current_setting('ws.public_user_id')::smallint;
RETURN (current_date + $1) at time zone _time_zone_name at time zone current_setting('ws.server_time_zone');
END;
$BODY$
LANGUAGE plpgsql;
Соответствующим образом необходимо преобразовывать время, которое получаем из базы данных и передаем на форму настроек (SettingsSelectSqlQuery):
CREATE OR REPLACE FUNCTION public.convert_time_to_user_timezone(time without time zone)
RETURNS time without time zone AS
$BODY$
-- используется для преобразования времени из часового пояса СЕРВЕРА к часовому поясу КЛИЕНТА
DECLARE
_time_zone_name text;
BEGIN
_time_zone_name = time_zone_info.name
FROM
"user" pu
LEFT JOIN time_zone_info USING (time_zone_info_id)
WHERE
pu.user_id = current_setting('ws.public_user_id')::smallint;
RETURN (current_date + $1) at time zone current_setting('ws.server_time_zone') at time zone _time_zone_name;
END;
$BODY$
LANGUAGE plpgsql;
После того, как мы обновили время запуска рассылки, необходимо перезапустить задачу в планировщике, чтобы она подтянула новые настройки.
Вернемся в серверный xml-файл и создадим команду SchedulerConditionRefreshCommand, которая будет обновлять расписание задачи:
<Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand" Type="SchedulerConditionRefreshCommand">
<Task Name="HappyBirthdayEmailSendTask" />
</Command>
Эту команду будем вызывать с формы настроек. А значит необходимо добавить команду в соответствующее разрешение, чтобы пользователь имел права доступа к ней:
<Permission Name="SettingsEditPermission">
<AccessPoint Name="SettingsEditAccessPoint" />
<SqlQuery Name="SettingsUpdateSqlQuery" />
<Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand" />
</Permission>
Теперь вернемся на форму настроек (TemplateSettings.xml) и добавим условие, проверяющее, изменилось ли время запуска рассылки:
<Condition Name="AutoSendBdayEmailTimeChandedCondition" Type="NotEqualCondition" Assembly="Conditions">
<Items>
<Item>
<DataConnection SourceDataConnection="SettingsPrimaryGetDataConnection">
<Fields>
<Field Name="AutoSendBdayEmailTime" />
</Fields>
</DataConnection>
</Item>
<Item>
<Object Name="AutoSendBdayEmailTimeDateTimePicker" />
</Item>
</Items>
<DataType Type="TimeSpanDataType" />
</Condition>
Чтобы с клиентской формы можно было вызывать команду на сервере, в платформе реализована команда CallCommand. Давайте создадим такую на форме настроек, чтобы на сервере вызывать команду HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand:
<Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommandCallCommand" Type="CallCommand" Assembly="Commands">
<Condition Name="AutoSendBdayEmailTimeChandedCondition" />
<Workflow Name="Template" />
<Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand" />
</Command>
И добавим ее вызов в последовательность команд на сохранение изменений (SaveSequentialCommand).
Условия на сервере
Вернемся в серверный файл Template.xml и добавим условие проверки, включена ли автоматическая рассылка поздравлений. Для этого перед тэгом <Commands>
добавим тэг <Conditions>
, в который добавим условие типа SqlQueryCondition:
<Condition Name="AutoSendBirthdayEmailSqlQueryCondition" Type="SqlQueryCondition">
<Text>
SELECT auto_send_bday_email FROM template.settings;
</Text>
</Condition>
Скорректируем задачу рассылки писем, добавив проверку условия:
<Task Name="HappyBirthdayEmailSendTask">
<Condition Type="Query">
<Text>
SELECT
extract(hour FROM auto_send_bday_email_time) AS "Hour",
extract(minute FROM auto_send_bday_email_time) AS "Minute"
FROM template.settings;
</Text>
</Condition>
<Commands>
<If>
<When>
<Condition Name="AutoSendBirthdayEmailSqlQueryCondition" />
</When>
<Then>
<Command Name="HappyBirthdayEmailSendCommand" />
</Then>
</If>
</Commands>
<ExecutionStrategy>ExecuteLastMissed</ExecutionStrategy>
</Task>
Задача будет выполняться по расписанию, но сама команда будет выполняться, если пользователь включит настройку.
Итоги
В этом уроке мы рассмотрели, как настроить планировщик задач на стороне сервера и как динамически менять параметры расписания задач. Также узнали о том, как с клиентских форм вызывать команды на стороне сервера.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.
Last updated