События и очереди

10 / 21
10 мин чтения
Введение
О чём говорим

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

Этот слой — про то, что происходит после важного действия, и про то, в каком процессе это действие должно выполняться: в текущем HTTP‑запросе, «после ответа» или в отдельном воркере.

Здесь две связанные темы:

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

Разбор идёт по актуальной документации Bitrix и исходникам ядра main 25.x. Там, где поведение критично, приводятся имена классов и файлов, чтобы факт можно было проверить.

В статье:

  • Laravel Events/Listeners/queued listeners и почему они закрывают большую часть сценариев без отдельной инфраструктуры,
  • Bitrix Bitrix\Main\Event, EventManager и EventResult, с разницей между новой D7‑моделью и режимом совместимости,
  • Laravel Queue (Database/Redis) + Horizon vs Bitrix агенты, Application::addBackgroundJob() и новый Messenger,
  • как выбрать между «синхронно», «после ответа» и «через очередь».

События: что это такое в коде

Событие — запись факта в системе:

  • OrderPaid, UserRegistered, InvoiceGenerated — факты в прошедшем времени,
  • BeforeOrderShip, OnBeforeUserAdd — точки контроля до изменения состояния.

Вызов сервиса говорит «сделай»: $receiptService->send($orderId). Событие говорит «произошло»: OrderPaid::dispatch($orderId). Разница видна на росте числа побочных действий.

💡 Типовой кейс
Оплата заказа должна отправить чек, начислить бонусы, поставить задачу на доставку, записать метрику. Без событий это 5–7 вызовов в контроллере, с событием — один dispatch и 5 независимых подписчиков.

Хорошее имя события — факт бизнеса (OrderPaid), а не шаг кода (AfterSaveAction, StepTwoFinished). Вторые — запах плохой модели: они описывают реализацию, а не домен.

Laravel: Events и Listeners

В Laravel событие — отдельный класс, listener — отдельный класс, связь описывается через event discovery или явную регистрацию в EventServiceProvider.

Базовый цикл

Событие — просто DTO с трейтами:

        namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class OrderPaid
{
    use Dispatchable;
    use SerializesModels;

    public function __construct(
        public readonly int $orderId,
        public readonly int $userId,
    ) {}
}

    

Listener — класс с методом handle. Зависимости резолвятся через контейнер:

        namespace App\Listeners;

use App\Events\OrderPaid;
use App\Services\ReceiptService;

final class SendReceipt
{
    public function __construct(
        private ReceiptService $receiptService,
    ) {}

    public function handle(OrderPaid $event): void
    {
        $this->receiptService->sendForOrder($event->orderId, $event->userId);
    }
}

    

Отправка — Event::dispatch(...) или статический шорткат:

        use App\Events\OrderPaid;

OrderPaid::dispatch($order->id, $order->user_id);

    

Если зарегистрировано несколько listener'ов, они выполняются по очереди в одном процессе. Порядок — в том, в котором Laravel их нашёл (или явно задан в $listen провайдера).

Queued listener: один интерфейс — и реакция уходит в очередь

Это — ключевая сильная сторона Laravel Events. Listener, который реализует ShouldQueue, автоматически сериализуется и отправляется в очередь, а handle выполняется воркером:

        namespace App\Listeners;

use App\Events\OrderPaid;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

final class SyncOrderToCrm implements ShouldQueue
{
    use InteractsWithQueue;

    public int $tries = 3;
    public int $backoff = 10;

    public function handle(OrderPaid $event): void
    {
        // Долгая синхронизация во внешнюю систему.
    }
}

    

Само событие никак не меняется — асинхронность превращается в техническое свойство listener'а.

События ядра Laravel

Кроме пользовательских событий, Laravel бросает свои:

  • Eloquentretrieved, creating, created, updating, updated, saving, saved, deleting, deleted, trashed, forceDeleted, restoring, restored, replicating;
  • AuthRegistered, Attempting, Authenticated, Login, Failed, Logout, PasswordReset, Verified;
  • QueueJobQueued, JobProcessing, JobProcessed, JobFailed;
  • Mail, Notification, Cache, Broadcasting — свои наборы.

То есть платформенный слой событий в Laravel есть, и подписаться на него так же просто, как на свои события.

Документация: Laravel Events.

Bitrix: Bitrix\Main\Event, EventManager, EventResult

