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

Skip to content

[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

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
->abstract()
->args([
abstract_arg('bus handler resolver'),
false,
service('event_dispatcher'),
])
->tag('monolog.logger', ['channel' => 'messenger'])
->call('setLogger', [service('logger')->ignoreOnInvalid()])
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.1
---

* New events: `HandlerStartingEvent`, `HandlerSuccessEvent`, `HandlerFailureEvent`

7.0
---

Expand Down
24 changes: 24 additions & 0 deletions src/Symfony/Component/Messenger/Event/AbstractHandlerEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Event;

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;

abstract class AbstractHandlerEvent
{
public function __construct(
public readonly Envelope $envelope,
public readonly HandlerDescriptor $handlerDescriptor,
Comment on lines +20 to +21
Copy link
Contributor Author

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?

) {
}
}
29 changes: 29 additions & 0 deletions src/Symfony/Component/Messenger/Event/HandlerFailureEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Event;

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;

/**
* Event dispatched after a handler fails.
*/
final class HandlerFailureEvent extends AbstractHandlerEvent
{
public function __construct(
Envelope $envelope,
HandlerDescriptor $handlerDescriptor,
public readonly \Throwable $exception,
) {
parent::__construct($envelope, $handlerDescriptor);
}
}
19 changes: 19 additions & 0 deletions src/Symfony/Component/Messenger/Event/HandlerStartingEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Event;

/**
* Event dispatched before a handler is called.
*/
final class HandlerStartingEvent extends AbstractHandlerEvent
Comment on lines +15 to +17
Copy link
Contributor

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?

Copy link
Contributor Author

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?

{
}
19 changes: 19 additions & 0 deletions src/Symfony/Component/Messenger/Event/HandlerSuccessEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Event;

/**
* Event dispatched after a handler succeeds.
*/
final class HandlerSuccessEvent extends AbstractHandlerEvent
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@

namespace Symfony\Component\Messenger\Middleware;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\HandlerFailureEvent;
use Symfony\Component\Messenger\Event\HandlerStartingEvent;
use Symfony\Component\Messenger\Event\HandlerSuccessEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\Exception\NoHandlerForMessageException;
Expand All @@ -35,6 +39,7 @@ class HandleMessageMiddleware implements MiddlewareInterface
public function __construct(
private HandlersLocatorInterface $handlersLocator,
private bool $allowNoHandlers = false,
private ?EventDispatcherInterface $eventDispatcher = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private ?EventDispatcherInterface $eventDispatcher = null,
private ?EventDispatcherInterface $dispatcher = null,

I like it more, but lets see what the others say

Copy link
Contributor Author

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?

) {
}

Expand All @@ -58,6 +63,10 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
continue;
}

$this->eventDispatcher?->dispatch(new HandlerStartingEvent($envelope, $handlerDescriptor));

$e = null;

try {
$handler = $handlerDescriptor->getHandler();
$batchHandler = $handlerDescriptor->getBatchHandler();
Expand Down Expand Up @@ -97,6 +106,14 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
} catch (\Throwable $e) {
$exceptions[$handlerDescriptor->getName()] = $e;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$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?

Copy link
Contributor Author

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.

Copy link
Contributor

@ruudk ruudk Nov 3, 2023

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

Copy link
Contributor Author

@BenMorel BenMorel Nov 3, 2023

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.

Copy link
Contributor

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?

Copy link
Contributor

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.

}

if (null !== $this->eventDispatcher) {
$event = (null !== $e)
? new HandlerFailureEvent($envelope, $handlerDescriptor, $e)
: new HandlerSuccessEvent($envelope, $handlerDescriptor);

$this->eventDispatcher->dispatch($event);
}
}

/** @var FlushBatchHandlersStamp $flushStamp */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

namespace Symfony\Component\Messenger\Tests\Middleware;

use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\HandlerFailureEvent;
use Symfony\Component\Messenger\Event\HandlerStartingEvent;
use Symfony\Component\Messenger\Event\HandlerSuccessEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\Exception\NoHandlerForMessageException;
Expand Down Expand Up @@ -356,6 +360,45 @@ public function testHandlerArgumentsStampNamedArgument()

$middleware->handle($envelope, $this->getStackMock());
}

public function testDispatchHandlerEvents()
{
$message = new DummyMessage('Hey');
$envelope = new Envelope($message);

$successHandler = $this->createMock(HandleMessageMiddlewareTestCallable::class);
$successHandler->expects($this->once())->method('__invoke');

$failureHandler = $this->createMock(HandleMessageMiddlewareTestCallable::class);
$failureHandler->expects($this->once())->method('__invoke')->willThrowException(
$exception = new \RuntimeException('Handler failed'),
);

$handlersLocator = new HandlersLocator([
DummyMessage::class => [
$successHandlerDescriptor = new HandlerDescriptor($successHandler, ['alias' => 'successHandler']),
$failureHandlerDescriptor = new HandlerDescriptor($failureHandler, ['alias' => 'failureHandler']),
],
]);

$dispatcher = $this->createMock(EventDispatcherInterface::class);

$handledStamp = new HandledStamp(null, $successHandlerDescriptor->getName());

$dispatcher->expects($this->exactly(4))
->method('dispatch')
->withConsecutive(
[new HandlerStartingEvent($envelope, $successHandlerDescriptor)],
[new HandlerSuccessEvent($envelope->with($handledStamp), $successHandlerDescriptor)],
[new HandlerStartingEvent($envelope->with($handledStamp), $failureHandlerDescriptor)],
[new HandlerFailureEvent($envelope->with($handledStamp), $failureHandlerDescriptor, $exception)],
);

$middleware = new HandleMessageMiddleware($handlersLocator, eventDispatcher: $dispatcher);

$this->expectException(HandlerFailedException::class);
$middleware->handle($envelope, new StackMiddleware());
}
}

class HandleMessageMiddlewareTestCallable
Expand Down