Транзакційні повідомлення: обробляйте повідомлення після того, як обробку завершено

Дата оновлення перекладу 2024-05-29

Транзакційні повідомлення: обробляйте повідомлення після того, як обробку завершено

Обробник повідомлень може dispatch нове повідомлення під час обробки інших у той же або інший автобус (якщо додаток має декілька автобусів). Будь-які помилки або виключення, які виникають у цьому процесі, можуть мати ненавмисні наслідки, типу:

  • Якщо ви використовуєте DoctrineTransactionMiddleware, а запущене повідомлення викликає виключення, то будь-які транзакції бази даних у початковому обробнику будуть відмінені.
  • Якщо повідомлення запущене в інший автобус, то запущене повідомлення буде оброблено, навіть якщо якийсь код пізніше у поточному обробнику викличе виключення.

Приклад процесу RegisterUser

Давайте в якості прикладу візьмемо додаток з автобусами команд та подій. Додаток запускає команду під назвою RegisterUser в автобус команд. Команда обробляється RegisterUserHandler, що створює обʼєкт User, зберігає цей обʼєкт у базі даних та запускає повідомлення UserRegistered в автобус подій.

Існує багато обробників повідомлення UserRegistered, один може відправляти вітальний лист новому користувачу. Ми використовуємо DoctrineTransactionMiddleware, щоб огорнути всі запити БД в одну транзакцію БД.

Проблема №1: Якщо під час відправки вітального листа викликається виключення, то користувач не буде створений, так як DoctrineTransactionMiddleware відкотиться до транзакції Doctrine, в які було створено користувача.

Проблема №2: Якщо виключення викликається при збереженні користувача у БД, вітальний лист все одно буде відправлений, так як він обробляється асинхронно.

Проміжкове ПЗ DispatchAfterCurrentBusMiddleware

Для багатьох додатків, бажана поведінка - обробляти лише повідомлення, які запускаються обробником після того, як обробник повністю завершив роботу. Це можна зробити, використовуючи DispatchAfterCurrentBusMiddleware і додавши штамп DispatchAfterCurrentBusStamp до конверта повідомлення :

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/Messenger/CommandHandler/RegisterUserHandler.php
namespace App\Messenger\CommandHandler;

use App\Entity\User;
use App\Messenger\Command\RegisterUser;
use App\Messenger\Event\UserRegistered;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp;

class RegisterUserHandler
{
    public function __construct(
        private MessageBusInterface $eventBus,
        private EntityManagerInterface $em,
    ) {
    }

    public function __invoke(RegisterUser $command): void
    {
        $user = new User($command->getUuid(), $command->getName(), $command->getEmail());
        $this->em->persist($user);

        // DispatchAfterCurrentBusStamp відмічає повідомлення події для обробки
        // лише якщо цей обробник не викликає виключення.

        $event = new UserRegistered($command->getUuid());
        $this->eventBus->dispatch(
            (new Envelope($event))
                ->with(new DispatchAfterCurrentBusStamp())
        );

        // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Messenger/EventSubscriber/WhenUserRegisteredThenSendWelcomeEmail.php
namespace App\Messenger\EventSubscriber;

use App\Entity\User;
use App\Messenger\Event\UserRegistered;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\RawMessage;

class WhenUserRegisteredThenSendWelcomeEmail
{
    public function __construct(
        private MailerInterface $mailer,
        EntityManagerInterface $em,
    ) {
    }

    public function __invoke(UserRegistered $event): void
    {
        $user = $this->em->getRepository(User::class)->find($event->getUuid());

        $this->mailer->send(new RawMessage('Welcome '.$user->getFirstName()));
    }
}

Це означає, що повідомлення UserRegistered не буде оброблене до тих пір, поки не буде виконано RegisterUserHandler і новий User не буде збережно у базу даних. Якщо RegisterUserHandler зіткнеться з виключенням, подія UserRegistered ніколи не буде оброблена. А якщо виключення буде викликане під час відправки вітального листа, транзакція Doctrine не буде відмінена.

Note

Якщо WhenUserRegisteredThenSendWelcomeEmail викликає виключення, воно буде огорнуте у DelayedMessageHandlingException. Використання DelayedMessageHandlingException::getExceptions надасть вам усі виключення, які викликаються під час обробки повідомлення з DispatchAfterCurrentBusStamp.

Проміжкове ПЗ dispatch_after_current_bus підключається за замовчуванням. Якщо ви конфігуруєте своє проміжкове ПЗ вручну, не забудьте зареєструвати dispatch_after_current_bus перед doctrine_transaction у ланцюжку проміжкового ПЗ. Також, проміжкове ПЗ dispatch_after_current_bus повинно бути завантажене для всіх використовуваних автобусів.