RabbitMQ i Symfony: Architektura zdarzeń i obsługa komunikatów

Dzisiejsze aplikacje wymagają coraz większej elastyczności i zdolności do radzenia sobie z dużą ilością danych, co często wymaga komunikacji asynchronicznej między różnymi komponentami. W tym kontekście, RabbitMQ i Symfony Messenger są idealnymi narzędziami do tworzenia architektury opartej na zdarzeniach i zarządzania komunikatami. W tym artykule przyjrzymy się, jak krok po kroku wykorzystać RabbitMQ oraz Symfony Messenger, by efektywnie wdrożyć asynchroniczną obsługę wiadomości.

Architektura Zdarzeń i Symfony Messenger

Symfony Messenger to komponent pozwalający na łatwą implementację wzorca wiadomości w Symfony. Można go używać do:

  • Wysyłania wiadomości do innych części aplikacji.
  • Obsługi zdarzeń i kolejkowania zadań.
  • Integracji z brokerami komunikatów, takimi jak RabbitMQ.

Krok 1: Instalacja Symfony Messenger i RabbitMQ

Na początek należy zainstalować potrzebne zależności. Symfony Messenger integruje się z RabbitMQ za pomocą symfony/amqp-messenger. Instalacja przebiega następująco:

				
					composer require symfony/messenger
composer require enqueue/amqp-bunny

				
			

Krok 2: Konfiguracja Symfony Messenger z RabbitMQ

Skonfiguruj Messenger w pliku config/packages/messenger.yaml:

				
					
framework:
  messenger:
    transports:
      async: 
        dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
        options:
          exchange:
            name: 'messages'
    routing:
      'App\Message\EmailNotification': async

				
			

Konfiguracja ta oznacza, że wiadomości typu EmailNotification będą przesyłane do kolejki async, która jest podłączona do RabbitMQ. Aby zdefiniować dsn, należy dodać wpis do .env:

				
					MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages

				
			

W tym wpisie guest:guest to domyślne poświadczenia RabbitMQ, a localhost:5672 to miejsce, gdzie działa RabbitMQ.

Krok 3: Tworzenie Klasy Wiadomości

Klasa wiadomości definiuje dane, które mają zostać przesłane. Tworzymy przykładową klasę EmailNotification, której zadaniem jest zawarcie danych o wiadomości e-mail do wysłania:

				
					namespace App\Message;

class EmailNotification
{
    private string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}

				
			

W klasie EmailNotification mamy prywatne pole $email i metodę getEmail() do jego pobrania. Ta klasa jest prostym sposobem na przenoszenie danych między komponentami.

Krok 4: Wysłanie Komunikatu

W Symfony możemy użyć Messenger do wysłania komunikatu za pomocą MessageBusInterface. Oto jak to zrobić:

				
					namespace App\Controller;

use App\Message\EmailNotification;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;

class RegistrationController extends AbstractController
{
    private MessageBusInterface $bus;

    public function __construct(MessageBusInterface $bus)
    {
        $this->bus = $bus;
    }

    /**
     * @Route("/register", name="register_user")
     */
    public function register(): Response
    {
        // Przykład logiki rejestracji użytkownika
        
        // Po zarejestrowaniu użytkownika wysyłamy wiadomość do kolejki
        $this->bus->dispatch(new EmailNotification('user@example.com'));

        return new Response('User registered and email notification dispatched');
    }
}

				
			

Tutaj:

  • Używamy MessageBusInterface do dispatch wiadomości EmailNotification po zarejestrowaniu użytkownika.
  • Metoda dispatch() dodaje komunikat do kolejki RabbitMQ.

Krok 5: Tworzenie Handlera Wiadomości

Handler to klasa, która przetwarza wiadomości przesłane przez Messenger. Stwórzmy EmailNotificationHandler, który odpowiada za obsługę komunikatu i wysłanie e-maila:

				
					namespace App\MessageHandler;

use App\Message\EmailNotification;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

class EmailNotificationHandler implements MessageHandlerInterface
{
    public function __invoke(EmailNotification $notification)
    {
        // Przykład logiki wysyłania e-maila
        $email = $notification->getEmail();

        // Wyobraźmy sobie, że mamy jakąś usługę MailerService
        mail($email, 'Welcome!', 'Thank you for registering with us.');

        echo "Email sent to: " . $email . "\n";
    }
}

				
			

Handler jest bardzo prosty:

  • Metoda __invoke() odbiera wiadomość EmailNotification.
  • Zawiera podstawową logikę wysyłania wiadomości e-mail.

Krok 6: Opcje Retry, Delay i Redelivery

Symfony Messenger pozwala na konfigurację opcji takich jak retry, delay i redelivery. To kluczowe aspekty, które zwiększają niezawodność systemu.

Retry (Ponowienie Prób)

Jeśli przetworzenie wiadomości nie powiedzie się, Symfony może automatycznie próbować ponownie przetworzyć komunikat. Konfiguracja może wyglądać tak:

				
					
framework:
  messenger:
    failure_transport: failed

    transports:
      async:
        dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
        retry_strategy:
          max_retries: 5
          delay: 1000
          multiplier: 2
          max_delay: 10000

				
			
  • max_retries: Maksymalna liczba prób przetworzenia wiadomości.
  • delay: Początkowe opóźnienie w milisekundach przed ponowną próbą.
  • multiplier: Każde kolejne opóźnienie jest zwiększane według tego współczynnika.
  • max_delay: Maksymalny czas opóźnienia pomiędzy ponownymi próbami.

Delay (Opóźnienie)

Możemy skonfigurować, aby wiadomość została przetworzona z pewnym opóźnieniem:

				
					use Symfony\Component\Messenger\Stamp\DelayStamp;

$this->bus->dispatch(new EmailNotification('user@example.com'), [
    new DelayStamp(5000) // Opóźnienie 5 sekund
]);

				
			

W ten sposób możemy opóźnić przetworzenie wiadomości o 5000 milisekund (5 sekund).

Redelivery (Przekierowanie do Kolejki Niepowodzeń)

Jeśli wiadomości nie można przetworzyć, możemy przekierować ją do osobnej kolejki, tzw. failure transport:

				
					
framework:
  messenger:
    failure_transport: failed

    transports:
      failed: 'doctrine://default?queue_name=failed'

				
			

W przypadku niepowodzenia, wiadomości trafią do kolejki failed, co pozwala na ich późniejsze przeanalizowanie i ewentualne przetworzenie.

Podsumowanie

RabbitMQ i Symfony Messenger to potężne narzędzia do tworzenia asynchronicznej komunikacji opartej na zdarzeniach. Dzięki klasom wiadomości, wysyłaniu komunikatów i odpowiednim handlerom, aplikacje stają się skalowalne, elastyczne i odporne na błędy. Opcje takie jak retry, delay i redelivery dodatkowo zwiększają niezawodność aplikacji, co sprawia, że Messenger idealnie sprawdza się w złożonych systemach.

Stosując takie podejście, zyskujemy:

  • Odporność na awarie: Nawet jeśli przetwarzanie się nie uda, system będzie próbować ponownie.
  • Skalowalność: Możliwość rozpraszania zadań i delegowania ich do wielu handlerów.
  • Elastyczność: Komunikaty można łatwo rozbudowywać, dodając nowe rodzaje wiadomości i handlerów.

Zachęcam do eksperymentowania z Messengerem w Symfony, aby zobaczyć, jak prosto można osiągnąć wydajną i skalowalną architekturę opartą na zdarzeniach.