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