Транзакційні повідомлення: обробляйте повідомлення після того, як обробку завершено
Дата оновлення перекладу 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 повинно бути завантажене для всіх використовуваних
автобусів.