В Bitrix события проходят через один EventManager. Моделей две — D7 и legacy — они работают бок о бок, но для нового кода используется только D7.

D7‑модель: класс события

Базовый вариант из доки — new \Bitrix\Main\Event($moduleId, $type, $parameters):

        public function __construct($moduleId, $type, $parameters = array(), $filter = null)

    

Конструктор принимает 4 аргумента. Если параметры переданы массивом — читаются через $event->getParameter('orderId'), если переданы объектом — через прямой доступ к readonly‑свойствам.

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

        php bitrix.php make:event OrderPaid -m my.shop --no-interaction

    

Получается local/modules/my.shop/lib/Public/Event/OrderPaidEvent.php:

        <?php declare(strict_types=1);

namespace My\Shop\Public\Event;

use Bitrix\Main\Event;

final class OrderPaidEvent extends Event
{
    public function __construct(
        public readonly int $orderId,
        public readonly int $userId,
    ) {
        parent::__construct('my.shop', self::class);
    }
}

    

Два приёма здесь важны:

  1. parent::__construct(moduleId, type) — без массива параметров: данные лежат в readonly‑свойствах и доступны прямым обращением.
  2. eventType: self::class — в качестве типа пишется FQCN класса события. EventManager умеет матчить и по строке, и по FQCN; FQCN убирает коллизии, когда в разных модулях случайно совпадают строки 'OrderPaid'.

Отправка:

        use My\Shop\Public\Event\OrderPaidEvent;

$event = new OrderPaidEvent(orderId: 101, userId: 55);
$event->send();

    

Обработчик читает свойства напрямую и возвращает EventResult:

        <?php declare(strict_types=1);

namespace My\Shop\Internals\Integration\My\Shop\EventHandler;

use Bitrix\Main\EventResult;
use My\Shop\Public\Event\OrderPaidEvent;

final class OrderPaidEventHandler
{
    public static function handle(OrderPaidEvent $event): EventResult
    {
        // Реакция на оплату.

        return new EventResult(EventResult::SUCCESS);
    }
}

    

Обработчик генерируется командой:

        php bitrix.php make:eventhandler OrderPaid \
    --event-module=my.shop --handler-module=my.shop --no-interaction

    

EventResult: четыре аргумента, не два

Частая ошибка при ручном создании результата — звать конструктор позиционно, игнорируя moduleId:

        public function __construct($type, $parameters = null, $moduleId = null, $handler = null)

    

Именованные аргументы делают намерение явным:

        return new EventResult(
    EventResult::ERROR,
    parameters: ['message' => Loc::getMessage('MY_SHOP_ORDER_INVALID')],
    moduleId: 'my.shop',
);

    
⚠️ Важно
moduleId — не для красоты: когда несколько обработчиков возвращают ошибку, он помогает понять, из какого модуля пришёл результат (в сервисе‑отправителе вызывается $event->getResults() и каждый результат несёт свой moduleId).

Статусы EventResult:

  • EventResult::SUCCESS = 1 — всё ок;
  • EventResult::ERROR = 2 — ошибка, в parameters полезно класть message / код;
  • EventResult::UNDEFINED = 0 — обработчик не распознал событие (например, if (!$event instanceof MyEvent) return new EventResult(EventResult::UNDEFINED)).

Регистрация обработчиков

Регистрация живёт в install/index.php модуля и выполняется один раз — хранится в таблице b_module_to_module:

        \Bitrix\Main\EventManager::getInstance()->registerEventHandler(
    fromModule: 'my.shop',
    eventType: \My\Shop\Public\Event\OrderPaidEvent::class,
    toModuleId: 'my.shop',
    toClass: \My\Shop\Internals\Integration\My\Shop\EventHandler\OrderPaidEventHandler::class,
    toMethod: 'handle',
);

    

В DoUninstall() — парный unRegisterEventHandler с теми же параметрами.

⚠️ Обратите внимание
Динамическая регистрация через EventManager::addEventHandler() тоже существует, но в актуальной доке её прямо не рекомендуют: она живёт до конца текущего запроса и усложняет анализ «кто на что подписан».

Режим совместимости: legacy‑события ядра

