Scheduler

Дата обновления перевода 2024-08-01

Scheduler

Компонент scheduler управляет планированием задач в вашем PHP-приложении, например
запуск задачи каждую ночь в 3 часа утра, раз в две недели, кроме праздников, или любое другое пользовательское расписание, которое вам может понадобиться.

Этот компонент полезен для планирования таких задач, как обслуживание (очистка базы данных, очистка кеша и т.д.), фоновая обработка (обработка очередей, синхронизация данных, и т. д.), периодические обновления данных, запланированные уведомления (электронные письма, оповещения) и многое другое.

Этот документ посвящен использованию компонента Scheduler в контексте полностекового приложения Symfony.

Установка

В приложениях, использующих Symfony Flex , выполните эту команду, чтобы
установить компонент scheduler:

1
$ composer require symfony/scheduler

Основы Symfony Scheduler

Основное преимущество использования этого компонента заключается в том, что автоматизация управляется вашим приложением, что дает вам большую гибкость, которая невозможна при использовании заданий cron (например, динамические расписания, основанные на определенных условиях).

По своей сути компонент Scheduler позволяет вам создать задачу (называемую сообщением), которая выполняется сервисом и повторяется по некоторому расписанию. Он имеет некоторые похожие черты с компонентом Symfony Messenger (например, сообщение, обработчик, автобус, транспорт и т.д.), но главное отличие в том, что Messenger не может решать повторяющиеся задачи через регулярные промежутки времени.

Рассмотрим следующий пример приложения, которое отправляет некоторые отчеты клиентам по расписанию. Сначала создайте сообщение Scheduler, которое представляет задачу создания отчета:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Scheduler/Message/SendDailySalesReports.php
namespace App\Scheduler\Message;

class SendDailySalesReports
{
    public function __construct(private int $id) {}

    public function getId(): int
    {
        return $this->id;
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Scheduler/Handler/SendDailySalesReportsHandler.php
namespace App\Scheduler\Handler;

use App\Scheduler\Message\SendDailySalesReports;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class SendDailySalesReportsHandler
{
    public function __invoke(SendDailySalesReports $message)
    {
        // ... сделать какую-то работу, чтобы отправить отчет клиентам
    }
}

Вместо того чтобы отправлять эти сообщения немедленно (как в компоненте Messenger), цель состоит в том, чтобы создавать их на основе заранее определенной частоты. Это возможно благодаря SchedulerTransport, специальному транспорту для сообщений Scheduler.

Транспорт автономно генерирует различные сообщения в соответствии с назначенной частотой. Следующие изображения иллюстрируют различия между обработкой сообщений в компонентах Messenger и Scheduler:

In Messenger:

Базовый цикл Symfony Messenger

In Scheduler:

Базовый цикл Symfony Scheduler

Еще одно важное отличие заключается в том, что сообщения в компоненте Scheduler являются повторяющимися. Они представлены через класс RecurringMessage.

Прикрепление повторяющихся сообщений к расписанию

Конфигурация частоты сообщений хранится в классе, который реализует ScheduleProviderInterface. Этот поставщик использует метод getSchedule(), чтобы вернуть расписание, содержащее различные повторяющиеся сообщения.

Атрибут AsSchedule, который по умолчанию ссылается на расписание с именем default, позволяет вам регистрировать в конкретном расписании:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

#[AsSchedule]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        // ...
    }
}

Tip

По умолчанию имя расписания - default, а имя транспорта следует синтаксису: cheduler_nameofyourschedule (например, cheduler_default).

Tip

Хорошей практикой является мемоизация своего расписания, чтобы избежать ненужной перестройки, если метод getSchedule() проверяется другим сервисом.

Планирование повторяющихся сообщений

Сообщение RecurringMessage - это сообщение, ассоциированное с триггером, который конфигурирует частоту появления сообщения. Symfony предоставляет различные типы триггеров:

CronExpressionTrigger
Триггер, который использует тот же синтаксис, что и утилита командной строки cron.
CallbackTrigger
Триггер, который использует обратный вызов для определения следующей даты запуска.
ExcludeTimeTrigger
Триггер, который исключает определенное время из заданного триггера.
JitterTrigger
Триггер, который добавляет случайный джиттер к заданному триггеру. Джиттер - это некоторое время, которое добавляется/вычитается к исходной дате/времени срабатывания. Это позволяет распределить нагрузку на запланированные задачи вместо того, чтобы запускать их все в одно и то же время.
PeriodicalTrigger
Триггер, который использует DateInterval, чтобы определить следующую дату запуска.

