> For the complete documentation index, see [llms.txt](https://wfsys.gitbook.io/wt-practice/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wfsys.gitbook.io/wt-practice/customization/lesson_scheduler.md).

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

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](/wt-practice/customization/lesson_custom_command_on_server.md#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](/wt-practice/main/lesson_list_form.md#podklyuchenie-shablonov-k-proektu).
{% endhint %}

В прошлом уроке мы узнали про планировщик задач [`<Scheduler>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/sheduler), который по расписанию или при старте сервера выполняет задачи. И создали задачу на отправку писем с поздравлениями при старте сервера:

{% code title="Template.xml" %}

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

{% endcode %}

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

<figure><img src="/files/BbOe4zj1ArZ0riHUwSEn" alt=""><figcaption></figcaption></figure>

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

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

* прописать его в серверном xml-файле, тогда разработчик будут сам его редактировать при необходимости;
* вынести настройки расписания в базу данных и через интерфейс приложения позволить пользователю самому управлять расписанием.

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

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

{% code title="Template.xml" %}

```xml
<Task Name="HappyBirthdayEmailSendTask">
  <Condition Type="Values">
    <Hour>10-15</Hour>
    <Minute>/20</Minute>
  </Condition>
  <Commands>
    <Command Name="HappyBirthdayEmailSendCommand" />
  </Commands>
</Task>
```

{% endcode %}

У атрибута `Type` тэга `<Condition>` сменили значение на **Values** и добавили вложенные тэги с настройкой времени. Доступны тэги: `<Month>`, `<Day>`, `<Hour>`, `<Minute>` и `<Second>`.

В тэге `<Hour>` мы задали период, в пределах которого будет выполняться задача, а в тэге `<Minute>` указали интервал, через который задача будет повторяться. Т.е. планировщик будет запускать нашу задачу каждый день в период с 10 до 15 часов с повтором каждые 20 минут.

{% hint style="info" %}
Время указывается в часовом поясе сервера!
{% endhint %}

У тэга `<Task>` есть необязательный вложенный тэг [`<ExecutionStrategy>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/sheduler#execution_strategy), который задает правило обработки пропущенных плановых запусков. Если тэг не указан, то будет использоваться значение **ExecuteMissed**, которое означает, что будут выполняться все пропущенные запуски.

В нашем случае нет необходимости выполнять все пропущенный запуски. Достаточно, чтобы выполнился последний пропущенный. Поэтому будем использовать значение ExecuteLastMissed для тэга `<ExecutionStrategy>`:

{% code title="Template.xml" %}

```xml
<Task Name="HappyBirthdayEmailSendTask">
  <Condition Type="Values">
    <Hour>10-15</Hour>
    <Minute>/20</Minute>
  </Condition>
  <Commands>
    <Command Name="HappyBirthdayEmailSendCommand" />
  </Commands>
  <ExecutionStrategy>ExecuteLastMissed</ExecutionStrategy>
</Task>
```

{% endcode %}

### Динамическое расписание

Для динамических расписаний у атрибута `Type` тэга `<Condition>` используется значение **Query**. В этом случае необходимо прописывать sql-запрос, с помощью которого будем получать настройки интервала выполнения задачи. Sql-запрос должен возвращать хотя бы один столбец с именем из списка: Month, Day, Hour, Minute, Second.

Давайте в таблицу **template.settings** добавим колонку, которую будем использовать для хранения времени отправки писем:

```sql
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;
```

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

{% code title="Template.xml" %}

```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>
```

{% endcode %}

Теперь наша задача будет выполняться при старте сервера, если был пропущен предыдущий запуск, и каждый день один раз в определенное время, которое указано в настройках.

### Рестарт задачи

Скорректируйте форму настроек, добавив на вкладку "Email" поле для редактирования времени отправки писем и CheckBox для включения/отключения автоматической рассылки:

<figure><img src="/files/nnTuJ59XmQU3T1M6ojf0" alt=""><figcaption></figcaption></figure>

Добавим в таблицу template.settings признак автоматической отправки:

```sql
ALTER TABLE template.settings
  ADD COLUMN auto_send_bday_email boolean NOT NULL DEFAULT false;
```

Ранее в уроках упоминалось, что платформа только дату и только время из DateTimePicker передает на сервер без приведения к UTC. Таким образом, время запуска рассылки будет сохранено в базе данных так, как его указали на форме. И сервер будет воспринимать это время в своем часовом поясе.

Чтобы этого не происходило, необходимо вручную скорректировать время при сохранении в базу данных (SettingsUpdateSqlQuery), используя для этого функцию:

```sql
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):

```sql
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;
```

После того, как мы обновили время запуска рассылки, необходимо перезапустить задачу в планировщике, чтобы она подтянула новые настройки.&#x20;

Вернемся в серверный xml-файл и создадим команду SchedulerConditionRefreshCommand, которая будет обновлять расписание задачи:

{% code title="Template.xml" %}

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

{% endcode %}

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

{% code title="Template.xml" %}

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

{% endcode %}

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

{% code title="TemplateSettings.xml" %}

```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>
```

{% endcode %}

Чтобы с клиентской формы можно было вызывать команду на сервере, в платформе реализована команда [CallCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/call_command). Давайте создадим такую на форме настроек, чтобы на сервере вызывать команду HappyBirthdayEmailSendTaskSchedulerConditionRefreshCommand:

{% code title="TemplateSettings.xml" %}

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

{% endcode %}

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

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

Вернемся в серверный файл Template.xml и добавим условие проверки, включена ли автоматическая рассылка поздравлений. Для этого перед тэгом `<Commands>` добавим тэг [`<Conditions>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/conditions), в который добавим условие типа [SqlQueryCondition](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/conditions/sql_query_condition):

{% code title="Template.xml" %}

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

{% endcode %}

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

{% code title="Template.xml" %}

```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>
```

{% endcode %}

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

## Итоги <a href="#results" id="results"></a>

В этом уроке мы рассмотрели, как настроить планировщик задач на стороне сервера и как динамически менять параметры расписания задач. Также узнали о том, как с клиентских форм вызывать команды на стороне сервера.

## Ответы <a href="#answer" id="answer"></a>

В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson24-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson24-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson24-answer.zip</a></td></tr></tbody></table>