Большая часть событий модулей iblock, sale, user, catalog (OnBeforeUserAdd, OnAfterIBlockElementUpdate, OnSaleOrderSaved) построена до D7 и продолжает жить в старом формате:

  • вместо объекта Event в обработчик приходит произвольный набор аргументов (массив полей, объект ORM, скаляры);
  • чтобы обработчик получил их как есть, регистрируется через registerEventHandlerCompatible;
  • вернуть false + $APPLICATION->ThrowException() — прервать операцию; вернуть массив ['FIELDS' => [...]] в OnBefore* — модифицировать входные поля.
        \Bitrix\Main\EventManager::getInstance()->registerEventHandlerCompatible(
    fromModule: 'main',
    eventType: 'OnBeforeUserAdd',
    toModuleId: 'my.shop',
    toClass: \My\Shop\Internals\Integration\Main\EventHandler\OnBeforeUserAddEventHandler::class,
    toMethod: 'handle',
);

    

Для нового кода — только registerEventHandler и свои Event‑классы. Режим совместимости нужен только при подписке на события ядра, которые ещё не переехали на D7.

Документация: События.

События не заменяют архитектуру

Обратная крайность — заворачивать в события каждый шаг: «валидация закончилась», «репозиторий сохранил», «сервис позвал другой сервис». Факт перестаёт быть фактом, поток выполнения размазывается по подпискам.

Рабочая граница:

  • контроллер оркестрирует пользовательский сценарий,
  • сервис делает бизнес‑работу,
  • событие публикуется на устойчивом бизнес‑факте (OrderPaid, а не AfterServiceMethodFinished),
  • listener/handler реализует независимые реакции.

Если сценарий и без события читается — события не нужны.

Очереди: когда отдать работу воркеру

Очереди отвечают на вопрос «где выполнять тяжёлую реакцию». Сигналы, что задачу не стоит делать в HTTP‑запросе:

  • операция зависит от внешнего API и может тормозить или падать,
  • CPU/IO нагрузка заметна на фоне обычного запроса,
  • пользователю не нужен результат немедленно,
  • требуются повторы при временных сбоях,
  • задачу нужно параллелить через несколько процессов.

Типовые кандидаты: отправка email, генерация PDF, ресайз картинок, импорт каталога, синхронизация с CRM/ERP, отправка webhook'ов, пересчёт индексов поиска.

Laravel и Bitrix решают эту задачу по‑разному: в Laravel очереди — один из базовых слоёв платформы, в Bitrix — три разных механизма с разными гарантиями.

Laravel: Queue, Scheduler, Horizon

Драйверы транспортов

Транспорт описывается в config/queue.php. Штатные драйверы:

  • database — таблица jobs в основной БД, простейший прод‑вариант;
  • redis — чаще всего используется вместе с Horizon (dashboard, auto‑scaling воркеров, метрики, failed jobs UI);
  • sync, null — для dev/тестов и специальных случаев.

Job — отдельный класс с handle()

        namespace App\Jobs;

use App\Services\InvoiceService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

final class GenerateInvoicePdf implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(
        public readonly int $orderId,
    ) {}

    public function handle(InvoiceService $invoiceService): void
    {
        $invoiceService->generatePdfForOrder($this->orderId);
    }
}

    

Постановка — через фасад Bus или статику на классе:

        GenerateInvoicePdf::dispatch($order->id);

GenerateInvoicePdf::dispatch($order->id)
    ->onQueue('heavy')
    ->delay(now()->addMinutes(10));

    

Воркер

        php artisan queue:work redis --queue=default,heavy --tries=3 --max-time=3600

    

Воркер живёт под Supervisor/systemd/Horizon, перезапускается по времени или памяти. Horizon добавляет поверх этого автомасштабирование, dashboard и метрики.

Scheduler: штатный слой расписания

Регулярные задачи описываются в app/Console/Kernel.php:

        protected function schedule(Schedule $schedule): void
{
    $schedule->command('reports:rebuild')->hourly();
    $schedule->job(new CleanupOldExports())->dailyAt('02:00');
}

    

В cron заводится одна строка:

        * * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

    

Scheduler — не «смежная тема», а штатный механизм фреймворка наравне с Queue.

terminate() и defer(): хвосты после ответа

Когда задача короткая и не требует ретраев, Laravel умеет выполнить её после отправки ответа через terminate‑метод middleware или defer() в контейнере. Это аналог Bitrix addBackgroundJob, но менее известный.

