Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Messenger] configure bus on transport #34247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

Tobion
Copy link
Contributor

@Tobion Tobion commented Nov 6, 2019

Q A
Branch? 4.4
Bug fix? no
New feature? yes
Deprecations? no
Tickets Fix #32030 and part 3) of #32049
License MIT
Doc PR
  1. Changed implementation of RoutableMessage bus to use the existing ReceivedStamp (and SentToFailureTransportStamp) to do the routing. This way we can get rid of BusNameStamp and several middlewares.
  2. The BusNameStamp was not a good solution because it does not respect that message handling is async. If you dispatch a message with bus A and delay (or it goes into retry), then rename the bus and redeploy, the message cannot be consumed suddenly because bus A does not exist anymore.
    This is why explicitly setting the bus on the transport seems like the best solution.
  3. Added bus option on the transport which fixes part 3) of [Messenger] make it easier to consume third-party messages #32049. The only thing that changes for people: If you defined more than one bus, you must set the "bus" config option on the transport. Messages received from this transport will be dispatched to this bus automatically. This replaces the --bus option of messenger:consume command and the BusNameStamp.
  4. Removed --bus option of messenger:consume which was not good: The worker config is usually in supervisor or some other tool independent of messenger code. So you could easily forget to update the worker config there if you changed a bus name and it will not work at all. Or you accidentally dispatch messages to the wrong bus. With the "bus" option it's always in sync and validated on container build instead of at runtime of the worker.
  5. With the removal of BusNameStamp we don't need to fallback bus inside RoutableMessageBus which was only for external messages or messages dispatched before its introduction for BC. With that we can make the default_busconfig optional even when having multiple buses which fixes [Messenger] Why are you forced to specify a "default bus"? #32030 and prevents unwanted autowiring. You can still use it though and is only a requirement for
    if (!$this->container->has('messenger.default_bus')) {
    $message = class_exists(Envelope::class) ? 'You need to define the "messenger.default_bus" configuration option.' : 'Try running "composer require symfony/messenger".';
    throw new \LogicException('The message bus is not enabled in your application. '.$message);
    }

    The only thing not handled is [Messenger] Why are you forced to specify a "default bus"? #32030 (comment) but that should be a separate issue. If you used named autowiring there is no problem. It highly depends on the situation and to me we should try to enhance the error message there when autowiring fails due to MessageBusInterface

@Tobion Tobion force-pushed the messenger-no-bus-name-stamp branch from 08bdab1 to 13fbf63 Compare November 6, 2019 13:09

return $this->fallbackBus->dispatch($envelope, $stamps);
/** @var SentToFailureTransportStamp|null $sentToFailureStamp */
$sentToFailureStamp = $envelope->last(SentToFailureTransportStamp::class);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part replaces the FailedMessageProcessingMiddleware and makes sure the correct bus is used for messages consumed from the failure transport (directly or via messenger:failed:retry)

@Tobion Tobion force-pushed the messenger-no-bus-name-stamp branch 2 times, most recently from 4541e3f to 083af0a Compare November 6, 2019 17:29
@Tobion Tobion changed the title [Messenger] WIP: configure bus on transport [Messenger] configure bus on transport Nov 6, 2019
@Tobion Tobion force-pushed the messenger-no-bus-name-stamp branch 2 times, most recently from d5535bf to 0361526 Compare November 6, 2019 18:11
@Tobion Tobion force-pushed the messenger-no-bus-name-stamp branch from 0361526 to a930c6f Compare November 6, 2019 19:07
@Tobion Tobion force-pushed the messenger-no-bus-name-stamp branch from a930c6f to bbb5cb3 Compare November 6, 2019 19:08
@Tobion
Copy link
Contributor Author

Tobion commented Nov 6, 2019

I've only deprecated BusNameStamp so existing messages can be deserialized without an error.

Copy link
Member

@weaverryan weaverryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for another thoughtful PR :).

I've started to review, but I'm hitting an immediate doubt. Currently, I could have 2 buses (command.bus and event.bus) and safely dispatch both of those to the same transport... because I don't care about priority and my app is simple and that's good enough for me. But with this PR, wouldn't I need to be careful to make sure that all "commands" are routed to a transport where bus is set to command.bus and all events are routed to a transport where bus is set to event.bus?

