diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8dadc45d9..9f2e1b2c8 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -30,7 +30,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [ '8.1', '8.2' ]
+ php: [ '8.2', '8.3' ]
steps:
- name: Checkout.
uses: actions/checkout@v2
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index be42e8f9a..5126dc889 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [ '8.1', '8.2' ]
+ php: [ '8.2', '8.3', '8.4' ]
steps:
- name: Checkout.
uses: actions/checkout@v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4c06012b..4bdf0fc75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,16 @@
Yii2 Queue Extension Change Log
===============================
-2.3.7 under development
+2.3.8 under development
-----------------------
+- Enh #516: Ensure Redis driver messages are consumed at least once (soul11201)
+- Bug #522: Fix SQS driver type error with custom value passed to `queue/listen` (flaviovs)
-- no changes in this release.
+
+2.3.7 April 29, 2024
+--------------------
+
+- Enh #509: Add StatisticsProviderInterface to get statistics from queue (kalmer)
2.3.6 October 03, 2023
diff --git a/Makefile b/Makefile
index 7f260cc75..c01e40b11 100644
--- a/Makefile
+++ b/Makefile
@@ -1,35 +1,35 @@
help: ## Display help information
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
-build: ## Build an image from a docker-compose file. Params: {{ v=8.1 }}. Default latest PHP 8.1
+build: ## Build an image from a docker-compose file. Params: {{ v=8.2 }}. Default latest PHP 8.2
@cp -n .env.example .env
- PHP_VERSION=$(filter-out $@,$(v)) docker-compose up -d --build
+ PHP_VERSION=$(filter-out $@,$(v)) docker compose up -d --build
make create-sqs-queue
make create-sqs-fifo-queue
-test: ## Run tests. Params: {{ v=8.1 }}. Default latest PHP 8.1
+test: ## Run tests. Params: {{ v=8.2 }}. Default latest PHP 8.2
make build
- PHP_VERSION=$(filter-out $@,$(v)) docker-compose run yii2-queue-php vendor/bin/phpunit --coverage-clover coverage.xml
+ PHP_VERSION=$(filter-out $@,$(v)) docker compose run yii2-queue-php vendor/bin/phpunit --coverage-clover coverage.xml
make down
down: ## Stop and remove containers, networks
- docker-compose down
+ docker compose down
-benchmark: ## Run benchmark. Params: {{ v=8.1 }}. Default latest PHP 8.1
- PHP_VERSION=$(filter-out $@,$(v)) docker-compose build --pull yii2-queue-php
- PHP_VERSION=$(filter-out $@,$(v)) docker-compose run yii2-queue-php tests/yii benchmark/waiting
+benchmark: ## Run benchmark. Params: {{ v=8.2 }}. Default latest PHP 8.2
+ PHP_VERSION=$(filter-out $@,$(v)) docker compose build --pull yii2-queue-php
+ PHP_VERSION=$(filter-out $@,$(v)) docker compose run yii2-queue-php tests/yii benchmark/waiting
make down
sh: ## Enter the container with the application
docker exec -it yii2-queue-php sh
-static-analyze: ## Run code static analyze. Params: {{ v=8.1 }}. Default latest PHP 8.1
- PHP_VERSION=$(filter-out $@,$(v)) docker-compose build --pull yii2-queue-php
- PHP_VERSION=$(filter-out $@,$(v)) docker-compose run yii2-queue-php vendor/bin/psalm --config=psalm.xml --shepherd --stats --php-version=$(v)
+static-analyze: ## Run code static analyze. Params: {{ v=8.2 }}. Default latest PHP 8.2
+ PHP_VERSION=$(filter-out $@,$(v)) docker compose build --pull yii2-queue-php
+ PHP_VERSION=$(filter-out $@,$(v)) docker compose run yii2-queue-php vendor/bin/psalm --config=psalm.xml --shepherd --stats --php-version=$(v)
make down
clean:
- docker-compose down
+ docker compose down
rm -rf tests/runtime/*
rm -f .php_cs.cache
rm -rf composer.lock
diff --git a/README.md b/README.md
index a28bd8f40..e336b02c2 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Documentation is at [docs/guide/README.md](docs/guide/README.md).
## Requirements
-- PHP 8.1 or higher.
+- PHP 8.2 or higher.
Installation
------------
diff --git a/composer.json b/composer.json
index 0da6de72b..be729e785 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "yiisoft/yii2-queue",
- "description": "Yii2 Queue Extension which supported DB, Redis, RabbitMQ, Beanstalk, SQS and Gearman",
+ "description": "Yii2 Queue Extension which supports queues based on DB, Redis, RabbitMQ, Beanstalk, SQS, and Gearman",
"type": "yii2-extension",
"keywords": ["yii", "queue", "async", "gii", "db", "redis", "rabbitmq", "beanstalk", "gearman", "sqs"],
"license": "BSD-3-Clause",
@@ -16,9 +16,9 @@
"docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide"
},
"require": {
- "php": ">=8.1",
- "yiisoft/yii2": "~2.0.14",
- "symfony/process": "^6.3",
+ "php": ">=8.2",
+ "yiisoft/yii2": "~2.0.50",
+ "symfony/process": "^7.0",
"laravel/serializable-closure": "^v1.3.0"
},
"require-dev": {
@@ -33,7 +33,7 @@
"enqueue/stomp": "^0.10.0",
"pda/pheanstalk": "^5.0.0",
"aws/aws-sdk-php": "3.285.0",
- "vimeo/psalm": "^5.10.0"
+ "vimeo/psalm": "^6.0.0"
},
"suggest": {
"ext-pcntl": "Need for process signals.",
diff --git a/docs/guide-zh-CN/retryable.md b/docs/guide-zh-CN/retryable.md
index 738cb1916..34f139e98 100644
--- a/docs/guide-zh-CN/retryable.md
+++ b/docs/guide-zh-CN/retryable.md
@@ -1,10 +1,10 @@
错误与重复执行
=========================
-在作业处理期间可以抛出异常。 当请求的服务和外部资源不可用时,由于代码编写的比较糟糕而导致的内部错误。
-在第二种情况下,可以在一段时间后重新尝试这个作业。
+作业的执行可能会失败。这可能是由于内部错误造成的,这些错误是由编写不当的代码导致的,应该首先修复。但是,它们也可能由于外部问题(例如服务或资源不可用)而失败。这可能会导致异常或超时。
+在后一种情况下,最好能够在一段时间后重试作业。有几种方法可以做到这一点。
-有几种方法可以做到这一点。
+>注意:下面描述的 ttr 功能需要安装 [PHP Process Control (pcntl)](https://www.php.net/manual/en/book.pcntl.php) 扩展,并且 worker 命令必须使用 --isolate 选项(默认启用)。
重试选项
-------------
@@ -21,7 +21,12 @@
],
```
- `ttr` 选项设置了在队列中保留工作的时间。如果一份作业在这段时间没有执行,它将返回队列进行重试。
+ `ttr` 选项设置了作业必须成功完成的秒数。因此,可能会发生两件事来使工作失败:
+ 1. 作业在`ttr`结束之前发生异常
+ 2. 完成作业所需的时间将比 ttr 长(超时),工作线程停止作业执行
+
+在这两种情况下,作业都将发送回队列进行重试。但请注意,在第一种情况下,即使作业在运行后立即停止,也不会立即重试,要等到`ttr`"用完"。也就是说,在将作业发送回队列之前,必须经过剩余的 ttr 秒数。
+
`attempts` 选项设置了最大的尝试次数。如果尝试已经结束,作业作还没有完成,它将从队列中移除。
这种将全局设置队列中的所有作业,如果您需要为多个作业进行不同的设置可以使用,
@@ -30,7 +35,7 @@
重试作业接口
----------------------
-Separate control of retry is implemented by `RetryableJobInterface` 接口。 示例:
+为了更好地控制重试逻辑,作业可以实现 `RetryableJobInterface` 接口。 示例:
```php
class SomeJob extends BaseObject implements RetryableJobInterface
@@ -85,9 +90,9 @@ Yii::$app->queue->on(Queue::EVENT_AFTER_ERROR, function (ExecEvent $event) {
限制
------------
-完全支持 [Beanstalk], [DB], [File] 和 [Redis] 驱动程序的重试工具。
-[Sync] 驱动不会重试失败的工作 [Gearman] 不支持重试。
-[RabbitMQ] 基本版支持,但重试编号无法得到。
+[Beanstalk], [DB], [File] 和 [Redis] 驱动程序实现了对可重试作业的完全支持。
+[Sync] 驱动不会重试失败的作业 [Gearman] 不支持重试作业。
+[RabbitMQ] 只有其基本的可重试支持,其中无法设置尝试次数。
[Beanstalk]: driver-beanstalk.md
[DB]: driver-db.md
@@ -95,4 +100,4 @@ Yii::$app->queue->on(Queue::EVENT_AFTER_ERROR, function (ExecEvent $event) {
[Redis]: driver-redis.md
[Sync]: driver-sync.md
[Gearman]: driver-gearman.md
-[RabbitMQ]: driver-amqp.md
\ No newline at end of file
+[RabbitMQ]: driver-amqp.md
diff --git a/psalm.xml b/psalm.xml
index d92109732..ec0f9676e 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -27,5 +27,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/InvalidJobException.php b/src/InvalidJobException.php
index ae345f1d2..eb7988fb3 100644
--- a/src/InvalidJobException.php
+++ b/src/InvalidJobException.php
@@ -33,7 +33,7 @@ public function __construct(
private readonly string $serialized,
string $message = '',
int $code = 0,
- Throwable $previous = null
+ ?Throwable $previous = null
)
{
parent::__construct($message, $code, $previous);
diff --git a/src/JobEvent.php b/src/JobEvent.php
index fab622150..31968b09c 100644
--- a/src/JobEvent.php
+++ b/src/JobEvent.php
@@ -27,7 +27,7 @@ abstract class JobEvent extends Event
/**
* @var Queue
* @inheritdoc
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public $sender;
/**
diff --git a/src/cli/InfoAction.php b/src/cli/InfoAction.php
new file mode 100644
index 000000000..7f3ed669c
--- /dev/null
+++ b/src/cli/InfoAction.php
@@ -0,0 +1,68 @@
+
+ *
+ * @property Controller $controller
+ */
+class InfoAction extends Action
+{
+ /**
+ * @var Queue
+ */
+ public Queue $queue;
+
+ /**
+ * Info about queue status.
+ */
+ public function run(): void
+ {
+ if (!($this->queue instanceof StatisticsProviderInterface)) {
+ throw new NotSupportedException('Queue does not support ' . StatisticsProviderInterface::class);
+ }
+
+ $this->controller->stdout('Jobs' . PHP_EOL, Console::FG_GREEN);
+ $statisticsProvider = $this->queue->getStatisticsProvider();
+
+ if ($statisticsProvider instanceof WaitingCountInterface) {
+ $this->controller->stdout('- waiting: ', Console::FG_YELLOW);
+ $this->controller->stdout($statisticsProvider->getWaitingCount() . PHP_EOL);
+ }
+
+ if ($statisticsProvider instanceof DelayedCountInterface) {
+ $this->controller->stdout('- delayed: ', Console::FG_YELLOW);
+ $this->controller->stdout($statisticsProvider->getDelayedCount() . PHP_EOL);
+ }
+
+ if ($statisticsProvider instanceof ReservedCountInterface) {
+ $this->controller->stdout('- reserved: ', Console::FG_YELLOW);
+ $this->controller->stdout($statisticsProvider->getReservedCount() . PHP_EOL);
+ }
+
+ if ($statisticsProvider instanceof DoneCountInterface) {
+ $this->controller->stdout('- done: ', Console::FG_YELLOW);
+ $this->controller->stdout($statisticsProvider->getDoneCount() . PHP_EOL);
+ }
+ }
+}
diff --git a/src/cli/VerboseBehavior.php b/src/cli/VerboseBehavior.php
index e4dd27613..f9ea59a55 100644
--- a/src/cli/VerboseBehavior.php
+++ b/src/cli/VerboseBehavior.php
@@ -31,7 +31,6 @@ class VerboseBehavior extends Behavior
public $owner;
/**
* @var Controller
- * @psalm-suppress PropertyNotSetInConstructor
*/
public Controller $command;
diff --git a/src/cli/WorkerEvent.php b/src/cli/WorkerEvent.php
index 6dc4bf3c6..530c8ebd5 100644
--- a/src/cli/WorkerEvent.php
+++ b/src/cli/WorkerEvent.php
@@ -27,12 +27,11 @@ class WorkerEvent extends Event
/**
* @var Queue
* @inheritdoc
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public $sender;
/**
* @var LoopInterface
- * @psalm-suppress PropertyNotSetInConstructor
*/
public LoopInterface $loop;
/**
diff --git a/src/closure/Behavior.php b/src/closure/Behavior.php
index 75b051181..f6a2b1dd4 100644
--- a/src/closure/Behavior.php
+++ b/src/closure/Behavior.php
@@ -33,7 +33,7 @@ class Behavior extends \yii\base\Behavior
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public $owner;
diff --git a/src/debug/Panel.php b/src/debug/Panel.php
index 2dcabeb8d..0d63cfddd 100644
--- a/src/debug/Panel.php
+++ b/src/debug/Panel.php
@@ -24,7 +24,6 @@
* Debug Panel.
*
* @author Roman Zhuravlev
- * @psalm-suppress PropertyNotSetInConstructor
*/
class Panel extends BasePanel implements ViewContextInterface
{
diff --git a/src/drivers/amqp_interop/Command.php b/src/drivers/amqp_interop/Command.php
index 1dcd2b5cd..b24f1cc25 100644
--- a/src/drivers/amqp_interop/Command.php
+++ b/src/drivers/amqp_interop/Command.php
@@ -23,7 +23,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/drivers/beanstalk/Command.php b/src/drivers/beanstalk/Command.php
index ebb54b4bf..2df4e720a 100644
--- a/src/drivers/beanstalk/Command.php
+++ b/src/drivers/beanstalk/Command.php
@@ -23,7 +23,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantPropertyType, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantPropertyType, NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
/**
diff --git a/src/drivers/db/Command.php b/src/drivers/db/Command.php
index 41ca3f434..4978bdd7f 100644
--- a/src/drivers/db/Command.php
+++ b/src/drivers/db/Command.php
@@ -13,6 +13,7 @@
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
use yii\queue\cli\Queue as CliQueue;
+use yii\queue\cli\InfoAction;
/**
* Manages application db-queue.
@@ -23,7 +24,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
/**
diff --git a/src/drivers/db/InfoAction.php b/src/drivers/db/InfoAction.php
index dbc5b4f7a..1280fa9a0 100644
--- a/src/drivers/db/InfoAction.php
+++ b/src/drivers/db/InfoAction.php
@@ -20,13 +20,15 @@
/**
* Info about queue status.
*
+ * @deprecated Will be removed in 3.0. Use yii\queue\cli\InfoAction instead.
+ *
* @author Roman Zhuravlev
*/
class InfoAction extends Action
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/drivers/db/Queue.php b/src/drivers/db/Queue.php
index a98f88a4c..c26ad79b1 100644
--- a/src/drivers/db/Queue.php
+++ b/src/drivers/db/Queue.php
@@ -17,13 +17,17 @@
use yii\di\Instance;
use yii\mutex\Mutex;
use yii\queue\cli\Queue as CliQueue;
+use yii\queue\interfaces\StatisticsInterface;
+use yii\queue\interfaces\StatisticsProviderInterface;
/**
* Db Queue.
*
+ * @property-read StatisticsProvider $statisticsProvider
+ *
* @author Roman Zhuravlev
*/
-class Queue extends CliQueue
+class Queue extends CliQueue implements StatisticsProviderInterface
{
/**
* @var Connection|array|string
@@ -86,8 +90,8 @@ public function run(bool $repeat, int $timeout = 0)
if ($this->handleMessage(
$payload['id'],
$payload['job'],
- (int)$payload['ttr'],
- (int)$payload['attempt']
+ (int) $payload['ttr'],
+ (int) $payload['attempt']
)) {
$this->release($payload);
}
@@ -281,4 +285,17 @@ private function getMutex(): Mutex
}
return $mutex;
}
+
+ private StatisticsInterface $_statisticsProvider;
+
+ /**
+ * @return StatisticsInterface
+ */
+ public function getStatisticsProvider(): StatisticsInterface
+ {
+ if (!isset($this->_statisticsProvider)) {
+ $this->_statisticsProvider = new StatisticsProvider($this);
+ }
+ return $this->_statisticsProvider;
+ }
}
diff --git a/src/drivers/db/StatisticsProvider.php b/src/drivers/db/StatisticsProvider.php
new file mode 100644
index 000000000..c5d0f3a9b
--- /dev/null
+++ b/src/drivers/db/StatisticsProvider.php
@@ -0,0 +1,98 @@
+
+ */
+class StatisticsProvider extends BaseObject implements
+ DoneCountInterface,
+ WaitingCountInterface,
+ DelayedCountInterface,
+ ReservedCountInterface,
+ StatisticsInterface
+{
+ /**
+ * @var Queue
+ */
+ protected Queue $queue;
+
+ public function __construct(Queue $queue, array $config = [])
+ {
+ $this->queue = $queue;
+ parent::__construct($config);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getWaitingCount(): int
+ {
+ /** @psalm-var \yii\db\Connection $this->queue->db */
+ return (int) (new Query())
+ ->from($this->queue->tableName)
+ ->andWhere(['channel' => $this->queue->channel])
+ ->andWhere(['reserved_at' => null])
+ ->andWhere(['delay' => 0])
+ ->count('*', $this->queue->db);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDelayedCount(): int
+ {
+ /** @psalm-var \yii\db\Connection $this->queue->db */
+ return (int) (new Query())
+ ->from($this->queue->tableName)
+ ->andWhere(['channel' => $this->queue->channel])
+ ->andWhere(['reserved_at' => null])
+ ->andWhere(['>', 'delay', 0])
+ ->count('*', $this->queue->db);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getReservedCount(): int
+ {
+ /** @psalm-var \yii\db\Connection $this->queue->db */
+ return (int) (new Query())
+ ->from($this->queue->tableName)
+ ->andWhere(['channel' => $this->queue->channel])
+ ->andWhere('[[reserved_at]] is not null')
+ ->andWhere(['done_at' => null])
+ ->count('*', $this->queue->db);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDoneCount(): int
+ {
+ /** @psalm-var \yii\db\Connection $this->queue->db */
+ return (int) (new Query())
+ ->from($this->queue->tableName)
+ ->andWhere(['channel' => $this->queue->channel])
+ ->andWhere('[[done_at]] is not null')
+ ->count('*', $this->queue->db);
+ }
+}
diff --git a/src/drivers/file/Command.php b/src/drivers/file/Command.php
index 354c373c9..abe94f4d3 100644
--- a/src/drivers/file/Command.php
+++ b/src/drivers/file/Command.php
@@ -13,6 +13,7 @@
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
use yii\queue\cli\Queue as CliQueue;
+use yii\queue\cli\InfoAction;
/**
* Manages application file-queue.
@@ -23,7 +24,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
/**
diff --git a/src/drivers/file/InfoAction.php b/src/drivers/file/InfoAction.php
index c91d895fd..a2bc34d28 100644
--- a/src/drivers/file/InfoAction.php
+++ b/src/drivers/file/InfoAction.php
@@ -18,13 +18,15 @@
/**
* Info about queue status.
*
+ * @deprecated Will be removed in 3.0. Use yii\queue\cli\InfoAction instead.
+ *
* @author Roman Zhuravlev
*/
class InfoAction extends Action
{
/**
* @var Queue
- * @psalm-suppress NonInvariantDocblockPropertyType, PropertyNotSetInConstructor
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/drivers/file/Queue.php b/src/drivers/file/Queue.php
index 5a7ee35b4..2bfcade70 100644
--- a/src/drivers/file/Queue.php
+++ b/src/drivers/file/Queue.php
@@ -16,13 +16,17 @@
use yii\base\NotSupportedException;
use yii\helpers\FileHelper;
use yii\queue\cli\Queue as CliQueue;
+use yii\queue\interfaces\StatisticsInterface;
+use yii\queue\interfaces\StatisticsProviderInterface;
/**
* File Queue.
*
+ * @property-read StatisticsProvider $statisticsProvider
+ *
* @author Roman Zhuravlev
*/
-class Queue extends CliQueue
+class Queue extends CliQueue implements StatisticsProviderInterface
{
/**
* @var string
@@ -257,6 +261,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri
if ($priority !== null) {
throw new NotSupportedException('Job priority is not supported in the driver.');
}
+ $id = 0;
$this->touchIndex(function (array &$data) use ($payload, $ttr, $delay, &$id) {
/** @var array{lastId: int, waiting: array, delayed: array} $data */
@@ -335,4 +340,17 @@ private function touchIndex(callable $callback): void
fclose($file);
}
}
+
+ private StatisticsInterface $_statisticsProvider;
+
+ /**
+ * @return StatisticsInterface
+ */
+ public function getStatisticsProvider(): StatisticsInterface
+ {
+ if (!isset($this->_statisticsProvider)) {
+ $this->_statisticsProvider = new StatisticsProvider($this);
+ }
+ return $this->_statisticsProvider;
+ }
}
diff --git a/src/drivers/file/StatisticsProvider.php b/src/drivers/file/StatisticsProvider.php
new file mode 100644
index 000000000..27cc1a75f
--- /dev/null
+++ b/src/drivers/file/StatisticsProvider.php
@@ -0,0 +1,96 @@
+
+ */
+class StatisticsProvider extends BaseObject implements
+ DoneCountInterface,
+ WaitingCountInterface,
+ DelayedCountInterface,
+ ReservedCountInterface,
+ StatisticsInterface
+{
+ /**
+ * @var Queue
+ */
+ protected Queue $queue;
+
+ public function __construct(Queue $queue, array $config = [])
+ {
+ $this->queue = $queue;
+ parent::__construct($config);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getWaitingCount(): int
+ {
+ /** @var array{waiting:array} $data */
+ $data = $this->getIndexData();
+ return !empty($data['waiting']) ? count($data['waiting']) : 0;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDelayedCount(): int
+ {
+ /** @var array{delayed:array} $data */
+ $data = $this->getIndexData();
+ return !empty($data['delayed']) ? count($data['delayed']) : 0;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getReservedCount(): int
+ {
+ /** @var array{reserved:array} $data */
+ $data = $this->getIndexData();
+ return !empty($data['reserved']) ? count($data['reserved']) : 0;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDoneCount(): int
+ {
+ /** @var array{lastId:int} $data */
+ $data = $this->getIndexData();
+ $total = isset($data['lastId']) ? $data['lastId'] : 0;
+ return $total - $this->getDelayedCount() - $this->getWaitingCount();
+ }
+
+ /**
+ * @psalm-suppress MissingReturnType
+ */
+ protected function getIndexData()
+ {
+ $fileName = $this->queue->path . '/index.data';
+ if (file_exists($fileName)) {
+ return call_user_func($this->queue->indexDeserializer, file_get_contents($fileName));
+ }
+
+ return [];
+ }
+}
diff --git a/src/drivers/gearman/Command.php b/src/drivers/gearman/Command.php
index 3bf256cac..d21c191e4 100644
--- a/src/drivers/gearman/Command.php
+++ b/src/drivers/gearman/Command.php
@@ -22,7 +22,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/drivers/redis/Command.php b/src/drivers/redis/Command.php
index 0872e319c..50da9ac51 100644
--- a/src/drivers/redis/Command.php
+++ b/src/drivers/redis/Command.php
@@ -13,6 +13,7 @@
use yii\console\Exception;
use yii\queue\cli\Command as CliCommand;
use yii\queue\cli\Queue as CliQueue;
+use yii\queue\cli\InfoAction;
/**
* Manages application redis-queue.
@@ -23,7 +24,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
/**
diff --git a/src/drivers/redis/InfoAction.php b/src/drivers/redis/InfoAction.php
index e052feb4a..97fe1fa1e 100644
--- a/src/drivers/redis/InfoAction.php
+++ b/src/drivers/redis/InfoAction.php
@@ -17,13 +17,15 @@
/**
* Info about queue status.
*
+ * @deprecated Will be removed in 3.0. Use yii\queue\cli\InfoAction instead.
+ *
* @author Roman Zhuravlev
*/
class InfoAction extends Action
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/drivers/redis/Queue.php b/src/drivers/redis/Queue.php
index f9004c941..48232e3c0 100644
--- a/src/drivers/redis/Queue.php
+++ b/src/drivers/redis/Queue.php
@@ -14,14 +14,18 @@
use yii\base\NotSupportedException;
use yii\di\Instance;
use yii\queue\cli\Queue as CliQueue;
+use yii\queue\interfaces\StatisticsInterface;
+use yii\queue\interfaces\StatisticsProviderInterface;
use yii\redis\Connection;
/**
* Redis Queue.
*
+ * @property-read StatisticsProvider $statisticsProvider
+ *
* @author Roman Zhuravlev
*/
-class Queue extends CliQueue
+class Queue extends CliQueue implements StatisticsProviderInterface
{
/**
* @var Connection|array|string
@@ -182,10 +186,10 @@ protected function moveExpired(string $from): void
{
$now = time();
if ($expired = $this->redis->zrevrangebyscore($from, $now, '-inf')) {
- $this->redis->zremrangebyscore($from, '-inf', $now);
foreach ($expired as $id) {
$this->redis->rpush("$this->channel.waiting", $id);
}
+ $this->redis->zremrangebyscore($from, '-inf', $now);
}
}
@@ -221,4 +225,17 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri
return $id;
}
+
+ private StatisticsInterface $_statisticsProvider;
+
+ /**
+ * @return StatisticsInterface
+ */
+ public function getStatisticsProvider(): StatisticsInterface
+ {
+ if (!isset($this->_statisticsProvider)) {
+ $this->_statisticsProvider = new StatisticsProvider($this);
+ }
+ return $this->_statisticsProvider;
+ }
}
diff --git a/src/drivers/redis/StatisticsProvider.php b/src/drivers/redis/StatisticsProvider.php
new file mode 100644
index 000000000..5df423f33
--- /dev/null
+++ b/src/drivers/redis/StatisticsProvider.php
@@ -0,0 +1,83 @@
+
+ */
+class StatisticsProvider extends BaseObject implements
+ DoneCountInterface,
+ WaitingCountInterface,
+ DelayedCountInterface,
+ ReservedCountInterface,
+ StatisticsInterface
+{
+ /**
+ * @var Queue
+ */
+ protected Queue $queue;
+
+
+ public function __construct(Queue $queue, array $config = [])
+ {
+ $this->queue = $queue;
+ parent::__construct($config);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getWaitingCount(): int
+ {
+ $prefix = $this->queue->channel;
+ return (int) $this->queue->redis->llen("$prefix.waiting");
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDelayedCount(): int
+ {
+ $prefix = $this->queue->channel;
+ return (int) $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getReservedCount(): int
+ {
+ $prefix = $this->queue->channel;
+ return (int) $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDoneCount(): int
+ {
+ $prefix = $this->queue->channel;
+ $waiting = $this->getWaitingCount();
+ $delayed = $this->getDelayedCount();
+ $reserved = $this->getReservedCount();
+ $total = (int) $this->queue->redis->get("$prefix.message_id");
+ return $total - $waiting - $delayed - $reserved;
+ }
+}
diff --git a/src/drivers/sqs/Command.php b/src/drivers/sqs/Command.php
index e5f90c490..9cb5850ad 100644
--- a/src/drivers/sqs/Command.php
+++ b/src/drivers/sqs/Command.php
@@ -24,7 +24,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/drivers/stomp/Command.php b/src/drivers/stomp/Command.php
index 71bbfd9d1..974a0d1c3 100644
--- a/src/drivers/stomp/Command.php
+++ b/src/drivers/stomp/Command.php
@@ -24,7 +24,7 @@ class Command extends CliCommand
{
/**
* @var Queue
- * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType
+ * @psalm-suppress NonInvariantDocblockPropertyType
*/
public CliQueue $queue;
diff --git a/src/interfaces/DelayedCountInterface.php b/src/interfaces/DelayedCountInterface.php
new file mode 100644
index 000000000..fcf19fd22
--- /dev/null
+++ b/src/interfaces/DelayedCountInterface.php
@@ -0,0 +1,24 @@
+
+ */
+interface DelayedCountInterface
+{
+ /**
+ * @return int
+ */
+ public function getDelayedCount(): int;
+}
diff --git a/src/interfaces/DoneCountInterface.php b/src/interfaces/DoneCountInterface.php
new file mode 100644
index 000000000..14c0930b6
--- /dev/null
+++ b/src/interfaces/DoneCountInterface.php
@@ -0,0 +1,24 @@
+
+ */
+interface DoneCountInterface
+{
+ /**
+ * @return int
+ */
+ public function getDoneCount(): int;
+}
diff --git a/src/interfaces/ReservedCountInterface.php b/src/interfaces/ReservedCountInterface.php
new file mode 100644
index 000000000..984a442c8
--- /dev/null
+++ b/src/interfaces/ReservedCountInterface.php
@@ -0,0 +1,24 @@
+
+ */
+interface ReservedCountInterface
+{
+ /**
+ * @return int
+ */
+ public function getReservedCount(): int;
+}
diff --git a/src/interfaces/StatisticsInterface.php b/src/interfaces/StatisticsInterface.php
new file mode 100644
index 000000000..34cbfde77
--- /dev/null
+++ b/src/interfaces/StatisticsInterface.php
@@ -0,0 +1,18 @@
+
+ */
+interface StatisticsProviderInterface
+{
+ /**
+ * @return StatisticsInterface
+ */
+ public function getStatisticsProvider(): StatisticsInterface;
+}
diff --git a/src/interfaces/WaitingCountInterface.php b/src/interfaces/WaitingCountInterface.php
new file mode 100644
index 000000000..ec20be06c
--- /dev/null
+++ b/src/interfaces/WaitingCountInterface.php
@@ -0,0 +1,24 @@
+
+ */
+interface WaitingCountInterface
+{
+ /**
+ * @return int
+ */
+ public function getWaitingCount(): int;
+}
diff --git a/src/serializers/JsonSerializer.php b/src/serializers/JsonSerializer.php
index 0483dcbfb..30010494d 100644
--- a/src/serializers/JsonSerializer.php
+++ b/src/serializers/JsonSerializer.php
@@ -29,7 +29,7 @@ class JsonSerializer extends BaseObject implements SerializerInterface
/**
* @var int
*/
- public $options = 0;
+ public int $options = 0;
/**
* @inheritdoc
diff --git a/tests/app/config/main.php b/tests/app/config/main.php
index cf47d8faf..66f104ed0 100644
--- a/tests/app/config/main.php
+++ b/tests/app/config/main.php
@@ -62,6 +62,7 @@
'class' => MysqlMutex::class,
'db' => 'mysql',
],
+ 'deleteReleased' => false,
],
'sqlite' => [
'class' => Connection::class,
@@ -71,6 +72,7 @@
'class' => DbQueue::class,
'db' => 'sqlite',
'mutex' => FileMutex::class,
+ 'deleteReleased' => false,
],
'pgsql' => [
'class' => Connection::class,
@@ -91,6 +93,7 @@
'db' => 'pgsql',
],
'mutexTimeout' => 0,
+ 'deleteReleased' => false,
],
'redis' => [
'class' => RedisConnection::class,
diff --git a/tests/cli/InfoActionTest.php b/tests/cli/InfoActionTest.php
new file mode 100644
index 000000000..de8a3b876
--- /dev/null
+++ b/tests/cli/InfoActionTest.php
@@ -0,0 +1,186 @@
+
+ */
+class InfoActionTest extends TestCase
+{
+ public function testWaitingCount(): void
+ {
+ $controller = $this->getMockBuilder(Controller::class)
+ ->setConstructorArgs(['testController', new Module('testModule')])
+ ->getMock();
+
+ $controller->expects(self::exactly(3))
+ ->method('stdout')
+ ->willReturnOnConsecutiveCalls(
+ [
+ 'Jobs' . PHP_EOL,
+ Console::FG_GREEN,
+ ],
+ [
+ '- waiting: ',
+ Console::FG_YELLOW,
+ ],
+ [
+ 10 . PHP_EOL
+ ]
+ );
+
+ $queue = $this->getMockBuilder(Queue::class)->getMock();
+
+ $provider = $this->getMockBuilder(WaitingCountProvider::class)
+ ->setConstructorArgs([$queue])
+ ->getMock();
+ $provider->expects(self::once())
+ ->method('getWaitingCount')
+ ->willReturn(10);
+
+ $queue->method('getStatisticsProvider')->willReturn($provider);
+
+ $action = (new InfoAction('infoAction', $controller, [
+ 'queue' => $queue,
+ ]));
+ $action->run();
+ }
+
+ public function testDelayedCount(): void
+ {
+ $controller = $this->getMockBuilder(Controller::class)
+ ->setConstructorArgs(['testController', new Module('testModule')])
+ ->getMock();
+
+ $controller->expects(self::exactly(3))
+ ->method('stdout')
+ ->willReturnOnConsecutiveCalls(
+ [
+ 'Jobs' . PHP_EOL,
+ Console::FG_GREEN,
+ ],
+ [
+ '- delayed: ',
+ Console::FG_YELLOW,
+ ],
+ [
+ 10 . PHP_EOL
+ ]
+ );
+
+ $queue = $this->getMockBuilder(Queue::class)->getMock();
+
+ $provider = $this->getMockBuilder(DelayedCountProvider::class)
+ ->setConstructorArgs([$queue])
+ ->getMock();
+ $provider->expects(self::once())
+ ->method('getDelayedCount')
+ ->willReturn(10);
+
+ $queue->method('getStatisticsProvider')->willReturn($provider);
+
+ $action = (new InfoAction('infoAction', $controller, [
+ 'queue' => $queue,
+ ]));
+ $action->run();
+ }
+
+ public function testReservedCount(): void
+ {
+ $controller = $this->getMockBuilder(Controller::class)
+ ->setConstructorArgs(['testController', new Module('testModule')])
+ ->getMock();
+
+ $controller->expects(self::exactly(3))
+ ->method('stdout')
+ ->willReturnOnConsecutiveCalls(
+ [
+ 'Jobs' . PHP_EOL,
+ Console::FG_GREEN,
+ ],
+ [
+ '- reserved: ',
+ Console::FG_YELLOW,
+ ],
+ [
+ 10 . PHP_EOL
+ ]
+ );
+
+ $queue = $this->getMockBuilder(Queue::class)->getMock();
+
+ $provider = $this->getMockBuilder(ReservedCountProvider::class)
+ ->setConstructorArgs([$queue])
+ ->getMock()
+ ;
+ $provider->expects(self::once())
+ ->method('getReservedCount')
+ ->willReturn(10);
+
+ $queue->method('getStatisticsProvider')->willReturn($provider);
+
+ $action = (new InfoAction('infoAction', $controller, [
+ 'queue' => $queue,
+ ]));
+ $action->run();
+ }
+
+ public function testDoneCount(): void
+ {
+ $controller = $this->getMockBuilder(Controller::class)
+ ->setConstructorArgs(['testController', new Module('testModule')])
+ ->getMock();
+
+ $controller->expects(self::exactly(3))
+ ->method('stdout')
+ ->willReturnOnConsecutiveCalls(
+ [
+ 'Jobs' . PHP_EOL,
+ Console::FG_GREEN,
+ ],
+ [
+ '- done: ',
+ Console::FG_YELLOW,
+ ],
+ [
+ 10 . PHP_EOL
+ ]
+ );
+
+ $queue = $this->getMockBuilder(Queue::class)->getMock();
+
+ $provider = $this->getMockBuilder(DoneCountProvider::class)
+ ->setConstructorArgs([$queue])
+ ->getMock();
+ $provider->expects(self::once())
+ ->method('getDoneCount')
+ ->willReturn(10);
+
+ $queue->method('getStatisticsProvider')->willReturn($provider);
+
+ $action = (new InfoAction('infoAction', $controller, [
+ 'queue' => $queue,
+ ]));
+ $action->run();
+ }
+}
diff --git a/tests/cli/Queue.php b/tests/cli/Queue.php
new file mode 100644
index 000000000..e7b5b981c
--- /dev/null
+++ b/tests/cli/Queue.php
@@ -0,0 +1,53 @@
+
+ */
+class Queue extends CliQueue implements StatisticsProviderInterface
+{
+ /**
+ * @inheritdoc
+ */
+ public function status($id): int
+ {
+ throw new NotSupportedException('"status" method is not supported.');
+ }
+ /**
+ * @inheritdoc
+ */
+ protected function pushMessage(string $payload, $ttr, $delay, $priority): int|string|null
+ {
+ throw new NotSupportedException('"pushMessage" method is not supported.');
+ }
+
+ private StatisticsInterface $_statisticsProvider;
+
+ /**
+ * @return StatisticsInterface
+ */
+ public function getStatisticsProvider(): StatisticsInterface
+ {
+ if (!isset($this->_statisticsProvider)) {
+ $this->_statisticsProvider = new BaseStatisticsProvider($this);
+ }
+ return $this->_statisticsProvider;
+ }
+}
diff --git a/tests/cli/providers/BaseStatisticsProvider.php b/tests/cli/providers/BaseStatisticsProvider.php
new file mode 100644
index 000000000..3e7acce1a
--- /dev/null
+++ b/tests/cli/providers/BaseStatisticsProvider.php
@@ -0,0 +1,34 @@
+
+ */
+class BaseStatisticsProvider extends BaseObject implements StatisticsInterface
+{
+ /**
+ * @var Queue
+ */
+ protected Queue $queue;
+
+ public function __construct(Queue $queue, array $config = [])
+ {
+ $this->queue = $queue;
+ parent::__construct($config);
+ }
+}
diff --git a/tests/cli/providers/DelayedCountProvider.php b/tests/cli/providers/DelayedCountProvider.php
new file mode 100644
index 000000000..17e7d6dcd
--- /dev/null
+++ b/tests/cli/providers/DelayedCountProvider.php
@@ -0,0 +1,29 @@
+
+ */
+class DelayedCountProvider extends BaseStatisticsProvider implements DelayedCountInterface
+{
+ /**
+ * @inheritdoc
+ */
+ public function getDelayedCount(): int
+ {
+ return 10;
+ }
+}
diff --git a/tests/cli/providers/DoneCountProvider.php b/tests/cli/providers/DoneCountProvider.php
new file mode 100644
index 000000000..1fa1b651d
--- /dev/null
+++ b/tests/cli/providers/DoneCountProvider.php
@@ -0,0 +1,29 @@
+
+ */
+class DoneCountProvider extends BaseStatisticsProvider implements DoneCountInterface
+{
+ /**
+ * @inheritdoc
+ */
+ public function getDoneCount(): int
+ {
+ return 10;
+ }
+}
diff --git a/tests/cli/providers/ReservedCountProvider.php b/tests/cli/providers/ReservedCountProvider.php
new file mode 100644
index 000000000..2b1cd8879
--- /dev/null
+++ b/tests/cli/providers/ReservedCountProvider.php
@@ -0,0 +1,29 @@
+
+ */
+class ReservedCountProvider extends BaseStatisticsProvider implements ReservedCountInterface
+{
+ /**
+ * @inheritdoc
+ */
+ public function getReservedCount(): int
+ {
+ return 10;
+ }
+}
diff --git a/tests/cli/providers/WaitingCountProvider.php b/tests/cli/providers/WaitingCountProvider.php
new file mode 100644
index 000000000..2ae306797
--- /dev/null
+++ b/tests/cli/providers/WaitingCountProvider.php
@@ -0,0 +1,29 @@
+
+ */
+class WaitingCountProvider extends BaseStatisticsProvider implements WaitingCountInterface
+{
+ /**
+ * @inheritdoc
+ */
+ public function getWaitingCount(): int
+ {
+ return 10;
+ }
+}
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 3c641de34..0efa42219 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -1,6 +1,5 @@
-version: "3.5"
+---
services:
-
# https://hub.docker.com/_/php/
yii2-queue-php:
container_name: yii2-queue-php
@@ -8,7 +7,7 @@ services:
context: ..
dockerfile: tests/docker/php/Dockerfile
args:
- PHP_VERSION: ${PHP_VERSION:-8.1}
+ PHP_VERSION: ${PHP_VERSION:-8.2}
volumes:
- ./runtime/.composer:/root/.composer
- ..:/code
@@ -72,6 +71,10 @@ services:
MYSQL_DATABASE: yii2_queue_test
networks:
net: {}
+ ulimits:
+ nofile:
+ soft: 262144
+ hard: 262144
# https://hub.docker.com/_/postgres/
postgres:
diff --git a/tests/docker/php/Dockerfile b/tests/docker/php/Dockerfile
index da2ee33bb..c76d6726d 100644
--- a/tests/docker/php/Dockerfile
+++ b/tests/docker/php/Dockerfile
@@ -1,7 +1,7 @@
# Important! Do not use this image in production!
ARG PHP_VERSION
-FROM --platform=linux/amd64 php:${PHP_VERSION}-cli-alpine
+FROM php:${PHP_VERSION}-cli-alpine
RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories \
&& echo https://dl-cdn.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories \
diff --git a/tests/drivers/db/TestCase.php b/tests/drivers/db/TestCase.php
index 61b36544a..57648e9a7 100644
--- a/tests/drivers/db/TestCase.php
+++ b/tests/drivers/db/TestCase.php
@@ -13,6 +13,7 @@
use tests\app\PriorityJob;
use tests\app\RetryJob;
use tests\drivers\CliTestCase;
+use Yii;
use yii\db\Query;
/**
@@ -108,8 +109,48 @@ public function testRemove(): void
$this->assertEquals(0, $actual);
}
+ public function testWaitingCount(): void
+ {
+ $this->getQueue()->push($this->createSimpleJob());
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getWaitingCount());
+ }
+
+ public function testDelayedCount(): void
+ {
+ $this->getQueue()->delay(5)->push($this->createSimpleJob());
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDelayedCount());
+ }
+
+ public function testReservedCount(): void
+ {
+ $this->getQueue()->messageHandler = function () {
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount());
+ return true;
+ };
+
+ $job = $this->createSimpleJob();
+ $this->getQueue()->push($job);
+ $this->getQueue()->run(false);
+ }
+
+ public function testDoneCount(): void
+ {
+ $this->getQueue()->messageHandler = static function () {
+ return true;
+ };
+
+ $job = $this->createSimpleJob();
+ $this->getQueue()->push($job);
+ $this->getQueue()->run(false);
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDoneCount());
+ }
+
protected function tearDown(): void
{
+ $this->getQueue()->messageHandler = null;
$this->getQueue()->db->createCommand()
->delete($this->getQueue()->tableName)
->execute();
diff --git a/tests/drivers/file/QueueTest.php b/tests/drivers/file/QueueTest.php
index 5fda9c247..cab469415 100644
--- a/tests/drivers/file/QueueTest.php
+++ b/tests/drivers/file/QueueTest.php
@@ -90,6 +90,42 @@ public function testRemove(): void
$this->assertFileDoesNotExist($this->getQueue()->path . "/job$id.data");
}
+ public function testWaitingCount(): void
+ {
+ $this->getQueue()->push($this->createSimpleJob());
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getWaitingCount());
+ }
+
+ public function testDelayedCount(): void
+ {
+ $this->getQueue()->delay(5)->push($this->createSimpleJob());
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDelayedCount());
+ }
+
+ public function testReservedCount(): void
+ {
+ $this->getQueue()->messageHandler = function () {
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount());
+ return true;
+ };
+
+ $this->getQueue()->push($this->createSimpleJob());
+ $this->getQueue()->run(false);
+ }
+
+ public function testDoneCount(): void
+ {
+ $this->startProcess(['php', 'yii', 'queue/listen', '1']);
+ $job = $this->createSimpleJob();
+ $this->getQueue()->push($job);
+
+ $this->assertSimpleJobDone($job);
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDoneCount());
+ }
+
/**
* @return Queue
*/
@@ -100,6 +136,7 @@ protected function getQueue(): Queue
protected function tearDown(): void
{
+ $this->getQueue()->messageHandler = null;
foreach (glob(Yii::getAlias("@runtime/queue/*")) as $fileName) {
unlink($fileName);
}
diff --git a/tests/drivers/redis/QueueTest.php b/tests/drivers/redis/QueueTest.php
index 48ac8ff47..6777c0a45 100644
--- a/tests/drivers/redis/QueueTest.php
+++ b/tests/drivers/redis/QueueTest.php
@@ -13,7 +13,9 @@
use tests\app\RetryJob;
use tests\drivers\CliTestCase;
use Yii;
+use yii\di\Instance;
use yii\queue\redis\Queue;
+use yii\redis\Connection;
/**
* Redis Queue Test.
@@ -90,6 +92,42 @@ public function testRemove(): void
$this->assertFalse((bool) $this->getQueue()->redis->hexists($this->getQueue()->channel . '.messages', $id));
}
+ public function testWaitingCount(): void
+ {
+ $this->getQueue()->push($this->createSimpleJob());
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getWaitingCount());
+ }
+
+ public function testDelayedCount(): void
+ {
+ $this->getQueue()->delay(5)->push($this->createSimpleJob());
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDelayedCount());
+ }
+
+ public function testReservedCount(): void
+ {
+ $this->getQueue()->messageHandler = function () {
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount());
+ return true;
+ };
+
+ $this->getQueue()->push($this->createSimpleJob());
+ $this->getQueue()->run(false);
+ }
+
+ public function testDoneCount(): void
+ {
+ $this->startProcess(['php', 'yii', 'queue/listen', '1']);
+ $job = $this->createSimpleJob();
+ $this->getQueue()->push($job);
+
+ $this->assertSimpleJobDone($job);
+
+ $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDoneCount());
+ }
+
/**
* @return Queue
*/
@@ -100,7 +138,58 @@ protected function getQueue(): Queue
protected function tearDown(): void
{
+ $this->getQueue()->messageHandler = null;
$this->getQueue()->redis->flushdb();
parent::tearDown();
}
+
+ /**
+ * Verify that Redis data persists when process crashes during moveExpired.
+ *
+ * Steps:
+ * 1. Push a delayed job into queue
+ * 2. Wait for the job to expire
+ * 3. Mock Redis to simulate crash during moveExpired
+ * 4. Successfully process job after recovery
+ */
+ public function testConsumeDelayedMessageAtLeastOnce(): void
+ {
+ $job = $this->createSimpleJob();
+ $this->getQueue()->delay(1)->push($job);
+ // Expect a single message to be received.
+ $messageCount = 0;
+ $this->getQueue()->messageHandler = static function () use(&$messageCount) {
+ $messageCount++;
+ return true;
+ };
+
+ // Ensure the delayed message can be consumed when more time passed than the delay is.
+ sleep(2);
+
+ // Based on the implemention, emulate a crash when redis "rpush"
+ // command should be executed.
+ $mockRedis = Instance::ensure([
+ 'class' => RedisCrashMock::class,
+ 'hostname' => getenv('REDIS_HOST') ?: 'localhost',
+ 'database' => getenv('REDIS_DB') ?: 1,
+ 'crashOnCommand' => 'rpush' // Crash when trying to move job to waiting queue.
+ ], Connection::class);
+
+ $queue = $this->getQueue();
+ $old = $queue->redis;
+ $queue->redis = $mockRedis;
+
+ try {
+ $queue->run(false);
+ } catch (\Exception $e) {
+ // Ignore exceptions.
+ } finally {
+ $queue->redis = $old;
+ }
+
+ // Ensure the red lock is invalid. The red lock is valid for 1s.
+ sleep(2);
+ $this->getQueue()->run(false);
+ $this->assertEquals(1, $messageCount);
+ }
}
diff --git a/tests/drivers/redis/RedisCrashMock.php b/tests/drivers/redis/RedisCrashMock.php
new file mode 100644
index 000000000..b332d6abe
--- /dev/null
+++ b/tests/drivers/redis/RedisCrashMock.php
@@ -0,0 +1,20 @@
+crashOnCommand) {
+ throw new \RuntimeException('Simulated Redis crash');
+ }
+ return parent::executeCommand($name, $params);
+ }
+}