Документация: Laravel Queues, Horizon, Scheduler.

Bitrix: три механизма под разные гарантии

В Bitrix нет одного универсального «Queue». Есть три механизма — и выбирать нужно по характеру задачи, а не по привычке.

Механизм Где живёт Подходит для Гарантии
CAgent Таблица b_agents, запуск на хитах и/или cron Регулярные задачи по расписанию Запускается, пока активен; однопоточный, блокируется на 10 минут при тяжёлой задаче
Application::addBackgroundJob() Тот же PHP‑процесс, после отдачи ответа Короткие хвосты (метрика, письмо, webhook) Нет: при падении процесса задача теряется
Bitrix\Main\Messenger Таблица b_main_messenger_message + воркер messenger:consume Длинные/параллельные задачи с ретраями Доставка гарантирована брокером, есть retry_strategy

Дальше — по каждому.

Агенты: расписание, не очередь

Агент — запись в b_agents, которая описывает функцию, интервал и тип (периодический/непериодический). API — класс CAgent (см. www/bitrix/modules/main/classes/general/agent.php):

        CAgent::AddAgent(
    "MyBonusSyncAgent();",  // Вызываемый код
    "my.shop",              // Идентификатор модуля
    "N",                    // "N" — непериодический, "Y" — периодический
    3600,                   // Интервал в секундах
);

    

Сама функция возвращает строку — код для следующего запуска; пустая строка — агент больше не планируется:

        function MyBonusSyncAgent(): string
{
    // Полезная работа.

    return "MyBonusSyncAgent();";
}

    

Важные свойства агентов:

  • Однопоточность. Система не запустит агента повторно, пока предыдущий вызов работает. Если агент завис — блокируется на 10 минут, только после этого считается упавшим.
  • Запуск: на хитах (настройка по умолчанию) или через cron (COption::SetOptionString('main', 'agents_use_crontab', 'Y') + запись в crontab * * * * * bitrix /usr/bin/php -f /home/bitrix/www/bitrix/modules/main/tools/cron_events.php). Для любой задачи, критичной по времени, cron обязателен — на хитах расписание плавает с посещаемостью.
  • Ограничения контекста: нет $USER, нет SITE_ID, нельзя полагаться на обычную авторизацию и язык по умолчанию. Код должен получать userId/siteId из параметров, а не из глобалок.
  • Удаление: CAgent::RemoveModuleAgents('my.shop') в DoUninstall().

Агент подходит для «каждые N минут сделать регулярную задачу» (пересчёт агрегатов, чистка, синхронизация курсов валют). Он не подходит для «обработать 500 независимых сообщений параллельно» — это не очередь, и попытка сделать «мини‑воркер внутри агента» (агент бегает по таблице задач и обрабатывает их по одной) упирается в однопоточность.

Документация: Агенты и фоновые задачи.

Background jobs: код после ответа, в том же процессе

Application::addBackgroundJob (см. www/bitrix/modules/main/lib/application.php:818) складывает callable в приоритетную очередь, которая выполняется в том же PHP‑процессе после отправки ответа клиенту (fastcgi_finish_request / onAfterEpilog):

        use Bitrix\Main\Application;

Application::getInstance()->addBackgroundJob(
    [EmailService::class, 'sendReceipt'],
    ['user@example.com', 101],
    Application::JOB_PRIORITY_NORMAL,
);

    

Константы приоритета:

  • Application::JOB_PRIORITY_NORMAL = 100 (по умолчанию),
  • Application::JOB_PRIORITY_LOW = 50.

Это не очередь: ничего не сохраняется в БД, нет ретраев, нет отдельного процесса. Если PHP‑воркер упал между отправкой ответа и выполнением job'а — задача потеряна без следа. Зато нет и инфраструктурной цены: это просто «последние миллисекунды перед завершением запроса».

Под что годится: отправить письмо, дёрнуть метрику, записать лог, отложить короткую синхронизацию.

