diff --git a/README.md b/README.md index 942e4155..5b593890 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ For production use, you should install an adapter package that matches your mess See the [adapter list](docs/guide/en/adapter-list.md) and follow the adapter-specific documentation for installation and configuration details. > If you don't have an external broker — whether for development, testing, or because you want to -> design around `QueueInterface` from day one and add a real broker later — you can run the queue +> design around `QueueProducerInterface` from day one and add a real broker later — you can run the queue > in [synchronous mode](docs/guide/en/synchronous-mode.md) (the adapter argument is optional). > In this mode messages are processed immediately in the same process, so it won't provide true > async execution, but the code stays the same when you switch to a real adapter. @@ -138,9 +138,11 @@ For setting up all classes manually, see the [Manual configuration](docs/guide/e To send a message to the queue, get the queue instance and call `push()`. Typically the queue is injected as a dependency: ```php +use Yiisoft\Queue\QueueProducerInterface; + final readonly class Foo { - public function __construct(private QueueInterface $queue) {} + public function __construct(private QueueProducerInterface $queue) {} public function bar(): void { @@ -159,11 +161,13 @@ By default, Yii Framework uses [yiisoft/yii-console](https://github.com/yiisoft/ ```bash ./yii queue:run # Handle all existing messages in the queue ./yii queue:listen [queueName] # Start a daemon listening for new messages permanently from the specified queue -./yii queue:listen-all [queueName [queueName2 [...]]] # Start a daemon listening for new messages permanently from all queues or specified list of queues (use with caution in production, recommended for dev only) +./yii queue:listen-all [queueName [queueName2 [...]]] # Start a daemon listening for new messages permanently from all consumer-capable queues or specified list of queues (use with caution in production, recommended for dev only) ``` See [Console commands](docs/guide/en/console-commands.md) for more details. +Producers use `Yiisoft\Queue\QueueProducerInterface` (`push()`, `status()`, `getName()`); consumers use `Yiisoft\Queue\QueueConsumerInterface` (`run()`, `listen()`). See [capability configuration](docs/guide/en/queue-capabilities.md) for the strict role map used when named queues are configured. + > In case you're running the queue in synchronous mode (no adapter), `queue:listen` logs an info message and exits. The messages are processed immediately when pushed. ## Documentation diff --git a/config/params.php b/config/params.php index 1c73db0c..03956a11 100644 --- a/config/params.php +++ b/config/params.php @@ -6,11 +6,13 @@ use Yiisoft\Queue\Command\ListenCommand; use Yiisoft\Queue\Command\RunCommand; use Yiisoft\Queue\Debug\QueueCollector; -use Yiisoft\Queue\Debug\QueueProviderInterfaceProxy; +use Yiisoft\Queue\Debug\QueueConsumerProviderProxy; +use Yiisoft\Queue\Debug\QueueProducerProviderProxy; use Yiisoft\Queue\Debug\QueueWorkerInterfaceProxy; use Yiisoft\Queue\Message\MessageHandlerInterface; use Yiisoft\Queue\Message\Serializer\MessageSerializer; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; use Yiisoft\Queue\Worker\WorkerInterface; return [ @@ -50,7 +52,8 @@ QueueCollector::class, ], 'trackedServices' => [ - QueueProviderInterface::class => [QueueProviderInterfaceProxy::class, QueueCollector::class], + QueueProducerProviderInterface::class => [QueueProducerProviderProxy::class, QueueCollector::class], + QueueConsumerProviderInterface::class => [QueueConsumerProviderProxy::class, QueueCollector::class], WorkerInterface::class => [QueueWorkerInterfaceProxy::class, QueueCollector::class], ], ], diff --git a/docs/guide/en/README.md b/docs/guide/en/README.md index 87700269..a2a5e2b5 100644 --- a/docs/guide/en/README.md +++ b/docs/guide/en/README.md @@ -9,6 +9,7 @@ Yii Queue is a framework-agnostic PHP queue library for running tasks asynchrono - [Adapter list](adapter-list.md) - [Synchronous mode](synchronous-mode.md) - [Queue names](queue-names.md) +- [Producer and consumer capabilities](queue-capabilities.md) ## Build and handle messages diff --git a/docs/guide/en/advanced-map.md b/docs/guide/en/advanced-map.md index ae2dab03..f36927de 100644 --- a/docs/guide/en/advanced-map.md +++ b/docs/guide/en/advanced-map.md @@ -5,7 +5,7 @@ Use this index when you need to customize internals: custom middleware, adapters ## Configuration and infrastructure - [Manual configuration without yiisoft/config](configuration-manual.md) — wiring queues, workers, and middleware factories without `yiisoft/config`. -- [Queue provider registry](#queue-provider-registry) — selecting and extending adapter factories. +- [Advanced queue names and providers](queue-names-advanced.md) — resolving named producer and consumer capabilities, composing providers, and implementing custom registries. - [Loops and worker processes](loops.md) — implementing custom runners, heartbeat hooks, and graceful shutdown (requires `pcntl`). - [Worker](worker.md) — resolving worker dependencies and starting workers. - [Performance tuning](performance-tuning.md) — profiling handlers, envelopes, and adapters. @@ -20,7 +20,7 @@ Use this index when you need to customize internals: custom middleware, adapters ## Queue adapters and interoperability -- [Custom queue provider implementations](queue-names-advanced.md#extending-the-registry) — bespoke selection logic, tenant registries, and fallback strategies. +- [Custom queue provider implementations](queue-names-advanced.md#combining-and-extending-providers) — bespoke selection logic, tenant registries, and fallback strategies. - [Consuming messages from external systems](consuming-messages-from-external-systems.md) — contract for third-party producers. ## Tooling and diagnostics @@ -30,15 +30,3 @@ Use this index when you need to customize internals: custom middleware, adapters ## Internals and contribution - [Internals guide](../../internals.md) — local QA tooling (PHPUnit, Infection, Psalm, Rector, ComposerRequireChecker). - -## Queue provider registry - -When multiple queue names share infrastructure, rely on `QueueProviderInterface`: - -- A queue name is passed to `QueueProviderInterface::get($queueName)` and resolved into a configured `QueueInterface` instance. -- Default implementation (`AdapterFactoryQueueProvider`) enforces a strict registry defined in `yiisoft/queue.queues`. Unknown names throw `QueueNotFoundException`. -- Alternative providers include: - - `PredefinedQueueProvider` — accepts a pre-built map of queue name → `QueueInterface` instance. - - `QueueFactoryProvider` — creates queue objects lazily from [`yiisoft/factory`](https://github.com/yiisoft/factory) definitions. - - `CompositeQueueProvider` — aggregates multiple providers and selects the first that knows the queue name. -- Implement `QueueProviderInterface` to introduce custom registries or fallback strategies, then register the implementation in DI. diff --git a/docs/guide/en/configuration-manual.md b/docs/guide/en/configuration-manual.md index 181874e7..58947487 100644 --- a/docs/guide/en/configuration-manual.md +++ b/docs/guide/en/configuration-manual.md @@ -8,7 +8,7 @@ To use the queue, you need to create instances of the following classes: 1. **Adapter** - handles the actual queue backend like AMQP, Redis, etc. 2. **Worker** - processes messages from the queue -3. **Queue** - the main entry point for pushing messages +3. **QueueProducer** - pushes messages; **QueueConsumer** consumes them when needed ### Example @@ -24,7 +24,8 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareDispatcher; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactory; use Yiisoft\Queue\Middleware\Push\PushMiddlewareConfig; use Yiisoft\Queue\Middleware\Push\PushMiddlewareFactory; -use Yiisoft\Queue\Queue; +use Yiisoft\Queue\QueueConsumer; +use Yiisoft\Queue\QueueProducer; use Yiisoft\Queue\Worker\Worker; // A PSR-11 container is required for resolving dependencies of middleware and handlers. @@ -70,16 +71,16 @@ $loop = new SimpleLoop(); // Create queue. Without an adapter the queue runs in synchronous mode (messages are processed // immediately on push). Pass an adapter (e.g., AMQP, Redis) for asynchronous processing. -$queue = new Queue( - $worker, - $loop, +$producer = new QueueProducer( $logger, $pushMiddlewareConfig, + worker: $worker, ); +$consumer = new QueueConsumer($worker, $loop, $logger); -// Now you can push messages +// Now you can push messages. With no adapter, the producer dispatches directly to the worker. $message = new DownloadFileMessage(url: 'https://example.com/file.pdf', destinationPath: '/tmp/file.pdf'); -$queue->push($message); +$producer->push($message); ``` ## Using Queue Provider @@ -89,18 +90,24 @@ For multiple queue names, use `PredefinedQueueProvider` (maps queue names to pre ```php use Yiisoft\Queue\Provider\PredefinedQueueProvider; -// PredefinedQueueProvider: pass fully built queue instances. +// PredefinedQueueProvider: pass fully built role instances in a strict role map. $provider = new PredefinedQueueProvider([ - 'queue1' => $queue1, - 'queue2' => $queue2, + 'queue1' => ['producer' => $producer1, 'consumer' => $consumer1], + 'queue2' => ['producer' => $producer2], ]); ``` ## Running the queue +Message consumption methods are available on `Yiisoft\Queue\QueueConsumerInterface`. +`QueueProducer` and `QueueConsumer` are separate capabilities. Obtain or construct the consumer role before calling these methods. + ### Processing existing messages ```php +use Yiisoft\Queue\QueueConsumerInterface; + +/** @var QueueConsumerInterface $queue */ $queue->run(); // Process all messages $queue->run(10); // Process up to 10 messages ``` @@ -108,6 +115,9 @@ $queue->run(10); // Process up to 10 messages ### Listening for new messages ```php +use Yiisoft\Queue\QueueConsumerInterface; + +/** @var QueueConsumerInterface $queue */ $queue->listen(); // Run indefinitely ``` diff --git a/docs/guide/en/console-commands.md b/docs/guide/en/console-commands.md index 0157e7f5..041bf0c1 100644 --- a/docs/guide/en/console-commands.md +++ b/docs/guide/en/console-commands.md @@ -6,7 +6,7 @@ If you are using [yiisoft/config](https://github.com/yiisoft/config) and [yiisof If you are using [symfony/console](https://github.com/symfony/console) directly, you should register the commands manually. -> **Note:** The default queue name list (used when no queue names are passed to a command) is only available when using [yiisoft/config](https://github.com/yiisoft/config) and [yiisoft/yii-console](https://github.com/yiisoft/yii-console). Without them, you must pass the queue name list explicitly to the command constructor. +> **Note:** `queue:run` and `queue:listen-all` use `QueueConsumerProviderInterface::getConsumerNames()` when no queue names are passed. Explicitly passed names are resolved with `getConsumer()` and must have a consumer role. In [yiisoft/app](https://github.com/yiisoft/app) the `yii` console binary is provided out of the box. If you are using [yiisoft/yii-console](https://github.com/yiisoft/yii-console) or `symfony/console` without that template, invoke these commands the same way you invoke other console commands in your application. @@ -17,7 +17,7 @@ The command `queue:run` obtains and handles messages until the queue is empty, t You can also narrow the scope of processed messages by specifying queue name(s) and maximum number of messages to process: -- Specify one or more queue names to process. Messages from other queues will be ignored. Defaults to all registered queue names. +- Specify one or more queue names to process. Messages from other queues will be ignored. Defaults to all registered consumer-capable queue names. - Use `--limit` to limit the number of messages processed. When set, command will exit either when all the messages are processed or when the maximum count is reached. The full command signature is: @@ -39,7 +39,7 @@ yii queue:listen [queueName] The following command iterates through multiple queues and is meant to be used in development environment only, as it consumes a lot of CPU for iterating through queues. You can pass to it: -- `queueName` argument(s). Specify one or more queue names to process. Messages from other queues will be ignored. Defaults to all registered queue names. +- `queueName` argument(s). Specify one or more queue names to process. Messages from other queues will be ignored. Defaults to all registered consumer-capable queue names. - `--limit` option to limit the number of messages processed before switching to another queue. E.g. you set `--limit` to 500 and right now you have 1000 messages in `queue1`. This command will consume only 500 of them, then it will switch to `queue2` to see if there are any messages there. Defaults to `0` (no limit). - `--pause` option to specify the number of seconds to pause between checking queues when no messages are found. Defaults to `1`. diff --git a/docs/guide/en/debug-integration-advanced.md b/docs/guide/en/debug-integration-advanced.md index b9de6616..283f9018 100644 --- a/docs/guide/en/debug-integration-advanced.md +++ b/docs/guide/en/debug-integration-advanced.md @@ -7,7 +7,7 @@ Use this guide when you need to understand which events are tracked by the queue The integration is based on `Yiisoft\Queue\Debug\QueueCollector` and captures: - Pushed messages grouped by queue name. -- Message status checks performed via `QueueInterface::status()`. +- Message status checks performed via `QueueProducerInterface::status()`. - Messages processed by a worker grouped by queue name. ## How it works @@ -16,10 +16,11 @@ The collector is enabled by registering it in Yii Debug and wrapping tracked ser Out of the box (see this package's `config/params.php`), the following services are wrapped: -- `Yiisoft\Queue\Provider\QueueProviderInterface` is wrapped with `Yiisoft\Queue\Debug\QueueProviderInterfaceProxy`. The proxy decorates returned queues with `Yiisoft\Queue\Debug\QueueDecorator` so that `push()` and `status()` calls are reported to the collector. +- `Yiisoft\Queue\Provider\QueueProducerProviderInterface` is wrapped with `Yiisoft\Queue\Debug\QueueProducerProviderProxy`, which returns `QueueProducerDecorator` instances so `push()` and `status()` calls are reported. +- `Yiisoft\Queue\Provider\QueueConsumerProviderInterface` is wrapped with `Yiisoft\Queue\Debug\QueueConsumerProviderProxy`, which returns typed consumer decorators. - `Yiisoft\Queue\Worker\WorkerInterface` is wrapped with `Yiisoft\Queue\Debug\QueueWorkerInterfaceProxy` to record message processing events. -To see data in the debug panel, obtain `QueueProviderInterface` and `WorkerInterface` from the DI container — the debug proxies are registered there and will not be active if the services are instantiated directly. +To see data in the debug panel, obtain the typed provider dependencies and `WorkerInterface` from the DI container — the proxies are registered there and will not be active if the services are instantiated directly. ## Manual configuration @@ -27,9 +28,11 @@ If you do not rely on the defaults supplied via [yiisoft/config](https://github. ```php use Yiisoft\Queue\Debug\QueueCollector; -use Yiisoft\Queue\Debug\QueueProviderInterfaceProxy; +use Yiisoft\Queue\Debug\QueueConsumerProviderProxy; +use Yiisoft\Queue\Debug\QueueProducerProviderProxy; use Yiisoft\Queue\Debug\QueueWorkerInterfaceProxy; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; use Yiisoft\Queue\Worker\WorkerInterface; return [ @@ -38,7 +41,8 @@ return [ QueueCollector::class, ], 'trackedServices' => [ - QueueProviderInterface::class => [QueueProviderInterfaceProxy::class, QueueCollector::class], + QueueProducerProviderInterface::class => [QueueProducerProviderProxy::class, QueueCollector::class], + QueueConsumerProviderInterface::class => [QueueConsumerProviderProxy::class, QueueCollector::class], WorkerInterface::class => [QueueWorkerInterfaceProxy::class, QueueCollector::class], ], ], diff --git a/docs/guide/en/error-handling-advanced.md b/docs/guide/en/error-handling-advanced.md index e1c31d37..2f0ff68f 100644 --- a/docs/guide/en/error-handling-advanced.md +++ b/docs/guide/en/error-handling-advanced.md @@ -18,7 +18,8 @@ This document covers advanced internals of the failure handling pipeline, built- - the message - the caught exception - - the queue instance + - the logical queue name + - an optional direct retry producer (provided for synchronous producer execution) 4. A failure pipeline is selected by queue name @@ -83,7 +84,7 @@ This interface has the only method `processFailure` with these parameters: - [`FailureHandlingRequest $request`](../../../src/Middleware/FailureHandling/FailureHandlingRequest.php) - a request for a message handling. It consists of - a [message](../../../src/Message/MessageInterface.php) - a `Throwable $exception` object thrown on the `request` handling - - a queue the message came from + - the logical queue name the message came from and, when available, a direct retry producer - `FailureHandlerInterface $handler` - failure strategy pipeline continuation. Your Middleware should call `$handler->handleFailure($request)` when the middleware itself should not interrupt failure pipeline execution. > Note: your strategy have to check by its own if it should be applied. Look into [`SendAgainMiddleware::suits()`](../../../src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php#L54) for an example. diff --git a/docs/guide/en/error-handling.md b/docs/guide/en/error-handling.md index f0978c05..71c14fcd 100644 --- a/docs/guide/en/error-handling.md +++ b/docs/guide/en/error-handling.md @@ -29,14 +29,18 @@ Here below is configuration via [yiisoft/config](https://github.com/yiisoft/conf FailureMiddlewareDispatcher::DEFAULT_PIPELINE => [ [ 'class' => SendAgainMiddleware::class, - '__construct()' => ['id' => 'default-first-resend', 'queue' => null], + '__construct()' => [ + 'id' => 'default-first-resend', + 'maxAttempts' => 1, + 'producerProvider' => QueueProducerProviderInterface::class, + ], ], - static fn (QueueFactoryInterface $factory) => new SendAgainMiddleware( - id: 'default-second-resend', - queue: $factory->get('failed-messages'), + static fn (QueueProducerProviderInterface $queues) => new SendAgainMiddleware( + id: 'default-second-resend', + maxAttempts: 1, + targetQueue: $queues->getProducer('failed-messages'), ), ], - 'failed-messages' => [ [ 'class' => ExponentialDelayMiddleware::class, @@ -46,8 +50,8 @@ Here below is configuration via [yiisoft/config](https://github.com/yiisoft/conf 'delayInitial' => 5, 'delayMaximum' => 60, 'exponent' => 1.5, - 'queue' => null, - ], + 'producerProvider' => QueueProducerProviderInterface::class, + ], ], ], ], @@ -83,7 +87,8 @@ Failures of messages that arrived in the `failed-messages` queue directly (bypas - `id` - A unique string. Allows to use this strategy more than once for the same message, just like in example above. - `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. - - `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue it came from. When the `queue` parameter is set to `null`, a message will be sent to the same queue it came from. + - `targetQueue` - An optional `QueueProducerInterface` for an explicit retry destination. When it is `null`, synchronous execution supplies its originating producer; asynchronous execution resolves the originating queue name through `producerProvider`. + - `producerProvider` - The `QueueProducerProviderInterface` used to resolve the source producer for asynchronous retries when no `targetQueue` is supplied. Configure it, or provide `targetQueue`; otherwise retry fails with a configuration error. State tracking: @@ -101,7 +106,8 @@ It's configured via constructor parameters, too. Here they are: - `delayInitial` - The initial delay that will be applied to a message for the first time. It must be a positive float. - `delayMaximum` - The maximum delay which can be applied to a single message. Must be above the `delayInitial`. - `exponent` - Message handling delay will be multiplied by exponent each time it fails. - - `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue it came from. When the `queue` parameter is set to `null`, a message will be sent to the same queue it came from. + - `queue` - An optional `QueueProducerInterface` retry destination. When it is `null`, synchronous execution supplies its originating producer; asynchronous execution resolves the originating queue name through `producerProvider`. + - `producerProvider` - The `QueueProducerProviderInterface` used for that asynchronous source-producer lookup. Requirements: diff --git a/docs/guide/en/loops.md b/docs/guide/en/loops.md index 0bd9b454..e20bfb17 100644 --- a/docs/guide/en/loops.md +++ b/docs/guide/en/loops.md @@ -4,7 +4,7 @@ Yii Queue uses `\Yiisoft\Queue\Cli\LoopInterface` to control long-running execut The loop is evaluated to determine whether it can continue: -- After each processed message (via `Queue::run()` / `Queue::listen()`). +- After each processed message (via `QueueConsumer::run()` / `QueueConsumer::listen()`). - On each iteration of `queue:listen-all`. When `canContinue()` returns `false`, consuming stops gracefully (as soon as the current message is finished). @@ -82,7 +82,7 @@ return [ ### Manual configuration (without `yiisoft/config`) -Instantiate the loop you want and pass it to `Queue` (and, depending on adapter, to adapter constructor as well): +Instantiate the loop you want and pass it to `QueueConsumer` (and, depending on adapter, to the adapter constructor as well): ```php use Yiisoft\Queue\Cli\SignalLoop; diff --git a/docs/guide/en/message-status.md b/docs/guide/en/message-status.md index bd9d0038..702841bb 100644 --- a/docs/guide/en/message-status.md +++ b/docs/guide/en/message-status.md @@ -4,14 +4,14 @@ Yii Queue can report the status of a message by its ID. The API surface is: -- `QueueInterface::status(string|int $id): MessageStatus` +- `QueueProducerInterface::status(string|int $id): MessageStatus` - `AdapterInterface::status(string|int $id): MessageStatus` Status tracking support depends on the adapter. If an adapter doesn't support status tracking or can't find the message by ID, it returns `MessageStatus::NOT_FOUND`. ## Getting a message ID -`QueueInterface::push()` returns a `MessageInterface`. When the adapter supports IDs, the returned message is typically wrapped into an `IdEnvelope`, which stores the ID in message metadata. +`QueueProducerInterface::push()` returns a `MessageInterface`. When the adapter supports IDs, the returned message is typically wrapped into an `IdEnvelope`, which stores the ID in message metadata. To read the ID: diff --git a/docs/guide/en/middleware-pipelines.md b/docs/guide/en/middleware-pipelines.md index 4ac31bec..cf4a55fb 100644 --- a/docs/guide/en/middleware-pipelines.md +++ b/docs/guide/en/middleware-pipelines.md @@ -28,7 +28,7 @@ Common reasons to add middlewares: Each message may pass through three independent pipelines: -- **Push pipeline** (executed when calling `QueueInterface::push()`). +- **Push pipeline** (executed when calling `QueueProducerInterface::push()`). - **Consume pipeline** (executed when a worker processes a message). - **Failure handling pipeline** (executed when message processing throws a `Throwable`). @@ -72,7 +72,7 @@ The required interface depends on the pipeline: ## Push pipeline -The push pipeline is executed when calling `QueueInterface::push()`. +The push pipeline is executed when calling `QueueProducerInterface::push()`. Push middlewares can: @@ -149,6 +149,6 @@ See [Configuration with yiisoft/config](configuration-with-config.md) for exampl ### Manual configuration (without yiisoft/config) -When configuring the component manually, you instantiate the middleware dispatchers and pass them to `Queue` / `Worker`. +When configuring the component manually, you instantiate the middleware dispatchers and pass them to `QueueProducer`, `QueueConsumer`, and `Worker` as appropriate. See [Manual configuration](configuration-manual.md) for a full runnable example. diff --git a/docs/guide/en/performance-tuning.md b/docs/guide/en/performance-tuning.md index 5e58002f..8d6723be 100644 --- a/docs/guide/en/performance-tuning.md +++ b/docs/guide/en/performance-tuning.md @@ -126,9 +126,18 @@ Use different queue names for different priority levels. See [Queue names](queue return [ 'yiisoft/queue' => [ 'queues' => [ - 'critical' => AmqpAdapter::class, - 'normal' => AmqpAdapter::class, - 'low' => AmqpAdapter::class, + 'critical' => [ + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], + 'normal' => [ + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], + 'low' => [ + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], ], ], ]; @@ -155,10 +164,22 @@ Create separate queues for different workload characteristics: return [ 'yiisoft/queue' => [ 'queues' => [ - 'fast' => AmqpAdapter::class, // Quick tasks (< 1s) - 'slow' => AmqpAdapter::class, // Long tasks (> 10s) - 'cpu-bound' => AmqpAdapter::class, // CPU-intensive - 'io-bound' => AmqpAdapter::class, // I/O-intensive + 'fast' => [ // Quick tasks (< 1s) + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], + 'slow' => [ // Long tasks (> 10s) + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], + 'cpu-bound' => [ // CPU-intensive + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], + 'io-bound' => [ // I/O-intensive + 'producer' => ['class' => \Yiisoft\Queue\QueueProducer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + 'consumer' => ['class' => \Yiisoft\Queue\QueueConsumer::class, '__construct()' => ['adapter' => AmqpAdapter::class]], + ], ], ], ]; @@ -383,7 +404,7 @@ Test with realistic message volumes and data: ```php // Load test script -$queue = $container->get(QueueInterface::class); +$queue = $container->get(QueueProducerInterface::class); $start = microtime(true); $count = 10000; diff --git a/docs/guide/en/queue-capabilities.md b/docs/guide/en/queue-capabilities.md new file mode 100644 index 00000000..189a0dae --- /dev/null +++ b/docs/guide/en/queue-capabilities.md @@ -0,0 +1,23 @@ +# Queue producer and consumer capabilities + +A logical queue name can independently expose a producer, a consumer, or both. Inject `QueueProducerInterface` to push/status messages and `QueueConsumerInterface` to run/listen. Console commands use only `QueueConsumerProviderInterface`; retry middleware uses a direct `QueueProducerInterface` or `QueueProducerProviderInterface`. + +Named providers use a strict nested role map. `getProducerNames()` and `getConsumerNames()` return only names with that role. Role definitions are created lazily and cached per name and role; failed lazy creation is cached and repeated lookups rethrow the same configuration error. + +```php +use Yiisoft\Queue\QueueConsumer; +use Yiisoft\Queue\QueueProducer; + +$definitions = [ + 'orders' => [ + 'producer' => ['class' => QueueProducer::class], + 'consumer' => ['class' => QueueConsumer::class], + ], + 'outbound-events' => ['producer' => ['class' => QueueProducer::class]], + 'inbound-events' => ['consumer' => ['class' => QueueConsumer::class]], +]; +``` + +`QueueFactoryProvider` accepts factory definitions in each role. `PredefinedQueueProvider` uses the same outer shape but each role value must already be its respective interface instance. A raw definition such as `'orders' => ['class' => QueueProducer::class]`, an empty role map, and unknown role keys are invalid. + +`QueueInterface`, `Queue`, and `QueueProviderInterface` were removed before release. Replace them with `QueueProducerInterface`, `QueueProducer` / `QueueConsumer`, and the relevant typed provider. Synchronous consumers retain no-op `run()` and `listen()` behavior when no adapter is configured. Default retry of an asynchronously consumed message resolves a producer for the execution queue name through a configured producer provider; if none is available it fails with an actionable configuration error rather than dropping the message. diff --git a/docs/guide/en/queue-names-advanced.md b/docs/guide/en/queue-names-advanced.md index 21cdd8cc..4e71b0e0 100644 --- a/docs/guide/en/queue-names-advanced.md +++ b/docs/guide/en/queue-names-advanced.md @@ -1,94 +1,77 @@ -# Advanced queue name internals +# Advanced queue names and providers -Use this reference when you need to understand how queue names map to adapters, how providers resolve queues, and how to implement your own provider. +A queue name is a logical message stream. It lets an application choose a producer and a consumer independently for that stream: a name may be producer-only, consumer-only, or have both capabilities. It is not itself a queue object and does not require both roles to use the same backend. -## How queue names are used in code +Most applications configure names through [`yiisoft/queue.queues`](queue-names.md) and inject the default producer directly. Use a provider when code must choose a named stream at runtime, a worker must resolve a named consumer, or your application constructs or combines queue registries itself. -- A queue name (string or `BackedEnum`) is passed to `Yiisoft\Queue\Provider\QueueProviderInterface::get($queueName)`. -- The provider returns a `Yiisoft\Queue\QueueInterface` instance configured for that name. -- `QueueInterface::getName()` can be used for introspection; it returns the logical name the queue was created with. +## Providers and capabilities -## Provider implementations +Providers translate a queue name into the capability the caller needs: -`QueueProviderInterface::get()` may throw the following exceptions when configuration is invalid: +- `QueueProducerProviderInterface::getProducer($name)` returns a `QueueProducerInterface` for pushing messages and obtaining their status. +- `QueueConsumerProviderInterface::getConsumer($name)` returns a `QueueConsumerInterface` for running or listening for messages. +- `hasProducer()` / `hasConsumer()` check whether a name exposes a role. `getProducerNames()` / `getConsumerNames()` list names for only that role. -- `Yiisoft\Queue\Provider\QueueNotFoundException` -- `Yiisoft\Queue\Provider\InvalidQueueConfigException` -- `Yiisoft\Queue\Provider\QueueProviderException` +Both lookup methods accept a string or `BackedEnum`. They throw `QueueNotFoundException` when the name is unknown or does not have the requested role. This separation prevents a producer-only queue from accidentally being used by a worker, and vice versa. -This package ships four provider strategies: +The default name is `QueueProducerProviderInterface::DEFAULT_QUEUE` (also available from `QueueConsumerProviderInterface`), whose value is `yii-queue`. -### AdapterFactoryQueueProvider (default) +## Role-map configuration -- Backed by the `yiisoft/queue.queues` params array. -- Each queue name maps to an adapter definition. -- Uses `yiisoft/factory` to create adapters lazily, then wraps them in a `Queue` with the given name. -- Enforces a strict name mapping: unknown queue names throw `QueueNotFoundException` immediately. +The built-in providers use a strict role map: `queues[name][producer|consumer]`. Every name must contain at least one role, and no keys other than `producer` and `consumer` are valid. The values are either factory definitions or ready instances, depending on the provider. -### PredefinedQueueProvider +Choose the provider by how the roles are created: -- Accepts a pre-built map of queue name → `QueueInterface` instance. -- Useful when you already have fully constructed queue objects and want to register them by name. -- Throws `QueueNotFoundException` for unknown names, `InvalidQueueConfigException` if a value is not a `QueueInterface`. +- Use `QueueFactoryProvider` when the values are [`yiisoft/factory`](https://github.com/yiisoft/factory) definitions. It creates and caches each role lazily, so resolving a producer does not construct the consumer for the same name. +- Use `PredefinedQueueProvider` when the values are already-built `QueueProducerInterface` or `QueueConsumerInterface` instances. It does not accept factory definitions. -Example: +`QueueFactoryProvider` is appropriate for container configuration: ```php -use Yiisoft\Queue\Provider\PredefinedQueueProvider; - -$provider = new PredefinedQueueProvider([ - 'emails' => $emailQueue, - 'reports' => $reportsQueue, -]); -$queueForEmails = $provider->get('emails'); -``` +use Yiisoft\Queue\Provider\QueueFactoryProvider; +use Yiisoft\Queue\QueueConsumer; +use Yiisoft\Queue\QueueProducer; -### QueueFactoryProvider +$provider = new QueueFactoryProvider([ + 'emails' => [ + 'producer' => ['class' => QueueProducer::class], + 'consumer' => ['class' => QueueConsumer::class], + ], + 'audit' => [ + 'producer' => ['class' => QueueProducer::class], + ], +], $container); -- Creates queue objects from [yiisoft/factory](https://github.com/yiisoft/factory) definitions indexed by queue name. -- Lazily instantiates and caches queues on first access. -- Throws `QueueNotFoundException` for unknown names. +$emailProducer = $provider->getProducer('emails'); +$emailConsumer = $provider->getConsumer('emails'); +``` -Example: +`PredefinedQueueProvider` is useful for manual wiring or tests, where the roles have already been constructed: ```php -use Yiisoft\Queue\Provider\QueueFactoryProvider; +use Yiisoft\Queue\Provider\PredefinedQueueProvider; -$provider = new QueueFactoryProvider( - [ - 'emails' => [ - 'class' => Queue::class, - '__construct()' => [$worker, $loop, $logger, $pushDispatcher, $adapter], - ], +$provider = new PredefinedQueueProvider([ + 'emails' => [ + 'producer' => $emailProducer, + 'consumer' => $emailConsumer, ], - $container, -); -$queueForEmails = $provider->get('emails'); + 'audit' => ['producer' => $auditProducer], +]); ``` -### CompositeQueueProvider +For configuration through `yiisoft/config`, see [Queue names](queue-names.md). For manual construction of producers and consumers, see [Manual configuration](configuration-manual.md). -- Accepts multiple providers and queries them in order. -- The first provider whose `has()` returns true for the queue name wins. -- Useful for mixing multiple providers, for example combining adapter-based and pre-built queues. +## Combining and extending providers -Example: +`CompositeQueueProvider` combines providers. It checks providers in constructor order and uses the first one that has the requested capability. Precedence is per role, so one provider can supply a producer while another supplies the consumer for the same name. ```php use Yiisoft\Queue\Provider\CompositeQueueProvider; -use Yiisoft\Queue\Provider\AdapterFactoryQueueProvider; -use Yiisoft\Queue\Provider\PredefinedQueueProvider; -$provider = new CompositeQueueProvider( - new AdapterFactoryQueueProvider($queue, $definitions, $container), - new PredefinedQueueProvider(['fallback' => $fallbackQueue]), -); - -$queueForEmails = $provider->get('emails'); +$provider = new CompositeQueueProvider($applicationQueues, $fallbackQueues); +$producer = $provider->getProducer('emails'); +$consumer = $provider->getConsumer('inbound-events'); ``` -## Implementing a custom provider - -- Implement `QueueProviderInterface` if you need bespoke selection logic (e.g., tenant-specific routing, remote lookups, or metrics-aware routing). -- Register your provider in the DI container and swap it in wherever `QueueProviderInterface` is used. -- Consider exposing diagnostics (e.g., list of available queues) through `getNames()`, console commands, or health checks so operators can verify the available queues at runtime. +Use a composite provider for layered configuration, such as application-specific queues with a fallback registry. Implement `QueueProducerProviderInterface`, `QueueConsumerProviderInterface`, or both when names come from another source—for example, a tenant-aware registry or an external configuration service. Register the typed interface that your caller needs in DI; do not expose a generic queue lookup. diff --git a/docs/guide/en/queue-names.md b/docs/guide/en/queue-names.md index 478f0dba..e5b0e1a6 100644 --- a/docs/guide/en/queue-names.md +++ b/docs/guide/en/queue-names.md @@ -1,143 +1,87 @@ # Queue names -A *queue name* is a logical namespace/identifier that maps to a queue configuration. +A *queue name* is a logical identifier for independently configured producer and consumer capabilities. A name can have a producer, a consumer, or both; it does not imply that the two roles use the same object. -In practice, a queue name is a string (for example, `yii-queue`, `emails`, `critical`) that selects which queue backend (adapter) messages are pushed to and which worker consumes them. +- Inject `QueueProducerInterface` to push messages to the default queue. +- Use `QueueProducerProviderInterface` to obtain a named producer with `getProducer()`. +- Use `QueueConsumerProviderInterface` to obtain a named consumer with `getConsumer()`; console commands use this provider. -At a high level: +The default name is `QueueProducerProviderInterface::DEFAULT_QUEUE` (also available from `QueueConsumerProviderInterface`) and is `yii-queue`. -- You configure one or more queue names. -- When producing messages, you either: - - use `QueueInterface` directly (single/default queue), or - - use `QueueProviderInterface` to get a queue for a specific queue name. -- When consuming messages, you run a worker command for a queue name (or a set of queue names). +## When to use named queues -Having multiple queue names is useful when you want to separate workloads, for example: +Use the default queue when all messages can share the same transport and worker behavior. Add names when messages need separate operational treatment: for example, to send high-priority and background work to different backends, run workers independently, or exchange only one message stream with another application. A name is an application-level routing decision; the `producer` and `consumer` roles under that name define how messages enter and leave that stream. -- **Different priorities**: `critical` vs `low`. -- **Different message types**: `emails`, `reports`, `webhooks`. -- **Different backends / connections**: fast Redis queue for short messages and RabbitMQ backend for long-running messages or inter-app communication. +## Configuration -The default queue name is `Yiisoft\Queue\Provider\QueueProviderInterface::DEFAULT_QUEUE` (`yii-queue`). - -## Quick start (yiisoft/config) - -When using [yiisoft/config](https://github.com/yiisoft/config), queue name configuration is stored in params under `yiisoft/queue.queues`. - -### 1. Start with a single queue (default) - -If you use only a single queue, you can inject `QueueInterface` directly. - -#### 1.1 Configure an Adapter - -An adapter is what actually delivers messages to a queue broker. Pick one from the -[adapter list](adapter-list.md), install it, and bind it in DI: +Named queues use a strict role map under `yiisoft/queue.queues`. Each name must contain one or both of `producer` and `consumer`; flat adapter or factory definitions are not supported. ```php use Yiisoft\Queue\Adapter\AdapterInterface; - -return [ - AdapterInterface::class => YourBrokerAdapter::class, -]; -``` - -Refer to the chosen adapter's documentation for connection settings and any additional bindings. - -If you don't have a broker yet, you can skip this step — the queue will run in -[synchronous mode](synchronous-mode.md) and process messages immediately on `push()`. You can -plug in a real adapter later without changing any call sites. - -#### 1.2. Configure a default queue name - -When you are using `yiisoft/config` and the default configs from this package are loaded, the default queue name is already present in params (so you don't need to add anything). The snippet below shows what is shipped by default in [config/params.php](../../../config/params.php): - -```php -use Yiisoft\Queue\Adapter\AdapterInterface; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; +use Yiisoft\Queue\QueueConsumer; +use Yiisoft\Queue\QueueProducer; return [ 'yiisoft/queue' => [ 'queues' => [ - QueueProviderInterface::DEFAULT_QUEUE => AdapterInterface::class, + // A queue with both capabilities. + QueueProducerProviderInterface::DEFAULT_QUEUE => [ + 'producer' => ['class' => QueueProducer::class, '__construct()' => ['adapter' => AdapterInterface::class]], + 'consumer' => ['class' => QueueConsumer::class, '__construct()' => ['adapter' => AdapterInterface::class]], + ], + // Produce-only and consume-only names are valid. + 'outbound-events' => [ + 'producer' => ['class' => QueueProducer::class, '__construct()' => ['adapter' => AdapterInterface::class]], + ], + 'inbound-events' => [ + 'consumer' => ['class' => QueueConsumer::class, '__construct()' => ['adapter' => AdapterInterface::class]], + ], ], ], ]; ``` -Pushing a message via DI: +`QueueFactoryProvider` resolves these role definitions lazily and caches each role independently. `PredefinedQueueProvider` has the same shape, but each value must be an already-created instance of its role interface. + +## Producing messages + +For the default queue, inject the producer directly: ```php -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; final readonly class SendWelcomeEmail { - public function __construct(private QueueInterface $queue) - { - } + public function __construct(private QueueProducerInterface $queue) {} public function run(string $email): void { - $this->queue->push(new SendEmailMessage(to: $email, subject: 'Welcome!', body: 'Thank you for registering.')); + $this->queue->push(new SendEmailMessage(to: $email)); } } ``` -### 2. Multiple queue names - -Add more queue names to the `params.php`: +For a named producer, request the producer capability explicitly: ```php -use Yiisoft\Queue\Provider\QueueProviderInterface; - -return [ - 'yiisoft/queue' => [ - 'queues' => [ - QueueProviderInterface::DEFAULT_QUEUE => \Yiisoft\Queue\Adapter\AdapterInterface::class, - 'critical' => \Yiisoft\Queue\Adapter\AdapterInterface::class, - 'emails' => \Yiisoft\Queue\Adapter\AdapterInterface::class, - ], - ], -]; -``` - -If you have multiple queue names, inject `QueueProviderInterface` and call `get('queue-name')`. - -```php -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; final readonly class SendTransactionalEmail { - public function __construct(private QueueProviderInterface $queueProvider) - { - } + public function __construct(private QueueProducerProviderInterface $queues) {} public function run(string $email): void { - $this->queueProvider - ->get('emails') - ->push(new SendEmailMessage(to: $email, subject: 'Welcome!', body: 'Thank you for registering.')); + $this->queues->getProducer('outbound-events')->push(new SendEmailMessage(to: $email)); } } ``` -`QueueProviderInterface` accepts both strings and `BackedEnum` values. `BackedEnum` values are normalized to strings — string-backed enums use their backing value directly, while int-backed enums are cast to string. - -```php -enum QueueChannel: string -{ - case Emails = 'emails'; - case Reports = 'reports'; -} - -// Using enum value: -$queueProvider->get(QueueChannel::Emails); // resolves to 'emails' -``` - -## Running workers (CLI) +Both typed providers accept strings and `BackedEnum` values. Use `getProducerNames()` or `getConsumerNames()` when enumerating only that role. -To consume messages you run console commands such as `queue:run`, `queue:listen`, and `queue:listen-all`. -See [Console commands](console-commands.md) for details. +## Running workers -## Advanced queues and providers +`queue:run` and `queue:listen-all` use all configured consumer names when no names are supplied. `queue:listen` and explicitly named commands resolve that name through `QueueConsumerProviderInterface`, so a producer-only name cannot be consumed. See [Console commands](console-commands.md) for details. -For adapter factories, provider registries, and custom error handling strategies see [Advanced queue name internals](queue-names-advanced.md). +For provider implementations and manually constructed role maps, see [Advanced queue name internals](queue-names-advanced.md). For migration details, see [Producer and consumer capabilities](queue-capabilities.md). diff --git a/docs/guide/en/synchronous-mode.md b/docs/guide/en/synchronous-mode.md index ce110955..4d4a7af0 100644 --- a/docs/guide/en/synchronous-mode.md +++ b/docs/guide/en/synchronous-mode.md @@ -4,7 +4,7 @@ Run tasks synchronously in the same process. Useful for: - developing and debugging an application; - writing tests; -- production setups where the application is built around `QueueInterface` from day one but +- production setups where the application is built around `QueueProducerInterface` from day one but doesn't have an external broker yet — you can switch to a real adapter later without touching the call sites. @@ -14,25 +14,23 @@ To enable it, create the queue instance without an adapter (the `adapter` argume $logger = $DIContainer->get(\Psr\Log\LoggerInterface::class); $worker = $DIContainer->get(\Yiisoft\Queue\Worker\WorkerInterface::class); -$loop = $DIContainer->get(\Yiisoft\Queue\Cli\LoopInterface::class); $pushMiddlewareConfig = $DIContainer->get( \Yiisoft\Queue\Middleware\Push\PushMiddlewareConfig::class ); -$queue = new Yiisoft\Queue\Queue( - $worker, - $loop, +$producer = new \Yiisoft\Queue\QueueProducer( $logger, $pushMiddlewareConfig, + worker: $worker, ); ``` -In synchronous mode every message passed to `push()` is processed immediately by the worker. +In synchronous mode every message passed to `$producer->push()` is processed immediately by the worker. The value returned from `push()` is the message after push-middlewares — without an `IdEnvelope`, since no adapter is involved to assign an ID. Limitations: -- `run()` does nothing and returns `0`. -- `listen()` logs an info message and returns without listening. +- A separately configured `QueueConsumer` without an adapter has `run()` return `0`. +- Its `listen()` logs an info message and returns without listening. - `status()` always returns `MessageStatus::NOT_FOUND` — there is no message storage to track IDs. diff --git a/psalm.xml b/psalm.xml index a2f03a7f..517f03a1 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,6 +17,7 @@ + diff --git a/src/Command/ListenAllCommand.php b/src/Command/ListenAllCommand.php index 51b55009..da3d53bd 100644 --- a/src/Command/ListenAllCommand.php +++ b/src/Command/ListenAllCommand.php @@ -11,19 +11,19 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Yiisoft\Queue\Cli\LoopInterface; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; #[AsCommand( 'queue:listen-all', 'Listens the all the given queues and executes messages as they come. ' . 'Meant to be used in development environment only. ' - . 'Listens all configured queues by default in case you\'re using yiisoft/config. ' + . 'Listens all consumer-capable configured queues by default. ' . 'Needs to be stopped manually.', )] final class ListenAllCommand extends Command { public function __construct( - private readonly QueueProviderInterface $queueProvider, + private readonly QueueConsumerProviderInterface $queueProvider, private readonly LoopInterface $loop, ) { parent::__construct(); @@ -38,7 +38,7 @@ public function configure(): void 'queue', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Queue name list to connect to', - $this->queueProvider->getNames(), + [], ) ->addOption( 'pause', @@ -61,10 +61,22 @@ public function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + /** @var string[] $queueNames */ + $queueNames = $input->getArgument('queue'); + if ($queueNames === []) { + $queueNames = $this->queueProvider->getConsumerNames(); + } + $queues = []; /** @var string $queue */ - foreach ($input->getArgument('queue') as $queue) { - $queues[] = $this->queueProvider->get($queue); + foreach ($queueNames as $queue) { + $queues[] = $this->queueProvider->getConsumer($queue); + } + + if ($queues === []) { + $output->writeln('No consumers are configured.'); + + return Command::SUCCESS; } $pauseSeconds = (int) $input->getOption('pause'); diff --git a/src/Command/ListenCommand.php b/src/Command/ListenCommand.php index cfc4c9db..e87a988a 100644 --- a/src/Command/ListenCommand.php +++ b/src/Command/ListenCommand.php @@ -9,7 +9,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; #[AsCommand( 'queue:listen', @@ -18,7 +18,7 @@ final class ListenCommand extends Command { public function __construct( - private readonly QueueProviderInterface $queueProvider, + private readonly QueueConsumerProviderInterface $queueProvider, ) { parent::__construct(); } @@ -29,7 +29,7 @@ public function configure(): void 'queue', InputArgument::OPTIONAL, 'Queue name to connect to', - QueueProviderInterface::DEFAULT_QUEUE, + QueueConsumerProviderInterface::DEFAULT_QUEUE, ); } @@ -37,7 +37,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $queueName = (string) $input->getArgument('queue'); - $this->queueProvider->get($queueName)->listen(); + $this->queueProvider->getConsumer($queueName)->listen(); return Command::SUCCESS; } diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index c9c0baa8..71f4c3c1 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; #[AsCommand( 'queue:run', @@ -19,7 +19,7 @@ final class RunCommand extends Command { public function __construct( - private readonly QueueProviderInterface $queueProvider, + private readonly QueueConsumerProviderInterface $queueProvider, ) { parent::__construct(); } @@ -30,7 +30,7 @@ public function configure(): void 'queue', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Queue name list to connect to.', - $this->queueProvider->getNames(), + [], ) ->addOption( 'limit', @@ -44,12 +44,18 @@ public function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + /** @var string[] $queueNames */ + $queueNames = $input->getArgument('queue'); + if ($queueNames === []) { + $queueNames = $this->queueProvider->getConsumerNames(); + } + /** @var string $queue */ - foreach ($input->getArgument('queue') as $queue) { + foreach ($queueNames as $queue) { + $queueConsumer = $this->queueProvider->getConsumer($queue); + $output->write("Processing queue $queue... "); - $count = $this->queueProvider - ->get($queue) - ->run((int) $input->getOption('limit')); + $count = $queueConsumer->run((int) $input->getOption('limit')); $output->writeln("Messages processed: $count."); } diff --git a/src/Debug/QueueCollector.php b/src/Debug/QueueCollector.php index cacafe94..92ddba5c 100644 --- a/src/Debug/QueueCollector.php +++ b/src/Debug/QueueCollector.php @@ -8,7 +8,6 @@ use Yiisoft\Yii\Debug\Collector\CollectorTrait; use Yiisoft\Yii\Debug\Collector\SummaryCollectorInterface; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; use function count; @@ -65,12 +64,12 @@ public function collectPush(string $queueName, MessageInterface $message, string ]; } - public function collectWorkerProcessing(MessageInterface $message, QueueInterface $queue): void + public function collectWorkerProcessing(MessageInterface $message, string $queueName): void { if (!$this->isActive()) { return; } - $this->processingMessages[$queue->getName()][] = $message; + $this->processingMessages[$queueName][] = $message; } public function getSummary(): array diff --git a/src/Debug/QueueConsumerDecorator.php b/src/Debug/QueueConsumerDecorator.php new file mode 100644 index 00000000..03670b45 --- /dev/null +++ b/src/Debug/QueueConsumerDecorator.php @@ -0,0 +1,22 @@ +queue->run($max); + } + + public function listen(): void + { + $this->queue->listen(); + } +} diff --git a/src/Debug/QueueConsumerProviderProxy.php b/src/Debug/QueueConsumerProviderProxy.php new file mode 100644 index 00000000..c842c5ad --- /dev/null +++ b/src/Debug/QueueConsumerProviderProxy.php @@ -0,0 +1,29 @@ +provider->getConsumer($name), $this->collector); + } + + public function hasConsumer(string|BackedEnum $name): bool + { + return $this->provider->hasConsumer($name); + } + + public function getConsumerNames(): array + { + return $this->provider->getConsumerNames(); + } +} diff --git a/src/Debug/QueueDecorator.php b/src/Debug/QueueDecorator.php deleted file mode 100644 index de0e901b..00000000 --- a/src/Debug/QueueDecorator.php +++ /dev/null @@ -1,53 +0,0 @@ -queue->status($id); - $this->collector->collectStatus((string) $id, $result, $callStack['file'] . ':' . $callStack['line']); - - return $result; - } - - public function push(MessageInterface $message): MessageInterface - { - /** @psalm-var array{file: string, line: int} $callStack */ - $callStack = debug_backtrace()[0]; - - $message = $this->queue->push($message); - $this->collector->collectPush($this->queue->getName(), $message, $callStack['file'] . ':' . $callStack['line']); - return $message; - } - - public function run(int $max = 0): int - { - return $this->queue->run($max); - } - - public function listen(): void - { - $this->queue->listen(); - } - - public function getName(): string - { - return $this->queue->getName(); - } -} diff --git a/src/Debug/QueueProducerDecorator.php b/src/Debug/QueueProducerDecorator.php new file mode 100644 index 00000000..f973c48d --- /dev/null +++ b/src/Debug/QueueProducerDecorator.php @@ -0,0 +1,33 @@ +queue->status($id); + $this->collector->collectStatus((string) $id, $result, $stack['file'] . ':' . $stack['line']); + return $result; + } + + public function push(MessageInterface $message): MessageInterface + { /** @psalm-var array{file: string, line: int} $stack */ $stack = debug_backtrace()[0]; + $message = $this->queue->push($message); + $this->collector->collectPush($this->queue->getName(), $message, $stack['file'] . ':' . $stack['line']); + return $message; + } + + public function getName(): string + { + return $this->queue->getName(); + } +} diff --git a/src/Debug/QueueProducerProviderProxy.php b/src/Debug/QueueProducerProviderProxy.php new file mode 100644 index 00000000..ed929f6d --- /dev/null +++ b/src/Debug/QueueProducerProviderProxy.php @@ -0,0 +1,29 @@ +provider->getProducer($name), $this->collector); + } + + public function hasProducer(string|BackedEnum $name): bool + { + return $this->provider->hasProducer($name); + } + + public function getProducerNames(): array + { + return $this->provider->getProducerNames(); + } +} diff --git a/src/Debug/QueueProviderInterfaceProxy.php b/src/Debug/QueueProviderInterfaceProxy.php deleted file mode 100644 index c28d1b20..00000000 --- a/src/Debug/QueueProviderInterfaceProxy.php +++ /dev/null @@ -1,34 +0,0 @@ -queueProvider->get($name); - - return new QueueDecorator($queue, $this->collector); - } - - public function has(string|BackedEnum $name): bool - { - return $this->queueProvider->has($name); - } - - public function getNames(): array - { - return $this->queueProvider->getNames(); - } -} diff --git a/src/Debug/QueueWorkerInterfaceProxy.php b/src/Debug/QueueWorkerInterfaceProxy.php index ae8ee9b1..b26c5cd5 100644 --- a/src/Debug/QueueWorkerInterfaceProxy.php +++ b/src/Debug/QueueWorkerInterfaceProxy.php @@ -5,7 +5,7 @@ namespace Yiisoft\Queue\Debug; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Worker\WorkerInterface; final class QueueWorkerInterfaceProxy implements WorkerInterface @@ -15,9 +15,12 @@ public function __construct( private readonly QueueCollector $collector, ) {} - public function process(MessageInterface $message, QueueInterface $queue): MessageInterface - { - $this->collector->collectWorkerProcessing($message, $queue); - return $this->worker->process($message, $queue); + public function process( + MessageInterface $message, + string $queueName, + ?QueueProducerInterface $retryProducer = null, + ): MessageInterface { + $this->collector->collectWorkerProcessing($message, $queueName); + return $this->worker->process($message, $queueName, $retryProducer); } } diff --git a/src/Middleware/Consume/ConsumeRequest.php b/src/Middleware/Consume/ConsumeRequest.php index af0d5f08..c229bc67 100644 --- a/src/Middleware/Consume/ConsumeRequest.php +++ b/src/Middleware/Consume/ConsumeRequest.php @@ -5,35 +5,33 @@ namespace Yiisoft\Queue\Middleware\Consume; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; final class ConsumeRequest { - public function __construct(private MessageInterface $message, private QueueInterface $queue) {} + public function __construct(private MessageInterface $message, private string $queueName) {} public function getMessage(): MessageInterface { return $this->message; } - public function getQueue(): QueueInterface + /** Logical name of the queue currently executing this message. */ + public function getQueueName(): string { - return $this->queue; + return $this->queueName; } public function withMessage(MessageInterface $message): self { $instance = clone $this; $instance->message = $message; - return $instance; } - public function withQueue(QueueInterface $queue): self + public function withQueueName(string $queueName): self { $instance = clone $this; - $instance->queue = $queue; - + $instance->queueName = $queueName; return $instance; } } diff --git a/src/Middleware/FailureHandling/FailureHandlingRequest.php b/src/Middleware/FailureHandling/FailureHandlingRequest.php index fb37eac3..03dc9888 100644 --- a/src/Middleware/FailureHandling/FailureHandlingRequest.php +++ b/src/Middleware/FailureHandling/FailureHandlingRequest.php @@ -6,15 +6,17 @@ use Throwable; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; final class FailureHandlingRequest { - public function __construct(private MessageInterface $message, private Throwable $exception, private QueueInterface $queue) {} + public function __construct( + private MessageInterface $message, + private Throwable $exception, + private string $queueName, + private ?QueueProducerInterface $retryProducer = null, + ) {} - /** - * @return MessageInterface - */ public function getMessage(): MessageInterface { return $this->message; @@ -25,16 +27,22 @@ public function getException(): Throwable return $this->exception; } - public function getQueue(): QueueInterface + /** Logical name of the queue which executed the message. */ + public function getQueueName(): string { - return $this->queue; + return $this->queueName; + } + + /** Direct retry target used by synchronous producer execution, if any. */ + public function getRetryProducer(): ?QueueProducerInterface + { + return $this->retryProducer; } public function withMessage(MessageInterface $message): self { $instance = clone $this; $instance->message = $message; - return $instance; } @@ -42,15 +50,13 @@ public function withException(Throwable $exception): self { $instance = clone $this; $instance->exception = $exception; - return $instance; } - public function withQueue(QueueInterface $queue): self + public function withQueueName(string $queueName): self { $instance = clone $this; - $instance->queue = $queue; - + $instance->queueName = $queueName; return $instance; } } diff --git a/src/Middleware/FailureHandling/FailureMiddlewareDispatcher.php b/src/Middleware/FailureHandling/FailureMiddlewareDispatcher.php index ef973f84..3647e6ad 100644 --- a/src/Middleware/FailureHandling/FailureMiddlewareDispatcher.php +++ b/src/Middleware/FailureHandling/FailureMiddlewareDispatcher.php @@ -37,7 +37,7 @@ public function dispatch( FailureHandlingRequest $request, FailureHandlerInterface $finishHandler, ): FailureHandlingRequest { - $queueName = $request->getQueue()->getName(); + $queueName = $request->getQueueName(); if (!isset($this->middlewareDefinitions[$queueName]) || $this->middlewareDefinitions[$queueName] === []) { $queueName = self::DEFAULT_PIPELINE; } diff --git a/src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php b/src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php index 7ab5bc64..524461c6 100644 --- a/src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php +++ b/src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php @@ -5,71 +5,75 @@ namespace Yiisoft\Queue\Middleware\FailureHandling\Implementation; use InvalidArgumentException; +use Yiisoft\Queue\Message\DelayEnvelope; use Yiisoft\Queue\Message\MessageInterface; +use Yiisoft\Queue\Middleware\FailureHandling\FailureEnvelope; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlingRequest; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlerInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareInterface; -use Yiisoft\Queue\Message\DelayEnvelope; -use Yiisoft\Queue\QueueInterface; -use Yiisoft\Queue\Middleware\FailureHandling\FailureEnvelope; +use Yiisoft\Queue\Provider\InvalidQueueConfigException; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; +use Yiisoft\Queue\QueueProducerInterface; +use Throwable; + +use function sprintf; -/** - * Failure strategy which resends the given message to a queue with an exponentially increasing delay. - * The delay mechanism **must** be implemented by the used {@see AdapterInterface} implementation. - */ +/** Resends failures with exponentially increasing adapter-supported delay. */ final class ExponentialDelayMiddleware implements FailureMiddlewareInterface { public const META_KEY_ATTEMPTS = 'failure-strategy-exponential-delay-attempts'; public const META_KEY_DELAY = 'failure-strategy-exponential-delay-delay'; - /** - * @param string $id A unique id to differentiate two and more instances of this class - * @param int $maxAttempts Maximum attempts count for this strategy with the given $id before it will give up - * @param float $delayInitial The first delay period - * @param float $delayMaximum The maximum delay period - * @param float $exponent Message handling delay will be increased by this multiplication each time it fails - * @param QueueInterface|null $queue - */ public function __construct( private readonly string $id, private readonly int $maxAttempts, private readonly float $delayInitial, private readonly float $delayMaximum, private readonly float $exponent, - private readonly ?QueueInterface $queue = null, + private readonly ?QueueProducerInterface $queue = null, + private readonly ?QueueProducerProviderInterface $producerProvider = null, ) { if ($maxAttempts <= 0) { throw new InvalidArgumentException("maxAttempts parameter must be a positive integer, $this->maxAttempts given."); } - if ($delayInitial <= 0) { throw new InvalidArgumentException("delayInitial parameter must be a positive float, $this->delayInitial given."); } - if ($delayMaximum < $delayInitial) { - throw new InvalidArgumentException("delayMaximum parameter must not be less then delayInitial, , $this->delayMaximum given."); + throw new InvalidArgumentException("delayMaximum parameter must not be less then delayInitial, $this->delayMaximum given."); } - if ($exponent <= 0) { throw new InvalidArgumentException("exponent parameter must not be zero or less, $this->exponent given."); } } - public function processFailure( - FailureHandlingRequest $request, - FailureHandlerInterface $handler, - ): FailureHandlingRequest { + public function processFailure(FailureHandlingRequest $request, FailureHandlerInterface $handler): FailureHandlingRequest + { $message = $request->getMessage(); - if ($this->suites($message)) { - $failureEnvelope = new FailureEnvelope($message, $this->createNewMeta($message)); - $delayEnvelope = new DelayEnvelope($failureEnvelope, $this->getDelay($failureEnvelope)); - $queue = $this->queue ?? $request->getQueue(); - $messageNew = $queue->push($delayEnvelope); - - return $request->withMessage($messageNew); + if (!$this->suites($message)) { + return $handler->handleFailure($request); } + $failure = new FailureEnvelope($message, $this->createNewMeta($message)); + $result = $this->producer($request)->push(new DelayEnvelope($failure, $this->getDelay($failure))); + return $request->withMessage($result); + } - return $handler->handleFailure($request); + private function producer(FailureHandlingRequest $request): QueueProducerInterface + { + if ($this->queue !== null) { + return $this->queue; + } + if ($request->getRetryProducer() !== null) { + return $request->getRetryProducer(); + } + if ($this->producerProvider === null) { + throw new InvalidQueueConfigException(sprintf('Cannot retry queue "%s": configure a producer target or QueueProducerProviderInterface.', $request->getQueueName())); + } + try { + return $this->producerProvider->getProducer($request->getQueueName()); + } catch (Throwable $exception) { + throw new InvalidQueueConfigException(sprintf('Cannot retry queue "%s": no producer capability is available.', $request->getQueueName()), previous: $exception); + } } private function suites(MessageInterface $message): bool @@ -79,29 +83,17 @@ private function suites(MessageInterface $message): bool private function createNewMeta(MessageInterface $message): array { - return [ - self::META_KEY_DELAY . "-$this->id" => $this->getDelay($message), - self::META_KEY_ATTEMPTS . "-$this->id" => $this->getAttempts($message) + 1, - ]; + return [self::META_KEY_DELAY . "-$this->id" => $this->getDelay($message), self::META_KEY_ATTEMPTS . "-$this->id" => $this->getAttempts($message) + 1]; } private function getAttempts(MessageInterface $message): int { - return (int) FailureEnvelope::fromMessage($message) - ->getFailureMetaValue(self::META_KEY_ATTEMPTS . "-$this->id", 0); + return (int) FailureEnvelope::fromMessage($message)->getFailureMetaValue(self::META_KEY_ATTEMPTS . "-$this->id", 0); } private function getDelay(MessageInterface $message): float { - $delayOriginal = (float) FailureEnvelope::fromMessage($message) - ->getFailureMetaValue(self::META_KEY_DELAY . "-$this->id", 0); - - if ($delayOriginal <= 0) { - $delayOriginal = $this->delayInitial; - } - - $result = $delayOriginal * $this->exponent; - - return min($result, $this->delayMaximum); + $original = (float) FailureEnvelope::fromMessage($message)->getFailureMetaValue(self::META_KEY_DELAY . "-$this->id", 0); + return min(($original <= 0 ? $this->delayInitial : $original) * $this->exponent, $this->delayMaximum); } } diff --git a/src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php b/src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php index f3bf6736..1de2074d 100644 --- a/src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php +++ b/src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php @@ -10,65 +10,61 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlingRequest; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlerInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\Provider\InvalidQueueConfigException; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; +use Yiisoft\Queue\QueueProducerInterface; +use Throwable; -/** - * Failure strategy which resends the given message to a queue. - */ +use function sprintf; + +/** Failure strategy which resends a message through a producer capability. */ final class SendAgainMiddleware implements FailureMiddlewareInterface { public const META_KEY_RESEND = 'failure-strategy-resend-attempts'; - /** - * @param string $id A unique id to differentiate two and more instances of this class - * @param int $maxAttempts Maximum attempts count for this strategy with the given $id before it will give up - * @param QueueInterface|null $targetQueue Messages will be sent to this queue if set. - * They will be resent to an original queue otherwise. - */ public function __construct( private readonly string $id, private readonly int $maxAttempts, - private readonly ?QueueInterface $targetQueue = null, + private readonly ?QueueProducerInterface $targetQueue = null, + private readonly ?QueueProducerProviderInterface $producerProvider = null, ) { if ($maxAttempts < 1) { throw new InvalidArgumentException("maxAttempts parameter must be a positive integer, $this->maxAttempts given."); } } - public function processFailure( - FailureHandlingRequest $request, - FailureHandlerInterface $handler, - ): FailureHandlingRequest { + public function processFailure(FailureHandlingRequest $request, FailureHandlerInterface $handler): FailureHandlingRequest + { $message = $request->getMessage(); - if ($this->suits($message)) { - $envelope = new FailureEnvelope($message, $this->createMeta($message)); - $envelope = ($this->targetQueue ?? $request->getQueue())->push($envelope); - - return $request->withMessage($envelope) - ->withQueue($this->targetQueue ?? $request->getQueue()); + if (!$this->suits($message)) { + return $handler->handleFailure($request); } - - return $handler->handleFailure($request); + $envelope = new FailureEnvelope($message, [$this->getMetaKey() => $this->getAttempts($message) + 1]); + $producer = $this->targetQueue ?? $request->getRetryProducer() ?? $this->sourceProducer($request); + $envelope = $producer->push($envelope); + return $request->withMessage($envelope); } - private function suits(MessageInterface $message): bool + private function sourceProducer(FailureHandlingRequest $request): QueueProducerInterface { - return $this->getAttempts($message) < $this->maxAttempts; + if ($this->producerProvider === null) { + throw new InvalidQueueConfigException(sprintf('Cannot retry queue "%s": configure a producer target or QueueProducerProviderInterface.', $request->getQueueName())); + } + try { + return $this->producerProvider->getProducer($request->getQueueName()); + } catch (Throwable $exception) { + throw new InvalidQueueConfigException(sprintf('Cannot retry queue "%s": no producer capability is available.', $request->getQueueName()), previous: $exception); + } } - private function createMeta(MessageInterface $message): array + private function suits(MessageInterface $message): bool { - return [$this->getMetaKey() => $this->getAttempts($message) + 1]; + return $this->getAttempts($message) < $this->maxAttempts; } private function getAttempts(MessageInterface $message): int { - $result = FailureEnvelope::fromMessage($message)->getFailureMetaValue($this->getMetaKey(), 0); - if ($result < 0) { - $result = 0; - } - - return (int) $result; + return max(0, (int) FailureEnvelope::fromMessage($message)->getFailureMetaValue($this->getMetaKey(), 0)); } private function getMetaKey(): string diff --git a/src/Middleware/Push/PushMiddlewareDispatcher.php b/src/Middleware/Push/PushMiddlewareDispatcher.php index 53c1b01e..ce2a89dc 100644 --- a/src/Middleware/Push/PushMiddlewareDispatcher.php +++ b/src/Middleware/Push/PushMiddlewareDispatcher.php @@ -6,10 +6,9 @@ use Closure; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\Queue; /** - * @internal Used internally by {@see Queue}. + * @internal Used internally by {@see QueueProducer}. */ final class PushMiddlewareDispatcher { diff --git a/src/Middleware/Push/SynchronousPushHandler.php b/src/Middleware/Push/SynchronousPushHandler.php index 2c7f7547..f35c1009 100644 --- a/src/Middleware/Push/SynchronousPushHandler.php +++ b/src/Middleware/Push/SynchronousPushHandler.php @@ -5,7 +5,7 @@ namespace Yiisoft\Queue\Middleware\Push; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Worker\WorkerInterface; /** @@ -15,12 +15,12 @@ final class SynchronousPushHandler implements PushHandlerInterface { public function __construct( private readonly WorkerInterface $worker, - private readonly QueueInterface $queue, + private readonly QueueProducerInterface $queue, ) {} public function handlePush(MessageInterface $message): MessageInterface { - $this->worker->process($message, $this->queue); + $this->worker->process($message, $this->queue->getName(), $this->queue); return $message; } diff --git a/src/Provider/CompositeQueueProvider.php b/src/Provider/CompositeQueueProvider.php index f46cd0b5..6218da60 100644 --- a/src/Provider/CompositeQueueProvider.php +++ b/src/Provider/CompositeQueueProvider.php @@ -5,57 +5,91 @@ namespace Yiisoft\Queue\Provider; use BackedEnum; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; +use Yiisoft\Queue\StringNormalizer; -use function array_merge; -use function array_unique; -use function array_values; +use function in_array; -/** - * Composite queue provider. - */ -final class CompositeQueueProvider implements QueueProviderInterface +/** Combines typed providers; earlier providers take precedence per capability. */ +final class CompositeQueueProvider implements QueueProducerProviderInterface, QueueConsumerProviderInterface { - /** - * @var QueueProviderInterface[] - */ - private readonly array $providers; - - /** - * @param QueueProviderInterface ...$providers Queue providers to use. - */ - public function __construct( - QueueProviderInterface ...$providers, - ) { - $this->providers = $providers; + /** @var list */ private array $producerProviders = []; + /** @var list */ private array $consumerProviders = []; + + public function __construct(QueueProducerProviderInterface|QueueConsumerProviderInterface ...$providers) + { + foreach ($providers as $provider) { + if ($provider instanceof QueueProducerProviderInterface) { + $this->producerProviders[] = $provider; + } + if ($provider instanceof QueueConsumerProviderInterface) { + $this->consumerProviders[] = $provider; + } + } } - public function get(string|BackedEnum $name): QueueInterface + public function getProducer(string|BackedEnum $name): QueueProducerInterface { - foreach ($this->providers as $provider) { - if ($provider->has($name)) { - return $provider->get($name); + foreach ($this->producerProviders as $provider) { + if ($provider->hasProducer($name)) { + return $provider->getProducer($name); } } - throw new QueueNotFoundException($name); + throw new QueueNotFoundException(StringNormalizer::normalize($name)); } - public function has(string|BackedEnum $name): bool + public function hasProducer(string|BackedEnum $name): bool { - foreach ($this->providers as $provider) { - if ($provider->has($name)) { + foreach ($this->producerProviders as $p) { + if ($p->hasProducer($name)) { return true; } - } - return false; + } return false; + } + + /** @return list */ + public function getProducerNames(): array + { + $result = []; + foreach ($this->producerProviders as $provider) { + foreach ($provider->getProducerNames() as $name) { + if (!in_array($name, $result, true)) { + $result[] = $name; + } + } + } return $result; } - public function getNames(): array + public function getConsumer(string|BackedEnum $name): QueueConsumerInterface { - $names = []; - foreach ($this->providers as $provider) { - $names[] = $provider->getNames(); + foreach ($this->consumerProviders as $provider) { + if ($provider->hasConsumer($name)) { + return $provider->getConsumer($name); + } } - return array_values(array_unique(array_merge(...$names))); + throw new QueueNotFoundException(StringNormalizer::normalize($name)); + } + + public function hasConsumer(string|BackedEnum $name): bool + { + foreach ($this->consumerProviders as $p) { + if ($p->hasConsumer($name)) { + return true; + } + } return false; + } + + /** @return list */ + public function getConsumerNames(): array + { + $result = []; + foreach ($this->consumerProviders as $provider) { + foreach ($provider->getConsumerNames() as $name) { + if (!in_array($name, $result, true)) { + $result[] = $name; + } + } + } return $result; } } diff --git a/src/Provider/PredefinedQueueProvider.php b/src/Provider/PredefinedQueueProvider.php index b11dbed4..7bb7335a 100644 --- a/src/Provider/PredefinedQueueProvider.php +++ b/src/Provider/PredefinedQueueProvider.php @@ -5,67 +5,110 @@ namespace Yiisoft\Queue\Provider; use BackedEnum; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\StringNormalizer; use function array_key_exists; use function array_keys; use function get_debug_type; +use function implode; +use function is_array; use function sprintf; +use function assert; +use function is_string; -/** - * Queue provider that uses a pre-defined map of queue name to queue instance. - */ -final class PredefinedQueueProvider implements QueueProviderInterface +/** Provides already-created producer and consumer instances from strict role maps. */ +final class PredefinedQueueProvider implements QueueProducerProviderInterface, QueueConsumerProviderInterface { - /** - * @psalm-var array - */ - private readonly array $queues; + /** @var array> */ + private array $queues = []; + /** @var list */ + private array $producerNames = []; + /** @var list */ + private array $consumerNames = []; - /** - * @param array $queues Map of queue name to queue instance. - * - * @psalm-param array $queues - * - * @throws InvalidQueueConfigException If a value in the array is not a {@see QueueInterface} instance. - */ + /** @param array $queues */ public function __construct(array $queues) { - foreach ($queues as $name => $queue) { - if (!$queue instanceof QueueInterface) { - throw new InvalidQueueConfigException( - sprintf( - 'Queue must implement "%s". For queue "%s" got "%s" instead.', - QueueInterface::class, + foreach ($queues as $name => $roles) { + if (!is_array($roles) || $roles === []) { + throw new InvalidQueueConfigException(sprintf('Queue "%s" must be a non-empty role map containing ready "producer" and/or "consumer" instances.', $name)); + } + $unknown = array_diff(array_keys($roles), ['producer', 'consumer']); + if ($unknown !== []) { + throw new InvalidQueueConfigException(sprintf('Queue "%s" has unknown role key(s) "%s". Only "producer" and "consumer" are allowed.', $name, implode('", "', $unknown))); + } + foreach ($roles as $role => $queue) { + $expected = $role === 'producer' ? QueueProducerInterface::class : QueueConsumerInterface::class; + if (!$queue instanceof $expected) { + $hint = is_array($queue) || is_string($queue) ? ' Use QueueFactoryProvider for factory definitions.' : ''; + throw new InvalidQueueConfigException(sprintf( + 'Queue "%s" role "%s" must be a ready instance of "%s"; got "%s" (configuration path queues.%s.%s).%s', $name, + $role, + $expected, get_debug_type($queue), - ), - ); + $name, + $role, + $hint, + )); + } + } + /** @var array $roles */ + $this->queues[$name] = $roles; + if (array_key_exists('producer', $roles)) { + $this->producerNames[] = $name; + } + if (array_key_exists('consumer', $roles)) { + $this->consumerNames[] = $name; } } - $this->queues = $queues; } - public function get(string|BackedEnum $name): QueueInterface + public function getProducer(string|BackedEnum $name): QueueProducerInterface { - $name = StringNormalizer::normalize($name); + $queue = $this->get($name, 'producer'); + assert($queue instanceof QueueProducerInterface); + return $queue; + } - if (!array_key_exists($name, $this->queues)) { - throw new QueueNotFoundException($name); - } + public function hasProducer(string|BackedEnum $name): bool + { + return array_key_exists('producer', $this->queues[StringNormalizer::normalize($name)] ?? []); + } - return $this->queues[$name]; + public function getProducerNames(): array + { + return $this->producerNames; } - public function has(string|BackedEnum $name): bool + public function getConsumer(string|BackedEnum $name): QueueConsumerInterface { - $name = StringNormalizer::normalize($name); - return array_key_exists($name, $this->queues); + $queue = $this->get($name, 'consumer'); + assert($queue instanceof QueueConsumerInterface); + return $queue; + } + + public function hasConsumer(string|BackedEnum $name): bool + { + return array_key_exists('consumer', $this->queues[StringNormalizer::normalize($name)] ?? []); + } + + public function getConsumerNames(): array + { + return $this->consumerNames; } - public function getNames(): array + private function get(string|BackedEnum $name, string $role): QueueProducerInterface|QueueConsumerInterface { - return array_keys($this->queues); + $name = StringNormalizer::normalize($name); + if (!array_key_exists($name, $this->queues)) { + throw new QueueNotFoundException($name); + } + if (!array_key_exists($role, $this->queues[$name])) { + throw new QueueNotFoundException(sprintf('Queue "%s" does not have the "%s" capability.', $name, $role)); + } + return $this->queues[$name][$role]; } } diff --git a/src/Provider/QueueConsumerProviderInterface.php b/src/Provider/QueueConsumerProviderInterface.php new file mode 100644 index 00000000..348d2b64 --- /dev/null +++ b/src/Provider/QueueConsumerProviderInterface.php @@ -0,0 +1,21 @@ + Names which have a configured consumer role. */ + public function getConsumerNames(): array; +} diff --git a/src/Provider/QueueFactoryProvider.php b/src/Provider/QueueFactoryProvider.php index 554c2b64..ed414653 100644 --- a/src/Provider/QueueFactoryProvider.php +++ b/src/Provider/QueueFactoryProvider.php @@ -6,112 +6,158 @@ use BackedEnum; use Psr\Container\ContainerInterface; +use Throwable; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Factory\StrictFactory; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\StringNormalizer; use function array_key_exists; use function array_keys; +use function get_debug_type; +use function implode; +use function is_array; use function sprintf; +use function assert; -/** - * This queue provider creates queue objects directly from definitions. - * - * @see https://github.com/yiisoft/definitions/ - * @see https://github.com/yiisoft/factory/ - */ -final class QueueFactoryProvider implements QueueProviderInterface +/** Lazily creates producer and consumer roles from strict nested role maps. */ +final class QueueFactoryProvider implements QueueProducerProviderInterface, QueueConsumerProviderInterface { - /** - * @psalm-var array - */ - private array $queues = []; - - private readonly StrictFactory $factory; - - /** - * @psalm-var list - */ - private readonly array $names; - - /** - * @param array $definitions Queue definitions indexed by queue names. - * @param ContainerInterface|null $container Container to use for dependencies resolving. - * @param bool $validate If definitions should be validated when set. - * - * @psalm-param array $definitions - * - * @throws InvalidQueueConfigException - */ + /** @var array> */ + private array $definitions; + /** @var array> */ + private array $resolved = []; + /** @var list */ + private array $producerNames = []; + /** @var list */ + private array $consumerNames = []; + + /** @param array $definitions */ public function __construct( array $definitions, - ?ContainerInterface $container = null, - bool $validate = true, + private readonly ?ContainerInterface $container = null, + private readonly bool $validate = true, ) { - $this->names = array_keys($definitions); - try { - $this->factory = new StrictFactory($definitions, $container, $validate); - } catch (InvalidConfigException $exception) { - throw new InvalidQueueConfigException($exception->getMessage(), previous: $exception); + /** @var array> $validatedDefinitions */ + $validatedDefinitions = $this->validateRoleMaps($definitions); + $this->definitions = $validatedDefinitions; + foreach ($this->definitions as $name => $roles) { + if (array_key_exists('producer', $roles)) { + $this->producerNames[] = $name; + } + if (array_key_exists('consumer', $roles)) { + $this->consumerNames[] = $name; + } } } - public function get(string|BackedEnum $name): QueueInterface + public function getProducer(string|BackedEnum $name): QueueProducerInterface { - $name = StringNormalizer::normalize($name); + $producer = $this->get($name, 'producer', QueueProducerInterface::class); + assert($producer instanceof QueueProducerInterface); + return $producer; + } - $queue = $this->getOrTryToCreate($name); - if ($queue === null) { - throw new QueueNotFoundException($name); - } + public function hasProducer(string|BackedEnum $name): bool + { + return array_key_exists('producer', $this->definitions[StringNormalizer::normalize($name)] ?? []); + } - return $queue; + public function getProducerNames(): array + { + return $this->producerNames; } - public function has(string|BackedEnum $name): bool + public function getConsumer(string|BackedEnum $name): QueueConsumerInterface { - $name = StringNormalizer::normalize($name); - return $this->factory->has($name); + $consumer = $this->get($name, 'consumer', QueueConsumerInterface::class); + assert($consumer instanceof QueueConsumerInterface); + return $consumer; } - public function getNames(): array + public function hasConsumer(string|BackedEnum $name): bool { - return $this->names; + return array_key_exists('consumer', $this->definitions[StringNormalizer::normalize($name)] ?? []); } - /** - * @throws InvalidQueueConfigException - */ - private function getOrTryToCreate(string $name): ?QueueInterface + public function getConsumerNames(): array { - if (array_key_exists($name, $this->queues)) { - return $this->queues[$name]; - } + return $this->consumerNames; + } - if (!$this->factory->has($name)) { - $this->queues[$name] = null; - return null; + /** @template T of QueueProducerInterface|QueueConsumerInterface @param class-string $expected @return T */ + private function get(string|BackedEnum $name, string $role, string $expected): QueueProducerInterface|QueueConsumerInterface + { + $name = StringNormalizer::normalize($name); + if (!array_key_exists($name, $this->definitions)) { + throw new QueueNotFoundException($name); + } + if (!array_key_exists($role, $this->definitions[$name])) { + throw new QueueNotFoundException(sprintf('Queue "%s" does not have the "%s" capability.', $name, $role)); + } + if (isset($this->resolved[$name][$role])) { + $result = $this->resolved[$name][$role]; + if ($result instanceof Throwable) { + throw $result; + } + return $result; } - try { - $queue = $this->factory->create($name); + $key = $name . ':' . $role; + $factory = new StrictFactory([$key => $this->definitions[$name][$role]], $this->container, $this->validate); + $result = $factory->create($key); + if (!$result instanceof $expected) { + throw new InvalidQueueConfigException(sprintf( + 'Queue "%s" role "%s" must implement "%s"; got "%s" (configuration path queues.%s.%s).', + $name, + $role, + $expected, + get_debug_type($result), + $name, + $role, + )); + } + assert($result instanceof QueueProducerInterface || $result instanceof QueueConsumerInterface); + $this->resolved[$name][$role] = $result; + return $result; + } catch (InvalidQueueConfigException $exception) { + $this->resolved[$name][$role] = $exception; + throw $exception; } catch (InvalidConfigException $exception) { - throw new InvalidQueueConfigException($exception->getMessage(), previous: $exception); + $wrapped = new InvalidQueueConfigException(sprintf( + 'Invalid queue "%s" role "%s" definition (configuration path queues.%s.%s): %s', + $name, + $role, + $name, + $role, + $exception->getMessage(), + ), previous: $exception); + $this->resolved[$name][$role] = $wrapped; + throw $wrapped; } + } - if (!$queue instanceof QueueInterface) { - throw new InvalidQueueConfigException( - sprintf( - 'Queue must implement "%s". For queue "%s" got "%s" instead.', - QueueInterface::class, - $name, - get_debug_type($queue), - ), - ); + /** @param array $definitions @return array> */ + private function validateRoleMaps(array $definitions): array + { + /** @var array> $result */ + $result = []; + foreach ($definitions as $name => $roles) { + if (!is_array($roles)) { + throw new InvalidQueueConfigException(sprintf('Queue "%s" must be a role map containing "producer" and/or "consumer"; got "%s".', $name, get_debug_type($roles))); + } + $keys = array_keys($roles); + $unknown = array_diff($keys, ['producer', 'consumer']); + if ($unknown !== []) { + throw new InvalidQueueConfigException(sprintf('Queue "%s" has unknown role key(s) "%s". Only "producer" and "consumer" are allowed.', $name, implode('", "', $unknown))); + } + if ($roles === []) { + throw new InvalidQueueConfigException(sprintf('Queue "%s" role map must contain "producer" and/or "consumer".', $name)); + } + /** @var array $roles */ + $result[$name] = $roles; } - - $this->queues[$name] = $queue; - return $queue; + return $result; } } diff --git a/src/Provider/QueueProducerProviderInterface.php b/src/Provider/QueueProducerProviderInterface.php new file mode 100644 index 00000000..5b1d91db --- /dev/null +++ b/src/Provider/QueueProducerProviderInterface.php @@ -0,0 +1,21 @@ + Names which have a configured producer role. */ + public function getProducerNames(): array; +} diff --git a/src/Provider/QueueProviderDefaults.php b/src/Provider/QueueProviderDefaults.php new file mode 100644 index 00000000..3b858a77 --- /dev/null +++ b/src/Provider/QueueProviderDefaults.php @@ -0,0 +1,11 @@ + - */ - public function getNames(): array; -} diff --git a/src/Queue.php b/src/Queue.php deleted file mode 100644 index b9bc5d48..00000000 --- a/src/Queue.php +++ /dev/null @@ -1,163 +0,0 @@ -name = StringNormalizer::normalize($name); - $this->dispatcher = new PushMiddlewareDispatcher( - middlewareFactory: $middlewareConfig->middlewareFactory, - middlewareDefinitions: [...$middlewareConfig->commonMiddlewareDefinitions, ...$middlewareDefinitions], - finishHandler: $this->isSynchronous() - ? new SynchronousPushHandler($this->worker, $this) - : new AdapterPushHandler($this->adapter), - ); - } - - public function getName(): string - { - return $this->name; - } - - public function push(MessageInterface $message): MessageInterface - { - $this->logger->debug( - 'Preparing to push message with message type "{messageType}".', - ['messageType' => $message->getType()], - ); - - $message = $this->dispatcher->dispatch($message); - - if ($this->isSynchronous()) { - $this->logger->info( - 'Processed message with message type "{messageType}" synchronously.', - ['messageType' => $message->getType()], - ); - return $message; - } - - $messageId = IdEnvelope::fromMessage($message)->getId(); - if ($messageId === null) { - $this->logger->info( - 'Pushed message with message type "{messageType}" to the queue. ID doesn\'t assigned.', - ['messageType' => $message->getType()], - ); - } else { - $this->logger->info( - 'Pushed message with message type "{messageType}" to the queue. Assigned ID #{id}.', - ['messageType' => $message->getType(), 'id' => $messageId], - ); - } - - return $message; - } - - public function run(int $max = 0): int - { - if ($this->isSynchronous()) { - $this->logger->debug( - 'Queue is in synchronous mode (no adapter). Messages are processed on push. run() does nothing.', - ); - return 0; - } - - $this->logger->debug('Start processing queue messages.'); - $count = 0; - - $handlerCallback = function (MessageInterface $message) use (&$max, &$count): bool { - if (($max > 0 && $max <= $count) || !$this->handle($message)) { - return false; - } - $count++; - - return true; - }; - - $this->adapter->runExisting($handlerCallback); - - $this->logger->info( - 'Processed {count} queue messages.', - ['count' => $count], - ); - - return $count; - } - - public function listen(): void - { - if ($this->isSynchronous()) { - $this->logger->info('Cannot listen without an adapter. Queue is in synchronous mode.'); - return; - } - - $this->logger->info('Start listening to the queue.'); - $this->adapter->subscribe(fn(MessageInterface $message) => $this->handle($message)); - $this->logger->info('Finish listening to the queue.'); - } - - public function status(string|int $id): MessageStatus - { - if ($this->isSynchronous()) { - return MessageStatus::NOT_FOUND; - } - - return $this->adapter->status($id); - } - - private function handle(MessageInterface $message): bool - { - $this->worker->process($message, $this); - - return $this->loop->canContinue(); - } - - /** - * @psalm-assert-if-false !null $this->adapter - */ - private function isSynchronous(): bool - { - return $this->adapter === null; - } -} diff --git a/src/QueueConsumer.php b/src/QueueConsumer.php new file mode 100644 index 00000000..68d21b67 --- /dev/null +++ b/src/QueueConsumer.php @@ -0,0 +1,65 @@ +name = StringNormalizer::normalize($name); + } + + public function run(int $max = 0): int + { + if ($this->adapter === null) { + $this->logger->debug('Queue is in synchronous mode (no adapter). Messages are processed on push. run() does nothing.'); + return 0; + } + $this->logger->debug('Start processing queue messages.'); + $count = 0; + $this->adapter->runExisting(function (MessageInterface $message) use (&$count, $max): bool { + if (($max > 0 && $count >= $max) || !$this->handle($message)) { + return false; + } + $count++; + return true; + }); + $this->logger->info('Processed {count} queue messages.', ['count' => $count]); + return $count; + } + + public function listen(): void + { + if ($this->adapter === null) { + $this->logger->info('Cannot listen without an adapter. Queue is in synchronous mode.'); + return; + } + $this->logger->info('Start listening to the queue.'); + $this->adapter->subscribe(fn(MessageInterface $message): bool => $this->handle($message)); + $this->logger->info('Finish listening to the queue.'); + } + + private function handle(MessageInterface $message): bool + { + $this->worker->process($message, $this->name); + return $this->loop->canContinue(); + } +} diff --git a/src/QueueConsumerInterface.php b/src/QueueConsumerInterface.php new file mode 100644 index 00000000..a0a72163 --- /dev/null +++ b/src/QueueConsumerInterface.php @@ -0,0 +1,25 @@ +name = StringNormalizer::normalize($name); + if ($adapter === null && $worker === null) { + throw new InvalidArgumentException('A synchronous queue producer requires a worker.'); + } + $this->dispatcher = new PushMiddlewareDispatcher( + middlewareFactory: $middlewareConfig->middlewareFactory, + middlewareDefinitions: [...$middlewareConfig->commonMiddlewareDefinitions, ...$middlewareDefinitions], + finishHandler: $adapter === null + ? new SynchronousPushHandler($worker, $this) + : new AdapterPushHandler($adapter), + ); + } + + public function getName(): string + { + return $this->name; + } + + public function push(MessageInterface $message): MessageInterface + { + $this->logger->debug('Preparing to push message with message type "{messageType}".', ['messageType' => $message->getType()]); + $message = $this->dispatcher->dispatch($message); + if ($this->adapter === null) { + $this->logger->info('Processed message with message type "{messageType}" synchronously.', ['messageType' => $message->getType()]); + return $message; + } + $id = IdEnvelope::fromMessage($message)->getId(); + $this->logger->info( + $id === null ? 'Pushed message with message type "{messageType}" to the queue. ID doesn\'t assigned.' : 'Pushed message with message type "{messageType}" to the queue. Assigned ID #{id}.', + ['messageType' => $message->getType(), 'id' => $id], + ); + return $message; + } + + public function status(string|int $id): MessageStatus + { + return $this->adapter?->status($id) ?? MessageStatus::NOT_FOUND; + } +} diff --git a/src/QueueProducerInterface.php b/src/QueueProducerInterface.php new file mode 100644 index 00000000..21906bc2 --- /dev/null +++ b/src/QueueProducerInterface.php @@ -0,0 +1,20 @@ +getId(); if ($messageId === null) { $this->logger->info('Processing message without ID.'); @@ -69,12 +72,12 @@ public function process(MessageInterface $message, QueueInterface $queue): Messa throw new RuntimeException(sprintf('Queue handler for message type "%s" does not exist.', $messageType)); } - $request = new ConsumeRequest($message, $queue); + $request = new ConsumeRequest($message, $queueName); $closure = fn(MessageInterface $message): mixed => $this->injector->invoke($handler, [$message]); try { return $this->consumeMiddlewareDispatcher->dispatch($request, $this->createConsumeHandler($closure))->getMessage(); } catch (Throwable $exception) { - $request = new FailureHandlingRequest($request->getMessage(), $exception, $request->getQueue()); + $request = new FailureHandlingRequest($request->getMessage(), $exception, $request->getQueueName(), $retryProducer); try { $result = $this->failureMiddlewareDispatcher->dispatch($request, $this->createFailureHandler()); diff --git a/src/Worker/WorkerInterface.php b/src/Worker/WorkerInterface.php index 8c69ea82..08ca5849 100644 --- a/src/Worker/WorkerInterface.php +++ b/src/Worker/WorkerInterface.php @@ -5,9 +5,14 @@ namespace Yiisoft\Queue\Worker; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; interface WorkerInterface { - public function process(MessageInterface $message, QueueInterface $queue): MessageInterface; + /** @param string $queueName Logical execution queue name. */ + public function process( + MessageInterface $message, + string $queueName, + ?QueueProducerInterface $retryProducer = null, + ): MessageInterface; } diff --git a/stubs/StubQueue.php b/stubs/StubQueue.php deleted file mode 100644 index f34c6d11..00000000 --- a/stubs/StubQueue.php +++ /dev/null @@ -1,41 +0,0 @@ -name; - } -} diff --git a/stubs/StubQueueConsumer.php b/stubs/StubQueueConsumer.php new file mode 100644 index 00000000..164922fc --- /dev/null +++ b/stubs/StubQueueConsumer.php @@ -0,0 +1,17 @@ +name; + } +} diff --git a/stubs/StubWorker.php b/stubs/StubWorker.php index 7a939006..9e8bec68 100644 --- a/stubs/StubWorker.php +++ b/stubs/StubWorker.php @@ -5,7 +5,7 @@ namespace Yiisoft\Queue\Stubs; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Worker\WorkerInterface; /** @@ -13,8 +13,11 @@ */ final class StubWorker implements WorkerInterface { - public function process(MessageInterface $message, QueueInterface $queue): MessageInterface - { + public function process( + MessageInterface $message, + string $queueName, + ?QueueProducerInterface $retryProducer = null, + ): MessageInterface { return $message; } } diff --git a/tests/Benchmark/QueueBench.php b/tests/Benchmark/QueueBench.php index 873f773d..4803c019 100644 --- a/tests/Benchmark/QueueBench.php +++ b/tests/Benchmark/QueueBench.php @@ -21,15 +21,18 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactory; use Yiisoft\Queue\Middleware\Push\PushMiddlewareConfig; use Yiisoft\Queue\Middleware\Push\PushMiddlewareFactory; -use Yiisoft\Queue\Queue; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducer; +use Yiisoft\Queue\QueueConsumer; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Tests\Benchmark\Support\VoidAdapter; use Yiisoft\Queue\Worker\Worker; use Yiisoft\Test\Support\Container\SimpleContainer; final class QueueBench { - private readonly QueueInterface $queue; + private readonly QueueProducerInterface $producer; + private readonly QueueConsumerInterface $consumer; private readonly MessageSerializer $serializer; private readonly VoidAdapter $adapter; @@ -56,13 +59,12 @@ public function __construct() $this->serializer = new MessageSerializer(new JsonMessageEncoder()); $this->adapter = new VoidAdapter($this->serializer); - $this->queue = new Queue( - $worker, - new SimpleLoop(0), + $this->producer = new QueueProducer( $logger, new PushMiddlewareConfig(new PushMiddlewareFactory($container, $callableFactory)), $this->adapter, ); + $this->consumer = new QueueConsumer($worker, new SimpleLoop(0), $logger, $this->adapter); } public function providePush(): Generator @@ -82,7 +84,7 @@ public function providePush(): Generator #[ParamProviders('providePush')] public function benchPush(array $params): void { - $this->queue->push($params['message']); + $this->producer->push($params['message']); } public function provideConsume(): Generator @@ -105,6 +107,6 @@ public function provideConsume(): Generator public function benchConsume(array $params): void { $this->adapter->message = $params['message']; - $this->queue->run(); + $this->consumer->run(); } } diff --git a/tests/Integration/MessageConsumingTest.php b/tests/Integration/MessageConsumingTest.php index c2878dff..f928f6f4 100644 --- a/tests/Integration/MessageConsumingTest.php +++ b/tests/Integration/MessageConsumingTest.php @@ -45,8 +45,8 @@ public function testMessagesConsumed(): void $messages = [1, 'foo', 'bar-baz']; foreach ($messages as $message) { - $worker->process(new GenericMessage('test', $message), $this->getQueue()); - $worker->process(new GenericMessage('test2', $message), $this->getQueue()); + $worker->process(new GenericMessage('test', $message), 'yii-queue'); + $worker->process(new GenericMessage('test2', $message), 'yii-queue'); } $this->assertEquals($messages, $this->messagesProcessed); @@ -72,7 +72,7 @@ public function testMessagesConsumedByHandlerClass(): void $messages = [1, 'foo', 'bar-baz']; foreach ($messages as $message) { - $worker->process(new GenericMessage(TestHandler::class, $message), $this->getQueue()); + $worker->process(new GenericMessage(TestHandler::class, $message), 'yii-queue'); } $this->assertEquals($messages, $handler->messagesProcessed); diff --git a/tests/Integration/MiddlewareTest.php b/tests/Integration/MiddlewareTest.php index 5d49587e..4ee51af5 100644 --- a/tests/Integration/MiddlewareTest.php +++ b/tests/Integration/MiddlewareTest.php @@ -11,7 +11,6 @@ use Yiisoft\Injector\Injector; use Yiisoft\Test\Support\Container\SimpleContainer; use Yiisoft\Test\Support\Log\SimpleLogger; -use Yiisoft\Queue\Cli\LoopInterface; use Yiisoft\Queue\Message\GenericMessage; use Yiisoft\Queue\Message\MessageInterface; use Yiisoft\Queue\Middleware\CallableFactory; @@ -25,8 +24,8 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactory; use Yiisoft\Queue\Middleware\Push\PushMiddlewareConfig; use Yiisoft\Queue\Middleware\Push\PushMiddlewareFactory; -use Yiisoft\Queue\Queue; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducer; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Tests\Integration\Support\TestMiddleware; use Yiisoft\Queue\Worker\Worker; use Yiisoft\Queue\Worker\WorkerInterface; @@ -59,13 +58,12 @@ public function testFullStackPush(): void ); $worker = $this->createMock(WorkerInterface::class); $worker->method('process')->willReturnArgument(0); - $queue = new Queue( - $worker, - $this->createMock(LoopInterface::class), + $queue = new QueueProducer( $this->createMock(LoggerInterface::class), $pushMiddlewareConfig, null, 'test', + $worker, new TestMiddleware('channel 1'), new TestMiddleware('channel 2'), new TestMiddleware('channel 3'), @@ -115,7 +113,7 @@ public function testFullStackConsume(): void ); $message = new GenericMessage('test', ['initial']); - $messageConsumed = $worker->process($message, $this->createMock(QueueInterface::class)); + $messageConsumed = $worker->process($message, 'test-queue'); self::assertEquals($stack, $messageConsumed->getPayload()); } @@ -127,7 +125,7 @@ public function testFullStackFailure(): void $message = new GenericMessage('simple', null); $queueCallback = static fn(MessageInterface $message): MessageInterface => $message; - $queue = $this->createMock(QueueInterface::class); + $queue = $this->createMock(QueueProducerInterface::class); $container = new SimpleContainer([SendAgainMiddleware::class => new SendAgainMiddleware('test-container', 1, $queue)]); $callableFactory = new CallableFactory($container); @@ -135,7 +133,7 @@ public function testFullStackFailure(): void $queue->method('getName')->willReturn('simple'); $middlewares = [ - 'simple' => [ + 'test-queue' => [ new SendAgainMiddleware('test', 1, $queue), [ 'class' => SendAgainMiddleware::class, @@ -163,7 +161,7 @@ public function testFullStackFailure(): void ); $iteration = 0; - $request = new FailureHandlingRequest($message, $exception, $queue); + $request = new FailureHandlingRequest($message, $exception, 'test-queue', $queue); $finalHandler = new FailureFinalHandler(); try { do { diff --git a/tests/Integration/QueueProviderTest.php b/tests/Integration/QueueProviderTest.php new file mode 100644 index 00000000..c2af9af7 --- /dev/null +++ b/tests/Integration/QueueProviderTest.php @@ -0,0 +1,105 @@ + 'factory-both']); + $provider = new QueueFactoryProvider([ + 'both' => [ + 'producer' => [ + 'class' => StubQueueProducer::class, + '__construct()' => ['name' => Reference::to('producer-name')], + ], + 'consumer' => StubQueueConsumer::class, + ], + 'producer-only' => ['producer' => StubQueueProducer::class], + 'consumer-only' => ['consumer' => StubQueueConsumer::class], + ], $container); + + self::assertSame(['both', 'producer-only'], $provider->getProducerNames()); + self::assertSame(['both', 'consumer-only'], $provider->getConsumerNames()); + self::assertSame('factory-both', $provider->getProducer('both')->getName()); + self::assertInstanceOf(StubQueueConsumer::class, $provider->getConsumer('both')); + self::assertInstanceOf(StubQueueProducer::class, $provider->getProducer('producer-only')); + self::assertInstanceOf(StubQueueConsumer::class, $provider->getConsumer('consumer-only')); + self::assertFalse($provider->hasConsumer('producer-only')); + self::assertFalse($provider->hasProducer('consumer-only')); + + $this->expectException(QueueNotFoundException::class); + $provider->getConsumer('producer-only'); + } + + public function testPredefinedRoleMapsAndListenCommandUseConsumerOnlyService(): void + { + $consumer = $this->createMock(QueueConsumerInterface::class); + $consumer->expects(self::once())->method('listen'); + $provider = new PredefinedQueueProvider([ + 'both' => ['producer' => new StubQueueProducer('predefined-both'), 'consumer' => new StubQueueConsumer()], + 'producer-only' => ['producer' => new StubQueueProducer()], + 'consumer-only' => ['consumer' => $consumer], + ]); + + self::assertSame(['both', 'producer-only'], $provider->getProducerNames()); + self::assertSame(['both', 'consumer-only'], $provider->getConsumerNames()); + self::assertInstanceOf(QueueProducerInterface::class, $provider->getProducer('both')); + self::assertInstanceOf(QueueConsumerInterface::class, $provider->getConsumer('both')); + self::assertFalse($provider->hasConsumer('producer-only')); + self::assertFalse($provider->hasProducer('consumer-only')); + + self::assertSame(0, (new ListenCommand($provider))->run(new StringInput('consumer-only'), new NullOutput())); + + try { + $provider->getProducer('consumer-only'); + self::fail('A consumer-only queue must not expose a producer service.'); + } catch (QueueNotFoundException) { + self::addToAssertionCount(1); + } + } + + public function testDebugProxiesPreserveSeparatedProviderRoles(): void + { + $provider = new PredefinedQueueProvider([ + 'mixed-name' => ['producer' => new StubQueueProducer('mixed-name')], + 'consumer-only' => ['consumer' => new StubQueueConsumer()], + ]); + $collector = new QueueCollector(); + $collector->startup(); + + $producerProvider = new QueueProducerProviderProxy($provider, $collector); + $consumerProvider = new QueueConsumerProviderProxy($provider, $collector); + $producer = $producerProvider->getProducer('mixed-name'); + $consumer = $consumerProvider->getConsumer('consumer-only'); + $producer->push(new GenericMessage('test', 'payload')); + + self::assertInstanceOf(QueueProducerDecorator::class, $producer); + self::assertInstanceOf(QueueConsumerDecorator::class, $consumer); + self::assertSame(['mixed-name'], $producerProvider->getProducerNames()); + self::assertSame(['consumer-only'], $consumerProvider->getConsumerNames()); + self::assertSame(1, $collector->getSummary()['countPushes']); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 45be8e54..592e821d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,7 +10,7 @@ use Psr\Log\NullLogger; use RuntimeException; use Yiisoft\Injector\Injector; -use Yiisoft\Queue\Provider\QueueProviderInterface; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; use Yiisoft\Test\Support\Container\SimpleContainer; use Yiisoft\Queue\Adapter\AdapterInterface; use Yiisoft\Queue\Cli\LoopInterface; @@ -22,7 +22,7 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactory; use Yiisoft\Queue\Middleware\Push\PushMiddlewareConfig; use Yiisoft\Queue\Middleware\Push\PushMiddlewareFactory; -use Yiisoft\Queue\Queue; +use Yiisoft\Queue\QueueProducer; use Yiisoft\Queue\Worker\Worker; use Yiisoft\Queue\Worker\WorkerInterface; @@ -32,7 +32,7 @@ abstract class TestCase extends BaseTestCase { protected ?ContainerInterface $container = null; - protected ?Queue $queue = null; + protected ?QueueProducer $queue = null; protected ?LoopInterface $loop = null; protected ?WorkerInterface $worker = null; protected array $eventHandlers = []; @@ -51,9 +51,9 @@ protected function setUp(): void } /** - * @return Queue The same object every time + * @return QueueProducer The same object every time */ - protected function getQueue(): Queue + protected function getQueue(): QueueProducer { if ($this->queue === null) { $this->queue = $this->createQueue(); @@ -91,15 +91,14 @@ protected function getContainer(): ContainerInterface protected function createQueue( ?AdapterInterface $adapter = null, - string|BackedEnum $name = QueueProviderInterface::DEFAULT_QUEUE, - ): Queue { - return new Queue( - $this->getWorker(), - $this->getLoop(), + string|BackedEnum $name = QueueProducerProviderInterface::DEFAULT_QUEUE, + ): QueueProducer { + return new QueueProducer( new NullLogger(), $this->getPushMiddlewareConfig(), $adapter, $name, + $adapter === null ? $this->getWorker() : null, ); } diff --git a/tests/Unit/Command/ListenAllCommandTest.php b/tests/Unit/Command/ListenAllCommandTest.php index 24175f58..dd457fff 100644 --- a/tests/Unit/Command/ListenAllCommandTest.php +++ b/tests/Unit/Command/ListenAllCommandTest.php @@ -5,38 +5,39 @@ namespace Yiisoft\Queue\Tests\Unit\Command; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Yiisoft\Queue\Cli\LoopInterface; use Yiisoft\Queue\Command\ListenAllCommand; use Yiisoft\Queue\Provider\PredefinedQueueProvider; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\Stubs\StubQueueProducer; final class ListenAllCommandTest extends TestCase { - public function testExecute(): void + public function testReportsWhenNoConsumersAreConfigured(): void { - $queue1 = $this->createMock(QueueInterface::class); - $queue1->expects($this->once())->method('run'); - $queue2 = $this->createMock(QueueInterface::class); - $queue2->expects($this->once())->method('run'); + $command = new ListenAllCommand(new PredefinedQueueProvider([]), $this->createMock(LoopInterface::class)); + $output = new BufferedOutput(); - $queueFactory = new PredefinedQueueProvider([ - 'queue1' => $queue1, - 'queue2' => $queue2, - ]); + self::assertSame(Command::SUCCESS, $command->run(new ArrayInput([], $command->getNativeDefinition()), $output)); + self::assertSame("No consumers are configured.\n", $output->fetch()); + } + public function testRunsOnlyConsumerRolesByDefault(): void + { + $consumer = $this->createMock(QueueConsumerInterface::class); + $consumer->expects($this->once())->method('run')->willReturn(0); $loop = $this->createMock(LoopInterface::class); $loop->method('canContinue')->willReturn(true, false); - - $command = new ListenAllCommand( - $queueFactory, - $loop, - ); + $command = new ListenAllCommand(new PredefinedQueueProvider([ + 'producer' => ['producer' => new StubQueueProducer()], + 'consumer' => ['consumer' => $consumer], + ]), $loop); $input = new ArrayInput([], $command->getNativeDefinition()); $input->setOption('pause', 0); - $exitCode = $command->run($input, $this->createMock(OutputInterface::class)); - - $this->assertEquals(0, $exitCode); + self::assertSame(0, $command->run($input, $this->createMock(OutputInterface::class))); } } diff --git a/tests/Unit/Command/ListenCommandTest.php b/tests/Unit/Command/ListenCommandTest.php index 46dda71f..fedceca5 100644 --- a/tests/Unit/Command/ListenCommandTest.php +++ b/tests/Unit/Command/ListenCommandTest.php @@ -8,62 +8,16 @@ use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\OutputInterface; use Yiisoft\Queue\Command\ListenCommand; -use Yiisoft\Queue\Provider\QueueProviderInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\Provider\PredefinedQueueProvider; +use Yiisoft\Queue\QueueConsumerInterface; final class ListenCommandTest extends TestCase { - public function testExecuteWithDefaultQueue(): void + public function testListensSelectedConsumer(): void { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once()) - ->method('listen'); - - $queueProvider = $this->createMock(QueueProviderInterface::class); - $queueProvider->expects($this->once()) - ->method('get') - ->with($this->equalTo('yii-queue')) - ->willReturn($queue); - - $input = new StringInput(''); - $command = new ListenCommand($queueProvider); - $exitCode = $command->run($input, $this->createMock(OutputInterface::class)); - - $this->assertEquals(0, $exitCode); - } - - public function testExecuteWithCustomQueue(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once()) - ->method('listen'); - - $queueProvider = $this->createMock(QueueProviderInterface::class); - $queueProvider->expects($this->once()) - ->method('get') - ->with($this->equalTo('custom-queue')) - ->willReturn($queue); - - $input = new StringInput('custom-queue'); - $command = new ListenCommand($queueProvider); - $exitCode = $command->run($input, $this->createMock(OutputInterface::class)); - - $this->assertEquals(0, $exitCode); - } - - public function testExecuteReturnsZero(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once()) - ->method('listen'); - - $queueProvider = $this->createMock(QueueProviderInterface::class); - $queueProvider->method('get')->willReturn($queue); - - $input = new StringInput(''); - $command = new ListenCommand($queueProvider); - $exitCode = $command->run($input, $this->createMock(OutputInterface::class)); - - $this->assertSame(0, $exitCode); + $consumer = $this->createMock(QueueConsumerInterface::class); + $consumer->expects($this->once())->method('listen'); + $command = new ListenCommand(new PredefinedQueueProvider(['queue' => ['consumer' => $consumer]])); + self::assertSame(0, $command->run(new StringInput('queue'), $this->createMock(OutputInterface::class))); } } diff --git a/tests/Unit/Command/RunCommandTest.php b/tests/Unit/Command/RunCommandTest.php index 1ddc02dc..9c5005a3 100644 --- a/tests/Unit/Command/RunCommandTest.php +++ b/tests/Unit/Command/RunCommandTest.php @@ -9,121 +9,31 @@ use Symfony\Component\Console\Output\OutputInterface; use Yiisoft\Queue\Command\RunCommand; use Yiisoft\Queue\Provider\PredefinedQueueProvider; -use Yiisoft\Queue\Provider\QueueProviderInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\Stubs\StubQueueProducer; final class RunCommandTest extends TestCase { - public function testExecuteWithSingleQueue(): void + public function testRunsSelectedConsumer(): void { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once()) - ->method('run') - ->with($this->equalTo(0)) - ->willReturn(5); - - $queueProvider = new PredefinedQueueProvider([ - 'test-queue' => $queue, - ]); - - $input = new StringInput('test-queue'); - $output = $this->createMock(OutputInterface::class); - $output->expects($this->once()) - ->method('write') - ->with($this->equalTo('Processing queue test-queue... ')); - $output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Messages processed: 5.')); - - $command = new RunCommand($queueProvider); - $exitCode = $command->run($input, $output); - - $this->assertEquals(0, $exitCode); - } - - public function testExecuteWithMultipleQueues(): void - { - $queue1 = $this->createMock(QueueInterface::class); - $queue1->expects($this->once()) - ->method('run') - ->with($this->equalTo(0)) - ->willReturn(3); - - $queue2 = $this->createMock(QueueInterface::class); - $queue2->expects($this->once()) - ->method('run') - ->with($this->equalTo(0)) - ->willReturn(7); - - $queueProvider = new PredefinedQueueProvider([ - 'queue1' => $queue1, - 'queue2' => $queue2, - ]); - - $output = $this->createMock(OutputInterface::class); - $output->expects($this->exactly(2)) - ->method('write'); - $output->expects($this->exactly(2)) - ->method('writeln'); - - $input = new StringInput('queue1 queue2'); - $command = new RunCommand($queueProvider); - $exitCode = $command->run($input, $output); - - $this->assertEquals(0, $exitCode); - } - - public function testExecuteWithLimitOption(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once()) - ->method('run') - ->with($this->equalTo(100)) - ->willReturn(10); - - $queueProvider = new PredefinedQueueProvider([ - 'test-queue' => $queue, - ]); - - $input = new StringInput('test-queue --limit=100'); + $consumer = $this->createMock(QueueConsumerInterface::class); + $consumer->expects($this->once())->method('run')->with(5)->willReturn(3); + $command = new RunCommand(new PredefinedQueueProvider(['queue' => ['consumer' => $consumer]])); $output = $this->createMock(OutputInterface::class); - $output->expects($this->once()) - ->method('write') - ->with($this->equalTo('Processing queue test-queue... ')); - $output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Messages processed: 10.')); - - $command = new RunCommand($queueProvider); - $exitCode = $command->run($input, $output); - - $this->assertEquals(0, $exitCode); + $output->expects($this->once())->method('write')->with('Processing queue queue... '); + $output->expects($this->once())->method('writeln')->with('Messages processed: 3.'); + self::assertSame(0, $command->run(new StringInput('queue --limit=5'), $output)); } - public function testExecuteWithDefaultQueues(): void + public function testDefaultRunSkipsProducerOnlyQueues(): void { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once()) - ->method('run') - ->with($this->equalTo(0)) - ->willReturn(2); - - $queueProvider = new PredefinedQueueProvider([ - QueueProviderInterface::DEFAULT_QUEUE => $queue, - ]); - - $input = new StringInput(''); - $output = $this->createMock(OutputInterface::class); - $output->expects($this->once()) - ->method('write') - ->with($this->equalTo('Processing queue ' . QueueProviderInterface::DEFAULT_QUEUE . '... ')); - $output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Messages processed: 2.')); - - $command = new RunCommand($queueProvider); - $exitCode = $command->run($input, $output); - - $this->assertEquals(0, $exitCode); + $consumer = $this->createMock(QueueConsumerInterface::class); + $consumer->expects($this->once())->method('run')->willReturn(0); + $command = new RunCommand(new PredefinedQueueProvider([ + 'producer' => ['producer' => new StubQueueProducer()], + QueueConsumerProviderInterface::DEFAULT_QUEUE => ['consumer' => $consumer], + ])); + self::assertSame(0, $command->run(new StringInput(''), $this->createMock(OutputInterface::class))); } } diff --git a/tests/Unit/Debug/QueueCollectorTest.php b/tests/Unit/Debug/QueueCollectorTest.php index 4083cffc..97e8f4d3 100644 --- a/tests/Unit/Debug/QueueCollectorTest.php +++ b/tests/Unit/Debug/QueueCollectorTest.php @@ -9,7 +9,6 @@ use Yiisoft\Yii\Debug\Tests\Shared\AbstractCollectorTestCase; use Yiisoft\Queue\Debug\QueueCollector; use Yiisoft\Queue\Message\GenericMessage; -use Yiisoft\Queue\Stubs\StubQueue; final class QueueCollectorTest extends AbstractCollectorTestCase { @@ -32,15 +31,15 @@ protected function collectTestData(CollectorInterface $collector): void $collector->collectPush('chan2', $this->pushMessage, 'push.php:31'); $collector->collectWorkerProcessing( $this->pushMessage, - new StubQueue('chan1'), + 'chan1', ); $collector->collectWorkerProcessing( $this->pushMessage, - new StubQueue('chan1'), + 'chan1', ); $collector->collectWorkerProcessing( $this->pushMessage, - new StubQueue('chan2'), + 'chan2', ); } diff --git a/tests/Unit/Debug/QueueDecoratorTest.php b/tests/Unit/Debug/QueueDecoratorTest.php index 394f4cb7..c408726d 100644 --- a/tests/Unit/Debug/QueueDecoratorTest.php +++ b/tests/Unit/Debug/QueueDecoratorTest.php @@ -6,122 +6,37 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Debug\QueueCollector; -use Yiisoft\Queue\Debug\QueueDecorator; +use Yiisoft\Queue\Debug\QueueConsumerDecorator; +use Yiisoft\Queue\Debug\QueueProducerDecorator; +use Yiisoft\Queue\Message\GenericMessage; use Yiisoft\Queue\MessageStatus; -use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; final class QueueDecoratorTest extends TestCase { - public function testStatus(): void + public function testProducerDecoratorDelegatesAndCollects(): void { - $queue = $this->createMock(QueueInterface::class); - $messageStatus = MessageStatus::WAITING; - $queue->expects($this->once())->method('status')->willReturn($messageStatus); - $collector = new QueueCollector(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - - $result = $decorator->status(''); - $this->assertEquals($messageStatus, $result); - } - - public function testPush(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once())->method('push'); - $message = $this->createMock(MessageInterface::class); - $collector = new QueueCollector(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - - $decorator->push($message); - } - - public function testPushCollectsCallLocation(): void - { - $message = $this->createMock(MessageInterface::class); - $queue = $this->createMock(QueueInterface::class); - $queue->method('getName')->willReturn('test-queue'); - $queue->method('push')->willReturn($message); + $message = new GenericMessage('test', null); + $producer = $this->createMock(QueueProducerInterface::class); + $producer->method('getName')->willReturn('queue'); + $producer->expects($this->once())->method('push')->with($message)->willReturn($message); + $producer->expects($this->once())->method('status')->with('1')->willReturn(MessageStatus::WAITING); $collector = new QueueCollector(); $collector->startup(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - - $line = __LINE__ + 1; - $decorator->push($message); - - $collected = $collector->getCollected(); - $this->assertSame( - ['message' => $message, 'line' => __FILE__ . ':' . $line], - $collected['pushes']['test-queue'][0], - ); + $decorator = new QueueProducerDecorator($producer, $collector); + self::assertSame($message, $decorator->push($message)); + self::assertSame(MessageStatus::WAITING, $decorator->status('1')); + self::assertArrayHasKey('queue', $collector->getCollected()['pushes']); } - public function testStatusCollectsCallLocation(): void + public function testConsumerDecoratorDelegates(): void { - $queue = $this->createMock(QueueInterface::class); - $queue->method('status')->willReturn(MessageStatus::WAITING); - $collector = new QueueCollector(); - $collector->startup(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - - $line = __LINE__ + 1; - $decorator->status('42'); - - $collected = $collector->getCollected(); - $this->assertSame( - ['id' => '42', 'status' => MessageStatus::WAITING->key(), 'line' => __FILE__ . ':' . $line], - $collected['statuses'][0], - ); - } - - public function testRun(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once())->method('run'); - $collector = new QueueCollector(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - - $decorator->run(5); - } - - public function testListen(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once())->method('listen'); - $collector = new QueueCollector(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - + $consumer = $this->createMock(QueueConsumerInterface::class); + $consumer->expects($this->once())->method('run')->with(2)->willReturn(1); + $consumer->expects($this->once())->method('listen'); + $decorator = new QueueConsumerDecorator($consumer, new QueueCollector()); + self::assertSame(1, $decorator->run(2)); $decorator->listen(); } - - public function testGetName(): void - { - $queue = $this->createMock(QueueInterface::class); - $queue->expects($this->once())->method('getName')->willReturn('hello'); - $collector = new QueueCollector(); - $decorator = new QueueDecorator( - $queue, - $collector, - ); - - $this->assertEquals('hello', $decorator->getName()); - } } diff --git a/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php b/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php index 2ba2ae42..cf6325d3 100644 --- a/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php +++ b/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php @@ -6,41 +6,36 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Debug\QueueCollector; -use Yiisoft\Queue\Debug\QueueDecorator; -use Yiisoft\Queue\Debug\QueueProviderInterfaceProxy; -use Yiisoft\Queue\Provider\QueueProviderInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\Debug\QueueConsumerDecorator; +use Yiisoft\Queue\Debug\QueueConsumerProviderProxy; +use Yiisoft\Queue\Debug\QueueProducerProviderProxy; +use Yiisoft\Queue\Debug\QueueProducerDecorator; +use Yiisoft\Queue\Provider\QueueConsumerProviderInterface; +use Yiisoft\Queue\Provider\QueueProducerProviderInterface; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; final class QueueProviderInterfaceProxyTest extends TestCase { - public function testGet(): void + public function testProducerProxyDecoratesOnlyProducerRole(): void { - $queueFactory = $this->createMock(QueueProviderInterface::class); - $queue = $this->createMock(QueueInterface::class); - $queueFactory->expects($this->once())->method('get')->willReturn($queue); - $collector = new QueueCollector(); - $factory = new QueueProviderInterfaceProxy($queueFactory, $collector); - - $this->assertInstanceOf(QueueDecorator::class, $factory->get('test')); - } - - public function testHas(): void - { - $queueFactory = $this->createMock(QueueProviderInterface::class); - $queueFactory->expects($this->once())->method('has')->with('test')->willReturn(true); - $collector = new QueueCollector(); - $factory = new QueueProviderInterfaceProxy($queueFactory, $collector); - - $this->assertTrue($factory->has('test')); + $producer = $this->createMock(QueueProducerInterface::class); + $provider = $this->createMock(QueueProducerProviderInterface::class); + $provider->method('getProducer')->willReturn($producer); + $proxy = new QueueProducerProviderProxy($provider, new QueueCollector()); + self::assertInstanceOf(QueueProducerDecorator::class, $proxy->getProducer('queue')); } - public function testGetNames(): void + public function testConsumerProxyDelegatesOnlyConsumerRole(): void { - $queueFactory = $this->createMock(QueueProviderInterface::class); - $queueFactory->expects($this->once())->method('getNames')->willReturn(['queue1', 'queue2']); - $collector = new QueueCollector(); - $factory = new QueueProviderInterfaceProxy($queueFactory, $collector); - - $this->assertSame(['queue1', 'queue2'], $factory->getNames()); + $consumer = $this->createMock(QueueConsumerInterface::class); + $provider = $this->createMock(QueueConsumerProviderInterface::class); + $provider->method('getConsumer')->willReturn($consumer); + $provider->method('hasConsumer')->with('queue')->willReturn(true); + $provider->method('getConsumerNames')->willReturn(['queue']); + $proxy = new QueueConsumerProviderProxy($provider, new QueueCollector()); + self::assertInstanceOf(QueueConsumerDecorator::class, $proxy->getConsumer('queue')); + self::assertTrue($proxy->hasConsumer('queue')); + self::assertSame(['queue'], $proxy->getConsumerNames()); } } diff --git a/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php b/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php index 56d58564..51885015 100644 --- a/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php +++ b/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php @@ -8,7 +8,6 @@ use Yiisoft\Queue\Debug\QueueCollector; use Yiisoft\Queue\Debug\QueueWorkerInterfaceProxy; use Yiisoft\Queue\Message\GenericMessage; -use Yiisoft\Queue\Stubs\StubQueue; use Yiisoft\Queue\Stubs\StubWorker; final class QueueWorkerInterfaceProxyTest extends TestCase @@ -20,7 +19,7 @@ public function testProcessDelegatesToWorker(): void $collector->startup(); $proxy = new QueueWorkerInterfaceProxy(new StubWorker(), $collector); - $result = $proxy->process($message, new StubQueue('chan')); + $result = $proxy->process($message, 'chan'); self::assertSame($message, $result); diff --git a/tests/Unit/Middleware/Consume/ConsumeRequestTest.php b/tests/Unit/Middleware/Consume/ConsumeRequestTest.php index 06bdac0e..9ee6aae8 100644 --- a/tests/Unit/Middleware/Consume/ConsumeRequestTest.php +++ b/tests/Unit/Middleware/Consume/ConsumeRequestTest.php @@ -6,7 +6,7 @@ use Yiisoft\Queue\Message\GenericMessage; use Yiisoft\Queue\Middleware\Consume\ConsumeRequest; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Tests\TestCase; final class ConsumeRequestTest extends TestCase @@ -14,10 +14,10 @@ final class ConsumeRequestTest extends TestCase public function testImmutable(): void { $message = new GenericMessage('test', 'test'); - $queue = $this->createMock(QueueInterface::class); - $consumeRequest = new ConsumeRequest($message, $queue); + $queue = $this->createMock(QueueProducerInterface::class); + $consumeRequest = new ConsumeRequest($message, 'test-queue'); $this->assertNotSame($consumeRequest, $consumeRequest->withMessage($message)); - $this->assertNotSame($consumeRequest, $consumeRequest->withQueue($queue)); + $this->assertNotSame($consumeRequest, $consumeRequest->withQueueName('other-queue')); } } diff --git a/tests/Unit/Middleware/Consume/MiddlewareDispatcherTest.php b/tests/Unit/Middleware/Consume/MiddlewareDispatcherTest.php index 7a14fcee..e1b0ab71 100644 --- a/tests/Unit/Middleware/Consume/MiddlewareDispatcherTest.php +++ b/tests/Unit/Middleware/Consume/MiddlewareDispatcherTest.php @@ -15,7 +15,6 @@ use Yiisoft\Queue\Middleware\Consume\ConsumeRequest; use Yiisoft\Queue\Middleware\Consume\ConsumeHandlerInterface; use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareFactory; -use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Stubs\InMemoryAdapter; use Yiisoft\Queue\Tests\Unit\Middleware\Consume\Support\TestCallableMiddleware; use Yiisoft\Queue\Tests\Unit\Middleware\Consume\Support\TestMiddleware; @@ -25,12 +24,11 @@ final class MiddlewareDispatcherTest extends TestCase public function testCallableMiddlewareCalled(): void { $request = $this->getConsumeRequest(); - $queue = $this->createMock(QueueInterface::class); $dispatcher = $this->createDispatcher()->withMiddlewares( [ - static function (ConsumeRequest $request) use ($queue): ConsumeRequest { - return $request->withMessage(new GenericMessage('test', 'New closure test data'))->withQueue($queue); + static function (ConsumeRequest $request): ConsumeRequest { + return $request->withMessage(new GenericMessage('test', 'New closure test data'))->withQueueName('other-queue'); }, ], ); @@ -178,7 +176,7 @@ private function getConsumeRequest(): ConsumeRequest { return new ConsumeRequest( new GenericMessage('handler', 'data'), - $this->createMock(QueueInterface::class), + 'test-queue', ); } } diff --git a/tests/Unit/Middleware/Consume/MiddlewareFactoryTest.php b/tests/Unit/Middleware/Consume/MiddlewareFactoryTest.php index 11d8af51..ccad2084 100644 --- a/tests/Unit/Middleware/Consume/MiddlewareFactoryTest.php +++ b/tests/Unit/Middleware/Consume/MiddlewareFactoryTest.php @@ -17,7 +17,6 @@ use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareFactory; use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareFactoryInterface; use Yiisoft\Queue\Middleware\InvalidMiddlewareDefinitionException; -use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Stubs\InMemoryAdapter; use Yiisoft\Queue\Tests\Unit\Middleware\Consume\Support\CallableObjectMiddleware; use Yiisoft\Queue\Tests\Unit\Middleware\Consume\Support\InvalidController; @@ -62,7 +61,7 @@ public function testCreateFromClosureResponse(): void $middleware = $this->getMiddlewareFactory($container)->createConsumeMiddleware( fn(): ConsumeRequest => new ConsumeRequest( new GenericMessage('test', 'test data'), - $this->createMock(QueueInterface::class), + 'test-queue', ), ); self::assertSame( @@ -223,7 +222,7 @@ private function getConsumeRequest(): ConsumeRequest { return new ConsumeRequest( new GenericMessage('handler', 'data'), - $this->createMock(QueueInterface::class), + 'test-queue', ); } } diff --git a/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php b/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php index ca3a51e5..5d497272 100644 --- a/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php +++ b/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php @@ -7,20 +7,21 @@ use Exception; use Yiisoft\Queue\Message\GenericMessage; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlingRequest; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Tests\TestCase; final class FailureHandlingRequestTest extends TestCase { public function testImmutable(): void { - $queue = $this->createMock(QueueInterface::class); + $queue = $this->createMock(QueueProducerInterface::class); $request1 = new FailureHandlingRequest( new GenericMessage('test', null), new Exception('exception 1'), + 'test-queue', $queue, ); - $request2 = $request1->withQueue($queue); + $request2 = $request1->withQueueName('other-queue'); $request3 = $request1->withException(new Exception('exception 2')); $request4 = $request1->withMessage(new GenericMessage('test2', null)); diff --git a/tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php b/tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php index 14695389..25a122b9 100644 --- a/tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php +++ b/tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php @@ -12,7 +12,7 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlingRequest; use Yiisoft\Queue\Middleware\FailureHandling\Implementation\ExponentialDelayMiddleware; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlerInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Message\DelayEnvelope; use Yiisoft\Queue\Tests\TestCase; @@ -119,7 +119,7 @@ public static function constructorRequirementsProvider(): array #[DataProvider('constructorRequirementsProvider')] public function testConstructorRequirements(bool $success, array $arguments): void { - $arguments[] = $this->createMock(QueueInterface::class); + $arguments[] = $this->createMock(QueueProducerInterface::class); if (!$success) { $this->expectException(InvalidArgumentException::class); @@ -132,7 +132,7 @@ public function testConstructorRequirements(bool $success, array $arguments): vo public function testPipelineSuccess(): void { $message = new GenericMessage('test', null); - $queue = $this->createMock(QueueInterface::class); + $queue = $this->createMock(QueueProducerInterface::class); $queue->method('push')->willReturnArgument(0); $middleware = new ExponentialDelayMiddleware( 'test', @@ -144,7 +144,7 @@ public function testPipelineSuccess(): void ); $nextHandler = $this->createMock(FailureHandlerInterface::class); $nextHandler->expects(self::never())->method('handleFailure'); - $request = new FailureHandlingRequest($message, new Exception('test'), $queue); + $request = new FailureHandlingRequest($message, new Exception('test'), 'test-queue', $queue); $result = $middleware->processFailure($request, $nextHandler); self::assertNotEquals($request, $result); @@ -166,7 +166,7 @@ public function testPipelineFailure(): void 'test', null, ))->withMeta([FailureEnvelope::META_FAILURE => [ExponentialDelayMiddleware::META_KEY_ATTEMPTS . '-test' => 2]]); - $queue = $this->createMock(QueueInterface::class); + $queue = $this->createMock(QueueProducerInterface::class); $middleware = new ExponentialDelayMiddleware( 'test', 1, @@ -178,7 +178,7 @@ public function testPipelineFailure(): void $nextHandler = $this->createMock(FailureHandlerInterface::class); $exception = new Exception('test'); $nextHandler->expects(self::once())->method('handleFailure')->willThrowException($exception); - $request = new FailureHandlingRequest($message, $exception, $queue); + $request = new FailureHandlingRequest($message, $exception, 'test-queue', $queue); $middleware->processFailure($request, $nextHandler); } } diff --git a/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php b/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php index ffabed6b..519e0544 100644 --- a/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php +++ b/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php @@ -16,7 +16,7 @@ use Yiisoft\Queue\Middleware\FailureHandling\Implementation\SendAgainMiddleware; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlerInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareInterface; -use Yiisoft\Queue\QueueInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Tests\TestCase; final class SendAgainMiddlewareTest extends TestCase @@ -160,6 +160,7 @@ public function testQueueSendingStrategies( null, ))->withMeta([FailureEnvelope::META_FAILURE => $metaInitial]), new Exception('testException'), + 'test-queue', $queue, ); $result = $strategy->processFailure($request, $handler); @@ -167,7 +168,7 @@ public function testQueueSendingStrategies( self::assertInstanceOf(FailureHandlingRequest::class, $result); } - private function getStrategy(string $strategyName, QueueInterface $queue): FailureMiddlewareInterface + private function getStrategy(string $strategyName, QueueProducerInterface $queue): FailureMiddlewareInterface { return match ($strategyName) { SendAgainMiddleware::class => new SendAgainMiddleware('', 2, $queue), @@ -200,7 +201,7 @@ private function getHandler(array $metaResult, bool $suites): FailureHandlerInte return $handler; } - private function getPreparedQueue(array $metaResult, bool $suites): QueueInterface + private function getPreparedQueue(array $metaResult, bool $suites): QueueProducerInterface { $queueAssertion = static function (MessageInterface $message) use ($metaResult): MessageInterface { Assert::assertEquals($metaResult, $message->getMeta()[FailureEnvelope::META_FAILURE] ?? []); @@ -208,7 +209,7 @@ private function getPreparedQueue(array $metaResult, bool $suites): QueueInterfa return $message; }; - $queue = $this->createMock(QueueInterface::class); + $queue = $this->createMock(QueueProducerInterface::class); $queue->expects($suites ? self::once() : self::never()) ->method('push') ->willReturnCallback($queueAssertion); diff --git a/tests/Unit/Middleware/FailureHandling/MiddlewareDispatcherTest.php b/tests/Unit/Middleware/FailureHandling/MiddlewareDispatcherTest.php index 451214ed..26d3ebe4 100644 --- a/tests/Unit/Middleware/FailureHandling/MiddlewareDispatcherTest.php +++ b/tests/Unit/Middleware/FailureHandling/MiddlewareDispatcherTest.php @@ -15,7 +15,6 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareDispatcher; use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlerInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactory; -use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Stubs\InMemoryAdapter; use Yiisoft\Queue\Tests\Unit\Middleware\FailureHandling\Support\TestCallableMiddleware; use Yiisoft\Queue\Tests\Unit\Middleware\FailureHandling\Support\TestMiddleware; @@ -175,7 +174,7 @@ private function getFailureHandlingRequest(): FailureHandlingRequest return new FailureHandlingRequest( new GenericMessage('handler', 'data'), new Exception('Test exception.'), - $this->createMock(QueueInterface::class), + 'test-queue', ); } } diff --git a/tests/Unit/Middleware/FailureHandling/MiddlewareFactoryTest.php b/tests/Unit/Middleware/FailureHandling/MiddlewareFactoryTest.php index b228e182..d67b4813 100644 --- a/tests/Unit/Middleware/FailureHandling/MiddlewareFactoryTest.php +++ b/tests/Unit/Middleware/FailureHandling/MiddlewareFactoryTest.php @@ -19,7 +19,6 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactoryInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareInterface; use Yiisoft\Queue\Middleware\InvalidMiddlewareDefinitionException; -use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Stubs\InMemoryAdapter; use Yiisoft\Queue\Tests\Unit\Middleware\FailureHandling\Support\CallableObjectMiddleware; use Yiisoft\Queue\Tests\Unit\Middleware\FailureHandling\Support\InvalidController; @@ -57,7 +56,7 @@ function (): FailureHandlingRequest { return new FailureHandlingRequest( new GenericMessage('test', 'test data'), new RuntimeException('test exception'), - $this->createMock(QueueInterface::class), + 'test-queue', ); }, ); @@ -208,7 +207,7 @@ private function getConsumeRequest(): FailureHandlingRequest return new FailureHandlingRequest( new GenericMessage('handler', 'data'), new Exception('test exception'), - $this->createMock(QueueInterface::class), + 'test-queue', ); } } diff --git a/tests/Unit/Provider/CompositeQueueProviderTest.php b/tests/Unit/Provider/CompositeQueueProviderTest.php index 906c125e..d4526b95 100644 --- a/tests/Unit/Provider/CompositeQueueProviderTest.php +++ b/tests/Unit/Provider/CompositeQueueProviderTest.php @@ -4,82 +4,32 @@ namespace Yiisoft\Queue\Tests\Unit\Provider; +use PHPUnit\Framework\TestCase; +use Yiisoft\Queue\Provider\CompositeQueueProvider; use Yiisoft\Queue\Provider\PredefinedQueueProvider; use Yiisoft\Queue\Provider\QueueNotFoundException; -use Yiisoft\Queue\Provider\CompositeQueueProvider; -use Yiisoft\Queue\Stubs\StubQueue; -use Yiisoft\Queue\Tests\TestCase; +use Yiisoft\Queue\Stubs\StubQueueConsumer; +use Yiisoft\Queue\Stubs\StubQueueProducer; final class CompositeQueueProviderTest extends TestCase { - public function testBase(): void + public function testCombinesRolesAndPreservesPrecedence(): void { - $queue1 = new StubQueue(); - $queue2 = new StubQueue(); + $firstProducer = new StubQueueProducer('first'); $provider = new CompositeQueueProvider( - new PredefinedQueueProvider(['queue1' => $queue1]), - new PredefinedQueueProvider(['queue2' => $queue2]), + new PredefinedQueueProvider(['queue' => ['producer' => $firstProducer]]), + new PredefinedQueueProvider(['queue' => ['producer' => new StubQueueProducer('second'), 'consumer' => new StubQueueConsumer()]]), ); - - $this->assertTrue($provider->has('queue1')); - $this->assertTrue($provider->has('queue2')); - $this->assertFalse($provider->has('queue3')); - - $this->assertSame($queue1, $provider->get('queue1')); - $this->assertSame($queue2, $provider->get('queue2')); + self::assertSame($firstProducer, $provider->getProducer('queue')); + self::assertInstanceOf(StubQueueConsumer::class, $provider->getConsumer('queue')); + self::assertSame(['queue'], $provider->getProducerNames()); + self::assertSame(['queue'], $provider->getConsumerNames()); } - public function testNotFound(): void + public function testMissingCapabilityThrows(): void { - $provider = new CompositeQueueProvider( - new PredefinedQueueProvider([ - 'queue1' => new StubQueue(), - ]), - ); - + $provider = new CompositeQueueProvider(new PredefinedQueueProvider(['queue' => ['producer' => new StubQueueProducer()]])); $this->expectException(QueueNotFoundException::class); - $this->expectExceptionMessage('Queue with name "not-exists" not found.'); - $provider->get('not-exists'); - } - - public function testGetNames(): void - { - $provider = new CompositeQueueProvider( - new PredefinedQueueProvider([ - 'queue1' => new StubQueue(), - 'queue2' => new StubQueue(), - ]), - new PredefinedQueueProvider([ - 'queue3' => new StubQueue(), - ]), - ); - - $names = $provider->getNames(); - - $this->assertSame(['queue1', 'queue2', 'queue3'], $names); - } - - public function testGetNamesWithDuplicates(): void - { - $provider = new CompositeQueueProvider( - new PredefinedQueueProvider([ - 'queue1' => new StubQueue(), - ]), - new PredefinedQueueProvider([ - 'queue1' => new StubQueue(), - 'queue2' => new StubQueue(), - ]), - ); - - $names = $provider->getNames(); - - $this->assertSame(['queue1', 'queue2'], $names); - } - - public function testGetNamesEmpty(): void - { - $provider = new CompositeQueueProvider(); - - $this->assertSame([], $provider->getNames()); + $provider->getConsumer('queue'); } } diff --git a/tests/Unit/Provider/PredefinedQueueProviderTest.php b/tests/Unit/Provider/PredefinedQueueProviderTest.php index 83ff06cb..982abc27 100644 --- a/tests/Unit/Provider/PredefinedQueueProviderTest.php +++ b/tests/Unit/Provider/PredefinedQueueProviderTest.php @@ -6,105 +6,50 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Provider\InvalidQueueConfigException; -use Yiisoft\Queue\Provider\QueueNotFoundException; use Yiisoft\Queue\Provider\PredefinedQueueProvider; -use Yiisoft\Queue\QueueInterface; -use Yiisoft\Queue\Stubs\StubQueue; +use Yiisoft\Queue\Provider\QueueNotFoundException; +use Yiisoft\Queue\Stubs\StubQueueConsumer; +use Yiisoft\Queue\Stubs\StubQueueProducer; use Yiisoft\Queue\Tests\Unit\Support\StringEnum; -use stdClass; - -use function sprintf; - final class PredefinedQueueProviderTest extends TestCase { - public function testBase(): void + public function testProvidesIndependentRoles(): void { - $queue = new StubQueue(); - $provider = new PredefinedQueueProvider([ - 'queue1' => $queue, - ]); + $producer = new StubQueueProducer(); + $consumer = new StubQueueConsumer(); + $provider = new PredefinedQueueProvider(['queue1' => ['producer' => $producer, 'consumer' => $consumer]]); - $this->assertSame($queue, $provider->get('queue1')); - $this->assertTrue($provider->has('queue1')); - $this->assertFalse($provider->has('not-exist-queue')); + self::assertSame($producer, $provider->getProducer('queue1')); + self::assertSame($consumer, $provider->getConsumer('queue1')); + self::assertSame(['queue1'], $provider->getProducerNames()); + self::assertSame(['queue1'], $provider->getConsumerNames()); } - public function testGetTwice(): void + public function testCapabilityIsolationAndEnumNames(): void { - $queue = new StubQueue(); - $provider = new PredefinedQueueProvider([ - 'queue1' => $queue, - ]); - - $queue1 = $provider->get('queue1'); - $queue2 = $provider->get('queue1'); - - $this->assertSame($queue1, $queue2); - } - - public function testGetNotExistQueue(): void - { - $provider = new PredefinedQueueProvider([ - 'queue1' => new StubQueue(), - ]); - + $provider = new PredefinedQueueProvider(['red' => ['producer' => new StubQueueProducer()]]); + self::assertTrue($provider->hasProducer(StringEnum::RED)); + self::assertFalse($provider->hasConsumer(StringEnum::RED)); $this->expectException(QueueNotFoundException::class); - $this->expectExceptionMessage('Queue with name "not-exist-queue" not found.'); - $provider->get('not-exist-queue'); - } - - public function testInvalidQueueConfig(): void - { - $this->expectException(InvalidQueueConfigException::class); - $this->expectExceptionMessage( - sprintf( - 'Queue must implement "%s". For queue "%s" got "%s" instead.', - QueueInterface::class, - 'queue1', - 'stdClass', - ), - ); - - /** @psalm-suppress InvalidArgument */ - new PredefinedQueueProvider([ - 'queue1' => new stdClass(), - ]); + $provider->getConsumer(StringEnum::RED); } - public function testGetHasByStringEnum(): void + public function testRejectsFlatAndInvalidRoleMaps(): void { - $queue = new StubQueue(); - $provider = new PredefinedQueueProvider([ - 'red' => $queue, - ]); - - $this->assertSame($queue, $provider->get(StringEnum::RED)); - $this->assertTrue($provider->has(StringEnum::RED)); - $this->assertFalse($provider->has(StringEnum::GREEN)); - } - - public function testEmpty(): void - { - $provider = new PredefinedQueueProvider([]); - - $this->assertFalse($provider->has('any')); + foreach ([['queue' => new StubQueueProducer()], ['queue' => []], ['queue' => ['unknown' => new StubQueueProducer()]]] as $queues) { + try { + new PredefinedQueueProvider($queues); + self::fail('Invalid role maps must be rejected.'); + } catch (InvalidQueueConfigException) { + self::addToAssertionCount(1); + } + } } - public function testGetNames(): void + public function testRejectsWrongRoleInstance(): void { - $provider = new PredefinedQueueProvider([ - 'queue1' => new StubQueue(), - 'queue2' => new StubQueue(), - ]); - - $this->assertSame(['queue1', 'queue2'], $provider->getNames()); - } - - public function testGetNamesEmpty(): void - { - $provider = new PredefinedQueueProvider([]); - - $this->assertSame([], $provider->getNames()); + $this->expectException(InvalidQueueConfigException::class); + new PredefinedQueueProvider(['queue' => ['producer' => new StubQueueConsumer()]]); } } diff --git a/tests/Unit/Provider/QueueFactoryProviderTest.php b/tests/Unit/Provider/QueueFactoryProviderTest.php index 3e9aadcf..902c1053 100644 --- a/tests/Unit/Provider/QueueFactoryProviderTest.php +++ b/tests/Unit/Provider/QueueFactoryProviderTest.php @@ -5,169 +5,56 @@ namespace Yiisoft\Queue\Tests\Unit\Provider; use PHPUnit\Framework\TestCase; -use Yiisoft\Definitions\Reference; -use Yiisoft\Queue\Adapter\AdapterInterface; use Yiisoft\Queue\Provider\InvalidQueueConfigException; use Yiisoft\Queue\Provider\QueueFactoryProvider; use Yiisoft\Queue\Provider\QueueNotFoundException; -use Yiisoft\Queue\QueueInterface; -use Yiisoft\Queue\Stubs\InMemoryAdapter; use Yiisoft\Queue\Stubs\StubLoop; -use Yiisoft\Queue\Stubs\StubQueue; -use Yiisoft\Queue\Tests\Unit\Support\StringEnum; -use Yiisoft\Test\Support\Container\SimpleContainer; - -use function sprintf; +use Yiisoft\Queue\Stubs\StubQueueConsumer; +use Yiisoft\Queue\Stubs\StubQueueProducer; final class QueueFactoryProviderTest extends TestCase { - public function testBase(): void + public function testLazilyCreatesRolesIndependently(): void { - $provider = new QueueFactoryProvider( - [ - 'queue1' => StubQueue::class, - ], - ); - - $queue = $provider->get('queue1'); - - $this->assertInstanceOf(StubQueue::class, $queue); - $this->assertTrue($provider->has('queue1')); - $this->assertFalse($provider->has('not-exist-queue')); + $provider = new QueueFactoryProvider(['queue' => ['producer' => StubQueueProducer::class, 'consumer' => StubQueueConsumer::class]]); + self::assertInstanceOf(StubQueueProducer::class, $provider->getProducer('queue')); + self::assertSame($provider->getProducer('queue'), $provider->getProducer('queue')); + self::assertInstanceOf(StubQueueConsumer::class, $provider->getConsumer('queue')); + self::assertSame(['queue'], $provider->getProducerNames()); + self::assertSame(['queue'], $provider->getConsumerNames()); } - public function testGetTwice(): void + public function testCapabilityIsolation(): void { - $provider = new QueueFactoryProvider( - [ - 'queue1' => StubQueue::class, - ], - ); - - $queue1 = $provider->get('queue1'); - $queue2 = $provider->get('queue1'); - - $this->assertSame($queue1, $queue2); - } - - public function testGetNotExistQueue(): void - { - $provider = new QueueFactoryProvider( - [ - 'queue1' => StubQueue::class, - ], - ); - + $provider = new QueueFactoryProvider(['producer-only' => ['producer' => StubQueueProducer::class]]); + self::assertTrue($provider->hasProducer('producer-only')); + self::assertFalse($provider->hasConsumer('producer-only')); $this->expectException(QueueNotFoundException::class); - $this->expectExceptionMessage('Queue with name "not-exist-queue" not found.'); - $provider->get('not-exist-queue'); - } - - public function testInvalidQueueConfig(): void - { - $definitions = [ - 'queue1' => [ - 'class' => StubQueue::class, - '__construct()' => 'hello', - ], - ]; - - $this->expectException(InvalidQueueConfigException::class); - $this->expectExceptionMessage( - 'Invalid definition: incorrect constructor arguments. Expected array, got string.', - ); - new QueueFactoryProvider($definitions); - } - - public function testInvalidQueueConfigOnGet(): void - { - $provider = new QueueFactoryProvider( - [ - 'queue1' => StubLoop::class, - ], - ); - - $this->expectException(InvalidQueueConfigException::class); - $this->expectExceptionMessage( - sprintf( - 'Queue must implement "%s". For queue "%s" got "%s" instead.', - QueueInterface::class, - 'queue1', - StubLoop::class, - ), - ); - $provider->get('queue1'); + $provider->getConsumer('producer-only'); } - public function testGetHasByStringEnum(): void + public function testRejectsFlatEmptyAndUnknownRoleMaps(): void { - $provider = new QueueFactoryProvider( - [ - 'red' => StubQueue::class, - ], - ); - - $queue = $provider->get(StringEnum::RED); - - $this->assertInstanceOf(StubQueue::class, $queue); - $this->assertTrue($provider->has(StringEnum::RED)); - $this->assertFalse($provider->has(StringEnum::GREEN)); + foreach ([['queue' => StubQueueProducer::class], ['queue' => []], ['queue' => ['unknown' => StubQueueProducer::class]]] as $definitions) { + try { + new QueueFactoryProvider($definitions); + self::fail('Invalid role maps must be rejected.'); + } catch (InvalidQueueConfigException) { + self::addToAssertionCount(1); + } + } } - public function testWithContainer(): void + public function testRejectsWrongRoleOnResolutionAndCachesFailure(): void { - $container = new SimpleContainer([ - AdapterInterface::class => new InMemoryAdapter(), - ]); - - $provider = new QueueFactoryProvider( - [ - 'queue1' => [ - 'class' => StubQueue::class, - '__construct()' => [ - 'adapter' => Reference::to(AdapterInterface::class), - ], - ], - ], - $container, - ); - - $queue = $provider->get('queue1'); - - $this->assertInstanceOf(StubQueue::class, $queue); - } - - public function testValidateFalse(): void - { - $provider = new QueueFactoryProvider( - [ - 'queue1' => [ - 'class' => StubQueue::class, - '__construct()' => 'hello', - ], - ], - validate: false, - ); - - $this->assertTrue($provider->has('queue1')); - } - - public function testGetNames(): void - { - $provider = new QueueFactoryProvider( - [ - 'queue1' => StubQueue::class, - 'queue2' => StubQueue::class, - ], - ); - - $this->assertSame(['queue1', 'queue2'], $provider->getNames()); - } - - public function testGetNamesEmpty(): void - { - $provider = new QueueFactoryProvider([]); - - $this->assertSame([], $provider->getNames()); + $provider = new QueueFactoryProvider(['queue' => ['producer' => StubLoop::class]]); + foreach ([1, 2] as $_) { + try { + $provider->getProducer('queue'); + self::fail('Wrong role must be rejected.'); + } catch (InvalidQueueConfigException) { + self::addToAssertionCount(1); + } + } } } diff --git a/tests/Unit/QueueTest.php b/tests/Unit/QueueTest.php index 254c8076..cc0a5502 100644 --- a/tests/Unit/QueueTest.php +++ b/tests/Unit/QueueTest.php @@ -4,148 +4,82 @@ namespace Yiisoft\Queue\Tests\Unit; -use Yiisoft\Queue\Cli\SignalLoop; -use Yiisoft\Queue\Message\IdEnvelope; +use Psr\Log\NullLogger; +use Yiisoft\Queue\QueueConsumer; +use Yiisoft\Queue\QueueConsumerInterface; +use Yiisoft\Queue\QueueProducerInterface; use Yiisoft\Queue\Message\GenericMessage; +use Yiisoft\Queue\Message\IdEnvelope; use Yiisoft\Queue\MessageStatus; use Yiisoft\Queue\Stubs\InMemoryAdapter; use Yiisoft\Queue\Tests\TestCase; -use function extension_loaded; +use function count; -// Test enum for BackedEnum testing enum TestQueue: string { - case DEFAULT = 'default'; case HIGH_PRIORITY = 'high-priority'; } final class QueueTest extends TestCase { - public function testPushSuccessful(): void - { - $adapter = new InMemoryAdapter(); - $queue = $this->createQueue($adapter); - $message = new GenericMessage('simple', null); - $queue->push($message); - - self::assertSame([$message], $adapter->getMessagesList()); - } - - public function testPushSynchronouslyProcessesMessage(): void + public function testProducerContract(): void { $queue = $this->createQueue(); - $message = new GenericMessage('simple', null); - - $queue->push($message); - $queue->push(clone $message); - - self::assertSame(2, $this->executionTimes); + self::assertInstanceOf(QueueProducerInterface::class, $queue); + self::assertFalse(method_exists(QueueProducerInterface::class, 'run')); } - public function testRunWithoutAdapterReturnsZero(): void + public function testPushAndStatus(): void { - $queue = $this->createQueue(); - $message = new GenericMessage('simple', null); - $queue->push($message); - $queue->push(clone $message); - - self::assertSame(0, $queue->run()); - self::assertSame(2, $this->executionTimes); - } - - public function testListenWithoutAdapter(): void - { - $queue = $this->createQueue(); - - $queue->listen(); - - $this->expectNotToPerformAssertions(); + $adapter = new InMemoryAdapter(); + $queue = $this->createQueue($adapter); + $envelope = $queue->push(new GenericMessage('simple', null)); + self::assertSame(1, count($adapter->getMessagesList())); + /** @var int|string $id */ + $id = $envelope->getMeta()[IdEnvelope::META_ID]; + self::assertSame(MessageStatus::WAITING, $queue->status($id)); } - public function testStatusReturnsNotFoundWithoutAdapter(): void + public function testSynchronousProducerProcessesMessage(): void { $queue = $this->createQueue(); - + $queue->push(new GenericMessage('simple', null)); + self::assertSame(1, $this->executionTimes); self::assertSame(MessageStatus::NOT_FOUND, $queue->status('1')); } - public function testRunWithAdapter(): void - { - $queue = $this->createQueue(new InMemoryAdapter()); - $message = new GenericMessage('simple', null); - $queue->push($message); - $queue->push(clone $message); - - self::assertSame(2, $queue->run()); - self::assertSame(2, $this->executionTimes); - } - - public function testRunPartlyWithAdapter(): void + public function testConsumerContractAndRun(): void { - $queue = $this->createQueue(new InMemoryAdapter()); - $message = new GenericMessage('simple', null); - $queue->push($message); - $queue->push(clone $message); - - self::assertSame(1, $queue->run(1)); + $adapter = new InMemoryAdapter(); + $producer = $this->createQueue($adapter); + $producer->push(new GenericMessage('simple', null)); + $consumer = new QueueConsumer($this->getWorker(), $this->getLoop(), new NullLogger(), $adapter); + self::assertInstanceOf(QueueConsumerInterface::class, $consumer); + self::assertSame(1, $consumer->run()); self::assertSame(1, $this->executionTimes); } - public function testListenWithAdapter(): void + public function testSynchronousConsumerIsNoOp(): void { - $queue = $this->createQueue(new InMemoryAdapter()); - $message = new GenericMessage('simple', null); - $queue->push($message); - $queue->push(clone $message); - - $queue->listen(); - - self::assertSame(2, $this->executionTimes); + $consumer = new QueueConsumer($this->getWorker(), $this->getLoop(), new NullLogger()); + self::assertSame(0, $consumer->run()); + $consumer->listen(); } - public function testStatusWithAdapter(): void + public function testProducerNameSupportsEnum(): void { - $queue = $this->createQueue(new InMemoryAdapter()); - $envelope = $queue->push(new GenericMessage('simple', null)); - - self::assertArrayHasKey(IdEnvelope::META_ID, $envelope->getMeta()); - /** @var int|string $id */ - $id = $envelope->getMeta()[IdEnvelope::META_ID]; - - self::assertSame(MessageStatus::WAITING, $queue->status($id)); - - $queue->run(); - self::assertSame(MessageStatus::DONE, $queue->status($id)); + self::assertSame('high-priority', $this->createQueue(name: TestQueue::HIGH_PRIORITY)->getName()); } - public function testRunWithSignalLoop(): void + public function testConsumerStopsAtLimit(): void { - if (!extension_loaded('pcntl')) { - $this->markTestSkipped('This rest requires PCNTL extension'); - } - - $this->loop = new SignalLoop(); - $queue = $this->createQueue(); - $message = new GenericMessage('simple', null); - $queue->push($message); - $queue->push(clone $message); - - self::assertSame(0, $queue->run()); - self::assertSame(2, $this->executionTimes); - } - - public function testGetName(): void - { - $queue = $this->createQueue(name: 'test-queue'); - - $this->assertSame('test-queue', $queue->getName()); - } - - public function testGetNameWithBackedEnum(): void - { - $queue = $this->createQueue(name: TestQueue::HIGH_PRIORITY); - - $this->assertSame('high-priority', $queue->getName()); + $adapter = new InMemoryAdapter(); + $producer = $this->createQueue($adapter); + $producer->push(new GenericMessage('simple', null)); + $producer->push(new GenericMessage('simple', null)); + $consumer = new QueueConsumer($this->getWorker(), $this->getLoop(), new NullLogger(), $adapter); + self::assertSame(1, $consumer->run(1)); + self::assertSame(1, $this->executionTimes); } } diff --git a/tests/Unit/Stubs/StubQueueTest.php b/tests/Unit/Stubs/StubQueueTest.php index d52c6799..9200429d 100644 --- a/tests/Unit/Stubs/StubQueueTest.php +++ b/tests/Unit/Stubs/StubQueueTest.php @@ -7,18 +7,16 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\MessageStatus; use Yiisoft\Queue\Message\GenericMessage; -use Yiisoft\Queue\Stubs\StubQueue; +use Yiisoft\Queue\Stubs\StubQueueProducer; final class StubQueueTest extends TestCase { public function testBase(): void { - $queue = new StubQueue(); + $queue = new StubQueueProducer(); $message = new GenericMessage('test', 42); $this->assertSame($message, $queue->push($message)); - $this->assertSame(0, $queue->run()); $this->assertSame(MessageStatus::DONE, $queue->status('test')); - $queue->listen(); } } diff --git a/tests/Unit/Stubs/StubWorkerTest.php b/tests/Unit/Stubs/StubWorkerTest.php index 21d5c608..e10b6cd1 100644 --- a/tests/Unit/Stubs/StubWorkerTest.php +++ b/tests/Unit/Stubs/StubWorkerTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Message\GenericMessage; -use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Stubs\StubWorker; final class StubWorkerTest extends TestCase @@ -17,7 +16,7 @@ public function testBase(): void $sourceMessage = new GenericMessage('test', 42); - $message = $worker->process($sourceMessage, $this->createMock(QueueInterface::class)); + $message = $worker->process($sourceMessage, 'test-queue'); $this->assertSame($sourceMessage, $message); $this->assertSame('test', $message->getType()); diff --git a/tests/Unit/WorkerTest.php b/tests/Unit/WorkerTest.php index e08a527a..786f7577 100644 --- a/tests/Unit/WorkerTest.php +++ b/tests/Unit/WorkerTest.php @@ -23,7 +23,6 @@ use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareFactoryInterface; use Yiisoft\Queue\Middleware\CallableFactory; -use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Tests\App\FakeHandler; use Yiisoft\Queue\Tests\App\StaticMessageHandler; use Yiisoft\Queue\Tests\TestCase; @@ -40,8 +39,7 @@ public function testMessageHandled(mixed $handler, array $containerServices): vo $container = new SimpleContainer($containerServices); $handlers = ['simple' => $handler]; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container, $logger); $worker->process($message, $queue); @@ -95,8 +93,7 @@ public function testMessageFailWithDefinitionUndefinedMethodHandler(): void $container = new SimpleContainer([FakeHandler::class => $handler]); $handlers = ['simple' => [FakeHandler::class, 'undefinedMethod']]; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container); $worker->process($message, $queue); @@ -112,8 +109,7 @@ public function testMessageFailWithDefinitionUndefinedClassHandler(): void $container = new SimpleContainer([FakeHandler::class => $handler]); $handlers = ['simple' => ['UndefinedClass', 'handle']]; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container, $logger); $worker->process($message, $queue); @@ -126,8 +122,7 @@ public function testMessageFailWithDefinitionClassNotFoundInContainerHandler(): $container = new SimpleContainer(); $handlers = ['simple' => [FakeHandler::class, 'handle']]; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container); $worker->process($message, $queue); @@ -141,8 +136,7 @@ public function testMessageFailWithDefinitionHandlerException(): void $container = new SimpleContainer([FakeHandler::class => $handler]); $handlers = ['simple' => [FakeHandler::class, 'handleWithException']]; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container, $logger); try { @@ -167,8 +161,7 @@ public function testHandlerNotFoundInContainer(): void $container = new SimpleContainer(); $handlers = []; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container); $this->expectException(RuntimeException::class); @@ -186,8 +179,7 @@ public function handle(): void {} ]); $handlers = []; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container); $this->expectException(RuntimeException::class); @@ -198,9 +190,7 @@ public function handle(): void {} public function testMessageFailureIsHandledSuccessfully(): void { $message = new GenericMessage('simple', null); - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); - $queue->method('getName')->willReturn('test-queue'); + $queue = 'test-queue'; $originalException = new RuntimeException('Consume failed'); /** @var ConsumeMiddlewareInterface&MockObject $consumeMiddleware */ @@ -246,8 +236,7 @@ public function testStaticMethodHandler(): void 'static-handler' => StaticMessageHandler::handle(...), ]; - /** @var MockObject&QueueInterface $queue */ - $queue = $this->createMock(QueueInterface::class); + $queue = 'test-queue'; $worker = $this->createWorkerByParams($handlers, $container); StaticMessageHandler::$wasHandled = false;