Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #43354 [Messenger] allow processing messages in batches (nico…
…las-grekas) This PR was merged into the 5.4 branch. Discussion ---------- [Messenger] allow processing messages in batches | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #36910 | License | MIT | Doc PR | - This replaces #42873 as it proposes an alternative approach to handling messages in batch. `BatchHandlerInterface` says it all: if a handler implements this interface, then it should expect a new `$ack` optional argument to be provided when `__invoke()` is called. When `$ack` is not provided, `__invoke()` is expected to handle the message synchronously as usual. But when `$ack` is provided, `__invoke()` is expected to buffer the message and its `$ack` function, and to return the number of pending messages in the batch. Batch handlers are responsible for deciding when they flush their buffers, calling the `$ack` functions while doing so. Best reviewed [ignoring whitespaces](https://github.com/symfony/symfony/pull/43354/files?w=1). Here is what a batch handler might look like: ```php class MyBatchHandler implements BatchHandlerInterface { use BatchHandlerTrait; public function __invoke(MyMessage $message, Acknowledger $ack = null) { return $this->handle($message, $ack); } private function process(array $jobs): void { foreach ($jobs as [$job, $ack]) { try { // [...] compute $result from $job $ack->ack($result); } catch (\Throwable $e) { $ack->nack($e); } } } } ``` By default, `$jobs` contains the messages to handle, but it can be anything as returned by `BatchHandlerTrait::schedule()` (eg a Symfony HttpClient response derived from the message, a promise, etc.). The size of the batch is controlled by `BatchHandlerTrait::shouldProcess()` (defaults to 10). The transport is acknowledged in batch, *after* the bus returned from dispatching (unlike what is done in #42873). This is especially important when considering transactions since we don't want to ack unless the transaction committed successfully. By default, pending batches are flushed when the worker is idle and when it stops. Commits ------- 81e52b2 [Messenger] allow processing messages in batches
- Loading branch information
Showing
14 changed files
with
758 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Messenger\Handler; | ||
|
||
use Symfony\Component\Messenger\Exception\LogicException; | ||
|
||
/** | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
class Acknowledger | ||
{ | ||
private $handlerClass; | ||
private $ack; | ||
private $error = null; | ||
private $result = null; | ||
|
||
/** | ||
* @param null|\Closure(\Throwable|null, mixed):void $ack | ||
*/ | ||
public function __construct(string $handlerClass, \Closure $ack = null) | ||
{ | ||
$this->handlerClass = $handlerClass; | ||
$this->ack = $ack ?? static function () {}; | ||
} | ||
|
||
/** | ||
* @param mixed $result | ||
*/ | ||
public function ack($result = null): void | ||
{ | ||
$this->doAck(null, $result); | ||
} | ||
|
||
public function nack(\Throwable $error): void | ||
{ | ||
$this->doAck($error); | ||
} | ||
|
||
public function getError(): ?\Throwable | ||
{ | ||
return $this->error; | ||
} | ||
|
||
/** | ||
* @return mixed | ||
*/ | ||
public function getResult() | ||
{ | ||
return $this->result; | ||
} | ||
|
||
public function isAcknowledged(): bool | ||
{ | ||
return null === $this->ack; | ||
} | ||
|
||
public function __destruct() | ||
{ | ||
if ($this->ack instanceof \Closure) { | ||
throw new LogicException(sprintf('The acknowledger was not called by the "%s" batch handler.', $this->handlerClass)); | ||
} | ||
} | ||
|
||
private function doAck(\Throwable $e = null, $result = null): void | ||
{ | ||
if (!$ack = $this->ack) { | ||
throw new LogicException(sprintf('The acknowledger cannot be called twice by the "%s" batch handler.', $this->handlerClass)); | ||
} | ||
$this->ack = null; | ||
$this->error = $e; | ||
$this->result = $result; | ||
$ack($e, $result); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
src/Symfony/Component/Messenger/Handler/BatchHandlerInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Messenger\Handler; | ||
|
||
/** | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
interface BatchHandlerInterface | ||
{ | ||
/** | ||
* @param Acknowledger|null $ack The function to call to ack/nack the $message. | ||
* The message should be handled synchronously when null. | ||
* | ||
* @return mixed The number of pending messages in the batch if $ack is not null, | ||
* the result from handling the message otherwise | ||
*/ | ||
//public function __invoke(object $message, Acknowledger $ack = null): mixed; | ||
|
||
/** | ||
* Flushes any pending buffers. | ||
* | ||
* @param bool $force Whether flushing is required; it can be skipped if not | ||
*/ | ||
public function flush(bool $force): void; | ||
} |
75 changes: 75 additions & 0 deletions
75
src/Symfony/Component/Messenger/Handler/BatchHandlerTrait.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Messenger\Handler; | ||
|
||
use Symfony\Component\Messenger\Exception\LogicException; | ||
|
||
/** | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
trait BatchHandlerTrait | ||
{ | ||
private $jobs = []; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function flush(bool $force): void | ||
{ | ||
if ($jobs = $this->jobs) { | ||
$this->jobs = []; | ||
$this->process($jobs); | ||
} | ||
} | ||
|
||
/** | ||
* @param Acknowledger|null $ack The function to call to ack/nack the $message. | ||
* The message should be handled synchronously when null. | ||
* | ||
* @return mixed The number of pending messages in the batch if $ack is not null, | ||
* the result from handling the message otherwise | ||
*/ | ||
private function handle(object $message, ?Acknowledger $ack) | ||
{ | ||
if (null === $ack) { | ||
$ack = new Acknowledger(get_debug_type($this)); | ||
$this->jobs[] = [$message, $ack]; | ||
$this->flush(true); | ||
|
||
return $ack->getResult(); | ||
} | ||
|
||
$this->jobs[] = [$message, $ack]; | ||
if (!$this->shouldFlush()) { | ||
return \count($this->jobs); | ||
} | ||
|
||
$this->flush(true); | ||
|
||
return 0; | ||
} | ||
|
||
private function shouldFlush(): bool | ||
{ | ||
return 10 <= \count($this->jobs); | ||
} | ||
|
||
/** | ||
* Completes the jobs in the list. | ||
* | ||
* @list<array{0: object, 1: Acknowledger}> $jobs A list of pairs of messages and their corresponding acknowledgers | ||
*/ | ||
private function process(array $jobs): void | ||
{ | ||
throw new LogicException(sprintf('"%s" should implement abstract method "process()".', get_debug_type($this))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.