Под что не годится: критичные интеграции, где нужна повторная попытка; длинные задачи (каждая секунда задерживает освобождение PHP‑worker'а пула).

Messenger: полноценные очереди с alpha‑статусом

Bitrix\Main\Messenger — новое и самое близкое к «настоящей» очереди в ядре Bitrix.

⚠️ Важно
Альфа‑статус обозначен явно в документации: обратная совместимость не гарантируется, поведение может меняться в обновлениях.

Условия:

  • Доступен с main 25.100.300.
  • Из коробки — один тип брокера: DbBroker (хранение в таблице b_main_messenger_message). Redis/AMQP в дистрибутив не входят; BrokerInterface открыт, можно реализовать свой (см. Bitrix\Main\Messenger\Internals\Config\BrokerManager::loadBrokerConfig()).
  • Режимы запуска — web (обработка через addBackgroundJob на хитах) и cli (воркер messenger:consume). Для прода — только cli: без run_mode => 'cli' команда messenger:consume молча выходит с сообщением Messenger not configured to run in CLI.

Минимальная связка — сообщение + обработчик + конфиг.

Сообщение (скаляры и массивы, никаких EntityObject):

        namespace My\Shop\Public\Message;

use Bitrix\Main\Messenger\Entity\AbstractMessage;

final class OrderPaidMessage extends AbstractMessage
{
    public function __construct(
        public readonly int $orderId,
        public readonly int $userId,
    ) {}
}

    

Обработчик наследуется от AbstractReceiver и реализует process(). Из process() можно бросать исключение — базовый run() сам поймает и вернёт сообщение в очередь для повторной попытки:

        namespace My\Shop\Internals\Integration\Self\MessageHandler;

use Bitrix\Main\Messenger\Entity\MessageInterface;
use Bitrix\Main\Messenger\Receiver\AbstractReceiver;
use My\Shop\Application\Service\OrderFulfillment;
use My\Shop\Public\Message\OrderPaidMessage;

final class OrderPaidHandler extends AbstractReceiver
{
    public function __construct(
        private readonly OrderFulfillment $fulfillment,
    ) {}

    protected function process(MessageInterface $message): void
    {
        if (!$message instanceof OrderPaidMessage) {
            return;
        }

        $this->fulfillment->onPaid($message->orderId, $message->userId);
    }
}

    

Конфиг очереди — в bitrix/.settings.php (или в .settings.php модуля, если очередь не межмодульная):

        'messenger' => [
    'value' => [
        'run_mode' => 'cli',
        'brokers' => [
            'default' => [
                'type' => 'db',
                'params' => [
                    'table' => \Bitrix\Main\Messenger\Internals\Storage\Db\Model\MessengerMessageTable::class,
                ],
            ],
        ],
        'queues' => [
            'orders' => [
                'handler' => \My\Shop\Internals\Integration\Self\MessageHandler\OrderPaidHandler::class,
                'broker'  => 'default',
                'module'  => 'my.shop',
                'retry_strategy' => [
                    'max_retries' => 5,
                    'delay'       => 2,
                    'multiplier'  => 2,
                    'max_delay'   => 60,
                ],
            ],
        ],
    ],
    'readonly' => true,
],

    

Дефолты retry_strategy (если не указывать): max_retries=3, delay=1, multiplier=2, max_delay=0 (без ограничения).

Отправка — метод send($queueId) на сообщении:

        $message = new OrderPaidMessage(orderId: 101, userId: 55);
$message->send('orders');

    

AbstractMessage::send() внутри достаёт MessageBus из ServiceLocator и вызывает $bus->send($this, $queueId) — руками MessageBus тащить не нужно. Для отложенной обработки передаётся параметр:

        use Bitrix\Main\Messenger\Entity\ProcessingParam\DelayParam;

$message->send('orders', [new DelayParam(600)]);

    

Воркер:

        php bitrix.php messenger:consume orders --time-limit=300 --sleep=1

    

Флагов --memory-limit у консьюмера сейчас нет (см. Bitrix\Main\Messenger\Cli\Command\ConsumeMessagesCommand). Защита от утечек — --time-limit, после которого процесс корректно завершается, а Supervisor/systemd поднимает новый.

Типы исключений, которые понимает AbstractReceiver:

  • UnprocessableMessageException — сообщение не подходит этому обработчику (в очередь прилетело чужое сообщение);
  • UnrecoverableMessageException — повтор бесполезен (ссылка на удалённый объект, битый JSON, невалидный бизнес‑ввод);
  • RecoverableMessageException — временная ошибка, можно указать задержку через getRetryDelay();
  • любое другое исключение — применяется retry_strategy очереди.

По сочетанию возможностей Messenger уже близок к тому, что понимают под «настоящей очередью». Мешают только три вещи: alpha‑статус, только DbBroker из коробки и отсутствие dashboard'а уровня Horizon.

Документация: Очереди сообщений.

Queued event handler: Bitrix‑версия ShouldQueue

В Bitrix нет интерфейса ShouldQueue, но идиома той же формы собирается руками за 5 строк. Event‑handler делается тонким и ставит сообщение в очередь:

        final class OrderPaidEventHandler
{
    public static function handle(OrderPaidEvent $event): EventResult
    {
        $message = new OrderPaidMessage($event->orderId, $event->userId);
        $message->send('orders');

        return new EventResult(EventResult::SUCCESS);
    }
}

    

Тяжёлая работа уходит в OrderPaidHandler воркера, а сам event остаётся доменным фактом. Для коротких хвостов вместо Messenger подойдёт addBackgroundJob.

Типовые ошибки

Кейс 1 Тяжёлый listener/handler внутри запроса.

Формально код разбит на классы, но внутри handler'а — два HTTP‑вызова и отправка письма. Пользователь всё равно ждёт. Лечение: handler тонкий, работа уходит в очередь или addBackgroundJob.

Кейс 2 Агент вместо очереди сообщений.

Агент запускается раз в N минут, внутри бегает по таблице my_pending_jobs и обрабатывает записи. Это работает до первой задачи на 15 минут — агент блокируется на 10 минут, параллелить его нельзя. Если сценарий «много независимых задач» — нужен Messenger.

Кейс 3 В очередь кладут мало данных.

Отправили ['orderId' => 101], а к моменту обработки заказ удалён, связи нет. Для отложенной обработки в сообщение кладут все поля, которые реально понадобятся обработчику — orderId, userId, externalId, amount. Это в доке Messenger отдельно подчёркнуто.

Кейс 4 Сложные объекты в DTO сообщения.

EntityObject, ресурсы, замыкания не сериализуются в JSON. Консьюмер бесконечно падает на десериализации, пока max_retries не доест TTL. Правило: только скаляры, массивы скаляров и простые value‑объекты с jsonSerialize().

Кейс 5 Запуск messenger:consume без --time-limit.

PHP не возвращает память пулу — за сутки воркер съест 2–4 ГБ. --time-limit=300 (+ перезапуск супервизором) решает задачу.

События называют шагами кода. AfterServiceMethodFinished, ProcessStepDone — это не факты домена, это журнал реализации. Событие называется так, чтобы PM или аналитик понимал его без чтения кода.

Сравнительная таблица: события и очереди

Шкала: от −2 до +2. Балл снимается только при технически обоснованном пробеле — не за «непривычность» и не за наличие legacy‑слоя рядом с современным.

Критерий Laravel Bitrix Комментарий
Читаемость D7/современной событийной модели +2 +2 Обе модели тривиальны: event‑класс → handler → регистрация. Legacy в Bitrix существует отдельно, для нового кода не обязателен.
Интеграция событий с ядром/платформой +1 +2 Laravel: Eloquent (13 событий жизненного цикла), Auth (Registered, Login, Failed, …), Queue, Mail, Broadcasting. Bitrix: события есть у всех модулей платформы (iblock, sale, user, catalog, main), включая OnBefore*, которые могут отменять операции через ThrowException. Bitrix глубже встроен за счёт готовых модулей.
Полноценные очереди как production‑дефолт +2 0 Laravel Queue (Redis/Database) + Horizon — зрелый слой с dashboard и автоскейлингом воркеров. Bitrix Messenger с main 25.100.300 имеет DTO/handler/retry_strategy/CLI‑воркер, но помечен alpha, из коробки только DbBroker, dashboard нет.
Отложенные короткие задачи «после ответа» +2 +2 Laravel: terminate() в middleware, defer() в контейнере. Bitrix: Application::addBackgroundJob() с двумя приоритетами (JOB_PRIORITY_NORMAL=100, JOB_PRIORITY_LOW=50) — штатный механизм ядра, задача выполняется после fastcgi_finish_request. Паритет.
Регулярные задачи по расписанию +2 +2 Laravel Scheduler — штатный слой (schedule:run из cron + описание в Kernel::schedule). Bitrix Agents — штатный механизм с UI в админке, поддерживает cron; минус — однопоточность и 10‑минутная блокировка на зависших задачах. Разные компромиссы, но оба механизма на уровне платформы.
Повторы, задержки, worker‑подход +2 +1 Laravel Queue: ретраи, backoff, failed_jobs, Horizon metrics, max_time/memory, контекст выполнения. Bitrix Messenger: retry_strategy (max_retries=3, delay=1, multiplier=2, max_delay=0 по умолчанию), три типа исключений, messenger:consume --time-limit. У Messenger нет --memory-limit и dashboard'а; alpha‑статус.
Итого за статью +11 +9
Общий счёт (накопительный) +88 +55 Счёт накапливается по мере выхода статей.
🛠 Практическое задание

Один бизнес‑факт и одна отложенная реакция

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

Сценарий общий: пользователь оплатил заказ, система должна сохранить факт оплаты, отправить письмо и отдельно обновить внешнюю CRM (медленный и нестабильный внешний API).

Laravel

  1. Создайте событие OrderPaid(int $orderId, int $userId).
  2. Listener SendReceipt — синхронный, отправляет email через Mail.
  3. Listener SyncOrderToCrm implements ShouldQueue — параметры tries=3, backoff=30.
  4. После успешной оплаты: OrderPaid::dispatch($order->id, $order->user_id).
  5. Поднимите очередь на database или redis:

php artisan queue:work database --queue=default --tries=3 --max-time=3600

  1. Проверьте три свойства:
    • ответ пользователю приходит быстро;
    • отправка чека и синхронизация CRM не живут в контроллере;
    • падение CRM не ломает сам сценарий оплаты — ретраи работают независимо.

Документация: Events, Queues, Horizon.

Bitrix

Базовая сборка — на Event + addBackgroundJob + агент:

  1. make:event OrderPaid -m my.shop — класс события с readonly orderId/userId.
  2. make:eventhandler OrderPaid --event-module=my.shop --handler-module=my.shop — обработчик.
  3. Регистрация в install/index.php через registerEventHandler с eventType: OrderPaidEvent::class.
  4. В обработчике отправка чека — через Application::getInstance()->addBackgroundJob([...]) (задача короткая, потеря при падении некритична).
  5. Синхронизация с CRM — через CAgent раз в 5 минут (агент берёт неподтверждённые записи из своей таблицы и пытается синхронизировать). Агент перевести на cron.

Продвинутая сборка — на Messenger:

  1. Добавить секцию messenger в bitrix/.settings.php с run_mode => 'cli' и брокером default.
  2. make:message OrderPaid -m my.shop — DTO.
  3. make:messagehandler OrderPaid --message-module=my.shop --handler-module=my.shop — receiver.
  4. Из обработчика события $message->send('orders').
  5. Запустить воркер:

php bitrix.php messenger:consume orders --time-limit=300 --sleep=1

Под Supervisor/systemd для production. Минусы, которые нужно помнить: alpha‑статус, только DbBroker, dashboard придётся собирать самостоятельно (SQL по b_main_messenger_message).

Документация: События, Агенты и фоновые задачи, Очереди сообщений.

Вывод

Laravel закрывает события и асинхронность одним цельным слоем: событие → listener → queue/job → worker → Horizon. Интерфейс ShouldQueue превращает синхронный listener в очередной без изменения модели.

Bitrix закрывает ту же задачу тремя разными механизмами: агенты (расписание), addBackgroundJob (хвост после ответа), Messenger (настоящая очередь). Все три штатные, но у каждого свои гарантии и ограничения — и сила проекта зависит от того, как команда разводит их между собой.

Главные правила, которые держат систему в порядке:

  • агент — это scheduled task, а не очередь;
  • addBackgroundJob — это хвост после ответа, а не worker;
  • Event — это доменный факт, а не вынесенная из контроллера функция;
  • Messenger даёт воркер и ретраи, но пока в alpha — критичные интеграции проверяйте на откат.

В следующей части — тема, которая живёт бок о бок с фоновой обработкой и производительностью: кэширование.

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.

AI Домовой История

0 / 25

Привет! Я помогу с вопросами по 1С-Битрикс.

Спрашивай про D7, ORM, компоненты или события.

Требуется авторизация

Войдите или зарегистрируйтесь, чтобы задавать вопросы AI-ассистенту.

Войти