Attaching a bus name to each message is quite powerful: a worker can consume many different messages and each will definitely go through the correct bus. If the main motivation for the PR is to make the BusNameStamp optional when consuming from an external transport, could we instead leave the BusNameStamp idea, but add a new default_receive_bus option to each transport? This would be the transport that would be use if no explicit BusNameStamp is added. We could even change the behavior to throw an exception of no BusNameStamp or default_receive_bus is present (and there is more than one bus) so that you don't accidentally route all external messages to the "default bus".

We can also, of course, make the default_bus config optional as this PR does - that's kind of a separate "thing" and it seems sensible to me.

Thanks!

->scalarNode('default_bus')->defaultNull()->end()
->scalarNode('default_bus')
->defaultNull()
->info('Bus name that will be used for autowiring MessageBusInterface. When there is only a single bus this happens automatically. With multiple buses you can set this option or use named autowiring based on the bus name or explicit wiring.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
->info('Bus name that will be used for autowiring MessageBusInterface. When there is only a single bus this happens automatically. With multiple buses you can set this option or use named autowiring based on the bus name or explicit wiring.')
->info('Bus name that will be used for autowiring MessageBusInterface. When there is only a single bus this happens automatically. With multiple buses, you can set this option or use named autowiring based on the bus name (or explicit wiring).')

Mostly, the last part of the sentence was confusing because it was "you can do X, or Y or Z" when really it's mean to read like "you can do X or Y (and Y is really 2 things). Hopefully this is clearer - it's minor regardless.

}

return $this->fallbackBus->dispatch($envelope, $stamps);
$envelope = $envelope->with(new ReceivedStamp($originalReceiver));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic does feel a bit unnatural here. For example, all the "sent to failure" stuff is done via an event listener, but then we wire this part of the logic right inside this class. It could still be done as an event listener (to look for the SentToFailureTransportStamp and then add the ReceivedStamp, couldn't it? I think the complexity is this doing this part of the process if it were in a different listener:

// in case the original receiver does not exist anymore, use the bus configured for failure transport

But I'm still not convinced this is the right place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually fixing a bug and a requirement for messenger:failed:retry to work with multiple buses.
If messages end in the failure transport because messages are missing the BusNameStamp (external) or because developers renamed the bus, then those messages currently cannot be consumed at all from the failure transport because they can never go to the right bus.
By doing these checks in the RoutableMessageBus we can use the configured bus of the original transport.

@Tobion
Copy link
Contributor Author

Tobion commented Nov 6, 2019

Currently, I could have 2 buses (command.bus and event.bus) and safely dispatch both of those to the same transport... because I don't care about priority and my app is simple and that's good enough for me. But with this PR, wouldn't I need to be careful to make sure that all "commands" are routed to a transport where bus is set to command.bus and all events are routed to a transport where bus is set to event.bus?

That is true. But there is actually a use-case for not receiving from the same bus used for sending: #31848
He worked around the BusNameStamp by using the --bus option.

If we keep the BusNameStamp how do you suggest to deal with my argument 2) above? People loose all messages just by renaming the bus and you cannot warn them. Or all messages get dispatched to the default bus which is likely not the right one (and we don't want to force specifying one).
I don't really see a solution for this except requiring to chose the bus explicitly as in this PR which can be validated at compile time.

@weaverryan
Copy link
Member

That is true. But there is actually a use-case for not receiving from the same bus used for sending: #31848
He worked around the BusNameStamp by using the --bus option.

Hmm, instead of a default_bus option, it could be called receive_bus and (most importantly) take precedent over BusNameStamp. Using receive_bus would be an advanced option (used for people consuming from external transports or people that want to handle via a different bus).

Or... the user in #31848 might be doing something we don't want to support - and maybe they should handle the received message in the same bus. And then, inside the handler, dispatch another message (which could be handled synchronously) into the "correct" bus.

If we keep the BusNameStamp how do you suggest to deal with my argument 2) above?

It's an edge case, but one that, indeed, we should worry about. To be fair, doesn't this PR have the same limitation, at least for the failure transport? From RoutableMessageBus:

// in case the original receiver does not exist anymore, use the bus configured for failure transport

That's less of an evil (because it affects less use-cases), but it still seems like we would have the same hole.

