From f12809e808a04ea4a1732737e9f3b36ef2895e5c Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 7 Jan 2025 16:19:32 +0200 Subject: [PATCH 01/32] SWR-16155 #comment Bandwidth Splitting --- README.md | 4 +- composer.json | 2 +- src/VhostsConsumer.php | 95 ++++++++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 72898252..de9abf6b 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 07 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 08 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.07 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.08 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 55307061..0d825408 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.07-dev" + "dev-master": "1.08-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumer.php b/src/VhostsConsumer.php index 911f35ff..08ee9068 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumer.php @@ -9,6 +9,10 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; +use PhpAmqpLib\Channel\AMQPChannel; +use PhpAmqpLib\Exception\AMQPChannelClosedException; +use PhpAmqpLib\Exception\AMQPConnectionClosedException; +use PhpAmqpLib\Exception\AMQPProtocolChannelException; use PhpAmqpLib\Exception\AMQPRuntimeException; use PhpAmqpLib\Message\AMQPMessage; use Salesmessage\LibRabbitMQ\Dto\ConnectionNameDto; @@ -114,13 +118,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; - /** @var RabbitMQQueue $connection */ - $connection = $this->manager->connection( - ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) - ); - $this->currentConnectionName = $connection->getConnectionName(); + $connection = $this->initConnection(); - $this->channel = $connection->getChannel(); $this->connectionMutex = new Mutex(false); $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); @@ -239,12 +238,7 @@ private function startConsuming() $jobsProcessed = 0; - /** @var RabbitMQQueue $connection */ - $connection = $this->manager->connection( - ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) - ); - $this->currentConnectionName = $connection->getConnectionName(); - $this->channel = $connection->getChannel(); + $connection = $this->initConnection(); $callback = function (AMQPMessage $message) use ($connection, &$jobsProcessed): void { $this->hasJob = true; @@ -278,21 +272,41 @@ private function startConsuming() } }; + $isSuccess = true; + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->basic_consume( - $this->currentQueueName, - $this->consumerTag, - false, - false, - false, - false, - $callback, - null, - $arguments - ); + try { + $this->channel->basic_consume( + $this->currentQueueName, + $this->consumerTag, + false, + false, + false, + false, + $callback, + null, + $arguments + ); + } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { + $isSuccess = false; + + $this->output->error(sprintf( + 'Start consuming. Vhost: "%s". Queue: "%s". Error: "%s". Code: %d', + $this->currentVhostName, + $this->currentQueueName, + $exception->getMessage(), + $exception->getCode() + )); + } + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); $this->updateLastProcessedAt(); + + if (false === $isSuccess) { + $this->goAheadOrWait(); + return $this->startConsuming(); + } } /** @@ -646,5 +660,40 @@ private function updateLastProcessedAt() ->setLastProcessedAt($timestamp); $this->internalStorageManager->updateVhostLastProcessedAt($vhostDto); } + + /** + * @return RabbitMQQueue + */ + private function initConnection() + { + $connection = $this->manager->connection( + ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) + ); + + try { + $channel = $connection->getChannel(); + } catch (AMQPConnectionClosedException $exception) { + $this->output->error(sprintf( + 'Init Connection Error: "%s". Vhost: "%s"', + $exception->getMessage(), + $this->currentVhostName + )); + + $vhostDto = new VhostApiDto([ + 'name' => $this->currentVhostName, + ]); + + $this->internalStorageManager->removeVhost($vhostDto); + $this->loadVhosts(); + $this->goAheadOrWait(); + + return $this->initConnection(); + } + + $this->currentConnectionName = $connection->getConnectionName(); + $this->channel = $channel; + + return $connection; + } } From 2357dc2796f84da5036a280ff104d0a1172219ce Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 7 Jan 2025 18:37:12 +0200 Subject: [PATCH 02/32] SWR-15367 #comment Bandwidth Splitting --- README.md | 11 +++++++---- composer.json | 2 +- src/Console/ConsumeVhostsCommand.php | 5 ++--- src/VhostsConsumer.php | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index de9abf6b..729c6018 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 08 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 09 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.08 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.09 --ignore-platform-reqs ``` The package will automatically register itself. @@ -42,6 +42,7 @@ groups: - test-queue-11 queues_mask: test batch_size: 100 + prefetch_count: 100 test-group-2: vhosts: - organization_20 @@ -53,6 +54,7 @@ groups: - test-queue-22 queues_mask: test batch_size: 100 + prefetch_count: 100 test-group-3: vhosts: - organization_30 @@ -63,7 +65,8 @@ groups: - test-queue-3 - test-queue-33 queues_mask: test - batch_size: 100, + batch_size: 100 + prefetch_count: 100 ``` ### Configuration @@ -628,7 +631,7 @@ There are two ways of consuming messages. Example: ```bash -php artisan lib-rabbitmq:consume-vhosts test-group-1 rabbitmq_vhosts --name=mq-vhosts-test-name --sleep=3 --memory=300 --max-jobs=5000 --max-time=600 --prefetch-count=100 --timeout=0 +php artisan lib-rabbitmq:consume-vhosts test-group-1 rabbitmq_vhosts --name=mq-vhosts-test-name --sleep=3 --memory=300 --max-jobs=5000 --max-time=600 --timeout=0 ``` ## Testing diff --git a/composer.json b/composer.json index 0d825408..4a83fe9d 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.08-dev" + "dev-master": "1.09-dev" }, "laravel": { "providers": [ diff --git a/src/Console/ConsumeVhostsCommand.php b/src/Console/ConsumeVhostsCommand.php index ebf7d311..187dff54 100644 --- a/src/Console/ConsumeVhostsCommand.php +++ b/src/Console/ConsumeVhostsCommand.php @@ -34,7 +34,6 @@ class ConsumeVhostsCommand extends WorkCommand {--max-priority=} {--consumer-tag} {--prefetch-size=0} - {--prefetch-count=1000} '; protected $description = 'Consume messages'; @@ -83,8 +82,8 @@ public function handle(): void $consumer->setConsumerTag($this->consumerTag()); $consumer->setMaxPriority((int) $this->option('max-priority')); $consumer->setPrefetchSize((int) $this->option('prefetch-size')); - $consumer->setPrefetchCount((int) $this->option('prefetch-count')); - $consumer->setBatchSize((int) ($groupConfigData['batch_size'] ?? 100)); + $consumer->setPrefetchCount((int) ($groupConfigData['prefetch_count'] ?? 1000)); + $consumer->setBatchSize((int) ($groupConfigData['batch_size'] ?? 1000)); if ($this->downForMaintenance() && $this->option('once')) { $consumer->sleep($this->option('sleep')); diff --git a/src/VhostsConsumer.php b/src/VhostsConsumer.php index 08ee9068..7dd1e290 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumer.php @@ -120,16 +120,6 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $connection = $this->initConnection(); - $this->connectionMutex = new Mutex(false); - - $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->basic_qos( - $this->prefetchSize, - $this->prefetchCount, - false - ); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); - $this->startConsuming(); while ($this->channel->is_consuming()) { @@ -693,6 +683,16 @@ private function initConnection() $this->currentConnectionName = $connection->getConnectionName(); $this->channel = $channel; + $this->connectionMutex = new Mutex(false); + + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); + $this->channel->basic_qos( + $this->prefetchSize, + $this->prefetchCount, + false + ); + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + return $connection; } } From 08c0bc49a896c4d53e856dc8107f18728e33c8be Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Wed, 8 Jan 2025 13:34:22 +0200 Subject: [PATCH 03/32] SWR-15367 #comment Bandwidth Splitting --- README.md | 4 +- composer.json | 2 +- src/LaravelLibRabbitMQServiceProvider.php | 2 + src/VhostsConsumer.php | 72 ++++++++++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 729c6018..a24e6504 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 09 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 10 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.09 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.10 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 4a83fe9d..505c67d9 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.09-dev" + "dev-master": "1.10-dev" }, "laravel": { "providers": [ diff --git a/src/LaravelLibRabbitMQServiceProvider.php b/src/LaravelLibRabbitMQServiceProvider.php index e7f7f779..378761b5 100644 --- a/src/LaravelLibRabbitMQServiceProvider.php +++ b/src/LaravelLibRabbitMQServiceProvider.php @@ -11,6 +11,7 @@ use Illuminate\Queue\Connectors\SyncConnector; use Illuminate\Queue\QueueManager; use Illuminate\Support\ServiceProvider; +use Psr\Log\LoggerInterface; use Salesmessage\LibRabbitMQ\Console\ConsumeCommand; use Salesmessage\LibRabbitMQ\Console\ConsumeVhostsCommand; use Salesmessage\LibRabbitMQ\Console\ScanVhostsCommand; @@ -60,6 +61,7 @@ public function register(): void return new VhostsConsumer( $this->app[InternalStorageManager::class], + $this->app[LoggerInterface::class], $this->app['queue'], $this->app['events'], $this->app[ExceptionHandler::class], diff --git a/src/VhostsConsumer.php b/src/VhostsConsumer.php index 7dd1e290..4e58f740 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumer.php @@ -15,6 +15,7 @@ use PhpAmqpLib\Exception\AMQPProtocolChannelException; use PhpAmqpLib\Exception\AMQPRuntimeException; use PhpAmqpLib\Message\AMQPMessage; +use Psr\Log\LoggerInterface; use Salesmessage\LibRabbitMQ\Dto\ConnectionNameDto; use Salesmessage\LibRabbitMQ\Dto\ConsumeVhostsFiltersDto; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; @@ -55,6 +56,7 @@ class VhostsConsumer extends Consumer /** * @param InternalStorageManager $internalStorageManager + * @param LoggerInterface $logger * @param QueueManager $manager * @param Dispatcher $events * @param ExceptionHandler $exceptions @@ -63,6 +65,7 @@ class VhostsConsumer extends Consumer */ public function __construct( private InternalStorageManager $internalStorageManager, + private LoggerInterface $logger, QueueManager $manager, Dispatcher $events, ExceptionHandler $exceptions, @@ -142,12 +145,27 @@ public function daemon($connectionName, $queue, WorkerOptions $options) } catch (AMQPRuntimeException $exception) { $this->output->error('Consuming AMQP Runtime exception. Error: ' . $exception->getMessage()); + $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.daemon.amqp_runtime_exception', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + $this->exceptions->report($exception); $this->kill(self::EXIT_ERROR, $this->workerOptions); } catch (Exception|Throwable $exception) { $this->output->error('Consuming exception. Error: ' . $exception->getMessage()); + $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.daemon.exception', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + $this->exceptions->report($exception); $this->stopWorkerIfLostConnection($exception); @@ -233,7 +251,8 @@ private function startConsuming() $callback = function (AMQPMessage $message) use ($connection, &$jobsProcessed): void { $this->hasJob = true; - if ($this->isSupportBatching($message)) { + $isSupportBatching = $this->isSupportBatching($message); + if ($isSupportBatching) { $this->addMessageToBatch($message); } else { $job = $this->getJobByMessage($message, $connection); @@ -249,6 +268,13 @@ private function startConsuming() $jobsProcessed )); + $this->logger->info('Salesmessage.LibRabbitMQ.VhostsConsumer.startConsuming.consume_message', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'num' => $jobsProcessed, + 'is_support_batching' => $isSupportBatching, + ]); + if ($jobsProcessed >= $this->batchSize) { $this->processBatch($connection); @@ -287,6 +313,14 @@ private function startConsuming() $exception->getMessage(), $exception->getCode() )); + + $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.startConsuming.exception', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); } $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); @@ -361,10 +395,26 @@ private function processBatch(RabbitMQQueue $connection): void $isBatchSuccess = true; $this->output->comment('Process batch jobs success. Job class: ' . $batchJobClass . 'Size: ' . $batchSize); + + $this->logger->info('Salesmessage.LibRabbitMQ.VhostsConsumer.processBatch.process_batch_jobs_success', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'batch_job_class' => $batchJobClass, + 'batch_size' => $batchSize, + ]); } catch (Throwable $exception) { $isBatchSuccess = false; $this->output->error('Process batch jobs error. Job class: ' . $batchJobClass . ' Error: ' . $exception->getMessage()); + + $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.processBatch.exception', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'batch_job_class' => $batchJobClass, + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); } unset($batchData); @@ -421,6 +471,11 @@ private function processSingleJob(RabbitMQJob $job): void $this->output->info('Process single job...'); + $this->logger->info('Salesmessage.LibRabbitMQ.VhostsConsumer.processSingleJob.success', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + ]); + if ($this->supportsAsyncSignals()) { $this->resetTimeoutHandler(); } @@ -437,6 +492,14 @@ private function ackMessage(AMQPMessage $message, bool $multiple = false): void $message->ack($multiple); } catch (Throwable $exception) { $this->output->error('Ack message error: ' . $exception->getMessage()); + + $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.ackMessage.exception', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); } } @@ -669,6 +732,13 @@ private function initConnection() $this->currentVhostName )); + $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.initConnection.exception', [ + 'vhost_name' => $this->currentVhostName, + 'queue_name' => $this->currentQueueName, + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + $vhostDto = new VhostApiDto([ 'name' => $this->currentVhostName, ]); From c5dcce8b37abfa2a0febc0bbd69840322247adfc Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Thu, 9 Jan 2025 11:31:23 +0200 Subject: [PATCH 04/32] SWR-15367 #comment Bandwidth Splitting --- src/Console/ScanVhostsCommand.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Console/ScanVhostsCommand.php b/src/Console/ScanVhostsCommand.php index dee26b94..b6200d86 100644 --- a/src/Console/ScanVhostsCommand.php +++ b/src/Console/ScanVhostsCommand.php @@ -11,7 +11,6 @@ use Salesmessage\LibRabbitMQ\Services\GroupsService; use Salesmessage\LibRabbitMQ\Services\QueueService; use Salesmessage\LibRabbitMQ\Services\VhostsService; -use Throwable; use Salesmessage\LibRabbitMQ\Services\InternalStorageManager; class ScanVhostsCommand extends Command @@ -21,7 +20,7 @@ class ScanVhostsCommand extends Command protected $description = 'Scan and index vhosts'; - private array $groups = ['test-group-1', 'test-group-2', 'test-group-3']; + private array $groups = []; /** * @param GroupsService $groupsService From f68fdb6bd9eabb0721c5df53f93647586b9f0cae Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Thu, 9 Jan 2025 14:21:17 +0200 Subject: [PATCH 05/32] SWR-15367 #comment Bandwidth Splitting --- README.md | 4 ++-- composer.json | 2 +- src/VhostsConsumer.php | 45 +++++++++++++++++++++++++++++------------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a24e6504..9556d2d6 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 10 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 11 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.10 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.11 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 505c67d9..d2e280d7 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-master": "1.11-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumer.php b/src/VhostsConsumer.php index 4e58f740..f1d10bb7 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumer.php @@ -110,6 +110,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) { $this->goAheadOrWait(); + $this->connectionMutex = new Mutex(false); + $this->configConnectionName = (string) $connectionName; $this->workerOptions = $options; @@ -121,9 +123,7 @@ public function daemon($connectionName, $queue, WorkerOptions $options) [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; - $connection = $this->initConnection(); - - $this->startConsuming(); + $connection = $this->startConsuming(); while ($this->channel->is_consuming()) { // Before reserving any jobs, we will make sure this queue is not paused and @@ -175,9 +175,10 @@ public function daemon($connectionName, $queue, WorkerOptions $options) if (false === $this->hasJob) { $this->output->info('Consuming sleep. No job...'); + $this->stopConsuming(); + $this->processBatch($connection); - $this->stopConsuming(); $this->goAheadOrWait(); $this->startConsuming(); @@ -231,7 +232,11 @@ protected function getStopStatus( }; } - private function startConsuming() + /** + * @return RabbitMQQueue + * @throws Exceptions\MutexTimeout + */ + private function startConsuming(): RabbitMQQueue { $this->output->info(sprintf( 'Start consuming. Vhost: "%s". Queue: "%s"', @@ -276,9 +281,10 @@ private function startConsuming() ]); if ($jobsProcessed >= $this->batchSize) { + $this->stopConsuming(); + $this->processBatch($connection); - $this->stopConsuming(); $this->goAheadOrWait(); $this->startConsuming(); } @@ -294,7 +300,7 @@ private function startConsuming() try { $this->channel->basic_consume( $this->currentQueueName, - $this->consumerTag, + $this->getTagName(), false, false, false, @@ -328,9 +334,13 @@ private function startConsuming() $this->updateLastProcessedAt(); if (false === $isSuccess) { + $this->stopConsuming(); + $this->goAheadOrWait(); return $this->startConsuming(); } + + return $connection; } /** @@ -507,10 +517,10 @@ private function ackMessage(AMQPMessage $message, bool $multiple = false): void * @return void * @throws Exceptions\MutexTimeout */ - private function stopConsuming() + private function stopConsuming(): void { $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->basic_cancel($this->consumerTag, true); + $this->channel->basic_cancel($this->getTagName(), true); $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } @@ -717,7 +727,7 @@ private function updateLastProcessedAt() /** * @return RabbitMQQueue */ - private function initConnection() + private function initConnection(): RabbitMQQueue { $connection = $this->manager->connection( ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) @@ -751,19 +761,26 @@ private function initConnection() } $this->currentConnectionName = $connection->getConnectionName(); - $this->channel = $channel; - - $this->connectionMutex = new Mutex(false); $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->basic_qos( + $channel->basic_qos( $this->prefetchSize, $this->prefetchCount, false ); $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + $this->channel = $channel; + return $connection; } + + /** + * @return string + */ + private function getTagName(): string + { + return $this->consumerTag . '_' . $this->currentVhostName; + } } From 74734dbcd1d5faccc13535e537633ea42c563127 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Thu, 9 Jan 2025 18:41:20 +0200 Subject: [PATCH 06/32] SWR-15367 #comment Bandwidth Splitting --- README.md | 4 ++-- composer.json | 2 +- src/Console/ScanVhostsCommand.php | 20 ++++++++++++-------- src/Services/InternalStorageManager.php | 4 ++-- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9556d2d6..9a08705d 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 11 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 12 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.11 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.12 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index d2e280d7..90e237af 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" }, "laravel": { "providers": [ diff --git a/src/Console/ScanVhostsCommand.php b/src/Console/ScanVhostsCommand.php index b6200d86..c17d2fb0 100644 --- a/src/Console/ScanVhostsCommand.php +++ b/src/Console/ScanVhostsCommand.php @@ -91,18 +91,20 @@ private function processVhost(array $vhostApiData): ?VhostApiDto $indexedSuccessfully = $this->internalStorageManager->indexVhost($vhostDto, $this->groups); if (!$indexedSuccessfully) { $this->warn(sprintf( - 'Skip indexation vhost: "%s". Messages ready: %d.', + 'Skip indexation vhost: "%s". Messages ready: %d. Messages unacknowledged: %d.', $vhostDto->getName(), - $vhostDto->getMessagesReady() + $vhostDto->getMessagesReady(), + $vhostDto->getMessagesUnacknowledged() )); return null; } $this->info(sprintf( - 'Successfully indexed vhost: "%s". Messages ready: %d.', + 'Successfully indexed vhost: "%s". Messages ready: %d. Messages unacknowledged: %d.', $vhostDto->getName(), - $vhostDto->getMessagesReady() + $vhostDto->getMessagesReady(), + $vhostDto->getMessagesUnacknowledged() )); $vhostQueues = $this->queueService->getAllVhostQueues($vhostDto); @@ -171,20 +173,22 @@ private function processVhostQueue(array $queueApiData): ?QueueApiDto $indexedSuccessfully = $this->internalStorageManager->indexQueue($queueApiDto, $this->groups); if (!$indexedSuccessfully) { $this->warn(sprintf( - 'Skip indexation queue: "%s". Vhost: %s. Messages ready: %d.', + 'Skip indexation queue: "%s". Vhost: %s. Messages ready: %d. Messages unacknowledged: %d.', $queueApiDto->getName(), $queueApiDto->getVhostName(), - $queueApiDto->getMessagesReady() + $queueApiDto->getMessagesReady(), + $queueApiDto->getMessagesUnacknowledged() )); return null; } $this->info(sprintf( - 'Successfully indexed queue: "%s". Vhost: %s. Messages ready: %d.', + 'Successfully indexed queue: "%s". Vhost: %s. Messages ready: %d. Messages unacknowledged: %d.', $queueApiDto->getName(), $queueApiDto->getVhostName(), - $queueApiDto->getMessagesReady() + $queueApiDto->getMessagesReady(), + $queueApiDto->getMessagesUnacknowledged() )); return $queueApiDto; diff --git a/src/Services/InternalStorageManager.php b/src/Services/InternalStorageManager.php index f903de13..9e0cd8a0 100644 --- a/src/Services/InternalStorageManager.php +++ b/src/Services/InternalStorageManager.php @@ -75,7 +75,7 @@ public function getVhostQueues(string $vhostName, string $by = 'name', bool $alp */ public function indexVhost(VhostApiDto $vhostDto, array $groups = []): bool { - if ($vhostDto->getMessagesReady() > 0) { + if (($vhostDto->getMessagesReady() > 0) || ($vhostDto->getMessagesUnacknowledged() > 0)) { return $this->addVhost($vhostDto, $groups); } @@ -185,7 +185,7 @@ private function getVhostStorageKeyPrefix(): string */ public function indexQueue(QueueApiDto $queueDto, array $groups): bool { - if ($queueDto->getMessagesReady() > 0) { + if (($queueDto->getMessagesReady() > 0) || ($queueDto->getMessagesUnacknowledged() > 0)) { return $this->addQueue($queueDto, $groups); } From be9fe63d48141753726ba76d2b4f203167e7fe0f Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Mon, 27 Jan 2025 17:43:13 +0200 Subject: [PATCH 07/32] SWR-16419 #comment RabbitMQ: Add Logging For Time Execution --- README.md | 4 +- composer.json | 2 +- src/VhostsConsumer.php | 182 ++++++++++++++++++++++++----------------- 3 files changed, 112 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 9a08705d..8b439ad9 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 12 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 14 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.12 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.14 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 90e237af..3944fa44 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.14-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumer.php b/src/VhostsConsumer.php index f1d10bb7..d2ccfc06 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumer.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; +use Illuminate\Support\Str; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Exception\AMQPChannelClosedException; use PhpAmqpLib\Exception\AMQPConnectionClosedException; @@ -54,6 +55,10 @@ class VhostsConsumer extends Consumer private array $batchMessages = []; + private ?string $processingUuid = null; + + private int|float $processingStartedAt = 0; + /** * @param InternalStorageManager $internalStorageManager * @param LoggerInterface $logger @@ -143,11 +148,7 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->channel->wait(null, true, (int) $this->workerOptions->timeout); $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } catch (AMQPRuntimeException $exception) { - $this->output->error('Consuming AMQP Runtime exception. Error: ' . $exception->getMessage()); - - $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.daemon.amqp_runtime_exception', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, + $this->logError('daemon.amqp_runtime_exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), ]); @@ -156,14 +157,10 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->kill(self::EXIT_ERROR, $this->workerOptions); } catch (Exception|Throwable $exception) { - $this->output->error('Consuming exception. Error: ' . $exception->getMessage()); - - $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.daemon.exception', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, - 'class' => get_class($exception), + $this->logError('daemon.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), ]); $this->exceptions->report($exception); @@ -196,7 +193,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->hasJob ); if (! is_null($status)) { - $this->output->info(['Consuming stop.', $status]); + $this->logInfo('consuming_stop', [ + 'status' => $status, + ]); return $this->stop($status, $this->workerOptions); } @@ -238,11 +237,10 @@ protected function getStopStatus( */ private function startConsuming(): RabbitMQQueue { - $this->output->info(sprintf( - 'Start consuming. Vhost: "%s". Queue: "%s"', - $this->currentVhostName, - $this->currentQueueName - )); + $this->processingUuid = $this->generateProcessingUuid(); + $this->processingStartedAt = microtime(true); + + $this->logInfo('startConsuming.init'); $arguments = []; if ($this->maxPriority) { @@ -266,17 +264,8 @@ private function startConsuming(): RabbitMQQueue $jobsProcessed++; - $this->output->info(sprintf( - 'Consume message. Vhost: "%s". Queue: "%s". Num: %s', - $this->currentVhostName, - $this->currentQueueName, - $jobsProcessed - )); - - $this->logger->info('Salesmessage.LibRabbitMQ.VhostsConsumer.startConsuming.consume_message', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, - 'num' => $jobsProcessed, + $this->logInfo('startConsuming.message_consumed', [ + 'processed_jobs_count' => $jobsProcessed, 'is_support_batching' => $isSupportBatching, ]); @@ -312,20 +301,10 @@ private function startConsuming(): RabbitMQQueue } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { $isSuccess = false; - $this->output->error(sprintf( - 'Start consuming. Vhost: "%s". Queue: "%s". Error: "%s". Code: %d', - $this->currentVhostName, - $this->currentQueueName, - $exception->getMessage(), - $exception->getCode() - )); - - $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.startConsuming.exception', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, - 'class' => get_class($exception), + $this->logError('startConsuming.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), ]); } @@ -343,6 +322,14 @@ private function startConsuming(): RabbitMQQueue return $connection; } + /** + * @return string + */ + private function generateProcessingUuid(): string + { + return sprintf('%s:%d:%s', $this->filtersDto->getGroup(), time(), Str::random(16)); + } + /** * @param AMQPMessage $message * @return string @@ -393,6 +380,8 @@ private function processBatch(RabbitMQQueue $connection): void $batchSize = count($batchJobMessages); if ($batchSize > 1) { + $batchTimeStarted = microtime(true); + $batchData = []; /** @var AMQPMessage $batchMessage */ foreach ($batchJobMessages as $batchMessage) { @@ -400,30 +389,28 @@ private function processBatch(RabbitMQQueue $connection): void $batchData[] = $job->getPayloadData(); } + $this->logInfo('processBatch.start', [ + 'batch_job_class' => $batchJobClass, + 'batch_size' => $batchSize, + ]); + try { $batchJobClass::collection($batchData); $isBatchSuccess = true; - $this->output->comment('Process batch jobs success. Job class: ' . $batchJobClass . 'Size: ' . $batchSize); - - $this->logger->info('Salesmessage.LibRabbitMQ.VhostsConsumer.processBatch.process_batch_jobs_success', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, + $this->logInfo('processBatch.finish', [ 'batch_job_class' => $batchJobClass, 'batch_size' => $batchSize, + 'executive_batch_time_seconds' => microtime(true) - $batchTimeStarted, ]); } catch (Throwable $exception) { $isBatchSuccess = false; - $this->output->error('Process batch jobs error. Job class: ' . $batchJobClass . ' Error: ' . $exception->getMessage()); - - $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.processBatch.exception', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, + $this->logError('processBatch.exception', [ 'batch_job_class' => $batchJobClass, - 'class' => get_class($exception), 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), ]); } @@ -472,6 +459,9 @@ private function getJobByMessage(AMQPMessage $message, RabbitMQQueue $connection */ private function processSingleJob(RabbitMQJob $job): void { + $timeStarted = microtime(true); + $this->logInfo('processSingleJob.start'); + if ($this->supportsAsyncSignals()) { $this->registerTimeoutHandler($job, $this->workerOptions); } @@ -479,16 +469,13 @@ private function processSingleJob(RabbitMQJob $job): void $this->runJob($job, $this->currentConnectionName, $this->workerOptions); $this->updateLastProcessedAt(); - $this->output->info('Process single job...'); - - $this->logger->info('Salesmessage.LibRabbitMQ.VhostsConsumer.processSingleJob.success', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, - ]); - if ($this->supportsAsyncSignals()) { $this->resetTimeoutHandler(); } + + $this->logInfo('processSingleJob.finish', [ + 'executive_job_time_seconds' => microtime(true) - $timeStarted, + ]); } /** @@ -501,14 +488,10 @@ private function ackMessage(AMQPMessage $message, bool $multiple = false): void try { $message->ack($multiple); } catch (Throwable $exception) { - $this->output->error('Ack message error: ' . $exception->getMessage()); - - $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.ackMessage.exception', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, - 'class' => get_class($exception), + $this->logError('ackMessage.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), ]); } } @@ -736,15 +719,7 @@ private function initConnection(): RabbitMQQueue try { $channel = $connection->getChannel(); } catch (AMQPConnectionClosedException $exception) { - $this->output->error(sprintf( - 'Init Connection Error: "%s". Vhost: "%s"', - $exception->getMessage(), - $this->currentVhostName - )); - - $this->logger->error('Salesmessage.LibRabbitMQ.VhostsConsumer.initConnection.exception', [ - 'vhost_name' => $this->currentVhostName, - 'queue_name' => $this->currentQueueName, + $this->logError('initConnection.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), ]); @@ -782,5 +757,66 @@ private function getTagName(): string { return $this->consumerTag . '_' . $this->currentVhostName; } + + /** + * @param string $message + * @param array $data + * @return void + */ + private function logInfo(string $message, array $data = []): void + { + $this->log($message, $data, false); + } + + /** + * @param string $message + * @param array $data + * @return void + */ + private function logError(string $message, array $data = []): void + { + $this->log($message, $data, true); + } + + /** + * @param string $message + * @param array $data + * @param bool $isError + * @return void + */ + private function log(string $message, array $data = [], bool $isError = false): void + { + $data['vhost_name'] = $this->currentVhostName; + $data['queue_name'] = $this->currentQueueName; + + $outputMessage = $message; + foreach ($data as $key => $value) { + if (in_array($key, ['trace', 'error_class'])) { + continue; + } + $outputMessage .= '. ' . ucfirst(str_replace('_', ' ', $key)) . ': ' . $value; + } + if ($isError) { + $this->output->error($outputMessage); + } else { + $this->output->info($outputMessage); + } + + $processingData = [ + 'uuid' => $this->processingUuid, + 'started_at' => $this->processingStartedAt, + ]; + if ($this->processingStartedAt) { + $processingData['executive_time_seconds'] = microtime(true) - $this->processingStartedAt; + } + $data['processing'] = $processingData; + + $logMessage = 'Salesmessage.LibRabbitMQ.VhostsConsumer.' . $message; + if ($isError) { + $this->logger->error($logMessage, $data); + } else { + $this->logger->info($logMessage, $data); + } + } } From d72cab9df03f1360c7ef9def27dfbf3d3bff602b Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 28 Jan 2025 14:22:10 +0200 Subject: [PATCH 08/32] SWR-16425 #comment RabbitMQ: Handle Broken Pipe Error --- README.md | 8 ++++---- composer.json | 2 +- src/VhostsConsumer.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8b439ad9..1362e988 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ RabbitMQ Queue driver for Laravel Only the latest version will get new features. Bug fixes will be provided using the following scheme: -| Package Version | Laravel Version | Bug Fixes Until | | -|-----------------|-----------------|---------------------|---------------------------------------------------------------------------------------------| -| 1 | 14 | December 26th, 2024 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| Package Version | Laravel Version | Bug Fixes Until | | +|-----------------|-----------------|--------------------|---------------------------------------------------------------------------------------------| +| 1 | 15 | January 28th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.14 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.15 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 3944fa44..cc8499bd 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.14-dev" + "dev-master": "1.15-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumer.php b/src/VhostsConsumer.php index d2ccfc06..08325715 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumer.php @@ -155,7 +155,7 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->exceptions->report($exception); - $this->kill(self::EXIT_ERROR, $this->workerOptions); + $this->kill(self::EXIT_SUCCESS, $this->workerOptions); } catch (Exception|Throwable $exception) { $this->logError('daemon.exception', [ 'message' => $exception->getMessage(), From 23623bf030579b51fc2f03a02483e1d2fd2674e0 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 4 Feb 2025 16:54:56 +0200 Subject: [PATCH 09/32] SWR-16426 #comment RabbitMQ: Change Worker Logic From Consuming To Pulling --- README.md | 5 +- composer.json | 2 +- src/Console/ConsumeVhostsCommand.php | 4 +- src/LaravelLibRabbitMQServiceProvider.php | 28 +- .../AbstractVhostsConsumer.php} | 281 +++++------------- src/VhostsConsumers/DirectConsumer.php | 136 +++++++++ src/VhostsConsumers/QueueConsumer.php | 220 ++++++++++++++ 7 files changed, 456 insertions(+), 220 deletions(-) rename src/{VhostsConsumer.php => VhostsConsumers/AbstractVhostsConsumer.php} (67%) create mode 100644 src/VhostsConsumers/DirectConsumer.php create mode 100644 src/VhostsConsumers/QueueConsumer.php diff --git a/README.md b/README.md index 1362e988..3493bd6f 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|--------------------|---------------------------------------------------------------------------------------------| -| 1 | 15 | January 28th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 16 | January 28th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.15 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.16 --ignore-platform-reqs ``` The package will automatically register itself. @@ -82,6 +82,7 @@ Add connection to `config/queue.php`: 'rabbitmq_vhosts' => [ 'driver' => 'rabbitmq_vhosts', + 'consumer_type' => env('RABBITMQ_VHOSTS_CONSUMER_TYPE', 'direct'), 'hosts' => [ [ 'host' => env('RABBITMQ_HOST', '127.0.0.1'), diff --git a/composer.json b/composer.json index cc8499bd..4abc03f3 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.16-dev" }, "laravel": { "providers": [ diff --git a/src/Console/ConsumeVhostsCommand.php b/src/Console/ConsumeVhostsCommand.php index 187dff54..d73a66c3 100644 --- a/src/Console/ConsumeVhostsCommand.php +++ b/src/Console/ConsumeVhostsCommand.php @@ -8,8 +8,8 @@ use Illuminate\Support\Str; use Salesmessage\LibRabbitMQ\Dto\ConsumeVhostsFiltersDto; use Salesmessage\LibRabbitMQ\Services\GroupsService; +use Salesmessage\LibRabbitMQ\VhostsConsumers\AbstractVhostsConsumer; use Symfony\Component\Console\Terminal; -use Salesmessage\LibRabbitMQ\VhostsConsumer; use Throwable; class ConsumeVhostsCommand extends WorkCommand @@ -70,7 +70,7 @@ public function handle(): void trim($groupConfigData['queues_mask'] ?? '') ); - /** @var VhostsConsumer $consumer */ + /** @var AbstractVhostsConsumer $consumer */ $consumer = $this->worker; $consumer->setFiltersDto($filtersDto); diff --git a/src/LaravelLibRabbitMQServiceProvider.php b/src/LaravelLibRabbitMQServiceProvider.php index 378761b5..7048f289 100644 --- a/src/LaravelLibRabbitMQServiceProvider.php +++ b/src/LaravelLibRabbitMQServiceProvider.php @@ -20,6 +20,8 @@ use Salesmessage\LibRabbitMQ\Services\InternalStorageManager; use Salesmessage\LibRabbitMQ\Services\QueueService; use Salesmessage\LibRabbitMQ\Services\VhostsService; +use Salesmessage\LibRabbitMQ\VhostsConsumers\DirectConsumer as VhostsDirectConsumer; +use Salesmessage\LibRabbitMQ\VhostsConsumers\QueueConsumer as VhostsQueueConsumer; class LaravelLibRabbitMQServiceProvider extends ServiceProvider { @@ -54,12 +56,28 @@ public function register(): void ); }); - $this->app->singleton(VhostsConsumer::class, function () { + $this->app->singleton(VhostsDirectConsumer::class, function () { $isDownForMaintenance = function () { return $this->app->isDownForMaintenance(); }; - return new VhostsConsumer( + return new VhostsDirectConsumer( + $this->app[InternalStorageManager::class], + $this->app[LoggerInterface::class], + $this->app['queue'], + $this->app['events'], + $this->app[ExceptionHandler::class], + $isDownForMaintenance, + null + ); + }); + + $this->app->singleton(VhostsQueueConsumer::class, function () { + $isDownForMaintenance = function () { + return $this->app->isDownForMaintenance(); + }; + + return new VhostsQueueConsumer( $this->app[InternalStorageManager::class], $this->app[LoggerInterface::class], $this->app['queue'], @@ -71,9 +89,13 @@ public function register(): void }); $this->app->singleton(ConsumeVhostsCommand::class, static function ($app) { + $consumerClass = ('direct' === config('queue.connections.rabbitmq_vhosts.consumer_type')) + ? VhostsDirectConsumer::class + : VhostsQueueConsumer::class; + return new ConsumeVhostsCommand( $app[GroupsService::class], - $app[VhostsConsumer::class], + $app[$consumerClass], $app['cache.store'] ); }); diff --git a/src/VhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php similarity index 67% rename from src/VhostsConsumer.php rename to src/VhostsConsumers/AbstractVhostsConsumer.php index 08325715..24e71508 100644 --- a/src/VhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -1,63 +1,61 @@ listenForSignals(); } - $lastRestart = $this->getTimestampOfLastQueueRestart(); - - [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; - - $connection = $this->startConsuming(); - - while ($this->channel->is_consuming()) { - // Before reserving any jobs, we will make sure this queue is not paused and - // if it is we will just pause this worker for a given amount of time and - // make sure we do not need to kill this worker process off completely. - if (! $this->daemonShouldRun($this->workerOptions, $this->configConnectionName, $this->currentQueueName)) { - $this->output->info('Consuming pause worker...'); - - $this->pauseWorker($this->workerOptions, $lastRestart); - - continue; - } - - // If the daemon should run (not in maintenance mode, etc.), then we can wait for a job. - try { - $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->wait(null, true, (int) $this->workerOptions->timeout); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); - } catch (AMQPRuntimeException $exception) { - $this->logError('daemon.amqp_runtime_exception', [ - 'message' => $exception->getMessage(), - 'trace' => $exception->getTraceAsString(), - ]); - - $this->exceptions->report($exception); - - $this->kill(self::EXIT_SUCCESS, $this->workerOptions); - } catch (Exception|Throwable $exception) { - $this->logError('daemon.exception', [ - 'message' => $exception->getMessage(), - 'trace' => $exception->getTraceAsString(), - 'error_class' => get_class($exception), - ]); - - $this->exceptions->report($exception); - - $this->stopWorkerIfLostConnection($exception); - } - - // If no job is got off the queue, we will need to sleep the worker. - if (false === $this->hasJob) { - $this->output->info('Consuming sleep. No job...'); - - $this->stopConsuming(); - - $this->processBatch($connection); - - $this->goAheadOrWait(); - $this->startConsuming(); - - $this->sleep($this->workerOptions->sleep); - } - - // Finally, we will check to see if we have exceeded our memory limits or if - // the queue should restart based on other indications. If so, we'll stop - // this worker and let whatever is "monitoring" it restart the process. - $status = $this->getStopStatus( - $this->workerOptions, - $lastRestart, - $startTime, - $jobsProcessed, - $this->hasJob - ); - if (! is_null($status)) { - $this->logInfo('consuming_stop', [ - 'status' => $status, - ]); - - return $this->stop($status, $this->workerOptions); - } - - $this->hasJob = false; - } + $this->vhostDaemon($connectionName, $options); } + + abstract protected function vhostDaemon($connectionName, WorkerOptions $options); /** * @param WorkerOptions $options @@ -235,97 +158,35 @@ protected function getStopStatus( * @return RabbitMQQueue * @throws Exceptions\MutexTimeout */ - private function startConsuming(): RabbitMQQueue - { - $this->processingUuid = $this->generateProcessingUuid(); - $this->processingStartedAt = microtime(true); - - $this->logInfo('startConsuming.init'); - - $arguments = []; - if ($this->maxPriority) { - $arguments['priority'] = ['I', $this->maxPriority]; - } - - $jobsProcessed = 0; - - $connection = $this->initConnection(); - - $callback = function (AMQPMessage $message) use ($connection, &$jobsProcessed): void { - $this->hasJob = true; - - $isSupportBatching = $this->isSupportBatching($message); - if ($isSupportBatching) { - $this->addMessageToBatch($message); - } else { - $job = $this->getJobByMessage($message, $connection); - $this->processSingleJob($job); - } + abstract protected function startConsuming(): RabbitMQQueue; - $jobsProcessed++; - - $this->logInfo('startConsuming.message_consumed', [ - 'processed_jobs_count' => $jobsProcessed, - 'is_support_batching' => $isSupportBatching, - ]); - - if ($jobsProcessed >= $this->batchSize) { - $this->stopConsuming(); - - $this->processBatch($connection); - - $this->goAheadOrWait(); - $this->startConsuming(); - } - - if ($this->workerOptions->rest > 0) { - $this->sleep($this->workerOptions->rest); - } - }; - - $isSuccess = true; - - $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - try { - $this->channel->basic_consume( - $this->currentQueueName, - $this->getTagName(), - false, - false, - false, - false, - $callback, - null, - $arguments - ); - } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { - $isSuccess = false; - - $this->logError('startConsuming.exception', [ - 'message' => $exception->getMessage(), - 'trace' => $exception->getTraceAsString(), - 'error_class' => get_class($exception), - ]); + /** + * @param AMQPMessage $message + * @param RabbitMQQueue $connection + * @return void + */ + protected function processAmqpMessage(AMQPMessage $message, RabbitMQQueue $connection): void + { + $isSupportBatching = $this->isSupportBatching($message); + if ($isSupportBatching) { + $this->addMessageToBatch($message); + } else { + $job = $this->getJobByMessage($message, $connection); + $this->processSingleJob($job); } - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); - - $this->updateLastProcessedAt(); - - if (false === $isSuccess) { - $this->stopConsuming(); - - $this->goAheadOrWait(); - return $this->startConsuming(); - } + $this->jobsProcessed++; - return $connection; + $this->logInfo('processAMQPMessage.message_consumed', [ + 'processed_jobs_count' => $this->jobsProcessed, + 'is_support_batching' => $isSupportBatching, + ]); } /** * @return string */ - private function generateProcessingUuid(): string + protected function generateProcessingUuid(): string { return sprintf('%s:%d:%s', $this->filtersDto->getGroup(), time(), Str::random(16)); } @@ -334,7 +195,7 @@ private function generateProcessingUuid(): string * @param AMQPMessage $message * @return string */ - private function getMessageClass(AMQPMessage $message): string + protected function getMessageClass(AMQPMessage $message): string { $body = json_decode($message->getBody(), true); @@ -345,7 +206,7 @@ private function getMessageClass(AMQPMessage $message): string * @param RabbitMQJob $job * @return void */ - private function isSupportBatching(AMQPMessage $message): bool + protected function isSupportBatching(AMQPMessage $message): bool { $class = $this->getMessageClass($message); @@ -358,7 +219,7 @@ private function isSupportBatching(AMQPMessage $message): bool * @param AMQPMessage $message * @return void */ - private function addMessageToBatch(AMQPMessage $message): void + protected function addMessageToBatch(AMQPMessage $message): void { $this->batchMessages[$this->getMessageClass($message)][] = $message; } @@ -369,7 +230,7 @@ private function addMessageToBatch(AMQPMessage $message): void * @throws Exceptions\MutexTimeout * @throws Throwable */ - private function processBatch(RabbitMQQueue $connection): void + protected function processBatch(RabbitMQQueue $connection): void { if (empty($this->batchMessages)) { return; @@ -440,7 +301,7 @@ private function processBatch(RabbitMQQueue $connection): void * @return RabbitMQJob * @throws Throwable */ - private function getJobByMessage(AMQPMessage $message, RabbitMQQueue $connection): RabbitMQJob + protected function getJobByMessage(AMQPMessage $message, RabbitMQQueue $connection): RabbitMQJob { $jobClass = $connection->getJobClass(); @@ -457,7 +318,7 @@ private function getJobByMessage(AMQPMessage $message, RabbitMQQueue $connection * @param RabbitMQJob $job * @return void */ - private function processSingleJob(RabbitMQJob $job): void + protected function processSingleJob(RabbitMQJob $job): void { $timeStarted = microtime(true); $this->logInfo('processSingleJob.start'); @@ -483,7 +344,7 @@ private function processSingleJob(RabbitMQJob $job): void * @param bool $multiple * @return void */ - private function ackMessage(AMQPMessage $message, bool $multiple = false): void + protected function ackMessage(AMQPMessage $message, bool $multiple = false): void { try { $message->ack($multiple); @@ -500,17 +361,12 @@ private function ackMessage(AMQPMessage $message, bool $multiple = false): void * @return void * @throws Exceptions\MutexTimeout */ - private function stopConsuming(): void - { - $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->basic_cancel($this->getTagName(), true); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); - } + abstract protected function stopConsuming(): void; /** * @return void */ - private function loadVhosts(): void + protected function loadVhosts(): void { $group = $this->filtersDto->getGroup(); $lastProcessedAtKey = $this->internalStorageManager->getLastProcessedAtKeyName($group); @@ -539,7 +395,7 @@ private function loadVhosts(): void /** * @return bool */ - private function switchToNextVhost(): bool + protected function switchToNextVhost(): bool { $nextVhost = $this->getNextVhost(); if (null === $nextVhost) { @@ -564,7 +420,7 @@ private function switchToNextVhost(): bool /** * @return string|null */ - private function getNextVhost(): ?string + protected function getNextVhost(): ?string { if (null === $this->currentVhostName) { return !empty($this->vhosts) ? (string) reset($this->vhosts) : null; @@ -581,7 +437,7 @@ private function getNextVhost(): ?string /** * @return void */ - private function loadVhostQueues(): void + protected function loadVhostQueues(): void { $group = $this->filtersDto->getGroup(); $lastProcessedAtKey = $this->internalStorageManager->getLastProcessedAtKeyName($group); @@ -610,7 +466,7 @@ private function loadVhostQueues(): void /** * @return bool */ - private function switchToNextQueue(): bool + protected function switchToNextQueue(): bool { $nextQueue = $this->getNextQueue(); if (null === $nextQueue) { @@ -625,7 +481,7 @@ private function switchToNextQueue(): bool /** * @return string|null */ - private function getNextQueue(): ?string + protected function getNextQueue(): ?string { if (null === $this->currentQueueName) { return !empty($this->vhostQueues) ? (string) reset($this->vhostQueues) : null; @@ -643,7 +499,7 @@ private function getNextQueue(): ?string * @param int $waitSeconds * @return bool */ - private function goAheadOrWait(int $waitSeconds = 1): bool + protected function goAheadOrWait(int $waitSeconds = 1): bool { if (false === $this->goAhead()) { $this->loadVhosts(); @@ -664,7 +520,7 @@ private function goAheadOrWait(int $waitSeconds = 1): bool /** * @return bool */ - private function goAhead(): bool + protected function goAhead(): bool { if ($this->switchToNextQueue()) { return true; @@ -680,7 +536,7 @@ private function goAhead(): bool /** * @return void */ - private function updateLastProcessedAt() + protected function updateLastProcessedAt() { if ((null === $this->currentVhostName) || (null === $this->currentQueueName)) { return; @@ -710,7 +566,7 @@ private function updateLastProcessedAt() /** * @return RabbitMQQueue */ - private function initConnection(): RabbitMQQueue + protected function initConnection(): RabbitMQQueue { $connection = $this->manager->connection( ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) @@ -753,7 +609,7 @@ private function initConnection(): RabbitMQQueue /** * @return string */ - private function getTagName(): string + protected function getTagName(): string { return $this->consumerTag . '_' . $this->currentVhostName; } @@ -763,7 +619,7 @@ private function getTagName(): string * @param array $data * @return void */ - private function logInfo(string $message, array $data = []): void + protected function logInfo(string $message, array $data = []): void { $this->log($message, $data, false); } @@ -773,7 +629,7 @@ private function logInfo(string $message, array $data = []): void * @param array $data * @return void */ - private function logError(string $message, array $data = []): void + protected function logError(string $message, array $data = []): void { $this->log($message, $data, true); } @@ -784,7 +640,7 @@ private function logError(string $message, array $data = []): void * @param bool $isError * @return void */ - private function log(string $message, array $data = [], bool $isError = false): void + protected function log(string $message, array $data = [], bool $isError = false): void { $data['vhost_name'] = $this->currentVhostName; $data['queue_name'] = $this->currentQueueName; @@ -811,12 +667,13 @@ private function log(string $message, array $data = [], bool $isError = false): } $data['processing'] = $processingData; - $logMessage = 'Salesmessage.LibRabbitMQ.VhostsConsumer.' . $message; + $logMessage = 'Salesmessage.LibRabbitMQ.VhostsConsumers.'; + $logMessage .= class_basename(static::class) . '.'; + $logMessage .= $message; if ($isError) { $this->logger->error($logMessage, $data); } else { $this->logger->info($logMessage, $data); } } -} - +} \ No newline at end of file diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php new file mode 100644 index 00000000..bb6e32a8 --- /dev/null +++ b/src/VhostsConsumers/DirectConsumer.php @@ -0,0 +1,136 @@ +getTimestampOfLastQueueRestart(); + + [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + + $connection = $this->startConsuming(); + + while (true) { + // Before reserving any jobs, we will make sure this queue is not paused and + // if it is we will just pause this worker for a given amount of time and + // make sure we do not need to kill this worker process off completely. + if (! $this->daemonShouldRun($this->workerOptions, $this->configConnectionName, $this->currentQueueName)) { + $this->output->info('Consuming pause worker...'); + + $this->pauseWorker($this->workerOptions, $lastRestart); + + continue; + } + + try { + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); + $amqpMessage = $this->channel->basic_get($this->currentQueueName); + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { + $amqpMessage = null; + + $this->logError('daemon.channel_exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), + ]); + } catch (AMQPRuntimeException $exception) { + $this->logError('daemon.amqp_runtime_exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + + $this->exceptions->report($exception); + + $this->kill(self::EXIT_SUCCESS, $this->workerOptions); + } catch (Exception|Throwable $exception) { + $this->logError('daemon.exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), + ]); + + $this->exceptions->report($exception); + + $this->stopWorkerIfLostConnection($exception); + } + + if (null === $amqpMessage) { + $this->output->info('Consuming sleep. No job...'); + + $this->stopConsuming(); + + $this->processBatch($connection); + + $this->goAheadOrWait(); + $connection = $this->startConsuming(); + + $this->sleep($this->workerOptions->sleep); + + continue; + } + + $this->processAmqpMessage($amqpMessage, $connection); + + if ($this->jobsProcessed >= $this->batchSize) { + $this->output->info('Consuming batch full...'); + + $this->stopConsuming(); + + $this->processBatch($connection); + + $this->goAheadOrWait(); + $connection = $this->startConsuming(); + + continue; + } + } + } + + /** + * @return RabbitMQQueue + * @throws Exceptions\MutexTimeout + */ + protected function startConsuming(): RabbitMQQueue + { + $this->processingUuid = $this->generateProcessingUuid(); + $this->processingStartedAt = microtime(true); + + $this->logInfo('startConsuming.init'); + + $arguments = []; + if ($this->maxPriority) { + $arguments['priority'] = ['I', $this->maxPriority]; + } + + $this->jobsProcessed = 0; + + $connection = $this->initConnection(); + + $this->updateLastProcessedAt(); + + return $connection; + } + + protected function stopConsuming(): void + { + return; + } +} + diff --git a/src/VhostsConsumers/QueueConsumer.php b/src/VhostsConsumers/QueueConsumer.php new file mode 100644 index 00000000..28320631 --- /dev/null +++ b/src/VhostsConsumers/QueueConsumer.php @@ -0,0 +1,220 @@ +getTimestampOfLastQueueRestart(); + + [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + + $connection = $this->startConsuming(); + + while ($this->channel->is_consuming()) { + // Before reserving any jobs, we will make sure this queue is not paused and + // if it is we will just pause this worker for a given amount of time and + // make sure we do not need to kill this worker process off completely. + if (! $this->daemonShouldRun($this->workerOptions, $this->configConnectionName, $this->currentQueueName)) { + $this->output->info('Consuming pause worker...'); + + $this->pauseWorker($this->workerOptions, $lastRestart); + + continue; + } + + // If the daemon should run (not in maintenance mode, etc.), then we can wait for a job. + try { + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); + $this->channel->wait(null, true, (int) $this->workerOptions->timeout); + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + } catch (AMQPRuntimeException $exception) { + $this->logError('daemon.amqp_runtime_exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + + $this->exceptions->report($exception); + + $this->kill(self::EXIT_SUCCESS, $this->workerOptions); + } catch (Exception|Throwable $exception) { + $this->logError('daemon.exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), + ]); + + $this->exceptions->report($exception); + + $this->stopWorkerIfLostConnection($exception); + } + + // If no job is got off the queue, we will need to sleep the worker. + if (false === $this->hasJob) { + $this->output->info('Consuming sleep. No job...'); + + $this->stopConsuming(); + + $this->processBatch($connection); + + $this->goAheadOrWait(); + $this->startConsuming(); + + $this->sleep($this->workerOptions->sleep); + } + + // Finally, we will check to see if we have exceeded our memory limits or if + // the queue should restart based on other indications. If so, we'll stop + // this worker and let whatever is "monitoring" it restart the process. + $status = $this->getStopStatus( + $this->workerOptions, + $lastRestart, + $startTime, + $jobsProcessed, + $this->hasJob + ); + if (! is_null($status)) { + $this->logInfo('consuming_stop', [ + 'status' => $status, + ]); + + return $this->stop($status, $this->workerOptions); + } + + $this->hasJob = false; + } + } + + /** + * @param WorkerOptions $options + * @param $lastRestart + * @param $startTime + * @param $jobsProcessed + * @param $hasJob + * @return int|null + */ + protected function getStopStatus( + WorkerOptions $options, + $lastRestart, + $startTime = 0, + $jobsProcessed = 0, + bool $hasJob = false + ): ?int + { + return match (true) { + $this->shouldQuit => static::EXIT_SUCCESS, + $this->memoryExceeded($options->memory) => static::EXIT_MEMORY_LIMIT, + $this->queueShouldRestart($lastRestart) => static::EXIT_SUCCESS, + $options->stopWhenEmpty && !$hasJob => static::EXIT_SUCCESS, + $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => static::EXIT_SUCCESS, + $options->maxJobs && $jobsProcessed >= $options->maxJobs => static::EXIT_SUCCESS, + default => null + }; + } + + /** + * @return RabbitMQQueue + * @throws Exceptions\MutexTimeout + */ + protected function startConsuming(): RabbitMQQueue + { + $this->processingUuid = $this->generateProcessingUuid(); + $this->processingStartedAt = microtime(true); + + $this->logInfo('startConsuming.init'); + + $arguments = []; + if ($this->maxPriority) { + $arguments['priority'] = ['I', $this->maxPriority]; + } + + $this->jobsProcessed = 0; + + $connection = $this->initConnection(); + + $callback = function (AMQPMessage $message) use ($connection): void { + $this->hasJob = true; + + $this->processAMQPMessage($message, $connection); + + if ($this->jobsProcessed >= $this->batchSize) { + $this->stopConsuming(); + + $this->processBatch($connection); + + $this->goAheadOrWait(); + $this->startConsuming(); + } + + if ($this->workerOptions->rest > 0) { + $this->sleep($this->workerOptions->rest); + } + }; + + $isSuccess = true; + + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); + try { + $this->channel->basic_consume( + $this->currentQueueName, + $this->getTagName(), + false, + false, + false, + false, + $callback, + null, + $arguments + ); + } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { + $isSuccess = false; + + $this->logError('startConsuming.exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), + ]); + } + + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + + $this->updateLastProcessedAt(); + + if (false === $isSuccess) { + $this->stopConsuming(); + + $this->goAheadOrWait(); + return $this->startConsuming(); + } + + return $connection; + } + + /** + * @return void + * @throws Exceptions\MutexTimeout + */ + protected function stopConsuming(): void + { + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); + $this->channel->basic_cancel($this->getTagName(), true); + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + } +} \ No newline at end of file From 7fb56ce7d065a4a69bada22f1fe9a9a61bb45f56 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 4 Feb 2025 16:56:38 +0200 Subject: [PATCH 10/32] SWR-16426 #comment RabbitMQ: Change Worker Logic From Consuming To Pulling --- src/VhostsConsumers/QueueConsumer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/VhostsConsumers/QueueConsumer.php b/src/VhostsConsumers/QueueConsumer.php index 28320631..f1ecaadb 100644 --- a/src/VhostsConsumers/QueueConsumer.php +++ b/src/VhostsConsumers/QueueConsumer.php @@ -217,4 +217,5 @@ protected function stopConsuming(): void $this->channel->basic_cancel($this->getTagName(), true); $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } -} \ No newline at end of file +} + From c49e31472e8a9fb43b2c84981c87369a0d7f83a0 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Thu, 6 Mar 2025 11:26:48 +0200 Subject: [PATCH 11/32] SWR-17033 #comment RabbitMQ: Add Timeout For Http Requests --- README.md | 4 ++-- composer.json | 2 +- src/Services/Api/RabbitApiClient.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3493bd6f..726b90a0 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|--------------------|---------------------------------------------------------------------------------------------| -| 1 | 16 | January 28th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 17 | January 28th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.16 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.17 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 4abc03f3..6b64eb6a 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.16-dev" + "dev-master": "1.17-dev" }, "laravel": { "providers": [ diff --git a/src/Services/Api/RabbitApiClient.php b/src/Services/Api/RabbitApiClient.php index 55e04328..2d9139ce 100644 --- a/src/Services/Api/RabbitApiClient.php +++ b/src/Services/Api/RabbitApiClient.php @@ -16,8 +16,8 @@ class RabbitApiClient public function __construct() { $this->client = new HttpClient([ - RequestOptions::TIMEOUT => 60, - RequestOptions::CONNECT_TIMEOUT => 60, + RequestOptions::TIMEOUT => 30, + RequestOptions::CONNECT_TIMEOUT => 30, ]); } From 8dd909358669e4b0bef634eec192228d30f819af Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Mon, 14 Apr 2025 11:14:20 +0300 Subject: [PATCH 12/32] SWR-17339: Separate API Host --- composer.json | 2 +- src/Services/Api/RabbitApiClient.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6b64eb6a..1b388457 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" }, "laravel": { "providers": [ diff --git a/src/Services/Api/RabbitApiClient.php b/src/Services/Api/RabbitApiClient.php index 2d9139ce..dfd16f1e 100644 --- a/src/Services/Api/RabbitApiClient.php +++ b/src/Services/Api/RabbitApiClient.php @@ -85,7 +85,7 @@ public function request( */ private function getBaseUrl(): string { - $host = $this->connectionConfig['hosts'][0]['host'] ?? ''; + $host = $this->connectionConfig['hosts'][0]['api_host'] ?? ''; $port = $this->connectionConfig['hosts'][0]['api_port'] ?? ''; $scheme = $this->connectionConfig['secure'] ? 'https://' : 'http://'; From d11fbd2d1ea5746e83e5d321c4968a2fc455af14 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Wed, 23 Apr 2025 16:38:56 +0300 Subject: [PATCH 13/32] SWR-17826 #comment Create Vhosts Fix --- README.md | 8 ++++---- composer.json | 2 +- src/Services/VhostsService.php | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 726b90a0..fc7dcd8e 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ RabbitMQ Queue driver for Laravel Only the latest version will get new features. Bug fixes will be provided using the following scheme: -| Package Version | Laravel Version | Bug Fixes Until | | -|-----------------|-----------------|--------------------|---------------------------------------------------------------------------------------------| -| 1 | 17 | January 28th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| Package Version | Laravel Version | Bug Fixes Until | | +|-----------------|-----------------|------------------|---------------------------------------------------------------------------------------------| +| 1 | 19 | April 23th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.17 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.19 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 1b388457..d47e62ce 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-master": "1.19-dev" }, "laravel": { "providers": [ diff --git a/src/Services/VhostsService.php b/src/Services/VhostsService.php index 30533192..2653319c 100644 --- a/src/Services/VhostsService.php +++ b/src/Services/VhostsService.php @@ -157,11 +157,12 @@ public function createVhost(string $vhostName, string $description): bool $isCreated = false; } + $isSuccess = false; if ($isCreated) { - $this->setVhostPermissions($vhostName); + $isSuccess = $this->setVhostPermissions($vhostName); } - return $isCreated; + return $isSuccess; } /** From 69fde420b82abd84a670640dadd00ac0bd290c3a Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Thu, 1 May 2025 16:41:16 +0300 Subject: [PATCH 14/32] SWR-17938 #comment Vhost Not Created Automatically --- README.md | 4 ++-- composer.json | 2 +- src/Queue/RabbitMQQueueBatchable.php | 28 +++++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc7dcd8e..16db9cd3 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|------------------|---------------------------------------------------------------------------------------------| -| 1 | 19 | April 23th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 20 | April 23th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.19 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.20 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index d47e62ce..2427ec70 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.19-dev" + "dev-master": "1.20-dev" }, "laravel": { "providers": [ diff --git a/src/Queue/RabbitMQQueueBatchable.php b/src/Queue/RabbitMQQueueBatchable.php index 94671d61..a596a6fc 100644 --- a/src/Queue/RabbitMQQueueBatchable.php +++ b/src/Queue/RabbitMQQueueBatchable.php @@ -65,7 +65,11 @@ protected function createChannel(): AMQPChannel { try { return parent::createChannel(); - } catch (AMQPConnectionClosedException) { + } catch (AMQPConnectionClosedException $exception) { + if ($this->isVhostFailedException($exception) && (false === $this->createNotExistsVhost())) { + throw $exception; + } + $this->reconnect(); return parent::createChannel(); } @@ -148,5 +152,27 @@ private function addQueueToIndex(string $queue): bool return $isQueueActivated && $isVhostActivated; } + + /** + * @param AMQPConnectionClosedException $exception + * @return bool + */ + private function isVhostFailedException(AMQPConnectionClosedException $exception): bool + { + $dto = new ConnectionNameDto($this->getConnectionName()); + $vhostName = (string) $dto->getVhostName(); + + $notFoundErrorMessage = sprintf('NOT_ALLOWED - vhost %s not found', $vhostName); + if ((403 === $exception->getCode()) && str_contains($exception->getMessage(), $notFoundErrorMessage)) { + return true; + } + + $deletedErrorMessage = sprintf('CONNECTION_FORCED - vhost \'%s\' is deleted', $vhostName); + if (str_contains($exception->getMessage(), $deletedErrorMessage)) { + return true; + } + + return false; + } } From 943000bbbbba2c94d1ece2a5093bcbba266d14a0 Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Fri, 9 May 2025 14:50:55 +0300 Subject: [PATCH 15/32] SWR-17749: Close connections on switch --- composer.json | 4 ++-- src/VhostsConsumers/AbstractVhostsConsumer.php | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 2427ec70..7fa4c8e6 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.20-dev" + "dev-master": "1.21-dev" }, "laravel": { "providers": [ @@ -56,4 +56,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index 24e71508..d4dff10b 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -566,10 +566,20 @@ protected function updateLastProcessedAt() /** * @return RabbitMQQueue */ - protected function initConnection(): RabbitMQQueue + protected function initConnection(): RabbitMQQueue { + // Close any existing connection/channel + if ($this->channel) { + try { + $this->channel->close(); + } catch (\Exception $e) { + // Ignore close errors + } + $this->channel = null; + } + $connection = $this->manager->connection( - ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) + ConnectionNameDto::getVhostConnectionName($this->currentVhostName, $this->configConnectionName) ); try { From 313aea2298502710aa9d4c923f824816a5daa29a Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Tue, 27 May 2025 16:00:55 +0300 Subject: [PATCH 16/32] SWR-18338: Close connections on switch --- composer.json | 2 +- src/Consumer.php | 2 + .../AbstractVhostsConsumer.php | 45 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 7fa4c8e6..518c5630 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.21-dev" + "dev-master": "1.22-dev" }, "laravel": { "providers": [ diff --git a/src/Consumer.php b/src/Consumer.php index a5b7b14a..0f69893f 100644 --- a/src/Consumer.php +++ b/src/Consumer.php @@ -32,6 +32,8 @@ class Consumer extends Worker /** @var AMQPChannel */ protected $channel; + protected $connection; + /** @var object|null */ protected $currentJob; diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index d4dff10b..a17629fe 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -74,8 +74,7 @@ public function __construct( ExceptionHandler $exceptions, callable $isDownForMaintenance, callable $resetScope = null - ) - { + ) { parent::__construct($manager, $events, $exceptions, $isDownForMaintenance, $resetScope); } @@ -124,7 +123,7 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->vhostDaemon($connectionName, $options); } - + abstract protected function vhostDaemon($connectionName, WorkerOptions $options); /** @@ -141,8 +140,7 @@ protected function getStopStatus( $startTime = 0, $jobsProcessed = 0, bool $hasJob = false - ): ?int - { + ): ?int { return match (true) { $this->shouldQuit => static::EXIT_SUCCESS, $this->memoryExceeded($options->memory) => static::EXIT_MEMORY_LIMIT, @@ -566,16 +564,14 @@ protected function updateLastProcessedAt() /** * @return RabbitMQQueue */ - protected function initConnection(): RabbitMQQueue + protected function initConnection(): RabbitMQQueue { - // Close any existing connection/channel - if ($this->channel) { + if ($this->connection) { try { - $this->channel->close(); + $this->connection->close(); } catch (\Exception $e) { - // Ignore close errors } - $this->channel = null; + $this->connection = null; } $connection = $this->manager->connection( @@ -584,6 +580,19 @@ protected function initConnection(): RabbitMQQueue try { $channel = $connection->getChannel(); + + $this->currentConnectionName = $connection->getConnectionName(); + + $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); + $channel->basic_qos( + $this->prefetchSize, + $this->prefetchCount, + false + ); + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + + $this->channel = $channel; + $this->connection = $connection; } catch (AMQPConnectionClosedException $exception) { $this->logError('initConnection.exception', [ 'message' => $exception->getMessage(), @@ -601,18 +610,6 @@ protected function initConnection(): RabbitMQQueue return $this->initConnection(); } - $this->currentConnectionName = $connection->getConnectionName(); - - $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $channel->basic_qos( - $this->prefetchSize, - $this->prefetchCount, - false - ); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); - - $this->channel = $channel; - return $connection; } @@ -686,4 +683,4 @@ protected function log(string $message, array $data = [], bool $isError = false) $this->logger->info($logMessage, $data); } } -} \ No newline at end of file +} From f3e707139395574f5e46345db847b1eab1fee4c5 Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Tue, 27 May 2025 19:45:42 +0300 Subject: [PATCH 17/32] SWR-18338: Reconnect on switch --- composer.json | 2 +- src/VhostsConsumers/AbstractVhostsConsumer.php | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 518c5630..d7030bab 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index a17629fe..b9ff6339 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -579,6 +579,10 @@ protected function initConnection(): RabbitMQQueue ); try { + if (!$connection->isConnected()) { + $connection->reconnect(); + } + $channel = $connection->getChannel(); $this->currentConnectionName = $connection->getConnectionName(); @@ -593,7 +597,7 @@ protected function initConnection(): RabbitMQQueue $this->channel = $channel; $this->connection = $connection; - } catch (AMQPConnectionClosedException $exception) { + } catch (AMQPConnectionClosedException|AMQPChannelClosedException $exception) { $this->logError('initConnection.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), From af7d75d484cddac82eed5e09b011733fb3eba28d Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Tue, 27 May 2025 20:41:31 +0300 Subject: [PATCH 18/32] SWR-18338: RabbitMQ Switch Logic --- composer.json | 2 +- src/VhostsConsumers/AbstractVhostsConsumer.php | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index d7030bab..ee14d50c 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.23-dev" + "dev-master": "1.24-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index b9ff6339..af5bda8d 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -566,12 +566,13 @@ protected function updateLastProcessedAt() */ protected function initConnection(): RabbitMQQueue { - if ($this->connection) { + if ($this->channel) { try { - $this->connection->close(); + $this->channel->close(); } catch (\Exception $e) { + // Ignore close errors } - $this->connection = null; + $this->channel = null; } $connection = $this->manager->connection( @@ -579,11 +580,7 @@ protected function initConnection(): RabbitMQQueue ); try { - if (!$connection->isConnected()) { - $connection->reconnect(); - } - - $channel = $connection->getChannel(); + $channel = $connection->getChannel(true); $this->currentConnectionName = $connection->getConnectionName(); From ba5b8eb70bc3b166b66e36415dfa5dfec9ed0e20 Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Thu, 29 May 2025 16:11:43 +0300 Subject: [PATCH 19/32] Merge pull request #8 from salesmessage/sleep-refactor Sleep refactor --- composer.json | 2 +- src/VhostsConsumers/AbstractVhostsConsumer.php | 11 ++++++++++- src/VhostsConsumers/DirectConsumer.php | 2 -- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index ee14d50c..4ba4e2c8 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.24-dev" + "dev-master": "1.25-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index af5bda8d..2365db94 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -57,6 +57,8 @@ abstract class AbstractVhostsConsumer extends Consumer protected int $jobsProcessed = 0; + protected bool $hadJobs = false; + /** * @param InternalStorageManager $internalStorageManager * @param LoggerInterface $logger @@ -165,6 +167,7 @@ abstract protected function startConsuming(): RabbitMQQueue; */ protected function processAmqpMessage(AMQPMessage $message, RabbitMQQueue $connection): void { + $this->hadJobs = true; $isSupportBatching = $this->isSupportBatching($message); if ($isSupportBatching) { $this->addMessageToBatch($message); @@ -500,7 +503,13 @@ protected function getNextQueue(): ?string protected function goAheadOrWait(int $waitSeconds = 1): bool { if (false === $this->goAhead()) { + if (!$this->hadJobs) { + $this->output->warning(sprintf('No jobs during iteration. Wait %d seconds...', $waitSeconds)); + $this->sleep($waitSeconds); + } + $this->loadVhosts(); + $this->hadJobs = false; if (empty($this->vhosts)) { $this->output->warning(sprintf('No active vhosts. Wait %d seconds...', $waitSeconds)); $this->sleep($waitSeconds); @@ -594,7 +603,7 @@ protected function initConnection(): RabbitMQQueue $this->channel = $channel; $this->connection = $connection; - } catch (AMQPConnectionClosedException|AMQPChannelClosedException $exception) { + } catch (AMQPConnectionClosedException | AMQPChannelClosedException $exception) { $this->logError('initConnection.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php index bb6e32a8..205a78cd 100644 --- a/src/VhostsConsumers/DirectConsumer.php +++ b/src/VhostsConsumers/DirectConsumer.php @@ -81,8 +81,6 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->goAheadOrWait(); $connection = $this->startConsuming(); - $this->sleep($this->workerOptions->sleep); - continue; } From adb520512c1cf254660f1c2f28c4822d1dd9464a Mon Sep 17 00:00:00 2001 From: Alex Rutski Date: Thu, 31 Jul 2025 11:08:19 +0300 Subject: [PATCH 20/32] SWR-18735: make vhost prefix configurable from env --- config/rabbitmq.php | 5 +++++ src/Services/VhostsService.php | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/config/rabbitmq.php b/config/rabbitmq.php index 2b6afd96..8b949d9c 100644 --- a/config/rabbitmq.php +++ b/config/rabbitmq.php @@ -30,4 +30,9 @@ */ 'worker' => env('RABBITMQ_WORKER', 'default'), + /* + * Vhost prefix for organization-specific vhosts. + */ + 'vhost_prefix' => env('RABBITMQ_VHOST_PREFIX', 'organization_'), + ]; diff --git a/src/Services/VhostsService.php b/src/Services/VhostsService.php index 2653319c..994373e4 100644 --- a/src/Services/VhostsService.php +++ b/src/Services/VhostsService.php @@ -9,8 +9,6 @@ class VhostsService { - public const VHOST_PREFIX = 'organization_'; - /** * @param RabbitApiClient $rabbitApiClient * @param LoggerInterface $logger @@ -204,7 +202,9 @@ public function setVhostPermissions(string $vhostName): bool */ public function getVhostName(int $organizationId): string { - return self::VHOST_PREFIX . $organizationId; + $vhostPrefix = config('queue.connections.rabbitmq_vhosts.vhost_prefix', 'organization_'); + + return $vhostPrefix . $organizationId; } /** From 19d92d3a302cb9c696205c939d92e2bf6c6a5959 Mon Sep 17 00:00:00 2001 From: Alex Rutski Date: Thu, 31 Jul 2025 11:21:43 +0300 Subject: [PATCH 21/32] SWR-18735: bump version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4ba4e2c8..53177aa2 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.25-dev" + "dev-master": "1.26-dev" }, "laravel": { "providers": [ From d7d088df3fe7348694a05e2f2a887aec42762403 Mon Sep 17 00:00:00 2001 From: alexrt23 <53480358+alexrt23@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:39:41 +0300 Subject: [PATCH 22/32] SWC-13137: update deps (#10) Co-authored-by: Alex Rutski --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 53177aa2..e7aacf7e 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "laravel/horizon": "^5.0", "orchestra/testbench": "^7.0|^8.0|^9.0", "laravel/pint": "^1.2", - "laravel/framework": "^9.0|^10.0|^11.0" + "laravel/framework": "^10.0|^11.0" }, "autoload": { "psr-4": { From 971f65b94e5944f349f4bd8964ff7a4a03025a37 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 16 Sep 2025 17:43:06 +0300 Subject: [PATCH 23/32] SWR-19885 #comment Implement Async Heartbeat For Vhosts Consumers --- README.md | 10 +- composer.json | 2 +- src/Console/ConsumeVhostsCommand.php | 10 +- .../AbstractVhostsConsumer.php | 124 +++++++++++++++++- src/VhostsConsumers/QueueConsumer.php | 8 +- 5 files changed, 139 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 16db9cd3..4bb8f78c 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ RabbitMQ Queue driver for Laravel Only the latest version will get new features. Bug fixes will be provided using the following scheme: -| Package Version | Laravel Version | Bug Fixes Until | | -|-----------------|-----------------|------------------|---------------------------------------------------------------------------------------------| -| 1 | 20 | April 23th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| Package Version | Laravel Version | Bug Fixes Until | | +|-----------------|-----------------|----------------------|---------------------------------------------------------------------------------------------| +| 1 | 27 | September 16th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.20 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.27 --ignore-platform-reqs ``` The package will automatically register itself. @@ -632,7 +632,7 @@ There are two ways of consuming messages. Example: ```bash -php artisan lib-rabbitmq:consume-vhosts test-group-1 rabbitmq_vhosts --name=mq-vhosts-test-name --sleep=3 --memory=300 --max-jobs=5000 --max-time=600 --timeout=0 +php artisan lib-rabbitmq:consume-vhosts test-group-1 rabbitmq_vhosts --name=mq-vhosts-test-name --sleep=3 --memory=300 --max-jobs=5000 --max-time=600 --timeout=0 --async-mode=1 ``` ## Testing diff --git a/composer.json b/composer.json index e7aacf7e..899884d4 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.26-dev" + "dev-master": "1.27-dev" }, "laravel": { "providers": [ diff --git a/src/Console/ConsumeVhostsCommand.php b/src/Console/ConsumeVhostsCommand.php index d73a66c3..1288a9c5 100644 --- a/src/Console/ConsumeVhostsCommand.php +++ b/src/Console/ConsumeVhostsCommand.php @@ -30,6 +30,7 @@ class ConsumeVhostsCommand extends WorkCommand {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed} {--rest=0 : Number of seconds to rest between jobs} + {--async-mode=0 : Async processing for some functionality (now only "heartbeat" is supported)} {--max-priority=} {--consumer-tag} @@ -84,6 +85,7 @@ public function handle(): void $consumer->setPrefetchSize((int) $this->option('prefetch-size')); $consumer->setPrefetchCount((int) ($groupConfigData['prefetch_count'] ?? 1000)); $consumer->setBatchSize((int) ($groupConfigData['batch_size'] ?? 1000)); + $consumer->setAsyncMode((bool) $this->option('async-mode')); if ($this->downForMaintenance() && $this->option('once')) { $consumer->sleep($this->option('sleep')); @@ -95,8 +97,10 @@ public function handle(): void // which jobs are coming through a queue and be informed on its progress. $this->listenForEvents(); - $connection = $this->argument('connection') - ?: $this->laravel['config']['queue.default']; + $queueConfigData = $this->laravel['config']['queue']; + $connectionName = $this->argument('connection') ?: ($queueConfigData['default'] ?? ''); + + $consumer->setConfig((array) ($queueConfigData['connections'][$connectionName] ?? [])); if (Terminal::hasSttyAvailable()) { $this->components->info(sprintf( @@ -107,7 +111,7 @@ public function handle(): void } $this->runWorker( - $connection, + $connectionName, '' ); } diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index 2365db94..8394177c 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -8,6 +8,7 @@ use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Str; +use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Exception\AMQPChannelClosedException; use PhpAmqpLib\Exception\AMQPConnectionClosedException; use PhpAmqpLib\Exception\AMQPProtocolChannelException; @@ -19,6 +20,7 @@ use Salesmessage\LibRabbitMQ\Dto\ConsumeVhostsFiltersDto; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; use Salesmessage\LibRabbitMQ\Dto\VhostApiDto; +use Salesmessage\LibRabbitMQ\Exceptions\MutexTimeout; use Salesmessage\LibRabbitMQ\Interfaces\RabbitMQBatchable; use Salesmessage\LibRabbitMQ\Mutex; use Salesmessage\LibRabbitMQ\Queue\Jobs\RabbitMQJob; @@ -29,6 +31,8 @@ abstract class AbstractVhostsConsumer extends Consumer { protected const MAIN_HANDLER_LOCK = 'vhost_handler'; + protected const HEALTHCHECK_HANDLER_LOCK = 'healthcheck_vhost_handler'; + protected ?OutputStyle $output = null; protected ?ConsumeVhostsFiltersDto $filtersDto = null; @@ -59,6 +63,12 @@ abstract class AbstractVhostsConsumer extends Consumer protected bool $hadJobs = false; + protected ?int $stopStatusCode = null; + + protected array $config = []; + + protected bool $asyncMode = false; + /** * @param InternalStorageManager $internalStorageManager * @param LoggerInterface $logger @@ -110,12 +120,30 @@ public function setBatchSize(int $batchSize): self return $this; } + /** + * @param array $config + * @return $this + */ + public function setConfig(array $config): self + { + $this->config = $config; + return $this; + } + + /** + * @param bool $asyncMode + * @return $this + */ + public function setAsyncMode(bool $asyncMode): self + { + $this->asyncMode = $asyncMode; + return $this; + } + public function daemon($connectionName, $queue, WorkerOptions $options) { $this->goAheadOrWait(); - $this->connectionMutex = new Mutex(false); - $this->configConnectionName = (string) $connectionName; $this->workerOptions = $options; @@ -123,6 +151,40 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->listenForSignals(); } + if ($this->asyncMode) { + $this->logInfo('daemon.AsyncMode.On'); + + $coroutineContextHandler = function () use ($connectionName, $options) { + $this->logInfo('daemon.AsyncMode.Coroutines.Running'); + + // we can't move it outside since Mutex should be created within coroutine context + $this->connectionMutex = new Mutex(true); + $this->startHeartbeatCheck(); + \go(function () use ($connectionName, $options) { + $this->vhostDaemon($connectionName, $options); + }); + }; + + if (extension_loaded('swoole')) { + $this->logInfo('daemon.AsyncMode.Swoole'); + + \Co\run($coroutineContextHandler); + } elseif (extension_loaded('openswoole')) { + $this->logInfo('daemon.AsyncMode.OpenSwoole'); + + \OpenSwoole\Runtime::enableCoroutine(true, \OpenSwoole\Runtime::HOOK_ALL); + \co::run($coroutineContextHandler); + } else { + throw new \Exception('Async mode is not supported. Check if Swoole extension is installed'); + } + + return; + } + + $this->logInfo('daemon.AsyncMode.Off'); + + $this->connectionMutex = new Mutex(false); + $this->startHeartbeatCheck(); $this->vhostDaemon($connectionName, $options); } @@ -623,6 +685,64 @@ protected function initConnection(): RabbitMQQueue return $connection; } + /** + * @return void + */ + protected function startHeartbeatCheck(): void + { + if (false === $this->asyncMode) { + return; + } + + $heartbeatInterval = (int) ($this->config['options']['heartbeat'] ?? 0); + if (!$heartbeatInterval) { + return; + } + + $heartbeatHandler = function () { + if ($this->shouldQuit || (null !== $this->stopStatusCode)) { + return; + } + + try { + /** @var AMQPStreamConnection $connection */ + $connection = $this->connection?->getConnection(); + if ((null === $connection) + || (false === $connection->isConnected()) + || $connection->isWriting() + || $connection->isBlocked() + ) { + return; + } + + $this->connectionMutex->lock(static::HEALTHCHECK_HANDLER_LOCK, 3); + $connection->checkHeartBeat(); + } catch (MutexTimeout) { + } catch (Throwable $exception) { + $this->logError('startHeartbeatCheck.exception', [ + 'eroor' => $exception->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + $this->shouldQuit = true; + } finally { + $this->connectionMutex->unlock(static::HEALTHCHECK_HANDLER_LOCK); + } + }; + + \go(function () use ($heartbeatHandler, $heartbeatInterval) { + $this->logInfo('startHeartbeatCheck.started'); + + while (true) { + sleep($heartbeatInterval); + $heartbeatHandler(); + if ($this->shouldQuit || !is_null($this->stopStatusCode)) { + return; + } + } + }); + } + /** * @return string */ diff --git a/src/VhostsConsumers/QueueConsumer.php b/src/VhostsConsumers/QueueConsumer.php index f1ecaadb..74076ae8 100644 --- a/src/VhostsConsumers/QueueConsumer.php +++ b/src/VhostsConsumers/QueueConsumer.php @@ -83,19 +83,19 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) // Finally, we will check to see if we have exceeded our memory limits or if // the queue should restart based on other indications. If so, we'll stop // this worker and let whatever is "monitoring" it restart the process. - $status = $this->getStopStatus( + $this->stopStatusCode = $this->getStopStatus( $this->workerOptions, $lastRestart, $startTime, $jobsProcessed, $this->hasJob ); - if (! is_null($status)) { + if (! is_null($this->stopStatusCode)) { $this->logInfo('consuming_stop', [ - 'status' => $status, + 'status' => $this->stopStatusCode, ]); - return $this->stop($status, $this->workerOptions); + return $this->stop($this->stopStatusCode, $this->workerOptions); } $this->hasJob = false; From 3589bd1b2c3d16323abdd4ad27f2da5922ca364b Mon Sep 17 00:00:00 2001 From: alexrt23 <53480358+alexrt23@users.noreply.github.com> Date: Thu, 18 Sep 2025 12:37:43 +0300 Subject: [PATCH 24/32] SOC2: Allow Laravel 12 (#12) * chore: allow Laravel 12 * chore: allow Laravel 12 --------- Co-authored-by: Alex Rutski --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 899884d4..d457561a 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "require": { "php": "^8.0", "ext-json": "*", - "illuminate/queue": "^9.0|^10.0|^11.0", + "illuminate/queue": "^9.0|^10.0|^11.0|^12.0", "php-amqplib/php-amqplib": "^v3.6" }, "require-dev": { @@ -20,7 +20,7 @@ "laravel/horizon": "^5.0", "orchestra/testbench": "^7.0|^8.0|^9.0", "laravel/pint": "^1.2", - "laravel/framework": "^10.0|^11.0" + "laravel/framework": "^10.0|^11.0|^12.0" }, "autoload": { "psr-4": { From bc4df30cfe1141975e26d3d60ebdd0df8a7d8433 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Tue, 23 Sep 2025 15:43:45 +0300 Subject: [PATCH 25/32] SWR-19884 #comment Change RabbitMQ Queues Policies --- README.md | 4 ++-- composer.json | 2 +- src/VhostsConsumers/AbstractVhostsConsumer.php | 2 ++ src/VhostsConsumers/DirectConsumer.php | 8 +++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4bb8f78c..6415ca98 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|----------------------|---------------------------------------------------------------------------------------------| -| 1 | 27 | September 16th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 28 | September 23th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.27 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.28 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index d457561a..c7fadad7 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.27-dev" + "dev-master": "1.28-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index 8394177c..b3f0941b 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -8,6 +8,7 @@ use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Str; +use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Exception\AMQPChannelClosedException; use PhpAmqpLib\Exception\AMQPConnectionClosedException; @@ -651,6 +652,7 @@ protected function initConnection(): RabbitMQQueue ); try { + /** @var AMQPChannel $channel */ $channel = $connection->getChannel(true); $this->currentConnectionName = $connection->getConnectionName(); diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php index 205a78cd..ecdae23c 100644 --- a/src/VhostsConsumers/DirectConsumer.php +++ b/src/VhostsConsumers/DirectConsumer.php @@ -41,6 +41,9 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) try { $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); $amqpMessage = $this->channel->basic_get($this->currentQueueName); + if (null !== $amqpMessage) { + $this->channel->basic_reject($amqpMessage->getDeliveryTag(), false); + } $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { $amqpMessage = null; @@ -112,11 +115,6 @@ protected function startConsuming(): RabbitMQQueue $this->logInfo('startConsuming.init'); - $arguments = []; - if ($this->maxPriority) { - $arguments['priority'] = ['I', $this->maxPriority]; - } - $this->jobsProcessed = 0; $connection = $this->initConnection(); From 6dc3d085455fefa79d35ca633ce6dc5c17c357b8 Mon Sep 17 00:00:00 2001 From: Viacheslav Shcherbyna Date: Thu, 25 Sep 2025 13:33:51 +0300 Subject: [PATCH 26/32] SWR-20043 #comment Add Logging For RabbitMQ Vhosts Consumers --- README.md | 4 +- composer.json | 2 +- .../AbstractVhostsConsumer.php | 108 ++++++++++++++---- src/VhostsConsumers/DirectConsumer.php | 36 +++++- src/VhostsConsumers/QueueConsumer.php | 55 ++++----- 5 files changed, 144 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 6415ca98..c4048569 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Only the latest version will get new features. Bug fixes will be provided using | Package Version | Laravel Version | Bug Fixes Until | | |-----------------|-----------------|----------------------|---------------------------------------------------------------------------------------------| -| 1 | 28 | September 23th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | +| 1 | 29 | September 25th, 2025 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) | ## Installation You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.28 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.29 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index c7fadad7..b74f81ac 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.28-dev" + "dev-master": "1.29-dev" }, "laravel": { "providers": [ diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index b3f0941b..7d99e10e 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -60,6 +60,8 @@ abstract class AbstractVhostsConsumer extends Consumer protected int|float $processingStartedAt = 0; + protected int $totalJobsProcessed = 0; + protected int $jobsProcessed = 0; protected bool $hadJobs = false; @@ -176,6 +178,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) \OpenSwoole\Runtime::enableCoroutine(true, \OpenSwoole\Runtime::HOOK_ALL); \co::run($coroutineContextHandler); } else { + $this->logError('daemon.AsyncMode.IsNotSupported'); + throw new \Exception('Async mode is not supported. Check if Swoole extension is installed'); } @@ -240,10 +244,11 @@ protected function processAmqpMessage(AMQPMessage $message, RabbitMQQueue $conne } $this->jobsProcessed++; + $this->totalJobsProcessed++; $this->logInfo('processAMQPMessage.message_consumed', [ 'processed_jobs_count' => $this->jobsProcessed, - 'is_support_batching' => $isSupportBatching, + 'is_support_batching' => $isSupportBatching ? 'Y' :'N', ]); } @@ -410,6 +415,10 @@ protected function processSingleJob(RabbitMQJob $job): void */ protected function ackMessage(AMQPMessage $message, bool $multiple = false): void { + $this->logInfo('ackMessage.start', [ + 'multiple' => $multiple, + ]); + try { $message->ack($multiple); } catch (Throwable $exception) { @@ -432,6 +441,8 @@ abstract protected function stopConsuming(): void; */ protected function loadVhosts(): void { + $this->logInfo('loadVhosts.start'); + $group = $this->filtersDto->getGroup(); $lastProcessedAtKey = $this->internalStorageManager->getLastProcessedAtKeyName($group); @@ -478,6 +489,9 @@ protected function switchToNextVhost(): bool } $this->currentQueueName = $nextQueue; + + $this->logInfo('switchToNextVhost.success'); + return true; } @@ -503,6 +517,8 @@ protected function getNextVhost(): ?string */ protected function loadVhostQueues(): void { + $this->logInfo('loadVhostQueues.start'); + $group = $this->filtersDto->getGroup(); $lastProcessedAtKey = $this->internalStorageManager->getLastProcessedAtKeyName($group); @@ -539,6 +555,9 @@ protected function switchToNextQueue(): bool } $this->currentQueueName = $nextQueue; + + $this->logInfo('switchToNextQueue.success'); + return true; } @@ -567,20 +586,26 @@ protected function goAheadOrWait(int $waitSeconds = 1): bool { if (false === $this->goAhead()) { if (!$this->hadJobs) { - $this->output->warning(sprintf('No jobs during iteration. Wait %d seconds...', $waitSeconds)); + $this->logWarning('goAheadOrWait.no_jobs_during_iteration', [ + 'wait_seconds' => $waitSeconds, + ]); + $this->sleep($waitSeconds); } $this->loadVhosts(); $this->hadJobs = false; if (empty($this->vhosts)) { - $this->output->warning(sprintf('No active vhosts. Wait %d seconds...', $waitSeconds)); + $this->logWarning('goAheadOrWait.no_active_vhosts', [ + 'wait_seconds' => $waitSeconds, + ]); + $this->sleep($waitSeconds); return $this->goAheadOrWait($waitSeconds); } - $this->output->info('Starting from the first vhost...'); + $this->logInfo('goAheadOrWait.starting_from_the_first_vhost'); return $this->goAheadOrWait($waitSeconds); } @@ -612,6 +637,8 @@ protected function updateLastProcessedAt() return; } + $this->logInfo('updateLastProcessedAt.start'); + $group = $this->filtersDto->getGroup(); $timestamp = time(); @@ -701,25 +728,42 @@ protected function startHeartbeatCheck(): void return; } + $this->logInfo('startHeartbeatCheck.start', [ + 'heartbeat_interval' => $heartbeatInterval, + ]); + $heartbeatHandler = function () { if ($this->shouldQuit || (null !== $this->stopStatusCode)) { + $this->logWarning('startHeartbeatCheck.quit', [ + 'should_quit' => $this->shouldQuit, + 'stop_status_code' => $this->stopStatusCode, + ]); + return; } try { - /** @var AMQPStreamConnection $connection */ + /** @var AMQPStreamConnection|null $connection */ $connection = $this->connection?->getConnection(); if ((null === $connection) || (false === $connection->isConnected()) || $connection->isWriting() || $connection->isBlocked() ) { + $this->logWarning('startHeartbeatCheck.incorrect_connection', [ + 'has_connection' => (null !== $connection) ? 'Y' : 'N', + 'is_connected' => $connection?->isConnected() ? 'Y' : 'N', + 'is_writing' => $connection->isWriting() ? 'Y' : 'N', + 'is_blocked' => $connection->isBlocked() ? 'Y' : 'N', + ]); + return; } $this->connectionMutex->lock(static::HEALTHCHECK_HANDLER_LOCK, 3); $connection->checkHeartBeat(); } catch (MutexTimeout) { + $this->logWarning('startHeartbeatCheck.mutex_timeout'); } catch (Throwable $exception) { $this->logError('startHeartbeatCheck.exception', [ 'eroor' => $exception->getMessage(), @@ -739,6 +783,11 @@ protected function startHeartbeatCheck(): void sleep($heartbeatInterval); $heartbeatHandler(); if ($this->shouldQuit || !is_null($this->stopStatusCode)) { + $this->logWarning('startHeartbeatCheck.go_quit', [ + 'should_quit' => $this->shouldQuit, + 'stop_status_code' => $this->stopStatusCode, + ]); + return; } } @@ -760,7 +809,17 @@ protected function getTagName(): string */ protected function logInfo(string $message, array $data = []): void { - $this->log($message, $data, false); + $this->log($message, $data, 'info'); + } + + /** + * @param string $message + * @param array $data + * @return void + */ + protected function logWarning(string $message, array $data = []): void + { + $this->log($message, $data, 'warning'); } /** @@ -770,19 +829,23 @@ protected function logInfo(string $message, array $data = []): void */ protected function logError(string $message, array $data = []): void { - $this->log($message, $data, true); + $this->log($message, $data, 'error'); } /** * @param string $message * @param array $data - * @param bool $isError + * @param string $logType * @return void */ - protected function log(string $message, array $data = [], bool $isError = false): void + protected function log(string $message, array $data = [], string $logType = 'info'): void { - $data['vhost_name'] = $this->currentVhostName; - $data['queue_name'] = $this->currentQueueName; + if (null !== $this->currentVhostName) { + $data['vhost_name'] = $this->currentVhostName; + } + if (null !== $this->currentQueueName) { + $data['queue_name'] = $this->currentQueueName; + } $outputMessage = $message; foreach ($data as $key => $value) { @@ -791,15 +854,17 @@ protected function log(string $message, array $data = [], bool $isError = false) } $outputMessage .= '. ' . ucfirst(str_replace('_', ' ', $key)) . ': ' . $value; } - if ($isError) { - $this->output->error($outputMessage); - } else { - $this->output->info($outputMessage); - } + + match ($logType) { + 'error' => $this->output->error($outputMessage), + 'warning' => $this->output->warning($outputMessage), + default => $this->output->info($outputMessage) + }; $processingData = [ 'uuid' => $this->processingUuid, 'started_at' => $this->processingStartedAt, + 'total_processed_jobs_count' => $this->totalJobsProcessed, ]; if ($this->processingStartedAt) { $processingData['executive_time_seconds'] = microtime(true) - $this->processingStartedAt; @@ -809,10 +874,11 @@ protected function log(string $message, array $data = [], bool $isError = false) $logMessage = 'Salesmessage.LibRabbitMQ.VhostsConsumers.'; $logMessage .= class_basename(static::class) . '.'; $logMessage .= $message; - if ($isError) { - $this->logger->error($logMessage, $data); - } else { - $this->logger->info($logMessage, $data); - } + + match ($logType) { + 'error' => $this->logger->error($logMessage, $data), + 'warning' => $this->logger->warning($logMessage, $data), + default => $this->logger->info($logMessage, $data) + }; } } diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php index ecdae23c..af3e7d67 100644 --- a/src/VhostsConsumers/DirectConsumer.php +++ b/src/VhostsConsumers/DirectConsumer.php @@ -18,11 +18,21 @@ class DirectConsumer extends AbstractVhostsConsumer { + /** + * @param $connectionName + * @param WorkerOptions $options + * @return int + * @throws \Salesmessage\LibRabbitMQ\Exceptions\MutexTimeout + * @throws \Throwable + */ protected function vhostDaemon($connectionName, WorkerOptions $options) { + $this->logInfo('daemon.start'); + $lastRestart = $this->getTimestampOfLastQueueRestart(); - [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + $startTime = hrtime(true) / 1e9; + $this->totalJobsProcessed = 0; $connection = $this->startConsuming(); @@ -31,7 +41,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) // if it is we will just pause this worker for a given amount of time and // make sure we do not need to kill this worker process off completely. if (! $this->daemonShouldRun($this->workerOptions, $this->configConnectionName, $this->currentQueueName)) { - $this->output->info('Consuming pause worker...'); + $this->logInfo('daemon.consuming_pause_worker'); $this->pauseWorker($this->workerOptions, $lastRestart); @@ -75,7 +85,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) } if (null === $amqpMessage) { - $this->output->info('Consuming sleep. No job...'); + $this->logInfo('daemon.consuming_sleep_no_job'); $this->stopConsuming(); @@ -90,7 +100,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->processAmqpMessage($amqpMessage, $connection); if ($this->jobsProcessed >= $this->batchSize) { - $this->output->info('Consuming batch full...'); + $this->logInfo('daemon.consuming_batch_full'); $this->stopConsuming(); @@ -101,6 +111,24 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) continue; } + + // Finally, we will check to see if we have exceeded our memory limits or if + // the queue should restart based on other indications. If so, we'll stop + // this worker and let whatever is "monitoring" it restart the process. + $this->stopStatusCode = $this->getStopStatus( + $this->workerOptions, + $lastRestart, + $startTime, + $this->totalJobsProcessed, + true + ); + if (! is_null($this->stopStatusCode)) { + $this->logWarning('daemon.consuming_stop', [ + 'status_code' => $this->stopStatusCode, + ]); + + return $this->stop($this->stopStatusCode, $this->workerOptions); + } } } diff --git a/src/VhostsConsumers/QueueConsumer.php b/src/VhostsConsumers/QueueConsumer.php index 74076ae8..b9be89ea 100644 --- a/src/VhostsConsumers/QueueConsumer.php +++ b/src/VhostsConsumers/QueueConsumer.php @@ -20,11 +20,21 @@ class QueueConsumer extends AbstractVhostsConsumer { protected bool $hasJob = false; + /** + * @param $connectionName + * @param WorkerOptions $options + * @return int|void + * @throws \Salesmessage\LibRabbitMQ\Exceptions\MutexTimeout + * @throws \Throwable + */ protected function vhostDaemon($connectionName, WorkerOptions $options) { + $this->logInfo('daemon.start'); + $lastRestart = $this->getTimestampOfLastQueueRestart(); - [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + $startTime = hrtime(true) / 1e9; + $this->totalJobsProcessed = 0; $connection = $this->startConsuming(); @@ -33,7 +43,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) // if it is we will just pause this worker for a given amount of time and // make sure we do not need to kill this worker process off completely. if (! $this->daemonShouldRun($this->workerOptions, $this->configConnectionName, $this->currentQueueName)) { - $this->output->info('Consuming pause worker...'); + $this->logInfo('daemon.consuming_pause_worker'); $this->pauseWorker($this->workerOptions, $lastRestart); @@ -68,7 +78,9 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) // If no job is got off the queue, we will need to sleep the worker. if (false === $this->hasJob) { - $this->output->info('Consuming sleep. No job...'); + $this->logInfo('daemon.consuming_sleep_no_job', [ + 'sleep_seconds' => $this->workerOptions->sleep, + ]); $this->stopConsuming(); @@ -87,12 +99,12 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->workerOptions, $lastRestart, $startTime, - $jobsProcessed, + $this->totalJobsProcessed, $this->hasJob ); if (! is_null($this->stopStatusCode)) { - $this->logInfo('consuming_stop', [ - 'status' => $this->stopStatusCode, + $this->logWarning('daemon.consuming_stop', [ + 'status_code' => $this->stopStatusCode, ]); return $this->stop($this->stopStatusCode, $this->workerOptions); @@ -102,33 +114,6 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) } } - /** - * @param WorkerOptions $options - * @param $lastRestart - * @param $startTime - * @param $jobsProcessed - * @param $hasJob - * @return int|null - */ - protected function getStopStatus( - WorkerOptions $options, - $lastRestart, - $startTime = 0, - $jobsProcessed = 0, - bool $hasJob = false - ): ?int - { - return match (true) { - $this->shouldQuit => static::EXIT_SUCCESS, - $this->memoryExceeded($options->memory) => static::EXIT_MEMORY_LIMIT, - $this->queueShouldRestart($lastRestart) => static::EXIT_SUCCESS, - $options->stopWhenEmpty && !$hasJob => static::EXIT_SUCCESS, - $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => static::EXIT_SUCCESS, - $options->maxJobs && $jobsProcessed >= $options->maxJobs => static::EXIT_SUCCESS, - default => null - }; - } - /** * @return RabbitMQQueue * @throws Exceptions\MutexTimeout @@ -164,6 +149,10 @@ protected function startConsuming(): RabbitMQQueue } if ($this->workerOptions->rest > 0) { + $this->logInfo('startConsuming.rest', [ + 'rest_seconds' => $this->workerOptions->rest, + ]); + $this->sleep($this->workerOptions->rest); } }; From 661bf840211a743c6ed06dda34c8f23e7d9a6799 Mon Sep 17 00:00:00 2001 From: Vladislav Okan Date: Tue, 14 Oct 2025 08:39:22 +0300 Subject: [PATCH 27/32] SWR-20219: Remove auto-reject messages --- composer.json | 4 ++-- src/VhostsConsumers/DirectConsumer.php | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index b74f81ac..a759a59f 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.29-dev" + "dev-master": "1.30-dev" }, "laravel": { "providers": [ @@ -56,4 +56,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php index af3e7d67..72abfba7 100644 --- a/src/VhostsConsumers/DirectConsumer.php +++ b/src/VhostsConsumers/DirectConsumer.php @@ -51,9 +51,6 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) try { $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); $amqpMessage = $this->channel->basic_get($this->currentQueueName); - if (null !== $amqpMessage) { - $this->channel->basic_reject($amqpMessage->getDeliveryTag(), false); - } $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { $amqpMessage = null; From ab1a4ebac1322db2899e0bc6e53d687fab925753 Mon Sep 17 00:00:00 2001 From: Alexander Ginko <120381488+ahinkoneklo@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:33:59 +0100 Subject: [PATCH 28/32] SWR-20482 Server: RabbitMQ Improvements: Deduplication (#16) * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication --- README.md | 22 +- composer.json | 6 +- config/rabbitmq.php | 27 ++ src/Consumer.php | 14 +- src/Contracts/RabbitMQConsumable.php | 15 + src/Interfaces/RabbitMQBatchable.php | 11 +- src/LaravelLibRabbitMQServiceProvider.php | 51 ++- src/Queue/RabbitMQQueue.php | 2 + src/Queue/RabbitMQQueueBatchable.php | 12 +- src/Services/Api/RabbitApiClient.php | 4 +- .../Deduplication/AppDeduplicationService.php | 18 ++ .../TransportLevel/DeduplicationService.php | 294 ++++++++++++++++++ .../TransportLevel/DeduplicationStore.php | 12 + .../TransportLevel/NullDeduplicationStore.php | 18 ++ .../RedisDeduplicationStore.php | 51 +++ src/Services/DlqDetector.php | 22 ++ src/Services/InternalStorageManager.php | 5 +- .../AbstractVhostsConsumer.php | 158 +++++++--- src/VhostsConsumers/DirectConsumer.php | 18 +- src/VhostsConsumers/QueueConsumer.php | 31 +- 20 files changed, 685 insertions(+), 106 deletions(-) create mode 100644 src/Contracts/RabbitMQConsumable.php create mode 100644 src/Services/Deduplication/AppDeduplicationService.php create mode 100644 src/Services/Deduplication/TransportLevel/DeduplicationService.php create mode 100644 src/Services/Deduplication/TransportLevel/DeduplicationStore.php create mode 100644 src/Services/Deduplication/TransportLevel/NullDeduplicationStore.php create mode 100644 src/Services/Deduplication/TransportLevel/RedisDeduplicationStore.php create mode 100644 src/Services/DlqDetector.php diff --git a/README.md b/README.md index c4048569..ae38fac9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Only the latest version will get new features. Bug fixes will be provided using You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.29 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.31 --ignore-platform-reqs ``` The package will automatically register itself. @@ -663,7 +663,19 @@ if not all the issues with the following command: composer fix:style ``` -## Contribution - -You can contribute to this package by discovering bugs and opening issues. Please, add to which version of package you -create pull request or issue. (e.g. [5.2] Fatal error on delayed job) +## Local Setup +- Configure all config items in `config/queue.php` section `connections.rabbitmq_vhosts` (see as example [rabbitmq.php](./config/rabbitmq.php)) +- Create `yml` file in the project root with name `rabbit-groups.yml` and content, for example like this (you can replace `vhosts` and `queues` with `vhosts_mask` and `queues_mask`): +```yaml +groups: + test-notes: + vhosts: + - organization_200005 + queues: + - local-myname.notes.200005 + batch_size: 3 + prefetch_count: 3 +``` +- Make sure that vhosts exist in RabbitMQ (if not - create them) +- Run command `php artisan lib-rabbitmq:scan-vhosts` within your project where this library is installed (this command fetches data from RabbitMQ to Redis) +- Run command for consumer `php artisan lib-rabbitmq:consume-vhosts test-notes rabbitmq_vhosts --name=mq-vhost-test-local-notes --memory=300 --timeout=0 --max-jobs=1000 --max-time=600 --async-mode=1` diff --git a/composer.json b/composer.json index a759a59f..54c2c477 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.30-dev" + "dev-master": "1.31-dev" }, "laravel": { "providers": [ @@ -43,7 +43,9 @@ } }, "suggest": { - "ext-pcntl": "Required to use all features of the queue consumer." + "ext-pcntl": "Required to use all features of the queue consumer.", + "ext-swoole": "Required to use async mode for healthcheck (alternative is ext-openswoole).", + "ext-openswoole": "Required to use async mode for healthcheck (alternative is ext-swoole)." }, "scripts": { "test": [ diff --git a/config/rabbitmq.php b/config/rabbitmq.php index 8b949d9c..9b457a84 100644 --- a/config/rabbitmq.php +++ b/config/rabbitmq.php @@ -25,6 +25,33 @@ 'options' => [ ], + /** + * Provided on 2 levels: transport and application. + */ + 'deduplication' => [ + 'transport' => [ + 'enabled' => env('RABBITMQ_DEDUP_TRANSPORT_ENABLED', false), + 'ttl' => env('RABBITMQ_DEDUP_TRANSPORT_TTL', 7200), + 'lock_ttl' => env('RABBITMQ_DEDUP_TRANSPORT_LOCK_TTL', 60), + /** + * Possible: ack, reject + */ + 'action_on_duplication' => env('RABBITMQ_DEDUP_TRANSPORT_ACTION', 'ack'), + /** + * Possible: ack, reject, requeue + */ + 'action_on_lock' => env('RABBITMQ_DEDUP_TRANSPORT_LOCK_ACTION', 'requeue'), + 'connection' => [ + 'driver' => env('RABBITMQ_DEDUP_TRANSPORT_DRIVER', 'redis'), + 'name' => env('RABBITMQ_DEDUP_TRANSPORT_CONNECTION_NAME', 'persistent'), + 'key_prefix' => env('RABBITMQ_DEDUP_TRANSPORT_KEY_PREFIX', 'mq_dedup'), + ], + ], + 'application' => [ + 'enabled' => env('RABBITMQ_DEDUP_APP_ENABLED', true), + ], + ], + /* * Set to "horizon" if you wish to use Laravel Horizon. */ diff --git a/src/Consumer.php b/src/Consumer.php index 0f69893f..f78dc241 100644 --- a/src/Consumer.php +++ b/src/Consumer.php @@ -9,8 +9,9 @@ use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Exception\AMQPRuntimeException; use PhpAmqpLib\Message\AMQPMessage; -use Throwable; use Salesmessage\LibRabbitMQ\Queue\RabbitMQQueue; +use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\DeduplicationService; +use Throwable; class Consumer extends Worker { @@ -122,7 +123,16 @@ function (AMQPMessage $message) use ($connection, $options, $connectionName, $qu $jobsProcessed++; - $this->runJob($job, $connectionName, $options); + /** @var DeduplicationService $transportDedupService */ + $transportDedupService = $this->container->make(DeduplicationService::class); + $transportDedupService->decorateWithDeduplication( + function () use ($job, $message, $connectionName, $queue, $options, $transportDedupService) { + $this->runJob($job, $connectionName, $options); + $transportDedupService->markAsProcessed($message, $queue); + }, + $message, + $queue + ); if ($this->supportsAsyncSignals()) { $this->resetTimeoutHandler(); diff --git a/src/Contracts/RabbitMQConsumable.php b/src/Contracts/RabbitMQConsumable.php new file mode 100644 index 00000000..4e45e3ec --- /dev/null +++ b/src/Contracts/RabbitMQConsumable.php @@ -0,0 +1,15 @@ + $batch + * @return list + */ + public static function getNotDuplicatedBatchedJobs(array $batch): array; + /** * Processing jobs array of static class * - * @param array $batch - * @return mixed + * @param list $batch */ public static function collection(array $batch): void; } diff --git a/src/LaravelLibRabbitMQServiceProvider.php b/src/LaravelLibRabbitMQServiceProvider.php index 7048f289..d8451bdb 100644 --- a/src/LaravelLibRabbitMQServiceProvider.php +++ b/src/LaravelLibRabbitMQServiceProvider.php @@ -3,12 +3,6 @@ namespace Salesmessage\LibRabbitMQ; use Illuminate\Contracts\Debug\ExceptionHandler; -use Illuminate\Queue\Connectors\BeanstalkdConnector; -use Illuminate\Queue\Connectors\DatabaseConnector; -use Illuminate\Queue\Connectors\NullConnector; -use Illuminate\Queue\Connectors\RedisConnector; -use Illuminate\Queue\Connectors\SqsConnector; -use Illuminate\Queue\Connectors\SyncConnector; use Illuminate\Queue\QueueManager; use Illuminate\Support\ServiceProvider; use Psr\Log\LoggerInterface; @@ -16,6 +10,10 @@ use Salesmessage\LibRabbitMQ\Console\ConsumeVhostsCommand; use Salesmessage\LibRabbitMQ\Console\ScanVhostsCommand; use Salesmessage\LibRabbitMQ\Queue\Connectors\RabbitMQVhostsConnector; +use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\DeduplicationService; +use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\DeduplicationStore; +use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\NullDeduplicationStore; +use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\RedisDeduplicationStore; use Salesmessage\LibRabbitMQ\Services\GroupsService; use Salesmessage\LibRabbitMQ\Services\InternalStorageManager; use Salesmessage\LibRabbitMQ\Services\QueueService; @@ -36,6 +34,8 @@ public function register(): void ); if ($this->app->runningInConsole()) { + $this->bindDeduplicationService(); + $this->app->singleton('rabbitmq.consumer', function () { $isDownForMaintenance = function () { return $this->app->isDownForMaintenance(); @@ -68,7 +68,8 @@ public function register(): void $this->app['events'], $this->app[ExceptionHandler::class], $isDownForMaintenance, - null + $this->app->get(DeduplicationService::class), + null, ); }); @@ -84,7 +85,8 @@ public function register(): void $this->app['events'], $this->app[ExceptionHandler::class], $isDownForMaintenance, - null + $this->app->get(DeduplicationService::class), + null, ); }); @@ -92,7 +94,7 @@ public function register(): void $consumerClass = ('direct' === config('queue.connections.rabbitmq_vhosts.consumer_type')) ? VhostsDirectConsumer::class : VhostsQueueConsumer::class; - + return new ConsumeVhostsCommand( $app[GroupsService::class], $app[$consumerClass], @@ -139,4 +141,35 @@ public function boot(): void return new RabbitMQVhostsConnector($this->app['events']); }); } + + /** + * Config params: + * @phpstan-import-type DeduplicationConfig from DeduplicationService + * + * @return void + */ + private function bindDeduplicationService(): void + { + $this->app->bind(DeduplicationStore::class, static function () { + /** @var DeduplicationConfig $config */ + $config = (array) config('queue.connections.rabbitmq_vhosts.deduplication.transport', []); + $enabled = (bool) ($config['enabled'] ?? false); + if (!$enabled) { + return new NullDeduplicationStore(); + } + + $connectionDriver = $config['connection']['driver'] ?? null; + if ($connectionDriver !== 'redis') { + throw new \InvalidArgumentException('For now only Redis connection is supported for deduplication'); + } + $connectionName = $config['connection']['name'] ?? null; + + $prefix = trim($config['connection']['key_prefix'] ?? ''); + if (empty($prefix)) { + throw new \InvalidArgumentException('Key prefix is required'); + } + + return new RedisDeduplicationStore($connectionName, $prefix); + }); + } } diff --git a/src/Queue/RabbitMQQueue.php b/src/Queue/RabbitMQQueue.php index f0973d75..00336fe6 100644 --- a/src/Queue/RabbitMQQueue.php +++ b/src/Queue/RabbitMQQueue.php @@ -22,6 +22,7 @@ use PhpAmqpLib\Exchange\AMQPExchangeType; use PhpAmqpLib\Message\AMQPMessage; use PhpAmqpLib\Wire\AMQPTable; +use Ramsey\Uuid\Uuid; use RuntimeException; use Throwable; use Salesmessage\LibRabbitMQ\Contracts\RabbitMQQueueContract; @@ -521,6 +522,7 @@ protected function createMessage($payload, int $attempts = 0): array $currentPayload = json_decode($payload, true); if ($correlationId = $currentPayload['id'] ?? null) { $properties['correlation_id'] = $correlationId; + $properties['message_id'] = Uuid::uuid7()->toString(); } if ($this->getConfig()->isPrioritizeDelayed()) { diff --git a/src/Queue/RabbitMQQueueBatchable.php b/src/Queue/RabbitMQQueueBatchable.php index a596a6fc..00399cf5 100644 --- a/src/Queue/RabbitMQQueueBatchable.php +++ b/src/Queue/RabbitMQQueueBatchable.php @@ -3,6 +3,7 @@ namespace Salesmessage\LibRabbitMQ\Queue; use PhpAmqpLib\Connection\AbstractConnection; +use Salesmessage\LibRabbitMQ\Contracts\RabbitMQConsumable; use Salesmessage\LibRabbitMQ\Dto\ConnectionNameDto; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; use Salesmessage\LibRabbitMQ\Dto\VhostApiDto; @@ -77,7 +78,16 @@ protected function createChannel(): AMQPChannel public function push($job, $data = '', $queue = null) { - $queue = $queue ?: $job->onQueue(); + if (!($job instanceof RabbitMQConsumable)) { + throw new \InvalidArgumentException('Job must implement RabbitMQConsumable'); + } + + if (!$queue) { + if (!method_exists($job, 'onQueue')) { + throw new \InvalidArgumentException('Job must implement onQueue method'); + } + $queue = $job->onQueue(); + } try { $result = parent::push($job, $data, $queue); diff --git a/src/Services/Api/RabbitApiClient.php b/src/Services/Api/RabbitApiClient.php index dfd16f1e..0f8fd3bc 100644 --- a/src/Services/Api/RabbitApiClient.php +++ b/src/Services/Api/RabbitApiClient.php @@ -70,7 +70,7 @@ public function request( $contents = $response->getBody()->getContents(); return (array) ($contents ? json_decode($contents, true) : []); - } catch (Throwable $exception) { + } catch (\Throwable $exception) { $rethrowException = $exception; if ($exception instanceof ClientException) { $rethrowException = new RabbitApiClientException($exception->getMessage()); @@ -109,4 +109,4 @@ private function getPassword(): string { return (string) ($this->connectionConfig['hosts'][0]['password'] ?? ''); } -} \ No newline at end of file +} diff --git a/src/Services/Deduplication/AppDeduplicationService.php b/src/Services/Deduplication/AppDeduplicationService.php new file mode 100644 index 00000000..2187557d --- /dev/null +++ b/src/Services/Deduplication/AppDeduplicationService.php @@ -0,0 +1,18 @@ +getState($message, $queueName); + try { + if ($messageState === DeduplicationService::IN_PROGRESS) { + $action = $this->applyActionOnLock($message); + $this->logger->warning('DeduplicationService.message_already_in_progress', [ + 'action' => $action, + 'message_id' => $message->get_properties()['message_id'] ?? null, + ]); + + return false; + } + + if ($messageState === DeduplicationService::PROCESSED) { + $action = $this->applyActionOnDuplication($message); + $this->logger->warning('DeduplicationService.message_already_processed', [ + 'action' => $action, + 'message_id' => $message->get_properties()['message_id'] ?? null, + ]); + + return false; + } + + $hasPutAsInProgress = $this->markAsInProgress($message, $queueName); + if ($hasPutAsInProgress === false) { + $action = $this->applyActionOnLock($message); + $this->logger->warning('DeduplicationService.message_already_in_progress.skip', [ + 'action' => $action, + 'message_id' => $message->get_properties()['message_id'] ?? null, + ]); + + return false; + } + + $handler(); + } catch (\Throwable $exception) { + if ($messageState === null) { + $this->release($message, $queueName); + } + + $this->logger->error('DeduplicationService.message_processing_exception', [ + 'released_message_id' => $message->get_properties()['message_id'] ?? null, + ]); + + throw $exception; + } + + return true; + } + + /** + * @param AMQPMessage $message + * @return string|null - @enum {self::IN_PROGRESS, self::PROCESSED} + */ + public function getState(AMQPMessage $message, ?string $queueName = null): ?string + { + if (!$this->isEnabled()) { + return null; + } + $messageId = $this->getMessageId($message, $queueName); + if ($messageId === null) { + return null; + } + + return $this->store->get($messageId); + } + + public function markAsProcessed(AMQPMessage $message, ?string $queueName = null): bool + { + $ttl = (int) ($this->getConfig('ttl') ?: self::DEFAULT_TTL); + if ($ttl <= 0 || $ttl > self::MAX_TTL) { + throw new \InvalidArgumentException(sprintf('Invalid TTL seconds. Should be between 1 sec and %d sec', self::MAX_TTL)); + } + + return $this->add($message, self::PROCESSED, $ttl, $queueName); + } + + public function release(AMQPMessage $message, ?string $queueName = null): void + { + if (!$this->isEnabled()) { + return; + } + + $messageId = $this->getMessageId($message, $queueName); + if ($messageId === null) { + return; + } + + $this->store->release($messageId); + } + + protected function markAsInProgress(AMQPMessage $message, ?string $queueName = null): bool + { + $ttl = (int) ($this->getConfig('lock_ttl') ?: self::DEFAULT_LOCK_TTL); + if ($ttl <= 0 || $ttl > self::MAX_LOCK_TTL) { + throw new \InvalidArgumentException(sprintf('Invalid TTL seconds. Should be between 1 and %d', self::MAX_LOCK_TTL)); + } + + return $this->add($message, self::IN_PROGRESS, $ttl, $queueName); + } + + /** + * Returns "true" if the message was not processed previously, and it's successfully been added to the store. + * Returns "false" if the message was already processed and it's a duplicate. + * + * @param AMQPMessage $message + * @param string $value + * @param int $ttl + * @return bool + */ + protected function add(AMQPMessage $message, string $value, int $ttl, ?string $queueName = null): bool + { + if (!$this->isEnabled()) { + return true; + } + + $messageId = $this->getMessageId($message, $queueName); + if ($messageId === null) { + return true; + } + + return $this->store->set($messageId, $value, $ttl, $value === self::PROCESSED); + } + + protected function getMessageId(AMQPMessage $message, ?string $queueName = null): ?string + { + $props = $message->get_properties(); + $messageId = $props['message_id'] ?? null; + if (!is_string($messageId) || empty($messageId)) { + return null; + } + + if (DlqDetector::isDlqMessage($message)) { + $messageId = 'dlq:' . $messageId; + } + + if (is_string($queueName) && $queueName !== '') { + $messageId = $queueName . ':' . $messageId; + } + + return $messageId; + } + + protected function applyActionOnLock(AMQPMessage $message): string + { + $action = $this->getConfig('action_on_lock', self::ACTION_REQUEUE); + if ($action === self::ACTION_REJECT) { + $message->reject(false); + } elseif ($action === self::ACTION_ACK) { + $message->ack(); + } else { + $action = $this->republishLockedMessage($message); + } + + return $action; + } + + protected function applyActionOnDuplication(AMQPMessage $message): string + { + $action = $this->getConfig('action_on_duplication', self::ACTION_ACK); + if ($action === self::ACTION_REJECT) { + $message->reject(false); + } else { + $message->ack(); + } + + return $action; + } + + /** + * Such a situation normally should not happen or can happen very rarely. + * Republish the locked message with a retry-count guard. + * It's necessary to avoid infinite redelivery loop. + * + * @param AMQPMessage $message + * @return string + */ + protected function republishLockedMessage(AMQPMessage $message): string + { + $props = $message->get_properties(); + $headers = []; + if (($props['application_headers'] ?? null) instanceof AMQPTable) { + $headers = $props['application_headers']->getNativeData(); + } + + $attempts = (int) ($headers[self::HEADER_LOCK_REQUEUE_COUNT] ?? 0); + ++$attempts; + + $maxAttempts = ((int) ($this->getConfig('lock_ttl', 30))) / self::WAIT_AFTER_PUBLISH; + if ($attempts > $maxAttempts) { + $this->logger->warning('DeduplicationService.republishLockedMessage.max_attempts_reached', [ + 'message_id' => $props['message_id'] ?? null, + ]); + $message->ack(); + + return self::ACTION_ACK; + } + + $headers[self::HEADER_LOCK_REQUEUE_COUNT] = $attempts; + + $newProps = $props; + $newProps['application_headers'] = new AMQPTable($headers); + + $newMessage = new AMQPMessage($message->getBody(), $newProps); + $channel = $message->getChannel(); + $channel->basic_publish($newMessage, $message->getExchange(), $message->getRoutingKey()); + + $this->logger->warning('DeduplicationService.republishLockedMessage.republish', [ + 'message_id' => $props['message_id'] ?? null, + 'attempts' => $attempts, + ]); + $message->ack(); + // it's necessary to avoid a high redelivery rate + // normally, such a situation is not expected (or expected very rarely) + sleep(self::WAIT_AFTER_PUBLISH); + + return self::ACTION_REQUEUE; + } + + protected function isEnabled(): bool + { + return (bool) $this->getConfig('enabled', false); + } + + protected function getConfig(string $key, mixed $default = null): mixed + { + $value = config("queue.connections.rabbitmq_vhosts.deduplication.transport.$key"); + + return $value !== null ? $value : $default; + } +} diff --git a/src/Services/Deduplication/TransportLevel/DeduplicationStore.php b/src/Services/Deduplication/TransportLevel/DeduplicationStore.php new file mode 100644 index 00000000..014e639f --- /dev/null +++ b/src/Services/Deduplication/TransportLevel/DeduplicationStore.php @@ -0,0 +1,12 @@ +getKey($messageKey); + return $this->connection()->get($key); + } + + public function set(string $messageKey, mixed $value, int $ttlSeconds, bool $withOverride = false): bool + { + if ($ttlSeconds <= 0) { + throw new \InvalidArgumentException('Invalid TTL seconds. Should be greater than 0.'); + } + + $key = $this->getKey($messageKey); + $args = [$key, $value, 'EX', $ttlSeconds]; + if (!$withOverride) { + $args[] = 'NX'; + } + + return (bool) $this->connection()->set(...$args); + } + + public function release(string $messageKey): void + { + $key = $this->getKey($messageKey); + $this->connection()->del($key); + } + + protected function connection(): Connection + { + return $this->connectionName ? Redis::connection($this->connectionName) : Redis::connection(); + } + + protected function getKey(string $messageKey): string + { + return $this->keyPrefix . ':' . $messageKey; + } +} diff --git a/src/Services/DlqDetector.php b/src/Services/DlqDetector.php new file mode 100644 index 00000000..997fb2f7 --- /dev/null +++ b/src/Services/DlqDetector.php @@ -0,0 +1,22 @@ +get_properties()['application_headers'] ?? null; + + if (!($headersTable instanceof AMQPTable)) { + return false; + } + + $headers = $headersTable->getNativeData(); + + return !empty($headers['x-death']) && !empty($headers['x-opt-deaths']); + } +} diff --git a/src/Services/InternalStorageManager.php b/src/Services/InternalStorageManager.php index 9e0cd8a0..2708f859 100644 --- a/src/Services/InternalStorageManager.php +++ b/src/Services/InternalStorageManager.php @@ -4,6 +4,7 @@ use Illuminate\Redis\Connections\PredisConnection; use Illuminate\Support\Facades\Redis; +use Illuminate\Support\Str; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; use Salesmessage\LibRabbitMQ\Dto\VhostApiDto; @@ -38,7 +39,7 @@ public function getVhosts(string $by = 'name', bool $alpha = true): array 'sort' => 'asc', ]); - return array_map(fn($value): string => str_replace_first( + return array_map(fn($value): string => Str::replaceFirst( $this->getVhostStorageKeyPrefix(), '', $value @@ -61,7 +62,7 @@ public function getVhostQueues(string $vhostName, string $by = 'name', bool $alp 'sort' => 'asc', ]); - return array_map(fn($value): string => str_replace_first( + return array_map(fn($value): string => Str::replaceFirst( $this->getQueueStorageKeyPrefix($vhostName), '', $value diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index 7d99e10e..ff30cf41 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -12,11 +12,10 @@ use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Exception\AMQPChannelClosedException; use PhpAmqpLib\Exception\AMQPConnectionClosedException; -use PhpAmqpLib\Exception\AMQPProtocolChannelException; -use PhpAmqpLib\Exception\AMQPRuntimeException; use PhpAmqpLib\Message\AMQPMessage; use Psr\Log\LoggerInterface; use Salesmessage\LibRabbitMQ\Consumer; +use Salesmessage\LibRabbitMQ\Contracts\RabbitMQConsumable; use Salesmessage\LibRabbitMQ\Dto\ConnectionNameDto; use Salesmessage\LibRabbitMQ\Dto\ConsumeVhostsFiltersDto; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; @@ -26,6 +25,8 @@ use Salesmessage\LibRabbitMQ\Mutex; use Salesmessage\LibRabbitMQ\Queue\Jobs\RabbitMQJob; use Salesmessage\LibRabbitMQ\Queue\RabbitMQQueue; +use Salesmessage\LibRabbitMQ\Services\Deduplication\AppDeduplicationService; +use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\DeduplicationService as TransportDeduplicationService; use Salesmessage\LibRabbitMQ\Services\InternalStorageManager; abstract class AbstractVhostsConsumer extends Consumer @@ -54,6 +55,7 @@ abstract class AbstractVhostsConsumer extends Consumer protected ?WorkerOptions $workerOptions = null; + /** @var array, array> */ protected array $batchMessages = []; protected ?string $processingUuid = null; @@ -72,6 +74,8 @@ abstract class AbstractVhostsConsumer extends Consumer protected bool $asyncMode = false; + protected ?Mutex $connectionMutex = null; + /** * @param InternalStorageManager $internalStorageManager * @param LoggerInterface $logger @@ -79,6 +83,7 @@ abstract class AbstractVhostsConsumer extends Consumer * @param Dispatcher $events * @param ExceptionHandler $exceptions * @param callable $isDownForMaintenance + * @param TransportDeduplicationService $transportDeduplicationService * @param callable|null $resetScope */ public function __construct( @@ -88,7 +93,8 @@ public function __construct( Dispatcher $events, ExceptionHandler $exceptions, callable $isDownForMaintenance, - callable $resetScope = null + protected TransportDeduplicationService $transportDeduplicationService, + callable $resetScope = null, ) { parent::__construct($manager, $events, $exceptions, $isDownForMaintenance, $resetScope); } @@ -223,7 +229,7 @@ protected function getStopStatus( /** * @return RabbitMQQueue - * @throws Exceptions\MutexTimeout + * @throws MutexTimeout */ abstract protected function startConsuming(): RabbitMQQueue; @@ -240,7 +246,7 @@ protected function processAmqpMessage(AMQPMessage $message, RabbitMQQueue $conne $this->addMessageToBatch($message); } else { $job = $this->getJobByMessage($message, $connection); - $this->processSingleJob($job); + $this->processSingleJob($job, $message); } $this->jobsProcessed++; @@ -262,18 +268,23 @@ protected function generateProcessingUuid(): string /** * @param AMQPMessage $message - * @return string + * @return non-empty-string */ protected function getMessageClass(AMQPMessage $message): string { $body = json_decode($message->getBody(), true); - return (string) ($body['data']['commandName'] ?? ''); + $messageClass = (string) ($body['data']['commandName'] ?? ''); + if (empty($messageClass)) { + throw new \RuntimeException('Message class is not defined'); + } + return $messageClass; } /** - * @param RabbitMQJob $job - * @return void + * @param AMQPMessage $message + * @return bool + * @throws \ReflectionException */ protected function isSupportBatching(AMQPMessage $message): bool { @@ -296,8 +307,8 @@ protected function addMessageToBatch(AMQPMessage $message): void /** * @param RabbitMQQueue $connection * @return void - * @throws Exceptions\MutexTimeout - * @throws Throwable + * @throws MutexTimeout + * @throws \Throwable */ protected function processBatch(RabbitMQQueue $connection): void { @@ -307,33 +318,52 @@ protected function processBatch(RabbitMQQueue $connection): void foreach ($this->batchMessages as $batchJobClass => $batchJobMessages) { $isBatchSuccess = false; - $batchSize = count($batchJobMessages); + if ($batchSize > 1) { $batchTimeStarted = microtime(true); + $uniqueMessagesForProcessing = []; $batchData = []; - /** @var AMQPMessage $batchMessage */ foreach ($batchJobMessages as $batchMessage) { - $job = $this->getJobByMessage($batchMessage, $connection); - $batchData[] = $job->getPayloadData(); + $this->transportDeduplicationService->decorateWithDeduplication( + function () use ($batchMessage, $connection, &$uniqueMessagesForProcessing, &$batchData) { + $job = $this->getJobByMessage($batchMessage, $connection); + $uniqueMessagesForProcessing[] = $batchMessage; + $batchData[] = $job->getPayloadData(); + }, + $batchMessage, + $this->currentQueueName + ); } - $this->logInfo('processBatch.start', [ - 'batch_job_class' => $batchJobClass, - 'batch_size' => $batchSize, - ]); - try { - $batchJobClass::collection($batchData); + if (AppDeduplicationService::isEnabled()) { + /** @var RabbitMQBatchable $batchJobClass */ + $batchData = $batchJobClass::getNotDuplicatedBatchedJobs($batchData); + } + + if (!empty($batchData)) { + $this->logInfo('processBatch.start', [ + 'batch_job_class' => $batchJobClass, + 'batch_size' => $batchSize, + ]); + + $batchJobClass::collection($batchData); + + $this->logInfo('processBatch.finish', [ + 'batch_job_class' => $batchJobClass, + 'batch_size' => $batchSize, + 'executive_batch_time_seconds' => microtime(true) - $batchTimeStarted, + ]); + } + $isBatchSuccess = true; + } catch (\Throwable $exception) { + foreach ($uniqueMessagesForProcessing as $batchMessage) { + $this->transportDeduplicationService->release($batchMessage, $this->currentQueueName); + } - $this->logInfo('processBatch.finish', [ - 'batch_job_class' => $batchJobClass, - 'batch_size' => $batchSize, - 'executive_batch_time_seconds' => microtime(true) - $batchTimeStarted, - ]); - } catch (Throwable $exception) { $isBatchSuccess = false; $this->logError('processBatch.exception', [ @@ -345,19 +375,28 @@ protected function processBatch(RabbitMQQueue $connection): void } unset($batchData); + } else { + $uniqueMessagesForProcessing = $batchJobMessages; } $this->connectionMutex->lock(static::MAIN_HANDLER_LOCK); - if ($isBatchSuccess) { - $lastBatchMessage = end($batchJobMessages); - $this->ackMessage($lastBatchMessage, true); - } else { - foreach ($batchJobMessages as $batchMessage) { - $job = $this->getJobByMessage($batchMessage, $connection); - $this->processSingleJob($job); + try { + if ($isBatchSuccess && !empty($uniqueMessagesForProcessing)) { + foreach ($uniqueMessagesForProcessing as $batchMessage) { + $this->transportDeduplicationService?->markAsProcessed($batchMessage, $this->currentQueueName); + } + + $lastBatchMessage = end($uniqueMessagesForProcessing); + $this->ackMessage($lastBatchMessage, true); + } else { + foreach ($uniqueMessagesForProcessing as $batchMessage) { + $job = $this->getJobByMessage($batchMessage, $connection); + $this->processSingleJob($job, $batchMessage); + } } + } finally { + $this->connectionMutex->unlock(static::MAIN_HANDLER_LOCK); } - $this->connectionMutex->unlock(static::MAIN_HANDLER_LOCK); } $this->updateLastProcessedAt(); @@ -368,26 +407,28 @@ protected function processBatch(RabbitMQQueue $connection): void * @param AMQPMessage $message * @param RabbitMQQueue $connection * @return RabbitMQJob - * @throws Throwable + * @throws \Throwable */ protected function getJobByMessage(AMQPMessage $message, RabbitMQQueue $connection): RabbitMQJob { $jobClass = $connection->getJobClass(); - return new $jobClass( + $job = new $jobClass( $this->container, $connection, $message, $this->currentConnectionName, $this->currentQueueName ); + + if (!is_subclass_of($job->getPayloadClass(), RabbitMQConsumable::class)) { + throw new \RuntimeException(sprintf('Job class %s must implement %s', $job->getPayloadClass(), RabbitMQConsumable::class)); + } + + return $job; } - /** - * @param RabbitMQJob $job - * @return void - */ - protected function processSingleJob(RabbitMQJob $job): void + protected function processSingleJob(RabbitMQJob $job, AMQPMessage $message): void { $timeStarted = microtime(true); $this->logInfo('processSingleJob.start'); @@ -396,7 +437,22 @@ protected function processSingleJob(RabbitMQJob $job): void $this->registerTimeoutHandler($job, $this->workerOptions); } - $this->runJob($job, $this->currentConnectionName, $this->workerOptions); + $this->transportDeduplicationService->decorateWithDeduplication( + function () use ($job, $message) { + if (AppDeduplicationService::isEnabled() && $job->getPayloadData()->isDuplicated()) { + $this->logWarning('processSingleJob.job_is_duplicated'); + $this->ackMessage($message); + + } else { + $this->runJob($job, $this->currentConnectionName, $this->workerOptions); + } + + $this->transportDeduplicationService->markAsProcessed($message, $this->currentQueueName); + }, + $message, + $this->currentQueueName, + ); + $this->updateLastProcessedAt(); if ($this->supportsAsyncSignals()) { @@ -421,7 +477,7 @@ protected function ackMessage(AMQPMessage $message, bool $multiple = false): voi try { $message->ack($multiple); - } catch (Throwable $exception) { + } catch (\Throwable $exception) { $this->logError('ackMessage.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), @@ -432,7 +488,7 @@ protected function ackMessage(AMQPMessage $message, bool $multiple = false): voi /** * @return void - * @throws Exceptions\MutexTimeout + * @throws MutexTimeout */ abstract protected function stopConsuming(): void; @@ -631,7 +687,7 @@ protected function goAhead(): bool /** * @return void */ - protected function updateLastProcessedAt() + protected function updateLastProcessedAt(): void { if ((null === $this->currentVhostName) || (null === $this->currentQueueName)) { return; @@ -690,7 +746,6 @@ protected function initConnection(): RabbitMQQueue $this->prefetchCount, false ); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); $this->channel = $channel; $this->connection = $connection; @@ -709,6 +764,8 @@ protected function initConnection(): RabbitMQQueue $this->goAheadOrWait(); return $this->initConnection(); + } finally { + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } return $connection; @@ -725,6 +782,7 @@ protected function startHeartbeatCheck(): void $heartbeatInterval = (int) ($this->config['options']['heartbeat'] ?? 0); if (!$heartbeatInterval) { + $this->logWarning('startHeartbeatCheck.heartbeat_interval_is_not_set'); return; } @@ -764,10 +822,10 @@ protected function startHeartbeatCheck(): void $connection->checkHeartBeat(); } catch (MutexTimeout) { $this->logWarning('startHeartbeatCheck.mutex_timeout'); - } catch (Throwable $exception) { + } catch (\Throwable $exception) { $this->logError('startHeartbeatCheck.exception', [ - 'eroor' => $exception->getMessage(), - 'trace' => $e->getTraceAsString(), + 'error' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), ]); $this->shouldQuit = true; diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php index 72abfba7..a17322a2 100644 --- a/src/VhostsConsumers/DirectConsumer.php +++ b/src/VhostsConsumers/DirectConsumer.php @@ -2,18 +2,10 @@ namespace Salesmessage\LibRabbitMQ\VhostsConsumers; -use Illuminate\Console\OutputStyle; -use Illuminate\Contracts\Debug\ExceptionHandler; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; -use Illuminate\Support\Str; use PhpAmqpLib\Exception\AMQPChannelClosedException; -use PhpAmqpLib\Exception\AMQPConnectionClosedException; use PhpAmqpLib\Exception\AMQPProtocolChannelException; use PhpAmqpLib\Exception\AMQPRuntimeException; -use PhpAmqpLib\Message\AMQPMessage; -use Psr\Log\LoggerInterface; use Salesmessage\LibRabbitMQ\Queue\RabbitMQQueue; class DirectConsumer extends AbstractVhostsConsumer @@ -22,7 +14,6 @@ class DirectConsumer extends AbstractVhostsConsumer * @param $connectionName * @param WorkerOptions $options * @return int - * @throws \Salesmessage\LibRabbitMQ\Exceptions\MutexTimeout * @throws \Throwable */ protected function vhostDaemon($connectionName, WorkerOptions $options) @@ -51,7 +42,6 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) try { $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); $amqpMessage = $this->channel->basic_get($this->currentQueueName); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } catch (AMQPProtocolChannelException|AMQPChannelClosedException $exception) { $amqpMessage = null; @@ -69,7 +59,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->exceptions->report($exception); $this->kill(self::EXIT_SUCCESS, $this->workerOptions); - } catch (Exception|Throwable $exception) { + } catch (\Throwable $exception) { $this->logError('daemon.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), @@ -79,9 +69,11 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->exceptions->report($exception); $this->stopWorkerIfLostConnection($exception); + } finally { + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } - if (null === $amqpMessage) { + if (!isset($amqpMessage)) { $this->logInfo('daemon.consuming_sleep_no_job'); $this->stopConsuming(); @@ -131,7 +123,6 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) /** * @return RabbitMQQueue - * @throws Exceptions\MutexTimeout */ protected function startConsuming(): RabbitMQQueue { @@ -154,4 +145,3 @@ protected function stopConsuming(): void return; } } - diff --git a/src/VhostsConsumers/QueueConsumer.php b/src/VhostsConsumers/QueueConsumer.php index b9be89ea..700bf8a1 100644 --- a/src/VhostsConsumers/QueueConsumer.php +++ b/src/VhostsConsumers/QueueConsumer.php @@ -2,18 +2,12 @@ namespace Salesmessage\LibRabbitMQ\VhostsConsumers; -use Illuminate\Console\OutputStyle; -use Illuminate\Contracts\Debug\ExceptionHandler; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; -use Illuminate\Support\Str; use PhpAmqpLib\Exception\AMQPChannelClosedException; -use PhpAmqpLib\Exception\AMQPConnectionClosedException; use PhpAmqpLib\Exception\AMQPProtocolChannelException; use PhpAmqpLib\Exception\AMQPRuntimeException; use PhpAmqpLib\Message\AMQPMessage; -use Psr\Log\LoggerInterface; +use Salesmessage\LibRabbitMQ\Exceptions\MutexTimeout; use Salesmessage\LibRabbitMQ\Queue\RabbitMQQueue; class QueueConsumer extends AbstractVhostsConsumer @@ -24,7 +18,7 @@ class QueueConsumer extends AbstractVhostsConsumer * @param $connectionName * @param WorkerOptions $options * @return int|void - * @throws \Salesmessage\LibRabbitMQ\Exceptions\MutexTimeout + * @throws MutexTimeout * @throws \Throwable */ protected function vhostDaemon($connectionName, WorkerOptions $options) @@ -54,7 +48,6 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) try { $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); $this->channel->wait(null, true, (int) $this->workerOptions->timeout); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } catch (AMQPRuntimeException $exception) { $this->logError('daemon.amqp_runtime_exception', [ 'message' => $exception->getMessage(), @@ -64,7 +57,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->exceptions->report($exception); $this->kill(self::EXIT_SUCCESS, $this->workerOptions); - } catch (Exception|Throwable $exception) { + } catch (\Throwable $exception) { $this->logError('daemon.exception', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), @@ -74,6 +67,8 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->exceptions->report($exception); $this->stopWorkerIfLostConnection($exception); + } finally { + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } // If no job is got off the queue, we will need to sleep the worker. @@ -116,7 +111,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) /** * @return RabbitMQQueue - * @throws Exceptions\MutexTimeout + * @throws MutexTimeout */ protected function startConsuming(): RabbitMQQueue { @@ -180,10 +175,10 @@ protected function startConsuming(): RabbitMQQueue 'trace' => $exception->getTraceAsString(), 'error_class' => get_class($exception), ]); + } finally { + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); } - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); - $this->updateLastProcessedAt(); if (false === $isSuccess) { @@ -198,13 +193,15 @@ protected function startConsuming(): RabbitMQQueue /** * @return void - * @throws Exceptions\MutexTimeout + * @throws MutexTimeout */ protected function stopConsuming(): void { $this->connectionMutex->lock(self::MAIN_HANDLER_LOCK); - $this->channel->basic_cancel($this->getTagName(), true); - $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + try { + $this->channel->basic_cancel($this->getTagName(), true); + } finally { + $this->connectionMutex->unlock(self::MAIN_HANDLER_LOCK); + } } } - From 820e511f9db0598effb9d7201f0f9aa5e5fda7c6 Mon Sep 17 00:00:00 2001 From: Alexander Ginko <120381488+ahinkoneklo@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:17:33 +0100 Subject: [PATCH 29/32] SWR-20483 Server: RabbitMQ Improvements: Quorum Queues (#17) * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20482 Server: RabbitMQ Improvements: Deduplication * SWR-20483 Server: RabbitMQ Improvements: Quorum Queues * SWR-20483 Server: RabbitMQ Improvements: Quorum Queues * SWR-20483 Server: RabbitMQ Improvements: Quorum Queues --- README.md | 2 +- composer.json | 2 +- src/Console/QueueDeclareCommand.php | 25 ++++++--- src/Contracts/RabbitMQConsumable.php | 5 ++ src/Horizon/RabbitMQQueue.php | 9 +++- src/Queue/Jobs/RabbitMQJob.php | 42 ++++++++++++++- src/Queue/Jobs/RabbitMQJobBatchable.php | 48 ----------------- src/Queue/QueueConfig.php | 39 ++++++++++++++ src/Queue/QueueConfigFactory.php | 9 ++++ src/Queue/RabbitMQQueue.php | 71 +++++++++++++++++++------ src/Queue/RabbitMQQueueBatchable.php | 14 ++--- 11 files changed, 184 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index ae38fac9..0c6c9cb2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Only the latest version will get new features. Bug fixes will be provided using You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.31 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.32 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 54c2c477..ff7d373d 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.31-dev" + "dev-master": "1.32-dev" }, "laravel": { "providers": [ diff --git a/src/Console/QueueDeclareCommand.php b/src/Console/QueueDeclareCommand.php index 396877ac..24e3c5d0 100644 --- a/src/Console/QueueDeclareCommand.php +++ b/src/Console/QueueDeclareCommand.php @@ -11,10 +11,11 @@ class QueueDeclareCommand extends Command protected $signature = 'lib-rabbitmq:queue-declare {name : The name of the queue to declare} {connection=rabbitmq : The name of the queue connection to use} - {--max-priority} + {--max-priority : Set x-max-priority (ignored for quorum)} {--durable=1} {--auto-delete=0} - {--quorum=0}'; + {--quorum=0 : Declare quorum queue (x-queue-type=quorum)} + {--quorum-initial-group-size= : x-quorum-initial-group-size when quorum is enabled}'; protected $description = 'Declare queue'; @@ -36,12 +37,24 @@ public function handle(RabbitMQConnector $connector): void $arguments = []; $maxPriority = (int) $this->option('max-priority'); - if ($maxPriority) { - $arguments['x-max-priority'] = $maxPriority; - } + $isQuorum = (bool) $this->option('quorum'); - if ($this->option('quorum')) { + if ($isQuorum) { $arguments['x-queue-type'] = 'quorum'; + + $initialGroupSize = (int) $this->option('quorum-initial-group-size'); + if ($initialGroupSize > 0) { + $arguments['x-quorum-initial-group-size'] = $initialGroupSize; + } + + if ($maxPriority) { + // quorum queues do not support priority; ignore and warn + $this->warn('Ignoring --max-priority for quorum queue.'); + } + } else { + if ($maxPriority) { + $arguments['x-max-priority'] = $maxPriority; + } } $queue->declareQueue( diff --git a/src/Contracts/RabbitMQConsumable.php b/src/Contracts/RabbitMQConsumable.php index 4e45e3ec..62364e27 100644 --- a/src/Contracts/RabbitMQConsumable.php +++ b/src/Contracts/RabbitMQConsumable.php @@ -7,9 +7,14 @@ */ interface RabbitMQConsumable { + public const MQ_TYPE_CLASSIC = 'classic'; + public const MQ_TYPE_QUORUM = 'quorum'; + /** * Check duplications on the application side. * It's mostly represented as an idempotency checker. */ public function isDuplicated(): bool; + + public function getQueueType(): string; } diff --git a/src/Horizon/RabbitMQQueue.php b/src/Horizon/RabbitMQQueue.php index 31d329d1..c98eb65e 100644 --- a/src/Horizon/RabbitMQQueue.php +++ b/src/Horizon/RabbitMQQueue.php @@ -12,6 +12,7 @@ use PhpAmqpLib\Exception\AMQPProtocolChannelException; use Salesmessage\LibRabbitMQ\Queue\Jobs\RabbitMQJob; use Salesmessage\LibRabbitMQ\Queue\RabbitMQQueue as BaseRabbitMQQueue; +use Salesmessage\LibRabbitMQ\Contracts\RabbitMQConsumable; class RabbitMQQueue extends BaseRabbitMQQueue { @@ -50,6 +51,10 @@ public function pushRaw($payload, $queue = null, array $options = []): int|strin { $payload = (new JobPayload($payload))->prepare($this->lastPushed ?? null)->value; + if (!isset($options['queue_type']) && isset($this->lastPushed) && is_object($this->lastPushed) && $this->lastPushed instanceof RabbitMQConsumable) { + $options['queue_type'] = $this->lastPushed->getQueueType(); + } + return tap(parent::pushRaw($payload, $queue, $options), function () use ($queue, $payload): void { $this->event($this->getQueue($queue), new JobPushed($payload)); }); @@ -64,7 +69,9 @@ public function later($delay, $job, $data = '', $queue = null): mixed { $payload = (new JobPayload($this->createPayload($job, $data)))->prepare($job)->value; - return tap(parent::laterRaw($delay, $payload, $queue), function () use ($payload, $queue): void { + $queueType = ($job instanceof RabbitMQConsumable) ? $job->getQueueType() : null; + + return tap(parent::laterRaw($delay, $payload, $queue, queueType: $queueType), function () use ($payload, $queue): void { $this->event($this->getQueue($queue), new JobPushed($payload)); }); } diff --git a/src/Queue/Jobs/RabbitMQJob.php b/src/Queue/Jobs/RabbitMQJob.php index 0622b8ec..a3c16b80 100644 --- a/src/Queue/Jobs/RabbitMQJob.php +++ b/src/Queue/Jobs/RabbitMQJob.php @@ -4,12 +4,14 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Queue\Job as JobContract; use Illuminate\Queue\Jobs\Job; use Illuminate\Support\Arr; use PhpAmqpLib\Exception\AMQPProtocolChannelException; use PhpAmqpLib\Message\AMQPMessage; use PhpAmqpLib\Wire\AMQPTable; +use Salesmessage\LibRabbitMQ\Contracts\RabbitMQConsumable; use Salesmessage\LibRabbitMQ\Horizon\RabbitMQQueue as HorizonRabbitMQQueue; use Salesmessage\LibRabbitMQ\Queue\RabbitMQQueue; @@ -126,8 +128,13 @@ public function release($delay = 0): void { parent::release(); + $consumableJob = $this->getPayloadData(); + if (!($consumableJob instanceof RabbitMQConsumable)) { + throw new \RuntimeException('Job must be an instance of RabbitMQJobBatchable'); + } + // Always create a new message when this Job is released - $this->rabbitmq->laterRaw($delay, $this->message->getBody(), $this->queue, $this->attempts()); + $this->rabbitmq->laterRaw($delay, $this->message->getBody(), $this->queue, $this->attempts(), $consumableJob->getQueueType()); // Releasing a Job means the message was failed to process. // Because this Job message is always recreated and pushed as new message, this Job message is correctly handled. @@ -135,6 +142,39 @@ public function release($delay = 0): void $this->rabbitmq->ack($this); } + /** + * @return object + * @throws \RuntimeException + */ + public function getPayloadData(): object + { + $payload = $this->payload(); + + $data = $payload['data']; + + if (str_starts_with($data['command'], 'O:')) { + return unserialize($data['command']); + } + + if ($this->container->bound(Encrypter::class)) { + return unserialize($this->container[Encrypter::class]->decrypt($data['command'])); + } + + throw new \RuntimeException('Unable to extract job data.'); + } + + /** + * Returns target class name + * + * @return mixed + */ + public function getPayloadClass(): string + { + $payload = $this->payload(); + + return $payload['data']['commandName']; + } + /** * Get the underlying RabbitMQ connection. */ diff --git a/src/Queue/Jobs/RabbitMQJobBatchable.php b/src/Queue/Jobs/RabbitMQJobBatchable.php index 8015c2ad..278ae917 100644 --- a/src/Queue/Jobs/RabbitMQJobBatchable.php +++ b/src/Queue/Jobs/RabbitMQJobBatchable.php @@ -2,8 +2,6 @@ namespace Salesmessage\LibRabbitMQ\Queue\Jobs; -use Illuminate\Contracts\Encryption\Encrypter; -use Illuminate\Queue\Jobs\JobName; use Salesmessage\LibRabbitMQ\Queue\Jobs\RabbitMQJob as BaseJob; /** @@ -11,50 +9,4 @@ */ class RabbitMQJobBatchable extends BaseJob { - /** - * Fire the job. - * - * @return void - */ - public function fire() - { - $payload = $this->payload(); - - [$class, $method] = JobName::parse($payload['job']); - - ($this->instance = $this->resolve($class))->{$method}($this, $payload['data']); - } - - /** - * Returns target class name - * - * @return mixed - */ - public function getPayloadClass(): string - { - $payload = $this->payload(); - - return $payload['data']['commandName']; - } - - /** - * @return object - * @throws \RuntimeException - */ - public function getPayloadData(): object - { - $payload = $this->payload(); - - $data = $payload['data']; - - if (str_starts_with($data['command'], 'O:')) { - return unserialize($data['command']); - } - - if ($this->container->bound(Encrypter::class)) { - return unserialize($this->container[Encrypter::class]->decrypt($data['command'])); - } - - throw new \RuntimeException('Unable to extract job data.'); - } } diff --git a/src/Queue/QueueConfig.php b/src/Queue/QueueConfig.php index 1bf5802f..22d7f2fb 100644 --- a/src/Queue/QueueConfig.php +++ b/src/Queue/QueueConfig.php @@ -30,6 +30,10 @@ class QueueConfig protected bool $quorum = false; + protected ?int $quorumInitialGroupSize = null; + + protected string $quorumQueuePostfix = ''; + protected array $options = []; /** @@ -247,6 +251,41 @@ public function setQuorum($quorum): QueueConfig return $this; } + /** + * When set, used to declare quorum queues with a specific initial group size. + */ + public function getQuorumInitialGroupSize(): ?int + { + return $this->quorumInitialGroupSize; + } + + public function setQuorumInitialGroupSize(?int $size): self + { + if ($size === null) { + $this->quorumInitialGroupSize = null; + return $this; + } + + if ($size <= 0) { + throw new \InvalidArgumentException('Invalid quorum group size'); + } + + $this->quorumInitialGroupSize = $size; + + return $this; + } + + public function getQuorumQueuePostfix(): string + { + return $this->quorumQueuePostfix; + } + + public function setQuorumQueuePostfix(string $postfix): self + { + $this->quorumQueuePostfix = $postfix; + return $this; + } + /** * Holds all unknown queue options provided in the connection config */ diff --git a/src/Queue/QueueConfigFactory.php b/src/Queue/QueueConfigFactory.php index 04f29166..26d0bc2a 100644 --- a/src/Queue/QueueConfigFactory.php +++ b/src/Queue/QueueConfigFactory.php @@ -68,6 +68,15 @@ protected static function getOptionsFromConfig(QueueConfig $queueConfig, array $ $queueConfig->setQuorum($quorum); } + // Feature: Quorum initial group size + if (Arr::has($queueOptions, 'quorum_initial_group_size')) { + $queueConfig->setQuorumInitialGroupSize((int) Arr::pull($queueOptions, 'quorum_initial_group_size')); + } + + if ($quorumPostfix = (string) Arr::pull($queueOptions, 'quorum_queue_postfix')) { + $queueConfig->setQuorumQueuePostfix($quorumPostfix); + } + // All extra options not defined $queueConfig->setOptions($queueOptions); } diff --git a/src/Queue/RabbitMQQueue.php b/src/Queue/RabbitMQQueue.php index 00336fe6..83ceb317 100644 --- a/src/Queue/RabbitMQQueue.php +++ b/src/Queue/RabbitMQQueue.php @@ -24,6 +24,7 @@ use PhpAmqpLib\Wire\AMQPTable; use Ramsey\Uuid\Uuid; use RuntimeException; +use Salesmessage\LibRabbitMQ\Contracts\RabbitMQConsumable; use Throwable; use Salesmessage\LibRabbitMQ\Contracts\RabbitMQQueueContract; use Salesmessage\LibRabbitMQ\Queue\Jobs\RabbitMQJob; @@ -102,13 +103,21 @@ public function size($queue = null): int */ public function push($job, $data = '', $queue = null) { + if (!($job instanceof RabbitMQConsumable)) { + throw new \RuntimeException('Job must be an instance of RabbitMQConsumable'); + } + + $options = [ + 'queue_type' => $job->getQueueType(), + ]; + return $this->enqueueUsing( $job, $this->createPayload($job, $this->getQueue($queue), $data), $queue, null, - function ($payload, $queue) { - return $this->pushRaw($payload, $queue); + function ($payload, $queue) use ($options) { + return $this->pushRaw($payload, $queue, $options); } ); } @@ -120,9 +129,12 @@ function ($payload, $queue) { */ public function pushRaw($payload, $queue = null, array $options = []): int|string|null { + $queueType = $options['queue_type'] ?? null; + unset($options['queue_type']); + [$destination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options); - $this->declareDestination($destination, $exchange, $exchangeType); + $this->declareDestination($destination, $exchange, $exchangeType, $queueType); [$message, $correlationId] = $this->createMessage($payload, $attempts); @@ -138,13 +150,19 @@ public function pushRaw($payload, $queue = null, array $options = []): int|strin */ public function later($delay, $job, $data = '', $queue = null): mixed { + if (!($job instanceof RabbitMQConsumable)) { + throw new \RuntimeException('Job must be an instance of RabbitMQConsumable'); + } + + $queueType = $job->getQueueType(); + return $this->enqueueUsing( $job, $this->createPayload($job, $this->getQueue($queue), $data), $queue, $delay, - function ($payload, $queue, $delay) { - return $this->laterRaw($delay, $payload, $queue); + function ($payload, $queue, $delay) use ($queueType) { + return $this->laterRaw($delay, $payload, $queue, queueType: $queueType); } ); } @@ -152,7 +170,7 @@ function ($payload, $queue, $delay) { /** * @throws AMQPProtocolChannelException */ - public function laterRaw($delay, string $payload, $queue = null, int $attempts = 0): int|string|null + public function laterRaw($delay, string $payload, $queue = null, int $attempts = 0, ?string $queueType = null): int|string|null { $ttl = $this->secondsUntil($delay) * 1000; @@ -161,12 +179,15 @@ public function laterRaw($delay, string $payload, $queue = null, int $attempts = // When no ttl just publish a new message to the exchange or queue if ($ttl <= 0) { + if ($queueType !== null) { + $options['queue_type'] = $queueType; + } return $this->pushRaw($payload, $queue, $options); } // Create a main queue to handle delayed messages [$mainDestination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options); - $this->declareDestination($mainDestination, $exchange, $exchangeType); + $this->declareDestination($mainDestination, $exchange, $exchangeType, $queueType); $destination = $this->getQueue($queue).'.delay.'.$ttl; @@ -196,7 +217,13 @@ public function bulk($jobs, $data = '', $queue = null): void protected function publishBatch($jobs, $data = '', $queue = null): void { foreach ($jobs as $job) { - $this->bulkRaw($this->createPayload($job, $queue, $data), $queue, ['job' => $job]); + if (!($job instanceof RabbitMQConsumable)) { + throw new \RuntimeException('Job must be an instance of RabbitMQConsumable'); + } + $this->bulkRaw($this->createPayload($job, $queue, $data), $queue, [ + 'job' => $job, + 'queue_type' => $job->getQueueType(), + ]); } $this->batchPublish(); @@ -207,9 +234,12 @@ protected function publishBatch($jobs, $data = '', $queue = null): void */ public function bulkRaw(string $payload, string $queue = null, array $options = []): int|string|null { + $queueType = $options['queue_type'] ?? null; + unset($options['queue_type']); + [$destination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options); - $this->declareDestination($destination, $exchange, $exchangeType); + $this->declareDestination($destination, $exchange, $exchangeType, $queueType); [$message, $correlationId] = $this->createMessage($payload, $attempts); @@ -599,15 +629,16 @@ public function close(): void /** * Get the Queue arguments. */ - protected function getQueueArguments(string $destination): array + protected function getQueueArguments(string $destination, ?string $queueType = null): array { + $isQuorum = $this->getConfig()->isQuorum() || $queueType === RabbitMQConsumable::MQ_TYPE_QUORUM; $arguments = []; // Messages without a priority property are treated as if their priority were 0. // Messages with a priority which is higher than the queue's maximum, are treated as if they were // published with the maximum priority. // Quorum queues does not support priority. - if ($this->getConfig()->isPrioritizeDelayed() && ! $this->getConfig()->isQuorum()) { + if ($this->getConfig()->isPrioritizeDelayed() && ! $isQuorum) { $arguments['x-max-priority'] = $this->getConfig()->getQueueMaxPriority(); } @@ -616,8 +647,14 @@ protected function getQueueArguments(string $destination): array $arguments['x-dead-letter-routing-key'] = $this->getFailedRoutingKey($destination); } - if ($this->getConfig()->isQuorum()) { + if ($isQuorum) { $arguments['x-queue-type'] = 'quorum'; + + // optional: initial group size for quorum queues + $initialGroupSize = $this->getConfig()->getQuorumInitialGroupSize(); + if ($initialGroupSize !== null) { + $arguments['x-quorum-initial-group-size'] = $initialGroupSize; + } } return $arguments; @@ -701,8 +738,12 @@ protected function isQueueDeclared(string $name): bool * * @throws AMQPProtocolChannelException */ - protected function declareDestination(string $destination, string $exchange = null, string $exchangeType = AMQPExchangeType::DIRECT): void - { + protected function declareDestination( + string $destination, + string $exchange = null, + string $exchangeType = AMQPExchangeType::DIRECT, + ?string $queueType = null, + ): void { // When an exchange is provided and no exchange is present in RabbitMQ, create an exchange. if ($exchange && ! $this->isExchangeExists($exchange)) { $this->declareExchange($exchange, $exchangeType); @@ -719,7 +760,7 @@ protected function declareDestination(string $destination, string $exchange = nu } // Create a queue for amq.direct publishing. - $this->declareQueue($destination, true, false, $this->getQueueArguments($destination)); + $this->declareQueue($destination, true, false, $this->getQueueArguments($destination, $queueType)); } /** diff --git a/src/Queue/RabbitMQQueueBatchable.php b/src/Queue/RabbitMQQueueBatchable.php index 00399cf5..d58b89a5 100644 --- a/src/Queue/RabbitMQQueueBatchable.php +++ b/src/Queue/RabbitMQQueueBatchable.php @@ -2,7 +2,6 @@ namespace Salesmessage\LibRabbitMQ\Queue; -use PhpAmqpLib\Connection\AbstractConnection; use Salesmessage\LibRabbitMQ\Contracts\RabbitMQConsumable; use Salesmessage\LibRabbitMQ\Dto\ConnectionNameDto; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; @@ -42,8 +41,7 @@ protected function publishBasic( $mandatory = false, $immediate = false, $ticket = null - ): void - { + ): void { try { parent::publishBasic($msg, $exchange, $destination, $mandatory, $immediate, $ticket); } catch (AMQPConnectionClosedException|AMQPChannelClosedException) { @@ -89,6 +87,10 @@ public function push($job, $data = '', $queue = null) $queue = $job->onQueue(); } + if ($job->getQueueType() === RabbitMQConsumable::MQ_TYPE_QUORUM) { + $queue .= $this->getConfig()->getQuorumQueuePostfix(); + } + try { $result = parent::push($job, $data, $queue); } catch (AMQPConnectionClosedException $exception) { @@ -111,11 +113,6 @@ public function push($job, $data = '', $queue = null) return $result; } - public function pushRaw($payload, $queue = null, array $options = []): int|string|null - { - return parent::pushRaw($payload, $queue, $options); - } - /** * @return bool * @throws \GuzzleHttp\Exception\GuzzleException @@ -185,4 +182,3 @@ private function isVhostFailedException(AMQPConnectionClosedException $exception return false; } } - From 640eded7b7ea6834974e433f8304f21a8cd3d596 Mon Sep 17 00:00:00 2001 From: Alexander Ginko <120381488+ahinkoneklo@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:38:39 +0100 Subject: [PATCH 30/32] SWR-20616 Server: Refactor RabbitMQ Vhosts Scan Command (#18) * SWR-20616 Server: Refactor RabbitMQ Vhosts Scan Command * SWR-20616 Server: Refactor RabbitMQ Vhosts Scan Command * SWR-20616 Server: Refactor RabbitMQ Vhosts Scan Command * SWR-20616 Server: Refactor RabbitMQ Vhosts Scan Command --- README.md | 2 +- composer.json | 2 +- src/Console/ScanVhostsCommand.php | 108 +++++++++++++++++++++--------- src/Services/VhostsService.php | 64 +++++++----------- 4 files changed, 101 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 0c6c9cb2..74dbd8b5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Only the latest version will get new features. Bug fixes will be provided using You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.32 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.33 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index ff7d373d..151bc3d1 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.32-dev" + "dev-master": "1.33-dev" }, "laravel": { "providers": [ diff --git a/src/Console/ScanVhostsCommand.php b/src/Console/ScanVhostsCommand.php index c17d2fb0..49c39fca 100644 --- a/src/Console/ScanVhostsCommand.php +++ b/src/Console/ScanVhostsCommand.php @@ -3,9 +3,6 @@ namespace Salesmessage\LibRabbitMQ\Console; use Illuminate\Console\Command; -use Illuminate\Redis\Connections\PredisConnection; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Redis; use Salesmessage\LibRabbitMQ\Dto\QueueApiDto; use Salesmessage\LibRabbitMQ\Dto\VhostApiDto; use Salesmessage\LibRabbitMQ\Services\GroupsService; @@ -16,11 +13,15 @@ class ScanVhostsCommand extends Command { protected $signature = 'lib-rabbitmq:scan-vhosts - {--sleep=10 : Number of seconds to sleep}'; + {--sleep=10 : Number of seconds to sleep} + {--max-time=0 : Maximum seconds the command can run before stopping} + {--with-output=true : Show output details during iteration} + {--max-memory=0 : Maximum memory usage in megabytes before stopping}'; protected $description = 'Scan and index vhosts'; - - private array $groups = []; + + private array $groups; + private bool $silent = false; /** * @param GroupsService $groupsService @@ -39,42 +40,72 @@ public function __construct( $this->groups = $this->groupsService->getAllGroupsNames(); } - /** - * @return int - */ - public function handle() + public function handle(): void { $sleep = (int) $this->option('sleep'); - - $vhosts = $this->vhostsService->getAllVhosts(); - $oldVhosts = $this->internalStorageManager->getVhosts(); - - if ($vhosts->isNotEmpty()) { - foreach ($vhosts as $vhost) { - $vhostDto = $this->processVhost($vhost); - if (null === $vhostDto) { - continue; - } + $maxTime = max(0, (int) $this->option('max-time')); + $this->silent = !filter_var($this->option('with-output'), FILTER_VALIDATE_BOOLEAN); - $oldVhostIndex = array_search($vhostDto->getName(), $oldVhosts, true); - if (false !== $oldVhostIndex) { - unset($oldVhosts[$oldVhostIndex]); - } + $maxMemoryMb = max(0, (int) $this->option('max-memory')); + $maxMemoryBytes = $maxMemoryMb > 0 ? $maxMemoryMb * 1024 * 1024 : 0; + + $startedAt = microtime(true); + + while (true) { + $iterationStartedAt = microtime(true); + + $this->processVhosts(); + + $iterationDuration = microtime(true) - $iterationStartedAt; + $totalRuntime = microtime(true) - $startedAt; + $memoryUsage = memory_get_usage(true); + $memoryPeakUsage = memory_get_peak_usage(true); + + $this->line(sprintf( + 'Iteration finished in %.2f seconds (total runtime %.2f seconds). Memory usage: %s (peak %s).', + $iterationDuration, + $totalRuntime, + $this->formatBytes($memoryUsage), + $this->formatBytes($memoryPeakUsage) + ), 'warning', forcePrint: $sleep === 0); + + if ($sleep === 0) { + return; } - } else { - $this->warn('Vhosts not found.'); - } - $this->removeOldsVhosts($oldVhosts); + if ($maxTime > 0 && $totalRuntime >= $maxTime) { + $this->line(sprintf('Stopping: reached max runtime of %d seconds.', $maxTime), 'warning', forcePrint: true); + return; + } - if ($sleep > 0) { - $this->line(sprintf('Sleep %d seconds...', $sleep)); + if ($maxMemoryBytes > 0 && $memoryUsage >= $maxMemoryBytes) { + $this->line(sprintf( + 'Stopping: memory usage %s exceeded max threshold of %s.', + $this->formatBytes($memoryUsage), + $this->formatBytes($maxMemoryBytes) + ), 'warning', forcePrint: true); + return; + } + $this->line(sprintf('Sleep %d seconds...', $sleep)); sleep($sleep); - return $this->handle(); + } + } + + private function processVhosts(): void + { + $oldVhostsMap = array_flip($this->internalStorageManager->getVhosts()); + + foreach ($this->vhostsService->getAllVhosts() as $vhost) { + $vhostDto = $this->processVhost($vhost); + if (null === $vhostDto) { + continue; + } + + unset($oldVhostsMap[$vhostDto->getName()]); } - return Command::SUCCESS; + $this->removeOldsVhosts(array_keys($oldVhostsMap)); } /** @@ -135,6 +166,11 @@ private function processVhost(array $vhostApiData): ?VhostApiDto return $vhostDto; } + private function formatBytes(int $bytes): string + { + return number_format($bytes / (1024 * 1024), 2) . ' MB'; + } + /** * @param array $oldVhosts * @return void @@ -220,5 +256,11 @@ private function removeOldVhostQueues(VhostApiDto $vhostDto, array $oldVhostQueu )); } } -} + public function line($string, $style = null, $verbosity = null, $forcePrint = false): void + { + if (!$this->silent || $style === 'error' || $forcePrint) { + parent::line($string, $style, $verbosity); + } + } +} diff --git a/src/Services/VhostsService.php b/src/Services/VhostsService.php index 994373e4..aa933f9e 100644 --- a/src/Services/VhostsService.php +++ b/src/Services/VhostsService.php @@ -2,7 +2,6 @@ namespace Salesmessage\LibRabbitMQ\Services; -use Illuminate\Support\Collection; use Psr\Log\LoggerInterface; use Salesmessage\LibRabbitMQ\Services\Api\RabbitApiClient; use Throwable; @@ -23,52 +22,37 @@ public function __construct( $this->rabbitApiClient->setConnectionConfig($connectionConfig); } - /** - * @param int $page - * @param int $pageSize - * @param Collection|null $vhosts - * @return Collection - */ - public function getAllVhosts( - int $page = 1, - int $pageSize = 500, - ?Collection $vhosts = null, - ): Collection + public function getAllVhosts(int $fromPage = 1): \Generator { - if (null === $vhosts) { - $vhosts = new Collection(); - } - - try { + while (true) { $data = $this->rabbitApiClient->request( 'GET', '/api/vhosts', [ - 'page' => $page, - 'page_size' => $pageSize, + 'page' => $fromPage, + 'page_size' => 500, 'columns' => 'name,messages,messages_ready,messages_unacknowledged', ]); - } catch (Throwable $exception) { - $this->logger->warning('Salesmessage.LibRabbitMQ.Services.VhostsService.getAllVhosts.exception', [ - 'message' => $exception->getMessage(), - 'code' => $exception->getCode(), - 'trace' => $exception->getTraceAsString(), - ]); - - $data = []; - } - $items = (array) ($data['items'] ?? []); - if (!empty($items)) { - $vhosts->push(...$items); + $items = $data['items'] ?? []; + if (!is_array($items) || !isset($data['page_count'])) { + throw new \LogicException('Unexpected response from RabbitMQ API'); + } + if (empty($items)) { + break; + } + + foreach ($items as $item) { + yield $item; + } + + $nextPage = $fromPage + 1; + $totalPages = (int) $data['page_count']; + if ($nextPage > $totalPages) { + break; + } + + $fromPage = $nextPage; } - - $nextPage = $page + 1; - $lastPage = (int) ($data['page_count'] ?? 1); - if ($lastPage >= $nextPage) { - return $this->getAllVhosts($nextPage, $pageSize, $vhosts); - } - - return $vhosts; } /** @@ -120,7 +104,7 @@ public function createVhostForOrganization(int $organizationId): bool { $vhostName = $this->getVhostName($organizationId); $description = $this->getVhostDescription($organizationId); - + return $this->createVhost($vhostName, $description); } From 6e2c65a986159daeb228e480ccbaf1ddc1b4a5f3 Mon Sep 17 00:00:00 2001 From: Alexander Ginko <120381488+ahinkoneklo@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:12:16 +0100 Subject: [PATCH 31/32] SWR-20618 Server: Implement Delivery Count Parameter For Qourum Queue (#19) * SWR-20618 Server: Implement Delivery Count Parameter For Qourum Queues RabbitMQ * SWR-20618 Server: Implement Delivery Count Parameter For Qourum Queues RabbitMQ * SWR-20618 Server: Implement Delivery Count Parameter For Qourum Queues RabbitMQ * SWR-20618 Server: Implement Delivery Count Parameter For Qourum Queues RabbitMQ --- README.md | 2 +- composer.json | 2 +- config/rabbitmq.php | 12 +++ src/LaravelLibRabbitMQServiceProvider.php | 3 + src/Services/DeliveryLimitService.php | 82 +++++++++++++++++++ .../AbstractVhostsConsumer.php | 11 ++- src/VhostsConsumers/DirectConsumer.php | 4 +- src/VhostsConsumers/QueueConsumer.php | 6 +- 8 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 src/Services/DeliveryLimitService.php diff --git a/README.md b/README.md index 74dbd8b5..59bab292 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Only the latest version will get new features. Bug fixes will be provided using You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.33 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.34 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index 151bc3d1..c8f905f1 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.33-dev" + "dev-master": "1.34-dev" }, "laravel": { "providers": [ diff --git a/config/rabbitmq.php b/config/rabbitmq.php index 9b457a84..751c5713 100644 --- a/config/rabbitmq.php +++ b/config/rabbitmq.php @@ -23,6 +23,18 @@ ], 'options' => [ + 'quorum' => [ + /** + * Max allowed delivery attempts (RabbitMQ x-delivery-count) for quorum queues. + * 0 disables the check. + */ + 'delivery_limit' => env('RABBITMQ_QUORUM_DELIVERY_LIMIT', 2), + /** + * Action when delivery_limit is reached. + * Possible values: 'ack', 'reject' + */ + 'on_limit_action' => env('RABBITMQ_QUORUM_ON_LIMIT_ACTION', 'reject'), + ], ], /** diff --git a/src/LaravelLibRabbitMQServiceProvider.php b/src/LaravelLibRabbitMQServiceProvider.php index d8451bdb..0e1fac2c 100644 --- a/src/LaravelLibRabbitMQServiceProvider.php +++ b/src/LaravelLibRabbitMQServiceProvider.php @@ -18,6 +18,7 @@ use Salesmessage\LibRabbitMQ\Services\InternalStorageManager; use Salesmessage\LibRabbitMQ\Services\QueueService; use Salesmessage\LibRabbitMQ\Services\VhostsService; +use Salesmessage\LibRabbitMQ\Services\DeliveryLimitService; use Salesmessage\LibRabbitMQ\VhostsConsumers\DirectConsumer as VhostsDirectConsumer; use Salesmessage\LibRabbitMQ\VhostsConsumers\QueueConsumer as VhostsQueueConsumer; @@ -69,6 +70,7 @@ public function register(): void $this->app[ExceptionHandler::class], $isDownForMaintenance, $this->app->get(DeduplicationService::class), + $this->app->get(DeliveryLimitService::class), null, ); }); @@ -86,6 +88,7 @@ public function register(): void $this->app[ExceptionHandler::class], $isDownForMaintenance, $this->app->get(DeduplicationService::class), + $this->app->get(DeliveryLimitService::class), null, ); }); diff --git a/src/Services/DeliveryLimitService.php b/src/Services/DeliveryLimitService.php new file mode 100644 index 00000000..5e85e436 --- /dev/null +++ b/src/Services/DeliveryLimitService.php @@ -0,0 +1,82 @@ +config->get('queue.connections.rabbitmq_vhosts.options', []); + $limit = (int) ($config['quorum']['delivery_limit'] ?? 0); + if ($limit <= 0) { + return true; + } + + if (!$this->isFromQuorumQueue($message)) { + return true; + } + + $deliveryCount = $this->getMessageDeliveryCount($message); + if ($deliveryCount < $limit) { + return true; + } + + $action = strtolower((string) ($config['quorum']['on_limit_action'] ?? 'reject')); + try { + $this->logger->warning('Salesmessage.LibRabbitMQ.Services.DeliveryLimitService.limitReached', [ + 'message_id' => $message->get_properties()['message_id'] ?? null, + 'action' => $action, + ]); + + if ($action === 'ack') { + $message->ack(); + } else { + $message->reject(false); + } + } catch (\Throwable $exception) { + $this->logger->error('Salesmessage.LibRabbitMQ.Services.DeliveryLimitService.handle.exception', [ + 'message' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + 'error_class' => get_class($exception), + ]); + } + + return false; + } + + private function getMessageDeliveryCount(AMQPMessage $message): int + { + $properties = $message->get_properties(); + /** @var AMQPTable|null $headers */ + $headers = $properties['application_headers'] ?? null; + if (!$headers instanceof AMQPTable) { + return 0; + } + + return (int) ($headers->getNativeData()['x-delivery-count'] ?? 0); + } + + private function isFromQuorumQueue(AMQPMessage $message): bool + { + $properties = $message->get_properties(); + /** @var AMQPTable|null $headers */ + $headers = $properties['application_headers'] ?? null; + if (!$headers instanceof AMQPTable) { + return false; + } + + $data = $headers->getNativeData(); + + return array_key_exists('x-delivery-count', $data); + } +} diff --git a/src/VhostsConsumers/AbstractVhostsConsumer.php b/src/VhostsConsumers/AbstractVhostsConsumer.php index ff30cf41..087af6e5 100644 --- a/src/VhostsConsumers/AbstractVhostsConsumer.php +++ b/src/VhostsConsumers/AbstractVhostsConsumer.php @@ -28,6 +28,7 @@ use Salesmessage\LibRabbitMQ\Services\Deduplication\AppDeduplicationService; use Salesmessage\LibRabbitMQ\Services\Deduplication\TransportLevel\DeduplicationService as TransportDeduplicationService; use Salesmessage\LibRabbitMQ\Services\InternalStorageManager; +use Salesmessage\LibRabbitMQ\Services\DeliveryLimitService; abstract class AbstractVhostsConsumer extends Consumer { @@ -94,6 +95,7 @@ public function __construct( ExceptionHandler $exceptions, callable $isDownForMaintenance, protected TransportDeduplicationService $transportDeduplicationService, + protected DeliveryLimitService $deliveryLimitService, callable $resetScope = null, ) { parent::__construct($manager, $events, $exceptions, $isDownForMaintenance, $resetScope); @@ -151,7 +153,7 @@ public function setAsyncMode(bool $asyncMode): self public function daemon($connectionName, $queue, WorkerOptions $options) { - $this->goAheadOrWait(); + $this->goAheadOrWait($options->sleep); $this->configConnectionName = (string) $connectionName; $this->workerOptions = $options; @@ -240,6 +242,11 @@ abstract protected function startConsuming(): RabbitMQQueue; */ protected function processAmqpMessage(AMQPMessage $message, RabbitMQQueue $connection): void { + if (!$this->deliveryLimitService->isAllowed($message)) { + $this->logWarning('processAMQPMessage.delivery_limit_reached'); + return; + } + $this->hadJobs = true; $isSupportBatching = $this->isSupportBatching($message); if ($isSupportBatching) { @@ -761,7 +768,7 @@ protected function initConnection(): RabbitMQQueue $this->internalStorageManager->removeVhost($vhostDto); $this->loadVhosts(); - $this->goAheadOrWait(); + $this->goAheadOrWait($this->workerOptions?->sleep ?? 1); return $this->initConnection(); } finally { diff --git a/src/VhostsConsumers/DirectConsumer.php b/src/VhostsConsumers/DirectConsumer.php index a17322a2..d16c66d0 100644 --- a/src/VhostsConsumers/DirectConsumer.php +++ b/src/VhostsConsumers/DirectConsumer.php @@ -80,7 +80,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->processBatch($connection); - $this->goAheadOrWait(); + $this->goAheadOrWait($this->workerOptions->sleep); $connection = $this->startConsuming(); continue; @@ -95,7 +95,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->processBatch($connection); - $this->goAheadOrWait(); + $this->goAheadOrWait($this->workerOptions->sleep); $connection = $this->startConsuming(); continue; diff --git a/src/VhostsConsumers/QueueConsumer.php b/src/VhostsConsumers/QueueConsumer.php index 700bf8a1..9381f8af 100644 --- a/src/VhostsConsumers/QueueConsumer.php +++ b/src/VhostsConsumers/QueueConsumer.php @@ -81,7 +81,7 @@ protected function vhostDaemon($connectionName, WorkerOptions $options) $this->processBatch($connection); - $this->goAheadOrWait(); + $this->goAheadOrWait($this->workerOptions->sleep); $this->startConsuming(); $this->sleep($this->workerOptions->sleep); @@ -139,7 +139,7 @@ protected function startConsuming(): RabbitMQQueue $this->processBatch($connection); - $this->goAheadOrWait(); + $this->goAheadOrWait($this->workerOptions->sleep); $this->startConsuming(); } @@ -184,7 +184,7 @@ protected function startConsuming(): RabbitMQQueue if (false === $isSuccess) { $this->stopConsuming(); - $this->goAheadOrWait(); + $this->goAheadOrWait($this->workerOptions->sleep); return $this->startConsuming(); } From 573043b6cc5748d95b6bceea382a3e512778e95a Mon Sep 17 00:00:00 2001 From: Alexander Ginko <120381488+ahinkoneklo@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:29:37 +0100 Subject: [PATCH 32/32] SWR-20483 Server: RabbitMQ Improvements: Quorum Queues (#20) * SWR-20483 Server: RabbitMQ Improvements: Quorum Queues * SWR-20483 Server: RabbitMQ Improvements: Quorum Queues --- README.md | 2 +- composer.json | 2 +- .../TransportLevel/DeduplicationService.php | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 59bab292..09e7892e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Only the latest version will get new features. Bug fixes will be provided using You can install this package via composer using this command: ``` -composer require salesmessage/php-lib-rabbitmq:^1.34 --ignore-platform-reqs +composer require salesmessage/php-lib-rabbitmq:^1.35 --ignore-platform-reqs ``` The package will automatically register itself. diff --git a/composer.json b/composer.json index c8f905f1..ae58c012 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.34-dev" + "dev-master": "1.35-dev" }, "laravel": { "providers": [ diff --git a/src/Services/Deduplication/TransportLevel/DeduplicationService.php b/src/Services/Deduplication/TransportLevel/DeduplicationService.php index fed93a73..610a0c5a 100644 --- a/src/Services/Deduplication/TransportLevel/DeduplicationService.php +++ b/src/Services/Deduplication/TransportLevel/DeduplicationService.php @@ -249,17 +249,19 @@ protected function republishLockedMessage(AMQPMessage $message): string $attempts = (int) ($headers[self::HEADER_LOCK_REQUEUE_COUNT] ?? 0); ++$attempts; - $maxAttempts = ((int) ($this->getConfig('lock_ttl', 30))) / self::WAIT_AFTER_PUBLISH; + $maxAttempts = 1 + (((int) ($this->getConfig('lock_ttl', 30))) / self::WAIT_AFTER_PUBLISH); if ($attempts > $maxAttempts) { $this->logger->warning('DeduplicationService.republishLockedMessage.max_attempts_reached', [ 'message_id' => $props['message_id'] ?? null, ]); - $message->ack(); + $message->reject(false); - return self::ACTION_ACK; + return self::ACTION_REJECT; } $headers[self::HEADER_LOCK_REQUEUE_COUNT] = $attempts; + // this header will be added during publishing, and we should not use counter from the previous message + unset($headers['x-delivery-count']); $newProps = $props; $newProps['application_headers'] = new AMQPTable($headers); @@ -275,6 +277,7 @@ protected function republishLockedMessage(AMQPMessage $message): string $message->ack(); // it's necessary to avoid a high redelivery rate // normally, such a situation is not expected (or expected very rarely) + // for example, when we have OOM sleep(self::WAIT_AFTER_PUBLISH); return self::ACTION_REQUEUE;