Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Exchange Routing and Binding Keys

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's change this delay back to one second... so we're not waiting all day for our photos to be processed.

... lines 1 - 23
class ImagePostController extends AbstractController
{
... lines 26 - 40
public function create(Request $request, ValidatorInterface $validator, PhotoFileManager $photoManager, EntityManagerInterface $entityManager, MessageBusInterface $messageBus)
{
... lines 43 - 63
$envelope = new Envelope($message, [
new DelayStamp(1000)
]);
... lines 67 - 69
}
... lines 71 - 98
}

Simple Setup: 1 Fanout Exchange per Queue

In messenger.yaml, the messages sent to each transport - async and async_priority_high - need to ultimately be delivered into two different queues so that we can consume them independently. And... we've accomplished that!

But there are two different ways that we could have done this. First, remember that in AMQP, messages are sent to an exchange, not a queue. Right now, when a message is routed to the async transport, Messenger sends that to an exchange called messages. You don't see that config here only because messages is the default exchange name in Messenger.

When a message is routed to the async_priority_high transport, Messenger sends that to an exchange called messages_high_priority. Each transport always sends to exactly one exchange.

Then, each exchange routes every message to a single queue, like the messages exchange sends to a messages queue... and messages_high_priority sends to a messages_high queue. There is not a routing key on the binding: Messenger binds each exchange to one queue... but with no routing key. That's how a "fanout" exchange works: it doesn't care about routing keys... it just sends each message to every queue bound to it.

1 Direct Exchange to 2 Queues

So that's one way to to solve this problem. The other way involves having only a single exchange... but making it smart enough to send some messages to the messages queue and other messages to messages_high. We do that with smarter binding and routing keys... which we already saw with the delays exchange.

Configuring a Direct Exchange

Let's refactor our transports to use this "smarter" system. Under the async transport, add options, then exchange, and set name to messages. If we stopped here, this would change nothing: this is the default exchange name in Messenger.

framework:
messenger:
... lines 3 - 19
transports:
... line 21
async:
... lines 23 - 25
options:
... line 27
exchange:
name: messages
... lines 30 - 53

But now, add a type key set to direct. This does change things: the default value is fanout. Add one more key below this: default_publish_routing_key set to normal.

framework:
messenger:
... lines 3 - 19
transports:
... line 21
async:
... lines 23 - 25
options:
... line 27
exchange:
name: messages
type: direct
default_publish_routing_key: normal
... lines 32 - 53

I'll talk about that in a second. Next, add a queues section. Let's "bind" this exchange to a queue called messages_normal. But we won't stop there! Under this, add binding_keys set to [normal].

framework:
messenger:
... lines 3 - 19
transports:
... line 21
async:
... lines 23 - 25
options:
... line 27
exchange:
name: messages
type: direct
default_publish_routing_key: normal
... lines 32 - 33
queues:
messages_normal:
binding_keys: [normal]
... lines 37 - 53

That word normal could be any string. But it's no accident that this matches what we set for default_publish_routing_key.

Deleting all the Exchanges and Queues

Instead of talking a ton about what this will do... let's... see it in action! Click to delete a photo: that should send a message to the async transport. Oh, but the AJAX call explodes! Open up the profiler to see the error. Ah:

Server channel error: 406, message: PRECONDITION_FAILED - inequivalent arg 'type' for exchange 'messages': received 'direct' but current is 'fanout'

The problem is that we already have an exchange called messages , which is a fanout type... but now we're trying to use it as a direct exchange. AMQP is warning us that we're trying to do something crazy!

So let's start over. Now that we're doing things a new way, let's hit the reset button and allow Messenger to create everything new.

Find your terminal - I'll log out of MySQL - and stop your worker... otherwise it will keep trying to create your exchanges and queues with the old config.

Then move back to the RabbitMQ admin, delete the messages exchange... then the messages_high_priority exchange. And even though the queues won't look different, to be extra safe, let's delete both of them too.

So we're back to no queues and only the original exchanges that AMQP created - which we're not using anyways - and the delays exchange. We're starting from scratch!

Go back to our site, delete the second image and... it looks like it worked! Cool! Let's see what happened inside RabbitMQ! Yea! We have a new exchange called messages and it's a direct type. Inside, it has a single binding that says:

When a message is sent to this exchange with a routing key called normal, it will be delivered to the queue called messages_normal.

This was all set up thanks to the queues and binding_keys config. This tells Messenger:

I want you to create a queue called messages_normal. Also, make sure that there is a binding on the exchange that will route any messages with a routing key set to normal to this queue.

But... did Messenger send the message with that routing key? Until now, other than the delay stuff, Messenger has been delivering our messages to AMQP with no routing key. The default_publish_routing_key config changes that. It says:

Hey! Whenever a message is routed to the async transport, I want you to send it to the messages exchange with a routing key set to normal.

This all means that if we look at the queues... yep! We have a message_normal queue with one message waiting inside! We did it!

Next, let's repeat this for the other transport. Then, we'll learn how this gives us the flexibility to dynamically control where a message will be delivered at the moment we dispatch it.

Leave a comment!

8
Login or Register to join the conversation

fanot vs direct exchange ?

Reply

Hey Jeremy,

Could you clarify your message adding a bit more context? :)

Cheers!

Reply
Default user avatar
Default user avatar Olya Ladygina | posted 3 years ago

How I can add binding_keys for queue when application is already running (not in config)?

Reply

Hi Olya Ladygina!

The binding_keys is only used by Symfony when it's "setting up" the queues. So basically, if you already have a queue, there is nothing stopping you from manually going into RabbitMQ and adding the binding key there. Symfony won't know or care that this happened :). I would also add it to the config, just for consistency (unless you're auto_setup: false, which means you're telling Symfony to never do any setup - in that case, binding_keys is truly 100% ignored and unnecessary).

Let me know if that makes sense!

Cheers!

Reply
Default user avatar
Default user avatar Olya Ladygina | weaverryan | posted 3 years ago

Thanks for the answer, but unfortunately this does not suit me, because I cannot know what binding_keys I need to add. And there is no point in manually adding or removing them, the application should do it.

Reply

Hi Olya Ladygina!

Ah, ok! Can you explain what you need to accomplish a bit more? I think I am misunderstanding, and not giving you a good answer :). What is the actual problem that you need to solve? That will help me see why you can't put the binding_keys into config.

Cheers!

Reply
Roman A. Avatar
Roman A. Avatar Roman A. | posted 3 years ago

Do you think that it is a really good way to initialize all settings kinda "side-effect" style? I think it is the wrong approach for a production server. Instead using the auto_setup option in the messenger component we have to configure all the things manually. It would be cool if you explain how to configure it by yourself in the Rabbit. Or at least just mention that it is totally not a good idea to use the auto_setup option in production.

Reply

Hey Roman,

Could you want this tutorial up to the 44 chapter, including 44? Here's where exactly we set auto_setup to false: https://symfonycasts.com/sc... . I suppose you will answer the question yourself after it. Or ping us again if it would be still fuzzy to you.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // v1.8.0
        "doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
        "doctrine/orm": "^2.5.11", // v2.6.3
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
        "sensio/framework-extra-bundle": "^5.3", // v5.3.1
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/flex": "^1.9", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/property-access": "4.3.*", // v4.3.2
        "symfony/property-info": "4.3.*", // v4.3.2
        "symfony/serializer": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.5", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.7
        "symfony/debug-bundle": "4.3.*", // v4.3.2
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/var-dumper": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}
userVoice