-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Messenger] Dispatch events before & after each handler #52425
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
base: 7.4
Are you sure you want to change the base?
Conversation
93f7c9d
to
2406c88
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YES !
2406c88
to
21c29d9
Compare
This must target 7.1, due to feature freeze period. |
private Envelope $envelope; | ||
private string $handlerName; | ||
|
||
public function __construct(Envelope $envelope, string $handlerName) | ||
{ | ||
$this->envelope = $envelope; | ||
$this->handlerName = $handlerName; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use constructor property promotion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! A couple questions:
- as we target PHP 8.1, do we want to keep
private
+ getters, or can we usepublic readonly
instead? (and if targeting Symfony 7 and PHP 8.2, straightreadonly class
with public promoted properties) - does Symfony allow a mix of promoted and non-promoted, like this:
final class HandlerFailureEvent extends AbstractHandlerEvent { public function __construct( Envelope $envelope, string $handlerName, public \Throwable $exception, ) { parent::__construct($envelope, $handlerName); } }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've settled on public readonly
as I could find other occurrences in the 7.0
branch. I also used a mix of promoted and non-promoted in HandlerFailureEvent
, as in the code sample above.
* Event dispatched before a handler is called. | ||
*/ | ||
final class HandlerStartingEvent extends AbstractHandlerEvent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name of the class does not make it clear, that it is dispatched before.
Maybe BeforeHandlerStartEvent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually thought about this, but wanted to keep things organized by prefix, like Worker*
events. I don't mind though, what about just BeforeHandlerEvent
?
@@ -35,6 +39,7 @@ class HandleMessageMiddleware implements MiddlewareInterface | |||
public function __construct( | |||
private HandlersLocatorInterface $handlersLocator, | |||
private bool $allowNoHandlers = false, | |||
private ?EventDispatcherInterface $eventDispatcher = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private ?EventDispatcherInterface $eventDispatcher = null, | |
private ?EventDispatcherInterface $dispatcher = null, |
I like it more, but lets see what the others say
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually SendMessageMiddleware
uses $eventDispatcher
, so I guess it's better to keep it consistent here?
@@ -97,6 +106,14 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope | |||
} catch (\Throwable $e) { | |||
$exceptions[$handlerDescriptor->getName()] = $e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$exceptions[$handlerDescriptor->getName()] = $e; | |
$exceptions[$handlerDescriptor->getName()] = $e; | |
$this->eventDispatcher?->dispatch(new HandlerFailureEvent($envelope, $handlerDescriptor->getName(), $e)); |
Same for the success case and remove the $e
variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind dispatching the failure event in the catch
block, however I'm a bit reluctant about dispatching the success event in the try
block, as a failure in any listener/subscriber of this event would mark the handler as failed.
That's why I kept things outside the try/catch
.
@@ -58,6 +63,10 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope | |||
continue; | |||
} | |||
|
|||
$this->eventDispatcher?->dispatch(new HandlerStartingEvent($envelope, $handlerDescriptor->getName())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if the $handlerDescriptor
is fully passed to this event, and that a subscriber can mutate it.
This would solve my use case that I tried to implement in:
It would allow event subscribers to:
- read everything from the HandlerDescriptor, so also the options
- set a new HandlerDescriptor that has a custom callable that does specific logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm all for adding the whole HandlerDescriptor
in the events, you're right that it opens up a whole lot of possibilities.
However, I have no idea of the potential consequences of live-replacing a handler, so I'll leave it to others to decide if making the HandlerDescriptor
replaceable is the way to go!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ro0NL @OskarStark What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit that I also don't know what consequences this can have
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ruudk Do you actually have a need to replace the HandlerDescriptor
at the moment, or can we leave it for a later PR to maybe make it replaceable in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having it would be great, but maybe I can use the event system already to do what I want. I agree that we can later always decide to open this up.
Could we at least add the full HandlerDescriptor instead of only the name? That would make it a bit better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we at least add the full HandlerDescriptor instead of only the name? That would make it a bit better.
Yes, definitely. I'll change this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ruudk events now have the full HandlerDescriptor
!
@@ -97,6 +106,14 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope | |||
} catch (\Throwable $e) { | |||
$exceptions[$handlerDescriptor->getName()] = $e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if a HandlerFailureEvent subscriber could silence an exception.
Right now, it's only possible to log them. But by always passing the exception to the HandlerFailureEvent and dispatching that, a subscriber could say "ignore this".
A use case could be to annotate #[IgnoreException(SomeTimeoutException::class)]
on the handler. In case of an exception, a HandlerFailureEvent subscriber would see that the handler wants to ignore this exception, and it will silence it.
From Messenger perspective all will be fine again.
Related to
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could add a HandlerFailureEvent::ignoreException(bool $ignore)
method indeed. I don't mind adding this feature, and again I'll leave it to others to decide if adding it is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ro0NL @OskarStark What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also fine, but then we should let a subscriber mutate the handler descriptor. See my previous comment.
Thank you, where is this documented please? I can't find any information on the Release Process page nor on the Releases page. Googling only gives me blog entries for older Symfony releases. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
21c29d9
to
a404917
Compare
a404917
to
2479453
Compare
public readonly Envelope $envelope, | ||
public readonly HandlerDescriptor $handlerDescriptor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean no abstract? What about AbstractWorkerMessageEvent
?
This PR was squashed before being merged into the 6.3 branch. Discussion ---------- Add info about feature freeze period Follows * symfony/symfony#52425 (comment) Commits ------- 809f169 Add info about feature freeze period
2479453
to
77b4b53
Compare
I've just rebased this PR against the new 7.1 branch. Is there something I can do to get it merged? As it stands, there are a few points in the comments that would need to be ruled on. Ideally I'd like to keep this PR simple for now, but keep things extendable if someone else wants to build extra features on top of it. |
What if the callHandler method was moved to an interface (HandlerExecutor) ? Then there can also be a Traceable version of it to measure and debug handling per handler. |
I like this a lot 🙌 It makes a lot of sense to be able to probe execution of the handler(s). This makes #50980 and #50998 absolute. As with this interface, I can now create my own HandlerExecutor middleware that to do transaction wrapping or exception ignoring. |
Exactly what I was thinking (its in the name I guess 😉 ). |
Context
We audit every message that goes through Messenger, together with each run of every handler, and its outcome (success/failure). At the moment, we have to do a lot of gymnastics in our subscriber to properly audit a message:
WorkerMessageHandledEvent
, we know that all handlers have succeeded, but in case the message was redelivered, we may have already audited some of the handler successes. So:HandledStamp
:AuditedHandledStamp
:AuditedHandledStamp
WorkerMessageFailedEvent
:HandlerFailedException
:HandleMessageMiddleware
to find the handler nameWorkerMessageHandledEvent
There is an additional issue with this: while this works fine for async messages consumed by workers, this logic isn't triggered for sync messages. Because we also use the worker events to keep a stack of message ids in our subscriber, and stamp each message with its parent id, our current auditing solution cannot properly infer the parent-child relationship of say, an async Event triggered by a sync Command.
Proposed solution
This PR aims to solve all the issues above, and simplify auditing a lot, by introducing 3 events triggered in the handler loop of
HandleMessageMiddleware
:HandlerStartingEvent
(dispatched right before calling the handler)HandlerSuccessEvent
HandlerFailureEvent