Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Prime Time with Messenger: Queues, Workers & more Fun!

weaverryan
November 22, 2019

Prime Time with Messenger: Queues, Workers & more Fun!

In Symfony 4.4, Messenger will lose its experimental label and officially become stable! In this talk, we'll go through Messenger from the ground-up: learning about messages, message handlers, transports and how to consume messages asynchronously via worker commands. Everything you need to make your app faster by delaying work until later.

We'll also talk about the many new features that were added to Messenger since Symfony 4.3, like retries, the failure transport and support for using Doctrine and Redis as transports.

Let's get to work with Messenger!

weaverryan

November 22, 2019
Tweet

More Decks by weaverryan

Other Decks in Technology

Transcript

  1. > Lead of the Symfony documentation team
 > Code story-teller

    for SymfonyCasts.com > Husband of the much more talented @leannapelham symfonycasts.com twitter.com/weaverryan Yo! I’m Ryan! > Father to my much more charming son, Beckett
  2. Messenger: • A message bus • A command bus •

    An event bus • A query bus • A school bus (beep beep) • A way to run code asynchronously @weaverryan
  3. // src/Controller/StroopwafelOrderController.php /** * @Route("/stroopwafel/order") */ public function index(Request $request)

    { $data = json_decode($request->getContent(), true); $topping = $data['topping'] ?? 'plain'; $size = $data['size'] ?? 'large'; $recipient = $data['recipient'] ?? 'Leanna'; // ... } POST /stroopwafel/order { "topping": "plain", "size": "medium", "recipient": "Nicolas" }
  4. public function index(Request $request) { // ... } $this->logger->info(sprintf( 'Putting

    %s dough on the waffle iron', $size )); usleep(500000); $this->logger->info('Adding some caramel'); usleep(500000); if ($topping !== 'plain') { usleep(500000); $this->logger->info(sprintf( 'Adding topping: '.$topping )); } $this->logger->info('Delivering to '.$recipient); usleep(500000);
  5. public function index(Request $request) { // ... } $this->logger->info(sprintf( 'Putting

    %s dough on the waffle iron', $size )); usleep(500000); $this->logger->info('Adding some caramel'); usleep(500000); if ($topping !== 'plain') { usleep(500000); $this->logger->info(sprintf( 'Adding topping: '.$topping )); } $this->logger->info('Delivering to '.$recipient); usleep(500000);
  6. <?php // src/Message/StroopwafelOrder.php namespace App\Message; class StroopwafelOrder { private $topping;

    private $size; private $recipient; public function __construct(string $topping, string $size, string $recipient) { $this->topping = $topping; $this->size = $size; $this->recipient = $recipient; } // getters }
  7. /** * @Route("/stroopwafel/order") */ public function index(Request $request) { //

    ... // ... } $order = new StroopwafelOrder( $topping, $size, $recipient ); $this->logger->info(sprintf( 'Putting %s dough on the waffle iron', $order->getSize() )); usleep(500000);
  8. <?php // src/MessageHandler/StroopwafelOrderHandler.php class StroopwafelOrderHandler { private $logger; public function

    __construct(LoggerInterface $logger) { $this->logger = $logger; } public function handleOrder(StroopwafelOrder $order) { $this->logger->info(sprintf('%s dough on iron', $order->getSize())); usleep(500000); $this->logger->info('Adding some caramel'); usleep(500000); // ... } }
  9. /** * @Route("/stroopwafel/order") */ public function index(Request $request, StroopwafelOrderHandler $handler)

    { $data = json_decode($request->getContent(), true); // ... $order = new StroopwafelOrder(…) $topping, $size, $recipient ); $handler->handleOrder($order); return new Response(204); }
  10. @weaverryan Calling a Service Hey StroopwafelHandler! Please make me a

    plain, small Stroopwafel for Nicolas! $stroopHandler->handleOrder( 'plain', 'small', 'Nicolas' );
  11. @weaverryan Using a "message" Hey StroopwafelHandler! Please handle this StroopwafelOrder!

    $order = new StroopwafelOrder(…) $stroopHandler->handleOrder( $order );
  12. @weaverryan Message Bus Hey "message bus"! Call whoever handles this

    message $order = new StroopwafelOrder(…) $messageBus->dispatch( $order );
  13. How does Messenger know to call StroopwafelOrderHandler when we dispatch

    a StroopwafelOrder? @weaverryan Connecting messages & handlers
  14. <?php namespace App\MessageHandler; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; class StroopwafelOrderHandler implements MessageHandlerInterface {

    // ... public function __invoke(StroopwafelOrder $order) { // … } } Type-hint: Tell Messenger what object it handles
  15. public function index(MessageBusInterface $messageBus) { // ... $order = new

    StroopwafelOrder( $topping, $size, $recipient ); $stroopHandler->handleOrder($order); return new Response(204); } $messageBus->dispatch($order); Call the bus
  16. Centralization Possibilities • Log each time a "message" is dispatched

    • Wrap each handler inside a transaction • Store the message somewhere else… then run some command to read & handle it later
  17. Transports: where to send/read messages #.env ###> symfony/messenger ### #

    Choose one of the transports below # MESSENGER_TRANSPORT_DSN=amqp://guest@localhost:5672/%2f/messages # MESSENGER_TRANSPORT_DSN=doctrine://default # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages ###< symfony/messenger ###
  18. Doctrine transport #.env ###> symfony/messenger ### # Choose one of

    the transports below # MESSENGER_TRANSPORT_DSN=amqp://guest@localhost:5672/%2f/messages MESSENGER_TRANSPORT_DSN=doctrine://default # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages ###< symfony/messenger ### New in 4.3
  19. Routing to a transport # config/packages/messenger.yaml framework: messenger: transports: async:

    '%env(MESSENGER_TRANSPORT_DSN)%' routing: 'App\Message\StroopwafelOrder': async
  20. How do we tell Messenger that we *are* ready to

    handle the message? (worker) @weaverryan Handling/Consuming
  21. @weaverryan Running the "worker" • Supervisord (see Symfony docs) •

    Automatically via your PaaS (e.g. SymfonyCloud, Heroku) • Run as many workers as you need
  22. @weaverryan > symfony run -d \ symfony console messenger:consume \

    --watch=config,src,templates,vendor What about Locally?
  23. Adding an Envelope public function orderStroop(MessageBusInterface $messageBus) { // ...

    $envelope = new Envelope($order); $messageBus->dispatch($envelope); // ... }
  24. Adding Some Stamps public function orderStroop(MessageBusInterface $messageBus) { // ...

    $envelope = new Envelope($order); $messageBus->dispatch($envelope, [ new DelayStamp(5000) ]); // ... }
  25. Middleware Add Stamps public function orderStroop(MessageBusInterface $messageBus) { // ...

    $envelope = new Envelope($order); $envelope = $messageBus->dispatch($envelope, [ new DelayStamp(5000) ]); dump($envelope); // ... }
  26. Route by priority framework: messenger: transports: async: # ... async_high_priority:

    #... routing: 'App\Message\StroopwafelOrder': async_high_priority 'App\Message\CoffeeOrder': async_high_priority 'App\Message\CleanBathroom': async 'App\Message\WashDishes': async
  27. // ... class CoffeeOrderHandler implements MessageHandlerInterface { // ... public

    function __invoke(CoffeeOrder $order) { if (rand(0, 10) > 2) { throw new OutOfBeansException('Ahhh!'); } $this->logger->info('I made some coffee for you'); } }
  28. class StroopwafelControllerTest extends WebTestCase { public function testOrderStroopwafel() { $client

    = static::createClient(); $client->request( 'POST', '/stroopwafel/order', json_encode(['topping' => 'plain']) ); $this->assertResponseIsSuccessful(); } }
  29. public function testOrderStroopwafel() { // ... $this->assertResponseIsSuccessful(); /** @var InMemoryTransport

    $transport */ $transport = self::$container ->get('messenger.transport.async'); } $this->assertCount(1, $transport->getSent()); $this->assertInstanceOf( StroopwafelOrder::class, $transport->getSent()[0]->getMessage() );
  30. @weaverryan A queue of new features •Redis Transport (4.3) (delay

    support 4.4) •PhpSerializer (4.3) •Worker Events (4.3) •Auto Clear EntityManager (4.4) •from_transport (4.3) •API Platform integration • … and …