I could argue that removing a bus, while there are messages in the queue that are "scheduled" to be sent to it is simply something you should not do. After all, users already cannot remove a message class / handler from their app if there are more messages associated with that class that are in the queue. This is maybe the same situation as that? It seems to me that if a bus is missing, handling should fail and it should go into the failure transport so the user can look into it and deal with it.

@teohhanhui
Copy link
Contributor

teohhanhui commented Nov 7, 2019

BusNameStamp feels like a hack... A message (Envelope) that knows about the message bus seems like a symptom of leaky abstraction.

@weaverryan
Copy link
Member

BusNameStamp feels like a hack... A message (Envelope) that knows about the message bus seems like a symptom of leaky abstraction.

I'm fine with a better implementation if you can suggest one ;). The goal is simple: if I dispatch a message to command.bus, the message should absolutely be handled by command.bus when it is consumed. I can see no other way of doing this than for the message itself to know which bus it should go into.

Btw, I'm totally cool with adding an additional way for the system to know which bus to use (like an option on the transport that defines the bus to use if no BusNameStamp is present). The original issue was about making it easier to consume "external" messages. It seems that would accomplish this.

@nicolas-grekas
Copy link
Member

What's the status here? I'm sorry I'm not into the topic much so I don't know how to help.
Is this PR on the critical path for the beta or can we consider it later?

@Tobion
Copy link
Contributor Author

Tobion commented Nov 9, 2019

I'm confident the Symfony Messenger should not promote features that only work (and not even reliable as I pointed out) when dispatching with messenger and consuming with messenger. For anyone who has experience with message queues, such things are just not reasonable and you face big surprises.
I would be fine if the BusNameStamp logic would be something people can opt-in to explicitly, e.g. by setting the "bus" option on the transport, that I added here, to something like "dynamic" or "same-as-sent-from". But I would not be ok to have this as default solution.

@smoench
Copy link
Contributor

smoench commented Nov 11, 2019

I already faced a problem with BusNameStamp in a client project. Lets assume we have some services. Service A is dispatching and Service B consuming from A plus dispatching messages. B saves those message information via ORM with doctrine_ping_connection and doctrine_close_connection middlewares registered. B also dispatching messages in entity listeners (🙊). But now we need another bus otherwise doctrine's UOW will crash as the connection has been closed before the flush/commit thanks to doctrine_close_connection-middleware. I ended up defining a service_a-bus as default in A plus adding this bus in B for consuming.

It would be great having this in 4.4. It would improve the DX.

@nicolas-grekas
Copy link
Member

Talking with @weaverryan on Slack, I think we should keep the BusNameStamp. Symfony Messenger talking to Symfony Messenger can legitimately provide a better/safer experience by default. This is what this stamp provides: traceability of the message in a way Symfony Messenger understands. Then, for interop with third-party apps, it's nice to not force them to use Symfony Messenger boilerplate.

Following this reasoning, it would make sense to me to allow binding a bus to a receiver - and this should be optional. When the stamp and the receiver don't match for the bus, we would need to decide for the best behavior: receiver wins, stamp wins, log, exception?

Anyway, I think it's too late to resolve this completly before 4.4/5.0
If there are things to deprecate in 5.1, let it be.

@Tobion
Copy link
Contributor Author

Tobion commented Nov 11, 2019

The main problem of the BusNameStamp to me is

If you dispatch a message with bus A and delay (or it goes into retry), then rename the bus and redeploy, the message cannot be consumed suddenly because bus A does not exist anymore.

IMO there should not be a default solution that allows people to fall into this unexpected and non-obvious trap. And if we keep this as default default behavior, I also don't see how to fix #32030.

As I don't have time to work on this PR to make a potential compromise, let's just postpone it. Maybe we can some up with a solution later.

@nicolas-grekas nicolas-grekas removed this from the 4.4 milestone Nov 12, 2019
@nicolas-grekas nicolas-grekas added this to the next milestone Nov 12, 2019
@fabpot
Copy link
Member

fabpot commented Aug 11, 2020

Closing then for now.

@fabpot fabpot closed this Aug 11, 2020
@nicolas-grekas nicolas-grekas modified the milestones: next, 5.2 Oct 5, 2020
@biforb4
Copy link

biforb4 commented Apr 15, 2021

any plans to continue with this?

@Tobion
Copy link
Contributor Author

Tobion commented Apr 27, 2021

Not at the moment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Messenger] Why are you forced to specify a "default bus"?
8 participants