Декораторы JitterTrigger и ExcludeTimeTriggerявляются декораторами и изменяют поведение триггера, который они оборачивают. Вы можете получить декорированный триггер, а также декораторы, вызвав методы inner() и decorators():

1
2
3
4
$trigger = new ExcludeTimeTrigger(new JitterTrigger(CronExpressionTrigger::fromSpec('#midnight', new MyMessage()));

$trigger->inner(); // CronExpressionTrigger
$trigger->decorators(); // [ExcludeTimeTrigger, JitterTrigger]

Большинство из них можно создать с помощью класса RecurringMessage, как показано в следующих примерах.

Триггеры выражений Cron

Прежде чем использовать триггеры cron, необходимо установить следующую зависимость:

1
$ composer require dragonmantank/cron-expression

Затем определите дату/время триггера, используя тот же синтаксис, что и в утилите командной строки cron:

1
2
3
4
RecurringMessage::cron('* * * * *', new Message());

// опционально вы можете определить часовой пояс, используемый выражением cron
RecurringMessage::cron('* * * * *', new Message(), new \DateTimeZone('Africa/Malabo'));

Tip

Загляните на сайт crontab.guru, если вам нужна помощь в построении/понимании выражений cron.

Вы также можете использовать некоторые специальные значения, которые представляют собой распространенные выражения cron:

  • @yearly, @annually - Запускать раз в год, в полночь 1го января - 0 0 1 1 *
  • @monthly - Запускать раз в месяц, в полночь, первого числа месяца - 0 0 1 * *
  • @weekly - Запускать раз в неделю, в полночь в воскресенье - 0 0 * * 0
  • @daily, @midnight - Запускать раз в день, в полночь - 0 0 * * *
  • @hourly - Запускать раз в час, в первую минуту - 0 * * * *

Например:

1
RecurringMessage::cron('@daily', new Message());

Tip

Вы также можете определять задачи cron, используя атрибут AsCronTask .

Хешированные выражения Cron

Если у вас много триггеров, запланированных на одно и то же время (например, в полночь, 0 0 * * *) это создаст очень длинный список расписаний на это конкретное время. Это может вызвать проблему, если у задачи есть утечка памяти.

Вы можете добавить символ хеша (#) в выражения для генерирования случайных значений. Несмотря на то, что значения случайны, они предсказуемы и последовательны, поскольку они генерируются на основе сообщения. Сообщение со строковым представлением my task и заданной частотой # # * * * будет иметь идемпотентную частоту 56 20 * * * (каждый день в 8:56 вечера).

Вы также можете использовать хеш-диапазоны (#(x-y)), чтобы определить список возможных значений для этой случайной части. Например, # #(0-7) * * * означает ежедневно, в некоторое время между полуночью и 7 утра. Использование # без диапазона создает диапазон из любого валидного значения для данного поля. # # # # - это сокращение для (0-59) #(0-23) #(1-28) #(1-12) #(0-6).

Вы также можете использовать некоторые специальные значения, которые представляют собой распространенные хешированные выражения cron:

????????? ????????????? ?
#hourly # * * * * (? ?????-?? ?????? ?????? ???)
#daily # # * * * (? ?????-?? ????? ?????? ????)
#weekly # # * * # (? ?????-?? ????? ?????? ??????)
#weekly@midnight # #(0-2) * * # (? #midnight ? ?????-?? ????, ?????? ??????)
#monthly # # # * * (? ?????-?? ????? ? ?????-?? ????, ??? ? ?????)
#monthly@midnight # #(0-2) # * * (? #midnight ? ?????-?? ????, ??? ? ?????)
#annually # # # # * (? ?????-?? ????? ? ?????-?? ????, ??? ? ???)
#annually@midnight # #(0-2) # # * (? #midnight ? ?????-?? ????, ??? ? ???)
#yearly # # # # * ????????? ??? #annually
#yearly@midnight # #(0-2) # # * ????????? ??? #annually@midnight
#midnight # #(0-2) * * * (? ?????-?? ????? ????? ????????? ? 2:59, ?????? ????)

Например:

1
RecurringMessage::cron('#midnight', new Message());

Note

Диапазон дней месяца составляет 1-28, это необходимо для учета февраля, в котором всего 28 дней.

Периодические триггеры

Эти триггеры позволяют конфигурировать частоты с использованием различных типов данных (string, integer, DateInterval). Они также поддерживают относительные форматы, определенные функциями PHP datetime:

1
2
3
4
5
6
7
RecurringMessage::every('10 seconds', new Message());
RecurringMessage::every('3 weeks', new Message());
RecurringMessage::every('first Monday of next month', new Message());

$from = new \DateTimeImmutable('13:47', new \DateTimeZone('Europe/Paris'));
$until = '2023-06-12';
RecurringMessage::every('first Monday of next month', new Message(), $from, $until);

Tip

Вы также можете определять периодические задачи, используя атрибут AsPeriodicTask .

Пользовательские триггеры

Пользовательские триггеры позволяют динамически конфигурировать любую частоту. Они создаются как сервисы, реализующие TriggerInterface.

Например, если вы хотите отправлять отчеты клиентам ежедневно, за исключением праздничных периодов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php
namespace App\Scheduler\Trigger;

class ExcludeHolidaysTrigger implements TriggerInterface
{
    public function __construct(private TriggerInterface $inner)
    {
    }

    // использовать этот метод, чтобы дать красиво отображаемое имя для
    // идентификации вашего триггера (облегчает отладку)
    public function __toString(): string
    {
        return $this->inner.' (except holidays)';
    }

    public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable
    {
        if (!$nextRun = $this->inner->getNextRunDate($run)) {
            return null;
        }

        // зациклить, пока вы не получите следующую дату запуска, и это не будет праздником
        while (!$this->isHoliday($nextRun) {
            $nextRun = $this->inner->getNextRunDate($nextRun);
        }

        return $nextRun;
    }

    private function isHoliday(\DateTimeImmutable $timestamp): bool
    {
        // добавить некоторую логику, чтобы определить, является ли заданная $timestamp праздником
        // вернуть true, если явялется, false - в других случаях
    }
}

Затем, определите ваше повторяющееся сообщение:

1
2
3
4
5
6
RecurringMessage::trigger(
    new ExcludeHolidaysTrigger(
        CronExpressionTrigger::fromSpec('@daily'),
    ),
    new SendDailySalesReports('...'),
);

Наконец, повторяющиеся сообщения должны быть прикреплены к расписанию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return $this->schedule ??= (new Schedule())
            ->with(
                RecurringMessage::trigger(
                    new ExcludeHolidaysTrigger(
                        CronExpressionTrigger::fromSpec('@daily'),
                    ),
                    new SendDailySalesReports()
                ),
                RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport())
            );
    }
}

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

Но что интересно знать, так это то, что он также предоставляет вам возможность генерировать сообщения динамически.

Динамическое видение для сгенерированных сообщений

Это особенно полезно, когда сообщение зависит от данных, хранящихся в базах данных или сторонних сервисах.

В продолжение предыдущего примера с генерированием отчетов: они зависят от запросов клиентов. В зависимости от конкретных требований может потребоваться создание любого количества отчетов с определенной периодичностью. Для таких динамических сценариев система предоставляет возможность динамически определять наше(и) сообщение(я), а не статически. Это достигается путем определения CallbackMessageProvider.

По сути, это означает, что вы можете динамически, во время выполнения, определить ваше(и) сообщение(я) через обратный вызов, который будет выполняться каждый раз, когда транспорт планировщика проверяет наличие сообщений для генерирования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return $this->schedule ??= (new Schedule())
            ->with(
                RecurringMessage::trigger(
                    new ExcludeHolidaysTrigger(
                        CronExpressionTrigger::fromSpec('@daily'),
                    ),
                // вместо того, чтобы быть статичным, как в предыдущем примере
                new CallbackMessageProvider([$this, 'generateReports'], 'foo')),
                RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport())
            );
    }

    public function generateReports(MessageContext $context)
    {
        // ...
        yield new SendDailySalesReports();
        yield new ReportSomethingReportSomethingElse();
    }
}

