HEAPS

Symfony Messenger: niezawodne zadania w tle na produkcji

Dowiedz się, jak Symfony Messenger pozwala wynieść ciężką pracę poza żądanie HTTP, bezpiecznie ponawiać nieudane zadania i utrzymać responsywność aplikacji PHP pod obciążeniem.

Dlaczego zadania w tle mają znaczenie w Symfony

Większość aplikacji Symfony zaczyna od prostego wzorca: użytkownik wysyła żądanie, kontroler wykonuje pracę, a odpowiedź wraca do przeglądarki. To działa przy odczycie karty produktu. Rozsypuje się, gdy wysyłasz faktury, skalujesz obrazy, wołasz zewnętrzne API albo synchronizujesz dane z systemem magazynowym.

Jeśli te zadania zostają w żądaniu HTTP, użytkownicy dłużej czekają, pojawiają się timeouty, a jedna wolna zależność potrafi zatrzymać cały checkout. Symfony Messenger rozwiązuje to przez wysłanie wiadomości teraz i przetworzenie jej później, w workerze poza żądaniem webowym.

Czym faktycznie jest Symfony Messenger

Messenger to magistrala wiadomości. Aplikacja tworzy mały obiekt opisujący pracę do wykonania, a potem wysyła go do magistrali. Transport (najczęściej Redis, Doctrine albo RabbitMQ) zapisuje wiadomość. Osobny worker konsoli pobiera kolejkę i uruchamia handler.

Ten podział daje trzy praktyczne korzyści:

  1. Szybsze odpowiedzi dla użytkowników.
  2. Automatyczne ponawianie, gdy zewnętrzna usługa chwilowo nie działa.
  3. Wyraźne oddzielenie „przyjmij pracę” od „dokończ pracę”.

Minimalny przykład

Zacznij od wiadomości, która niesie tylko dane potrzebne handlerowi:

namespace App\Message;

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

Następnie napisz handler, który wykonuje właściwą pracę:

namespace App\MessageHandler;

use App\Message\SendOrderConfirmation;
use App\Repository\OrderRepository;
use App\Service\Mailer\OrderMailer;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class SendOrderConfirmationHandler
{
    public function __construct(
        private OrderRepository $orders,
        private OrderMailer $mailer,
    ) {
    }

    public function __invoke(SendOrderConfirmation $message): void
    {
        $order = $this->orders->get($message->orderId);
        $this->mailer->sendConfirmation($order);
    }
}

W kontrolerze albo serwisie wyślij wiadomość zamiast wysyłać maila bezpośrednio:

use App\Message\SendOrderConfirmation;
use Symfony\Component\Messenger\MessageBusInterface;

public function placeOrder(MessageBusInterface $bus, int $orderId): void
{
    // Najpierw zapisz zamówienie, potem kolejkuj efekty uboczne.
    $bus->dispatch(new SendOrderConfirmation($orderId));
}

Konfiguracja transportu asynchronicznego

W config/packages/messenger.yaml skieruj ciężkie wiadomości do prawdziwej kolejki:

framework:
    messenger:
        failure_transport: failed

        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2
                    max_delay: 10000
            failed: 'doctrine://default?queue_name=failed'

        routing:
            'App\Message\SendOrderConfirmation': async

Typowy DSN Redis wygląda jak redis://localhost:6379/messages. Dla zespołów już pracujących na MySQL lub PostgreSQL transport Doctrine to dobry start. Redis albo RabbitMQ warto wdrożyć, gdy rośnie wolumen.

Jak prawidłowo uruchamiać workery

Workery to procesy długożyjące. Uruchamiaj je przez Supervisor, systemd albo orchestrator kontenerów:

php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M

--time-limit i --memory-limit wymuszają czysty restart, zanim wycieki pamięci staną się incydentem produkcyjnym. Po każdym deployu restartuj workery, żeby załadowały nowy kod.

Ponawianie, awarie i obserwowalność

Tymczasowe błędy sieciowe powinny wracać do kolejki. Błędy trwałe powinny się zatrzymać. Messenger wspiera exponential backoff przez retry_strategy. Wiadomości, które nadal padają, trafiają do failure transport, gdzie możesz je podejrzeć i ponowić:

php bin/console messenger:failed:show
php bin/console messenger:failed:retry

Dodawaj strukturalne logi w handlerach. Loguj klasę wiadomości, id encji i numer próby. Dzięki temu awarie kolejki diagnozuje się dużo łatwiej niż po ogólnym wpisie „mail failed”.

Zasady projektowe, które utrzymują Messenger w ryzach

Trzymaj wiadomości małe i serializowalne. Preferuj identyfikatory zamiast pełnych grafów encji. Duże obiekty w kolejce bolą przy każdej zmianie modelu domenowego.

Rób handlery idempotentne. Mail z potwierdzeniem nie powinien wyjść dwa razy tylko dlatego, że worker padł po sukcesie, a przed potwierdzeniem wiadomości. Zapisz flagę „confirmation sent” albo użyj unikalnego id wiadomości u dostawcy.

Wysyłaj wiadomość po commicie transakcji. Jeśli zakolejujesz job, a potem transakcja się wycofa, worker może przetworzyć zamówienie, którego nigdy nie było. Użyj middleware Doctrine albo dispatch w listenerze po commicie, gdy spójność ma znaczenie.

Jeden handler, jedna odpowiedzialność. Wiadomość SendOrderConfirmation powinna wysłać mail potwierdzający. Faktury, stock i CRM mogą być osobnymi wiadomościami z tego samego zdarzenia domenowego.

Kiedy Messenger nie jest dobrym narzędziem

Nie każda wolna operacja należy do kolejki. Jeśli użytkownik musi zobaczyć wynik od razu (ekran potwierdzenia płatności, walidacja na żywo), zostaw pracę synchroniczną i ją zoptymalizuj. Messenger sprawdza się, gdy biznes akceptuje krótkie opóźnienie w zamian za niezawodność.

Podsumowanie

Symfony Messenger to jedno z narzędzi o najwyższej dźwigni w nowoczesnym stacku Symfony. Chroni czas odpowiedzi, amortyzuje kapryśne integracje i zamienia efekty uboczne w jawne, testowalne jednostki pracy. Jeśli Twoja aplikacja PHP nadal wysyła maile, generuje PDF albo woła zewnętrzne API w kontrolerach, przeniesienie tej pracy do Messengera zwykle najszybciej poprawia stabilność produkcji.