# Урок 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wfsys.gitbook.io/wt-practice/customization/lesson_scheduler.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
