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

Skip to content

[Messenger] Provides a safe single handler strategy #36958

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
gquemener opened this issue May 25, 2020 · 18 comments
Closed

[Messenger] Provides a safe single handler strategy #36958

gquemener opened this issue May 25, 2020 · 18 comments

Comments

@gquemener
Copy link
Contributor

gquemener commented May 25, 2020

Description
(This discussion is following #29167)

Single handler strategy is common when using a command bus (as a command should be handled by one and only one handler).
Currently, the only way to enforce this strategy is through the HandleTrait which performs the check after the message has been handled (which can have harmful consequences).

I propose to implement a safe (meaning that prevents any handler to be called) check.
Once this is implemented, I don't see any reason to keep the HandleTrait.

So far, I see a few ways to achieve that.

Add a allowMultipleHandlers flag to the HandleMessageMiddleware.

Then it would be a matter of

        $handlers = $this->handlersLocator->getHandlers($envelope);
        if (!$this->allowMultipleHandlers && count($handlers) > 1) {
            throw new TooManyHandlersForMessageException(sprintf('Too many handlers are registered for message "%s".', $context['class']));
        }
        foreach ($handlers as $handlerDescriptor) {

This flag should be true by default to be BC.

Add a new implementation of HandlersLocatorInterface

This implementation would throw an exception when more than one handler is located. The correct implementation would most likelly be injected through the MessengerPass or the FrameworkExtension.

Add the check during compilation

The check could even live in the MessengerPass or the FrameworkExtension itself. The drawback is that this would couple it with the framework and prevent single handler strategy to be used without the framework.

@gquemener
Copy link
Contributor Author

gquemener commented May 25, 2020

Here's an implementation example that follows the idea behind the second option : https://github.com/prooph/service-bus/blob/master/src/Plugin/Router/SingleHandlerRouter.php.

The only difference is that it does not throw an exception, but instead provides the latest defined handler for a message (by overriding any existing value).

@ogizanagi
Copy link
Contributor

ogizanagi commented May 25, 2020

Once this is implemented, I don't see any reason to keep the HandleTrait.

The HandleTrait exists in order to ease implementing query buses as well, thus accessing the handler returned value. Ensuring there is only one result, meaning one handler responded, is a consequence of the desire to access a single result.

Regarding this feature request, as we probably want to detect the issue as soon as possible, I'd recommend performing the check during compilation. But if we go there, the same kind of feature should be available to check messages have at least one handler as well, while this is only checked at runtime right now (in the middleware).

@gquemener
Copy link
Contributor Author

gquemener commented May 25, 2020

The HandleTrait exists in order to ease implementing query buses as well, thus accessing the handler returned value. Ensuring there is only one result, meaning one handler responded, is a consequence of the desire to access a single result.

I see, it does make sense indeed. However, I'd probably rather have this piece of code live within my codebase, rather than vendor, but that's up to everyone I guess.

In the end, I would be in favor of both having these checks within the component (to be able to use it as a standalone component and raise errors at runtime) and to have some integrations through the Framework bundle (to early detect errors through compilation).

If I had to pick one, then I would personally prefer the runtime checks.

@carsonbot
Copy link

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?

@gquemener
Copy link
Contributor Author

Indeed dear bot, I haven't use symfony/messenger as a command bus for a while.
Is there any news @ogizanagi ?

@carsonbot carsonbot removed the Stalled label Feb 18, 2021
@ogizanagi
Copy link
Contributor

No, nothing has been contributed in this regard from what I know.

@gquemener
Copy link
Contributor Author

gquemener commented Feb 18, 2021

Ok, I'll just leave some notes here for my future self:

Consider defining a command bus as follow:

framework:
    messenger:
        # The bus that is going to be injected when injecting MessageBusInterface
        default_bus: command.bus
        buses:
            command.bus:
                default_middleware: exactly_one_handler # That part is new
                middleware:
                    - validation
                    - doctrine_transaction
            query.bus:
                middleware:
                    - validation
            event.bus:
                default_middleware: allow_no_handlers
                middleware:
                    - validation

@gquemener
Copy link
Contributor Author

gquemener commented Feb 18, 2021

As the HandlersLocator returns a Generator, detecting that multiple handlers have been registered become inefficient, thought it looks like a trivial solution.

$handlers = $this->handlersLocator->getHandlers($envelope);
if (!$this->allowMultipleHandlers && count(iterator_to_array($handlers)) > 1) {
    throw new MultipleHandlersForMessageException(sprintf('Multiple handler for message "%s"', $context['class']));
}

As stated above another solution is to delegate this logic to the HandlersLocatorInterface dependency:

  • ExactlyOneHandlerLocator: used when default_middleware: exactly_one_handler is configured
  • AtLeastOneHandlerLocator: configured by default, throws a NoHandlerForMessageException when necessary
  • HandlersLocator: used when allow_no_handlers is configured

What do you think?

gquemener added a commit to gquemener/symfony that referenced this issue Feb 19, 2021
@jbdelhommeau
Copy link
Contributor

Very useful and coherant for compliant CQRS pattern.

@webdevilopers
Copy link

Thanks for the invitation @gquemener :

I must confess that I never came across a use case to let a command be handled by multiple handlers. So far I thought that this is actually an anti-pattern.
Currently - using CQRS + Event Sourcing - we have a single command handled by a single handler that only changes state on a single (event-sourced= aggregate root). But a single command can result in multiple Events!

If any other state has to be changed then a process manager / saga catches the event and creates a new command. Most of the time this will happen asynch.

Hope this helps.

@gquemener
Copy link
Contributor Author

gquemener commented Mar 3, 2021

I agree, and that is the way a command bus should be used.

However, symfony/messenger doesn't provide a way to enforce it, and I'm wondering if that's a pain point in the DX for someone.

AFAIK, this fact (that multiple handlers can be silently registered for a single command) is as old as the symfony/messenger component, and has never (or has it?) raised concerns. Thus, I only see two solutions : it has never hurt anyone (and there's no reason to implement a safe guard) OR it has, but no-one detected it/reported it.

@carsonbot
Copy link

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

@gquemener
Copy link
Contributor Author

Hello @carsonbot, I've opened a PR related to this issue. Would you mind reopening it, please?

@ogizanagi ogizanagi reopened this Oct 3, 2021
@xabbuh xabbuh removed the Stalled label Oct 4, 2021
gquemener added a commit to gquemener/symfony that referenced this issue Oct 5, 2021
gquemener added a commit to gquemener/symfony that referenced this issue Oct 5, 2021
@carsonbot
Copy link

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?

@carsonbot
Copy link

Friendly reminder that this issue exists. If I don't hear anything I'll close this.

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

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