Урок 24. Планировщик задач

Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.

Инструкция по подключению шаблонов находится по ссылке.

В прошлом уроке мы узнали про планировщик задач <Scheduler>, который по расписанию или при старте сервера выполняет задачи. И создали задачу на отправку писем с поздравлениями при старте сервера:

Template.xml
<Scheduler>
  <Task Name="HappyBirthdayEmailSendTask">
    <Condition Type="OnStart" />
    <Commands>
      <Command Name="HappyBirthdayEmailSendCommand" />
    </Commands>
  </Task>
</Scheduler>

Когда мы первый раз запустили сервер с задачей о рассылки писем, движок создал в базе данных таблицу public.schedule. В эту таблицу сервер будет писать последнее время выполнения каждой задачи. После выполнения нашей задачи в таблице появилась запись:

Расписание запусков

Для задач можно настроить расписание, по которому они будут выполнятся. Есть два способа задать расписание:

  • прописать его в серверном xml-файле, тогда разработчик будут сам его редактировать при необходимости;

  • вынести настройки расписания в базу данных и через интерфейс приложения позволить пользователю самому управлять расписанием.

Статическое расписание

Для того чтобы задать статического расписания для выполнения задачи, изменим код следующим образом:

Template.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>:

Template.xml
<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;

Переделаем синтаксис задачи:

Template.xml
<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, которая будет обновлять расписание задачи:

Template.xml
<Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand" Type="SchedulerConditionRefreshCommand">
  <Task Name="HappyBirthdayEmailSendTask" />
</Command>

Эту команду будем вызывать с формы настроек. А значит необходимо добавить команду в соответствующее разрешение, чтобы пользователь имел права доступа к ней:

Template.xml
<Permission Name="SettingsEditPermission">
  <AccessPoint Name="SettingsEditAccessPoint" />
  <SqlQuery Name="SettingsUpdateSqlQuery" />
  <Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand" />
</Permission>

Теперь вернемся на форму настроек (TemplateSettings.xml) и добавим условие, проверяющее, изменилось ли время запуска рассылки:

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:

TemplateSettings.xml
<Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommandCallCommand" Type="CallCommand" Assembly="Commands">
  <Condition Name="AutoSendBdayEmailTimeChandedCondition" />
  <Workflow Name="Template" />
  <Command Name="HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand" />
</Command>

И добавим ее вызов в последовательность команд на сохранение изменений (SaveSequentialCommand).

Условия на сервере

Вернемся в серверный файл Template.xml и добавим условие проверки, включена ли автоматическая рассылка поздравлений. Для этого перед тэгом <Commands> добавим тэг <Conditions>, в который добавим условие типа SqlQueryCondition:

Template.xml
<Condition Name="AutoSendBirthdayEmailSqlQueryCondition" Type="SqlQueryCondition">
  <Text>
    SELECT auto_send_bday_email FROM template.settings;
  </Text>
</Condition>

Скорректируем задачу рассылки писем, добавив проверку условия:

Template.xml
<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