Исследование альтернатив для создания ваших повторяющихся сообщений

Существует также другой способ создания RecurringMessage, и это можно сделать добавив один из этих атрибутов к сервису или команде: AsPeriodicTask и AsCronTask.

Для обоих атрибутов у вас есть возможность определить расписание, которое будет использоваться с помощью опции schedule. По умолчанию будет использоваться расписание с именем default. Также по умолчанию будет вызван метод __invoke вашего сервиса, но также можно указать метод для вызова через опцию method. и при необходимости вы можете указать аргументы через опцию arguments.

Пример AsCronTask

Это самый простой способ определить триггер cron с таким атрибутом:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Scheduler/Task/SendDailySalesReports.php
namespace App\Scheduler\Task;

use Symfony\Component\Scheduler\Attribute\AsCronTask;

#[AsCronTask('0 0 * * *')]
class SendDailySalesReports
{
    public function __invoke()
    {
        // ...
    }
}

Атрибут принимает дополнительные параметры для настройки триггера:

1
2
3
4
5
6
7
8
// рандомно добавляет до 6 секунд к времени триггера, чтобы избежать скачков нагрузки
#[AsCronTask('0 0 * * *', jitter: 6)]

// определяет имя метода, который нужно вызвать вместо него, а также аргументы для передачи ему
#[AsCronTask('0 0 * * *', method: 'sendEmail', arguments: ['email' => 'admin@example.com'])]

// определяет часовой пояс для использования
#[AsCronTask('0 0 * * *', timezone: 'Africa/Malabo')]

Пример AsPeriodicTask

Это самый простой способ определить периодический триггер с помощью данного атрибута:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Scheduler/Task/SendDailySalesReports.php
namespace App\Scheduler\Task;

use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;

#[AsPeriodicTask(frequency: '1 day', from: '2022-01-01', until: '2023-06-12')]
class SendDailySalesReports
{
    public function __invoke()
    {
        // ...
    }
}

Note

Опции from и until являются необязательными. Если они не заданы, задача будет выполняться бесконечно.

Атрибут #[AsPeriodicTask] принимает множество параметров для настройки триггера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// частота может быть определена как целое число, представляющее количество секунд
#[AsPeriodicTask(frequency: 86400)]

// рандомно добавляет до 6 секунд к времени триггера, чтобы избежать скачков нагрузки
#[AsPeriodicTask(frequency: '1 day', jitter: 6)]

// определяет имя метода, который будет вызываться вместо него, а также аргументы для передачи ему
#[AsPeriodicTask(frequency: '1 day', method: 'sendEmail', arguments: ['email' => 'admin@symfony.com'])]
class SendDailySalesReports
{
    public function sendEmail(string $email): void
    {
        // ...
    }
}

// определяет часовой пояс для использования
#[AsPeriodicTask(frequency: '1 day', timezone: 'Africa/Malabo')]

Управление запланированными сообщениями

Изменение запланированных сообщений в реальном времени

Хотя планировать расписание заранее полезно, редко когда расписание остается статичным в течение долгого времени. По истечении определенного периода некоторые RecurringMessages могут устареть, в то время как другие могут потребовать внесения в планирование.

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

Следуя примеру с генерированием отчетов, описанному выше, компания может проводить некоторые рекламные акции в определенные периоды (и о них нужно сообщать многократно в течение определенного периода времени) или удаление старых отчетов должно быть остановлено при определенных обстоятельствах.

Именно поэтому в Scheduler встроен механизм динамической модификации расписания и он учитывает все изменения в режиме реального времени.

Стратегии для добавления, удаления и изменения записей в расписании

Расписание предоставляет вам возможность
add(), remove(), или clear() все связанные с ним повторяющиеся сообщения, что приводит к сбросу и повторному вычислению стека повторяющихся сообщений в памяти.

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

Однако если необходимо полностью удалить повторяющееся сообщение и его повторение, Schedule предлагает метод
remove() или removeById(). Это может быть особенно полезным в вашем случае, особенно если вам нужно остановить генерирование повторяющихся сообщений, что подразумевает удаление старых отчетов.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
                $this->removeOldReports;
            );
    }

    // ...

    public function removeCleanUpMessage()
    {
        $this->getSchedule()->getSchedule()->remove($this->removeOldReports);
    }
}

// src/Scheduler/Handler/.php
namespace App\Scheduler\Handler;

#[AsMessageHandler]
class CleanUpOldSalesReportHandler
{
    public function __invoke(CleanUpOldSalesReport $cleanUpOldSalesReport): void
    {
        // do some work here...

        if ($isFinished) {
            $this->mySchedule->removeCleanUpMessage();
        }
    }
}

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

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

Однако в планировщике также имеется система событий, которая интегрирована в полностековое приложение Symfony путем прививки к событиям Symfony Messenger. Эти события развертываются через слушателя, предоставляя удобные средства для реагирования.

Управление запланированными сообщениями через события

Стратегическое управление событиями

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

  • PRE_RUN_EVENT
  • POST_RUN_EVENT
  • FAILURE_EVENT

Доступ к расписанию - важнейшая функция, позволяющая легко добавлять или
удалять типы сообщений. Кроме того, можно будет получить доступ к текущему обрабатываемому сообщению и его контексту.

В нашем сценарии вы можете прослушать PRE_RUN_EVENT и проверить, выполняется ли определенное условие. Например, вы можете решить добавить повторяющееся сообщение для повторной очистки старых отчетов с той же или другой конфигурациями или добавить любое другое повторяющееся сообщение (сообщения).

Если бы вы решили обработать удаление повторяющегося сообщения, вы могли бы сделать это в слушателе данного события. Важно отметить, что оно раскрывает специфическую функцию shouldCancel(),
которая позволяет предотвратить передачу и обработку удаленного повторяющегося сообщения его обработчиком:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
            );
            ->before(function(PreRunEvent $event) {
                $message = $event->getMessage();
                $messageContext = $event->getMessageContext();

                // может получить доступ к расписанию
                $schedule = $event->getSchedule()->getSchedule();

                // можеть быть напрямую нацелен на обрабатываемое RecurringMessage
                $schedule->removeById($messageContext->id);

                // разрешить вызов ShouldCancel() и избежать обработки сообщения
                    $event->shouldCancel(true);
            }
            ->after(function(PostRunEvent $event) {
                // Делайте, что хотите
            }
            ->onFailure(function(FailureEvent $event) {
                // Делайте, что хотите
            }
    }
}

События Scheduler

PreRunEvent

Класс события: PreRunEvent

PreRunEvent позволяет изменять Schedule или отменить сообщение до его потребления:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\PreRunEvent;

public function onMessage(PreRunEvent $event): void
{
    $schedule = $event->getSchedule();
    $context = $event->getMessageContext();
    $message = $event->getMessage();

    // сделать что-то с раписанием, контекстом или сообщением

    // и/или отменить сообщение
    $event->shouldCancel(true);
}

Выполните эту команду, чтобы узнать, какие слушатели зарегистрированы для этого события
и их приоритеты:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\PreRunEvent"

PostRunEvent

Класс события: PostRunEvent

PostRunEvent позволяет изменять Schedule после потребления сообщения:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\PostRunEvent;

public function onMessage(PostRunEvent $event): void
{
    $schedule = $event->getSchedule();
    $context = $event->getMessageContext();
    $message = $event->getMessage();

    // сделать что-то с расписанием, контекстом или сообщением
}

Выполните эту команду, чтобы узнать, какие слушатели зарегистрированы для этого события
и их приоритеты:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\PostRunEvent"

FailureEvent

Класс события: FailureEvent

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\FailureEvent;

public function onMessage(FailureEvent $event): void
{
    $schedule = $event->getSchedule();
    $context = $event->getMessageContext();
    $message = $event->getMessage();

    $error = $event->getError();

    // сделать что-то с расписанием, контекстом, сообщением или ошибкой (логирование, ...)

    // и/или игнорировать событие неудачи
    $event->shouldIgnore(true);
}

Выполните эту команду, чтобы узнать, какие слушатели зарегистрированы для этого события
и их приоритеты:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\FailureEvent"

Потребление сообщений

Компонент Scheduler предлагает два способа потребления сообщений, в зависимости от ваших потребностей: использовать команду messenger:consume или создать работника программно. Первое решение является рекомендуемым при использовании компонента Scheduler в контексте полностекового приложения Symfony, второй вариант больше подходит при использовании компонента Scheduler в качестве самостоятельного компонента.

Запуск работника

После определения и прикрепления повторяющихся сообщений к расписанию, вам понадобится механизм для генерирования и потребления сообщений в соответствии с заданной частотой. Для этого компонент Scheduler использует команду messenger:consume из компонента Messenger:

1
2
3
4
$ php bin/console messenger:consume scheduler_nameofyourschedule

# использовать -vv, если вам нужны детали о том, что происходит
$ php bin/console messenger:consume scheduler_nameofyourschedule -vv
Symfony Scheduler - сгенерировать и потребить

Программное создание потребителя

Альтернативой предыдущему решению является создание и вызов работника, который будет потреблять сообщения. Компонент поставляется с готовым работником с именем Scheduler, который вы можете использовать в своем коде:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\Scheduler\Scheduler;

$schedule = (new Schedule())
    ->with(
        RecurringMessage::trigger(
            new ExcludeHolidaysTrigger(
                CronExpressionTrigger::fromSpec('@daily'),
            ),
            new SendDailySalesReports()
        ),
    );

$scheduler = new Scheduler(handlers: [
    SendDailySalesReports::class => new SendDailySalesReportsHandler(),
    // добавить больше обработчиков, если у вас больше типов сообщений
], schedules: [
    $schedule,
    // планировщик может принимать столько расписаний, сколько вам надо
]);

// наконец, запустите планировщик единожды, когда он будет готов
$scheduler->run();

Note

Класс Scheduler может быть использован
при использовании компонента Scheduler в качестве отдельного компонента. Если вы используете его в контексте фреймворка, настоятельно рекомендуется использовать команду messenger:consume, как объяснялось в предыдущем разделе.

Отладка расписания

Команда debug:scheduler предоставляет список расписаний вместе с их
повторяющимися сообщениями. Вы можете сузить список до конкретного расписания:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ php bin/console debug:scheduler

  Scheduler
  =========

  default
  -------

    ------------------- ------------------------- ----------------------
    Триггер             Поставщик                  Следующий запуск
    ------------------- ------------------------- ----------------------
    every 2 days        App\Messenger\Foo(0:17..)  Sun, 03 Dec 2023 ...
    15 4 */3 * *        App\Messenger\Foo(0:17..)  Mon, 18 Dec 2023 ...
   -------------------- -------------------------- ---------------------

# вы также можете указать дату, чтобы использовать для следующей даты запуска:
$ php bin/console debug:scheduler --date=2025-10-18

# вы также можете указать дату для использования для следующей даты запуска для планировщика:
$ php bin/console debug:scheduler name_of_schedule --date=2025-10-18

# использовать опцию --all, чтобы также отобразить завершенные повторяющиеся сообщения
$ php bin/console debug:scheduler --all

Эффективное управление с Symfony Scheduler

Когда работник перезапускается или отключается на некоторое время, транспорт планировщика не сможет генерировать сообщения (поскольку они создаются на лету транспортом планировщика).
Это означает, что все сообщения, запланированные к отправке в период бездействия работника, не будут отправлены, и планировщик потеряет информацию о последнем обработанном сообщении. После перезапуска он повторно вычислит сообщения, которые должны быть сгенерированы с этого момента.

Для примера рассмотрим повторяющееся сообщение, которое должно отправляться каждые 3 дня. Если работник будет перезапущен на 2-й день, сообщение будет отправлено через 3 дня после перезапуска, на 5-й день.

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

Поэтому планировщик позволяет запоминать дату последнего выполнения сообщения с помощью опции statefulкомпонента Cache). Это позволяет системе сохранять состояние расписания, гарантируя, что при перезапуске работника он возобновит работу с того места, на котором остановился.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
            )
            ->stateful($this->cache)
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
            )
            ->lock($this->lockFactory->createLock('my-lock')
    }
}

Tip

Время обработки сообщения имеет значение. Если оно занимает много времени, все последующие обработки сообщения могут быть отложены. Поэтому рекомендуется предвидеть это и планировать частоты, превышающие время обработки сообщения.

Кроме того, для лучшего масштабирования расписаний у вас есть опция обернуть ваше сообщение в RedispatchMessage. Это позволит вам указать транспорт, на который будет пересылаться ваше сообщение перед дальнейшей пересылкой в соответствующий обработчик:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return $this->schedule ??= (new Schedule())
            ->with(
                RecurringMessage::every('5 seconds', new RedispatchMessage(new Message(), 'async'))
            );
    }
}

При использовании RedispatchMessage Symfony прикрепляет к сообщению ScheduledStamp, что поможет вам идентифицировать эти сообщения при необходимости.