From 23abdee3660e564cc1c50530785c3e2d893cf9a8 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Thu, 12 Oct 2023 23:07:17 +0400 Subject: [PATCH 01/16] Setting the minimum PHP version to 8.1 (#496) --- .editorconfig | 3 + .gitattributes | 3 +- .github/workflows/main.yml | 30 ++- .github/workflows/static.yml | 49 +++++ .gitignore | 4 + Makefile | 18 +- README.md | 4 + composer.json | 33 ++- phpunit.xml.dist | 37 ++-- psalm.xml | 25 +++ src/ExecEvent.php | 5 +- src/InvalidJobException.php | 22 +- src/Job.php | 19 -- src/JobEvent.php | 14 +- src/JobInterface.php | 5 +- src/LogBehavior.php | 24 ++- src/PushEvent.php | 9 +- src/Queue.php | 111 +++++----- src/RetryableJob.php | 19 -- src/RetryableJobInterface.php | 12 +- src/cli/Action.php | 12 +- src/cli/Command.php | 45 ++-- src/cli/LoopInterface.php | 5 +- src/cli/Queue.php | 36 ++-- src/cli/Signal.php | 60 ------ src/cli/SignalLoop.php | 26 +-- src/cli/Verbose.php | 19 -- src/cli/VerboseBehavior.php | 26 +-- src/cli/WorkerEvent.php | 7 +- src/closure/Behavior.php | 17 +- src/closure/Job.php | 22 +- src/debug/Panel.php | 4 +- src/debug/views/detail.php | 3 + src/debug/views/summary.php | 4 +- src/drivers/amqp/Command.php | 43 ---- src/drivers/amqp/Queue.php | 167 --------------- src/drivers/amqp_interop/Command.php | 11 +- src/drivers/amqp_interop/Queue.php | 178 +++++++++------- src/drivers/beanstalk/Command.php | 19 +- src/drivers/beanstalk/InfoAction.php | 9 +- src/drivers/beanstalk/Queue.php | 33 +-- src/drivers/db/Command.php | 24 +-- src/drivers/db/InfoAction.php | 9 +- src/drivers/db/Queue.php | 30 +-- .../db/migrations/M161119140200Queue.php | 10 +- .../db/migrations/M170307170300Later.php | 16 +- .../db/migrations/M170509001400Retry.php | 10 +- .../db/migrations/M170601155600Priority.php | 16 +- .../migrations/M211218163000JobQueueSize.php | 10 +- src/drivers/file/Command.php | 20 +- src/drivers/file/InfoAction.php | 27 +-- src/drivers/file/Queue.php | 32 +-- src/drivers/gearman/Command.php | 13 +- src/drivers/gearman/Queue.php | 49 ++--- src/drivers/redis/Command.php | 24 +-- src/drivers/redis/InfoAction.php | 9 +- src/drivers/redis/Queue.php | 40 ++-- src/drivers/sqs/Command.php | 17 +- src/drivers/sqs/Queue.php | 56 +++-- src/drivers/stomp/Command.php | 17 +- src/drivers/stomp/Queue.php | 88 ++++---- src/drivers/sync/Queue.php | 27 +-- src/gii/Generator.php | 4 +- src/gii/default/job.php | 5 +- src/gii/form.php | 3 + src/serializers/IgbinarySerializer.php | 5 +- src/serializers/JsonSerializer.php | 6 +- src/serializers/PhpSerializer.php | 5 +- src/serializers/Serializer.php | 19 -- src/serializers/SerializerInterface.php | 5 +- support/ide-helper.php | 65 ++++++ tests/JobEventTest.php | 9 +- tests/TestCase.php | 5 +- tests/app/PriorityJob.php | 10 +- tests/app/RetryJob.php | 12 +- tests/app/SimpleJob.php | 10 +- tests/app/benchmark/Controller.php | 13 +- tests/app/benchmark/waiting/Action.php | 29 +-- tests/app/benchmark/waiting/Job.php | 14 +- tests/app/config/console.php | 14 +- tests/app/config/main.php | 70 ++++--- tests/bootstrap.php | 3 +- tests/closure/ClosureJob.php | 8 +- tests/closure/ClosureTest.php | 15 +- tests/docker-compose.yml | 18 +- tests/docker/php/5.6/Dockerfile | 30 --- tests/docker/php/7.0/Dockerfile | 39 ---- tests/docker/php/7.1/Dockerfile | 29 --- tests/docker/php/7.2/Dockerfile | 29 --- tests/docker/php/7.3/Dockerfile | 29 --- tests/docker/php/7.4/Dockerfile | 18 -- tests/docker/php/8.0/Dockerfile | 17 -- tests/docker/php/Dockerfile | 36 ++++ tests/docker/php/entrypoint.sh | 16 +- tests/docker/wait-for-it.sh | 178 ---------------- tests/drivers/CliTestCase.php | 18 +- tests/drivers/TestCase.php | 17 +- tests/drivers/amqp/QueueTest.php | 37 ---- tests/drivers/amqp_interop/AmqpTestCase.php | 125 +++++++++++ tests/drivers/amqp_interop/QueueTest.php | 194 ++++++++---------- tests/drivers/beanstalk/QueueTest.php | 29 +-- tests/drivers/db/MysqlQueueTest.php | 5 +- tests/drivers/db/PgsqlQueueTest.php | 5 +- tests/drivers/db/SqliteQueueTest.php | 5 +- tests/drivers/db/TestCase.php | 21 +- tests/drivers/file/QueueTest.php | 23 ++- tests/drivers/gearman/QueueTest.php | 15 +- tests/drivers/redis/QueueTest.php | 21 +- tests/drivers/sqs/FifoQueueTest.php | 17 +- tests/drivers/sqs/QueueTest.php | 17 +- tests/drivers/stomp/QueueTest.php | 22 +- tests/drivers/sync/QueueTest.php | 11 +- tests/serializers/IgbinarySerializerTest.php | 7 +- tests/serializers/JsonSerializerTest.php | 13 +- tests/serializers/PhpSerializerTest.php | 5 +- tests/serializers/TestCase.php | 12 +- tests/yii | 1 - 117 files changed, 1450 insertions(+), 1674 deletions(-) create mode 100644 .github/workflows/static.yml create mode 100644 psalm.xml delete mode 100644 src/Job.php delete mode 100644 src/RetryableJob.php delete mode 100644 src/cli/Signal.php delete mode 100644 src/cli/Verbose.php delete mode 100644 src/drivers/amqp/Command.php delete mode 100644 src/drivers/amqp/Queue.php delete mode 100644 src/serializers/Serializer.php create mode 100644 support/ide-helper.php delete mode 100644 tests/docker/php/5.6/Dockerfile delete mode 100644 tests/docker/php/7.0/Dockerfile delete mode 100644 tests/docker/php/7.1/Dockerfile delete mode 100644 tests/docker/php/7.2/Dockerfile delete mode 100644 tests/docker/php/7.3/Dockerfile delete mode 100644 tests/docker/php/7.4/Dockerfile delete mode 100644 tests/docker/php/8.0/Dockerfile create mode 100644 tests/docker/php/Dockerfile delete mode 100755 tests/docker/wait-for-it.sh delete mode 100644 tests/drivers/amqp/QueueTest.php create mode 100644 tests/drivers/amqp_interop/AmqpTestCase.php diff --git a/.editorconfig b/.editorconfig index 61bd74c9e7..458e2d425a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,3 +16,6 @@ trim_trailing_whitespace = false [Makefile] indent_style = tab + +[*.yml] +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 0e3c6ca146..6e08b1d559 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,7 @@ /.gitattributes export-ignore /.gitignore export-ignore /.php_cs export-ignore -/.travis.yml export-ignore /Makefile export-ignore /phpunit.xml.dist export-ignore +/support export-ignore +/psalm.xml export-ignore diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ffa601f0aa..36ec37335c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,24 @@ name: build -on: [push, workflow_dispatch, pull_request] +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - 'UPGRADE.md' + - '.gitignore' + - '.gitattributes' + + push: + branches: [ 'master' ] + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - 'UPGRADE.md' + - '.gitignore' + - '.gitattributes' env: COMPOSE_PROJECT_NAME: yii2-queue @@ -20,9 +38,15 @@ jobs: strategy: fail-fast: false matrix: - php: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0' ] + php: [ '8.1', '8.2' ] steps: - - name: Checkout + - name: Checkout. uses: actions/checkout@v2 + - name: PHP Unit tests for PHP ${{ matrix.php }} run: make test v=${{ matrix.php }} + + - name: Upload coverage to Codecov. + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000000..d9d868552c --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,49 @@ +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - 'UPGRADE.md' + - '.gitignore' + - '.gitattributes' + - 'phpunit.xml.dist' + + push: + branches: [ 'master' ] + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - 'UPGRADE.md' + - '.gitignore' + - '.gitattributes' + - 'phpunit.xml.dist' + +name: static analysis + +env: + COMPOSE_PROJECT_NAME: yii2-queue + COMPOSE_FILE: tests/docker-compose.yml + AWS_SQS_ENABLED: ${{ secrets.AWS_SQS_ENABLED }} + AWS_SQS_URL: ${{ secrets.AWS_SQS_URL }} + AWS_KEY: ${{ secrets.AWS_KEY }} + AWS_SECRET: ${{ secrets.AWS_SECRET }} + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_SQS_FIFO_ENABLED: ${{ secrets.AWS_SQS_FIFO_ENABLED }} + AWS_SQS_FIFO_URL: ${{ secrets.AWS_SQS_FIFO_URL }} + AWS_SQS_FIFO_MESSAGE_GROUP_ID: ${{ secrets.AWS_SQS_FIFO_MESSAGE_GROUP_ID }} +jobs: + psalm: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ '8.1', '8.2' ] + steps: + - name: Checkout. + uses: actions/checkout@v2 + + - name: Run static analyzes for PHP ${{ matrix.php }} + run: make static-analyze v=${{ matrix.php }} diff --git a/.gitignore b/.gitignore index 6ccbccc58b..71e20a2728 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ composer.lock # Env variables .env + +# PHP Unit cache +.phpunit.result.cache +.phpunit.cache diff --git a/Makefile b/Makefile index ba996b739d..138991334c 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,30 @@ 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=5.6 }}. Default latest PHP 5.6 +build: ## Build an image from a docker-compose file. Params: {{ v=8.1 }}. Default latest PHP 8.1 @cp -n .env.example .env PHP_VERSION=$(filter-out $@,$(v)) docker-compose up -d --build -test: ## Run tests. Params: {{ v=5.6 }}. Default latest PHP 5.6 +test: ## Run tests. 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/phpunit --colors=always -v --debug + 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 -benchmark: ## Run benchmark. Params: {{ v=5.6 }}. Default latest PHP 5.6 +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 make down sh: ## Enter the container with the application - docker exec -it yii2-queue-php bash + docker exec -it yii2-queue-php sh -check-cs: - docker-compose build php72 - docker-compose run php72 php-cs-fixer fix --diff --dry-run - docker-compose down +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) + make down clean: docker-compose down diff --git a/README.md b/README.md index 8b5fed3a63..a28bd8f40b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Documentation is at [docs/guide/README.md](docs/guide/README.md). [![Total Downloads](https://poser.pugx.org/yiisoft/yii2-queue/downloads.svg)](https://packagist.org/packages/yiisoft/yii2-queue) [![Build Status](https://github.com/yiisoft/yii2-queue/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-queue/actions) +## Requirements + +- PHP 8.1 or higher. + Installation ------------ diff --git a/composer.json b/composer.json index 200893e77a..7bec3f66d2 100644 --- a/composer.json +++ b/composer.json @@ -16,22 +16,24 @@ "docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide" }, "require": { - "php": ">=5.5.0", + "php": ">=8.1", "yiisoft/yii2": "~2.0.14", - "symfony/process": "^3.3||^4.0||^5.0||^6.0" + "symfony/process": "^6.3", + "laravel/serializable-closure": "^v1.3.0" }, "require-dev": { + "phpunit/phpunit": "^10.3.0", "yiisoft/yii2-redis": "~2.0.0", - "php-amqplib/php-amqplib": "^2.8.0||^3.0.0", - "enqueue/amqp-lib": "^0.8||^0.9.10||^0.10.0", - "pda/pheanstalk": "~3.2.1", - "opis/closure": "*", "yiisoft/yii2-debug": "~2.1.0", "yiisoft/yii2-gii": "~2.2.0", - "phpunit/phpunit": "4.8.34", + "php-amqplib/php-amqplib": "^3.0.0", + "enqueue/amqp-lib": "^0.10.0", + "enqueue/amqp-bunny": "^0.10.0", + "enqueue/amqp-ext": "^0.10.8", + "enqueue/stomp": "^0.10.0", + "pda/pheanstalk": "3.2.1", "aws/aws-sdk-php": ">=2.4", - "enqueue/stomp": "^0.8.39||^0.10.0", - "cweagans/composer-patches": "^1.7" + "vimeo/psalm": "^5.10.0" }, "suggest": { "ext-pcntl": "Need for process signals.", @@ -46,7 +48,6 @@ "autoload": { "psr-4": { "yii\\queue\\": "src", - "yii\\queue\\amqp\\": "src/drivers/amqp", "yii\\queue\\amqp_interop\\": "src/drivers/amqp_interop", "yii\\queue\\beanstalk\\": "src/drivers/beanstalk", "yii\\queue\\db\\": "src/drivers/db", @@ -66,22 +67,12 @@ "config": { "allow-plugins": { "yiisoft/yii2-composer": true, - "cweagans/composer-patches": true + "php-http/discovery": true } }, "extra": { "branch-alias": { "dev-master": "2.x-dev" - }, - "composer-exit-on-patch-failure": true, - "patches": { - "phpunit/phpunit-mock-objects": { - "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" - }, - "phpunit/phpunit": { - "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", - "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch" - } } }, "repositories": [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d40b0c8eda..5babaa3c2b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,25 @@ - - - - ./tests - ./tests/app - ./tests/docker - ./tests/runtime - - + + + + ./tests + ./tests/app + ./tests/docker + ./tests/runtime + + + + + ./src + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000000..8a030fcb1b --- /dev/null +++ b/psalm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/src/ExecEvent.php b/src/ExecEvent.php index 0689a6ff12..75945cea71 100644 --- a/src/ExecEvent.php +++ b/src/ExecEvent.php @@ -1,4 +1,7 @@ * @since 2.1.1 */ -class InvalidJobException extends \Exception +class InvalidJobException extends Exception { - /** - * @var string - */ - private $serialized; - - /** * @param string $serialized * @param string $message * @param int $code * @param Throwable|null $previous */ - public function __construct($serialized, $message = '', $code = 0, Throwable $previous = null) + public function __construct( + private string $serialized, + string $message = '', + int $code = 0, + Throwable $previous = null + ) { - $this->serialized = $serialized; parent::__construct($message, $code, $previous); } /** * @return string of serialized message that cannot be unserialized to a job */ - final public function getSerialized() + final public function getSerialized(): string { return $this->serialized; } diff --git a/src/Job.php b/src/Job.php deleted file mode 100644 index 45bab7ba9c..0000000000 --- a/src/Job.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ -interface Job extends JobInterface -{ -} diff --git a/src/JobEvent.php b/src/JobEvent.php index c6c0a041f2..8c56a1db66 100644 --- a/src/JobEvent.php +++ b/src/JobEvent.php @@ -1,4 +1,7 @@ 'afterPush', @@ -46,7 +48,7 @@ public function events() /** * @param PushEvent $event */ - public function afterPush(PushEvent $event) + public function afterPush(PushEvent $event): void { $title = $this->getJobTitle($event); Yii::info("$title is pushed.", Queue::class); @@ -55,7 +57,7 @@ public function afterPush(PushEvent $event) /** * @param ExecEvent $event */ - public function beforeExec(ExecEvent $event) + public function beforeExec(ExecEvent $event): void { $title = $this->getExecTitle($event); Yii::info("$title is started.", Queue::class); @@ -65,7 +67,7 @@ public function beforeExec(ExecEvent $event) /** * @param ExecEvent $event */ - public function afterExec(ExecEvent $event) + public function afterExec(ExecEvent $event): void { $title = $this->getExecTitle($event); Yii::endProfile($title, Queue::class); @@ -78,7 +80,7 @@ public function afterExec(ExecEvent $event) /** * @param ExecEvent $event */ - public function afterError(ExecEvent $event) + public function afterError(ExecEvent $event): void { $title = $this->getExecTitle($event); Yii::endProfile($title, Queue::class); @@ -92,7 +94,7 @@ public function afterError(ExecEvent $event) * @param cli\WorkerEvent $event * @since 2.0.2 */ - public function workerStart(cli\WorkerEvent $event) + public function workerStart(cli\WorkerEvent $event): void { $title = 'Worker ' . $event->sender->getWorkerPid(); Yii::info("$title is started.", Queue::class); @@ -106,7 +108,7 @@ public function workerStart(cli\WorkerEvent $event) * @param cli\WorkerEvent $event * @since 2.0.2 */ - public function workerStop(cli\WorkerEvent $event) + public function workerStop(cli\WorkerEvent $event): void { $title = 'Worker ' . $event->sender->getWorkerPid(); Yii::endProfile($title, Queue::class); @@ -121,7 +123,7 @@ public function workerStop(cli\WorkerEvent $event) * @return string * @since 2.0.2 */ - protected function getJobTitle(JobEvent $event) + protected function getJobTitle(JobEvent $event): string { $name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job'; return "[$event->id] $name"; @@ -132,7 +134,7 @@ protected function getJobTitle(JobEvent $event) * @return string * @since 2.0.2 */ - protected function getExecTitle(ExecEvent $event) + protected function getExecTitle(ExecEvent $event): string { $title = $this->getJobTitle($event); $extra = "attempt: $event->attempt"; diff --git a/src/PushEvent.php b/src/PushEvent.php index b4a4949f24..947e0a6bdb 100644 --- a/src/PushEvent.php +++ b/src/PushEvent.php @@ -1,4 +1,7 @@ serializer = Instance::ensure($this->serializer, SerializerInterface::class); - if (!is_numeric($this->ttr)) { - throw new InvalidConfigException('Default TTR must be integer.'); - } - $this->ttr = (int) $this->ttr; if ($this->ttr <= 0) { throw new InvalidConfigException('Default TTR must be greater that zero.'); } - if (!is_numeric($this->attempts)) { - throw new InvalidConfigException('Default attempts count must be integer.'); - } - $this->attempts = (int) $this->attempts; if ($this->attempts <= 0) { throw new InvalidConfigException('Default attempts count must be greater that zero.'); } @@ -109,10 +102,10 @@ public function init() /** * Sets TTR for job execute. * - * @param int|mixed $value + * @param int|null $value * @return $this */ - public function ttr($value) + public function ttr(?int $value): static { $this->pushTtr = $value; return $this; @@ -121,10 +114,10 @@ public function ttr($value) /** * Sets delay for later execute. * - * @param int|mixed $value + * @param int|null $value * @return $this */ - public function delay($value) + public function delay(?int $value): static { $this->pushDelay = $value; return $this; @@ -133,10 +126,10 @@ public function delay($value) /** * Sets job priority. * - * @param mixed $value + * @param int|string|null $value * @return $this */ - public function priority($value) + public function priority(int|string|null $value): static { $this->pushPriority = $value; return $this; @@ -146,9 +139,9 @@ public function priority($value) * Pushes job into queue. * * @param JobInterface|mixed $job - * @return string|null id of a job message + * @return int|string|null id of a job message */ - public function push($job) + public function push($job): int|string|null { $event = new PushEvent([ 'job' => $job, @@ -173,18 +166,10 @@ public function push($job) throw new InvalidArgumentException('Job must be instance of JobInterface.'); } - if (!is_numeric($event->ttr)) { - throw new InvalidArgumentException('Job TTR must be integer.'); - } - $event->ttr = (int) $event->ttr; if ($event->ttr <= 0) { throw new InvalidArgumentException('Job TTR must be greater that zero.'); } - if (!is_numeric($event->delay)) { - throw new InvalidArgumentException('Job delay must be integer.'); - } - $event->delay = (int) $event->delay; if ($event->delay < 0) { throw new InvalidArgumentException('Job delay must be positive.'); } @@ -197,34 +182,34 @@ public function push($job) } /** - * @param string $message + * @param string $payload * @param int $ttr time to reserve in seconds * @param int $delay * @param mixed $priority - * @return string id of a job message + * @return string|int|null id of a job message */ - abstract protected function pushMessage($message, $ttr, $delay, $priority); + abstract protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null; /** * Uses for CLI drivers and gets process ID of a worker. * * @since 2.0.2 */ - public function getWorkerPid() + public function getWorkerPid(): ?int { return null; } /** - * @param string $id of a job message + * @param int|string $id of a job message * @param string $message * @param int $ttr time to reserve * @param int $attempt number * @return bool */ - protected function handleMessage($id, $message, $ttr, $attempt) + protected function handleMessage(int|string $id, string $message, int $ttr, int $attempt): bool { - list($job, $error) = $this->unserializeMessage($message); + [$job, $error] = $this->unserializeMessage($message); $event = new ExecEvent([ 'id' => $id, 'job' => $job, @@ -241,10 +226,7 @@ protected function handleMessage($id, $message, $ttr, $attempt) } try { $event->result = $event->job->execute($this); - } catch (\Exception $error) { - $event->error = $error; - return $this->handleError($event); - } catch (\Throwable $error) { + } catch (\Exception|\Throwable $error) { $event->error = $error; return $this->handleError($event); } @@ -255,11 +237,10 @@ protected function handleMessage($id, $message, $ttr, $attempt) /** * Unserializes. * - * @param string $id of the job * @param string $serialized message * @return array pair of a job and error that */ - public function unserializeMessage($serialized) + public function unserializeMessage(string $serialized): array { try { $job = $this->serializer->unserialize($serialized); @@ -282,7 +263,7 @@ public function unserializeMessage($serialized) * @return bool * @internal */ - public function handleError(ExecEvent $event) + public function handleError(ExecEvent $event): bool { $event->retry = $event->attempt < $this->attempts; if ($event->error instanceof InvalidJobException) { @@ -295,35 +276,35 @@ public function handleError(ExecEvent $event) } /** - * @param string $id of a job message + * @param int|string $id of a job message * @return bool */ - public function isWaiting($id) + public function isWaiting(int|string $id): bool { return $this->status($id) === self::STATUS_WAITING; } /** - * @param string $id of a job message + * @param int|string $id of a job message * @return bool */ - public function isReserved($id) + public function isReserved(int|string $id): bool { return $this->status($id) === self::STATUS_RESERVED; } /** - * @param string $id of a job message + * @param int|string $id of a job message * @return bool */ - public function isDone($id) + public function isDone(int|string $id): bool { return $this->status($id) === self::STATUS_DONE; } /** - * @param string $id of a job message + * @param string|int $id of a job message * @return int status code */ - abstract public function status($id); + abstract public function status(int|string $id): int; } diff --git a/src/RetryableJob.php b/src/RetryableJob.php deleted file mode 100644 index 0641457b06..0000000000 --- a/src/RetryableJob.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ -interface RetryableJob extends RetryableJobInterface -{ -} diff --git a/src/RetryableJobInterface.php b/src/RetryableJobInterface.php index 2a13fb7585..08791db777 100644 --- a/src/RetryableJobInterface.php +++ b/src/RetryableJobInterface.php @@ -1,4 +1,7 @@ queue && ($this->controller instanceof Command)) { + if ($this->controller instanceof Command) { $this->queue = $this->controller->queue; } if (!($this->controller instanceof ConsoleController)) { @@ -50,7 +52,7 @@ public function init() * @param string $string * @return string */ - protected function format($string) + protected function format(string $string): string { return call_user_func_array([$this->controller, 'ansiFormat'], func_get_args()); } diff --git a/src/cli/Command.php b/src/cli/Command.php index 5a8758e4d5..ef993ba534 100644 --- a/src/cli/Command.php +++ b/src/cli/Command.php @@ -1,4 +1,7 @@ VerboseBehavior::class, ]; /** * @var bool isolate mode. It executes a job in a child process. */ - public $isolate = true; + public bool $isolate = true; /** - * @var string path to php interpreter that uses to run child processes. + * @var string|null path to php interpreter that uses to run child processes. * If it is undefined, PHP_BINARY will be used. * @since 2.0.3 */ - public $phpBinary; - + public ?string $phpBinary = null; /** * @inheritdoc */ - public function options($actionID) + public function options($actionID): array { $options = parent::options($actionID); if ($this->canVerbose($actionID)) { @@ -77,7 +79,7 @@ public function options($actionID) /** * @inheritdoc */ - public function optionAliases() + public function optionAliases(): array { return array_merge(parent::optionAliases(), [ 'v' => 'verbose', @@ -89,13 +91,13 @@ public function optionAliases() * @return bool * @since 2.0.2 */ - abstract protected function isWorkerAction($actionID); + abstract protected function isWorkerAction(string $actionID): bool; /** * @param string $actionID * @return bool */ - protected function canVerbose($actionID) + protected function canVerbose(string $actionID): bool { return $actionID === 'exec' || $this->isWorkerAction($actionID); } @@ -104,7 +106,7 @@ protected function canVerbose($actionID) * @param string $actionID * @return bool */ - protected function canIsolate($actionID) + protected function canIsolate(string $actionID): bool { return $this->isWorkerAction($actionID); } @@ -112,7 +114,7 @@ protected function canIsolate($actionID) /** * @inheritdoc */ - public function beforeAction($action) + public function beforeAction($action): bool { if ($this->canVerbose($action->id) && $this->verbose) { $this->queue->attachBehavior('verbose', ['command' => $this] + $this->verboseConfig); @@ -123,7 +125,7 @@ public function beforeAction($action) $this->phpBinary = PHP_BINARY; } $this->queue->messageHandler = function ($id, $message, $ttr, $attempt) { - return $this->handleMessage($id, $message, $ttr, $attempt); + return $this->handleMessage($id, $message, (int)$ttr, (int)$attempt); }; } @@ -141,7 +143,7 @@ public function beforeAction($action) * @return int exit code * @internal It is used with isolate mode. */ - public function actionExec($id, $ttr, $attempt, $pid) + public function actionExec(?string $id, int $ttr, int $attempt, int $pid): int { if ($this->queue->execute($id, file_get_contents('php://stdin'), $ttr, $attempt, $pid ?: null)) { return self::EXEC_DONE; @@ -152,15 +154,14 @@ public function actionExec($id, $ttr, $attempt, $pid) /** * Handles message using child process. * - * @param string|null $id of a message + * @param int|string|null $id of a message * @param string $message - * @param int $ttr time to reserve + * @param int|null $ttr time to reserve * @param int $attempt number * @return bool - * @throws * @see actionExec() */ - protected function handleMessage($id, $message, $ttr, $attempt) + protected function handleMessage(int|string|null $id, string $message, ?int $ttr, int $attempt): bool { // Child process command: php yii queue/exec "id" "ttr" "attempt" "pid" $cmd = [ @@ -196,7 +197,7 @@ protected function handleMessage($id, $message, $ttr, $attempt) } return $result === self::EXEC_DONE; } catch (ProcessRuntimeException $error) { - list($job) = $this->queue->unserializeMessage($message); + [$job] = $this->queue->unserializeMessage($message); return $this->queue->handleError(new ExecEvent([ 'id' => $id, 'job' => $job, diff --git a/src/cli/LoopInterface.php b/src/cli/LoopInterface.php index 746f35485f..9742362857 100644 --- a/src/cli/LoopInterface.php +++ b/src/cli/LoopInterface.php @@ -1,4 +1,7 @@ getComponents(false) as $id => $component) { if ($component === $this) { return Inflector::camel2id($id); @@ -80,7 +82,7 @@ protected function getCommandId() /** * @inheritdoc */ - public function bootstrap($app) + public function bootstrap($app): void { if ($app instanceof ConsoleApp) { $app->controllerMap[$this->getCommandId()] = [ @@ -97,10 +99,11 @@ public function bootstrap($app) * @return null|int exit code * @since 2.0.2 */ - protected function runWorker(callable $handler) + protected function runWorker(callable $handler): ?int { $this->_workerPid = getmypid(); /** @var LoopInterface $loop */ + /** @psalm-suppress UndefinedClass */ $loop = Yii::createObject($this->loopConfig, [$this]); $event = new WorkerEvent(['loop' => $loop]); @@ -109,9 +112,8 @@ protected function runWorker(callable $handler) return $event->exitCode; } - $exitCode = null; try { - call_user_func($handler, function () use ($loop, $event) { + $handler(function () use ($loop, $event) { $this->trigger(self::EVENT_WORKER_LOOP, $event); return $event->exitCode === null && $loop->canContinue(); }); @@ -130,7 +132,7 @@ protected function runWorker(callable $handler) * @return int|null * @since 2.0.2 */ - public function getWorkerPid() + public function getWorkerPid(): ?int { return $this->_workerPid; } @@ -138,7 +140,7 @@ public function getWorkerPid() /** * @inheritdoc */ - protected function handleMessage($id, $message, $ttr, $attempt) + protected function handleMessage(int|string $id, string $message, int $ttr, int $attempt): bool { if ($this->messageHandler) { return call_user_func($this->messageHandler, $id, $message, $ttr, $attempt); @@ -156,7 +158,7 @@ protected function handleMessage($id, $message, $ttr, $attempt) * @return bool * @internal for worker command only */ - public function execute($id, $message, $ttr, $attempt, $workerPid) + public function execute(string $id, string $message, int $ttr, int $attempt, ?int $workerPid): bool { $this->_workerPid = $workerPid; return parent::handleMessage($id, $message, $ttr, $attempt); diff --git a/src/cli/Signal.php b/src/cli/Signal.php deleted file mode 100644 index cd3a8547f4..0000000000 --- a/src/cli/Signal.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class Signal -{ - private static $exit = false; - - - /** - * Checks exit signals - * Used mainly by [[yii\queue\Queue]] to check, whether job execution - * loop can be continued. - * @return bool - */ - public static function isExit() - { - if (function_exists('pcntl_signal')) { - // Installs a signal handler - static $handled = false; - if (!$handled) { - foreach ([SIGTERM, SIGINT, SIGHUP] as $signal) { - pcntl_signal($signal, function () { - static::setExitFlag(); - }); - } - $handled = true; - } - - // Checks signal - if (!static::$exit) { - pcntl_signal_dispatch(); - } - } - - return static::$exit; - } - - /** - * Sets exit flag to `true` - * Method can be used to simulate exit signal for methods that use - * [[isExit()]] to check whether execution loop can be continued. - */ - public static function setExitFlag() - { - static::$exit = true; - } -} diff --git a/src/cli/SignalLoop.php b/src/cli/SignalLoop.php index 3dbcd83dea..46d8897ae6 100644 --- a/src/cli/SignalLoop.php +++ b/src/cli/SignalLoop.php @@ -1,4 +1,7 @@ exitSignals as $signal) { - pcntl_signal($signal, function () { + pcntl_signal($signal, static function () { self::$exit = true; }); } foreach ($this->suspendSignals as $signal) { - pcntl_signal($signal, function () { + pcntl_signal($signal, static function () { self::$pause = true; }); } foreach ($this->resumeSignals as $signal) { - pcntl_signal($signal, function () { + pcntl_signal($signal, static function () { self::$pause = false; }); } @@ -94,7 +96,7 @@ public function init() * * @inheritdoc */ - public function canContinue() + public function canContinue(): bool { if (extension_loaded('pcntl') && function_exists('pcntl_signal_dispatch')) { pcntl_signal_dispatch(); diff --git a/src/cli/Verbose.php b/src/cli/Verbose.php deleted file mode 100644 index c17c9a0278..0000000000 --- a/src/cli/Verbose.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ -class Verbose extends VerboseBehavior -{ -} diff --git a/src/cli/VerboseBehavior.php b/src/cli/VerboseBehavior.php index 4f421b3835..e25d95b909 100644 --- a/src/cli/VerboseBehavior.php +++ b/src/cli/VerboseBehavior.php @@ -1,4 +1,7 @@ 'beforeExec', @@ -56,7 +58,7 @@ public function events() /** * @param ExecEvent $event */ - public function beforeExec(ExecEvent $event) + public function beforeExec(ExecEvent $event): void { $this->jobStartedAt = microtime(true); $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); @@ -69,7 +71,7 @@ public function beforeExec(ExecEvent $event) /** * @param ExecEvent $event */ - public function afterExec(ExecEvent $event) + public function afterExec(ExecEvent $event): void { $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); $this->command->stdout($this->jobTitle($event), Console::FG_GREY); @@ -84,7 +86,7 @@ public function afterExec(ExecEvent $event) /** * @param ExecEvent $event */ - public function afterError(ExecEvent $event) + public function afterError(ExecEvent $event): void { $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); $this->command->stdout($this->jobTitle($event), Console::FG_GREY); @@ -110,7 +112,7 @@ public function afterError(ExecEvent $event) * @return string * @since 2.0.2 */ - protected function jobTitle(ExecEvent $event) + protected function jobTitle(ExecEvent $event): string { $name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job'; $extra = "attempt: $event->attempt"; @@ -124,7 +126,7 @@ protected function jobTitle(ExecEvent $event) * @param WorkerEvent $event * @since 2.0.2 */ - public function workerStart(WorkerEvent $event) + public function workerStart(WorkerEvent $event): void { $this->workerStartedAt = time(); $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); @@ -137,7 +139,7 @@ public function workerStart(WorkerEvent $event) * @param WorkerEvent $event * @since 2.0.2 */ - public function workerStop(WorkerEvent $event) + public function workerStop(WorkerEvent $event): void { $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); $pid = $event->sender->getWorkerPid(); @@ -152,7 +154,7 @@ public function workerStop(WorkerEvent $event) * @return string * @since 2.0.2 */ - protected function formatDuration($value) + protected function formatDuration($value): string { $seconds = $value % 60; $value = ($value - $seconds) / 60; diff --git a/src/cli/WorkerEvent.php b/src/cli/WorkerEvent.php index e1ff4cbe2a..eb92a5af15 100644 --- a/src/cli/WorkerEvent.php +++ b/src/cli/WorkerEvent.php @@ -1,4 +1,7 @@ 'beforePush', @@ -48,9 +50,14 @@ public function events() * Converts the closure to a job object. * @param PushEvent $event */ - public function beforePush(PushEvent $event) + public function beforePush(PushEvent $event): void { - $serialized = opis_serialize($event->job); + SerializableClosure::setSecretKey(uniqid('', true)); + $serialized = serialize( + new SerializableClosure(function () use ($event) { + return $event->job; + }) + ); $event->job = new Job(); $event->job->serialized = $serialized; } diff --git a/src/closure/Job.php b/src/closure/Job.php index 9ec2d15877..ec4ed605e1 100644 --- a/src/closure/Job.php +++ b/src/closure/Job.php @@ -1,4 +1,7 @@ serialized); - if ($unserialized instanceof \Closure) { - return $unserialized(); + $closure = unserialize($this->serialized)->getClosure(); + $nativeClosure = $closure(); + + if ($nativeClosure instanceof Native) { + return $nativeClosure(); } - return $unserialized->execute($queue); + + return $nativeClosure->execute($queue); } } diff --git a/src/debug/Panel.php b/src/debug/Panel.php index 7cb0511391..11bee1130e 100644 --- a/src/debug/Panel.php +++ b/src/debug/Panel.php @@ -1,4 +1,7 @@ - diff --git a/src/drivers/amqp/Command.php b/src/drivers/amqp/Command.php deleted file mode 100644 index 311629430d..0000000000 --- a/src/drivers/amqp/Command.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class Command extends CliCommand -{ - /** - * @var Queue - */ - public $queue; - - - /** - * @inheritdoc - */ - protected function isWorkerAction($actionID) - { - return $actionID === 'listen'; - } - - /** - * Listens amqp-queue and runs new jobs. - * It can be used as daemon process. - */ - public function actionListen() - { - $this->queue->listen(); - } -} diff --git a/src/drivers/amqp/Queue.php b/src/drivers/amqp/Queue.php deleted file mode 100644 index 6cd4bd0dbc..0000000000 --- a/src/drivers/amqp/Queue.php +++ /dev/null @@ -1,167 +0,0 @@ - - */ -class Queue extends CliQueue -{ - public $host = 'localhost'; - public $port = 5672; - public $user = 'guest'; - public $password = 'guest'; - public $queueName = 'queue'; - public $exchangeName = 'exchange'; - public $vhost = '/'; - /** - * @var int The periods of time PHP pings the broker in order to prolong the connection timeout. In seconds. - * @since 2.3.1 - */ - public $heartbeat = 0; - /** - * Send keep-alive packets for a socket connection - * @var bool - * @since 2.3.6 - */ - public $keepalive = false; - /** - * @var string command class name - */ - public $commandClass = Command::class; - - /** - * @var AMQPStreamConnection - */ - protected $connection; - /** - * @var AMQPChannel - */ - protected $channel; - - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () { - $this->close(); - }); - } - - /** - * Listens amqp-queue and runs new jobs. - */ - public function listen() - { - $this->open(); - $callback = function (AMQPMessage $payload) { - $id = $payload->get('message_id'); - list($ttr, $message) = explode(';', $payload->body, 2); - if ($this->handleMessage($id, $message, $ttr, 1)) { - $payload->delivery_info['channel']->basic_ack($payload->delivery_info['delivery_tag']); - } - }; - $this->channel->basic_qos(null, 1, null); - $this->channel->basic_consume($this->queueName, '', false, false, false, false, $callback); - while (count($this->channel->callbacks)) { - $this->channel->wait(); - } - } - - /** - * @inheritdoc - */ - protected function pushMessage($message, $ttr, $delay, $priority) - { - if ($delay) { - throw new NotSupportedException('Delayed work is not supported in the driver.'); - } - if ($priority !== null) { - throw new NotSupportedException('Job priority is not supported in the driver.'); - } - - $this->open(); - $id = uniqid('', true); - $this->channel->basic_publish( - new AMQPMessage("$ttr;$message", [ - 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, - 'message_id' => $id, - ]), - $this->exchangeName - ); - - return $id; - } - - /** - * @inheritdoc - */ - public function status($id) - { - throw new NotSupportedException('Status is not supported in the driver.'); - } - - /** - * Opens connection and channel. - */ - protected function open() - { - if ($this->channel) { - return; - } - $this->connection = new AMQPStreamConnection( - $this->host, - $this->port, - $this->user, - $this->password, - $this->vhost, - false, - 'AMQPLAIN', - null, - 'en_US', - 3.0, - 3.0, - null, - $this->keepalive, - $this->heartbeat, - 0.0, - null - ); - $this->channel = $this->connection->channel(); - $this->channel->queue_declare($this->queueName, false, true, false, false); - $this->channel->exchange_declare($this->exchangeName, 'direct', false, true, false); - $this->channel->queue_bind($this->queueName, $this->exchangeName); - } - - /** - * Closes connection and channel. - */ - protected function close() - { - if (!$this->channel) { - return; - } - $this->channel->close(); - $this->connection->close(); - } -} diff --git a/src/drivers/amqp_interop/Command.php b/src/drivers/amqp_interop/Command.php index b81aef59d1..e1bbe5ad9d 100644 --- a/src/drivers/amqp_interop/Command.php +++ b/src/drivers/amqp_interop/Command.php @@ -1,4 +1,7 @@ queue->listen(); } diff --git a/src/drivers/amqp_interop/Queue.php b/src/drivers/amqp_interop/Queue.php index 9f565526b8..c7f25b0c91 100644 --- a/src/drivers/amqp_interop/Queue.php +++ b/src/drivers/amqp_interop/Queue.php @@ -1,4 +1,7 @@ * @since 2.0.2 */ class Queue extends CliQueue { - const ATTEMPT = 'yii-attempt'; - const TTR = 'yii-ttr'; - const DELAY = 'yii-delay'; - const PRIORITY = 'yii-priority'; - const ENQUEUE_AMQP_LIB = 'enqueue/amqp-lib'; - const ENQUEUE_AMQP_EXT = 'enqueue/amqp-ext'; - const ENQUEUE_AMQP_BUNNY = 'enqueue/amqp-bunny'; + public const ATTEMPT = 'yii-attempt'; + public const TTR = 'yii-ttr'; + public const DELAY = 'yii-delay'; + public const PRIORITY = 'yii-priority'; + public const ENQUEUE_AMQP_LIB = 'enqueue/amqp-lib'; + public const ENQUEUE_AMQP_EXT = 'enqueue/amqp-ext'; + public const ENQUEUE_AMQP_BUNNY = 'enqueue/amqp-bunny'; /** * The connection to the broker could be configured as an array of options * or as a DSN string like amqp:, amqps:, amqps://user:pass@localhost:1000/vhost. * - * @var string + * @var string|null */ - public $dsn; + public ?string $dsn = null; /** * The message queue broker's host. * * @var string|null */ - public $host; + public ?string $host = null; /** * The message queue broker's port. * * @var string|null */ - public $port; + public ?string $port = null; /** * This is RabbitMQ user which is used to login on the broker. * * @var string|null */ - public $user; + public ?string $user = null; /** * This is RabbitMQ password which is used to login on the broker. * * @var string|null */ - public $password; + public ?string $password = null; /** * Virtual hosts provide logical grouping and separation of resources. * * @var string|null */ - public $vhost; + public ?string $vhost = null; /** * The time PHP socket waits for an information while reading. In seconds. * * @var float|null */ - public $readTimeout; + public ?float $readTimeout = null; /** * The time PHP socket waits for an information while witting. In seconds. * * @var float|null */ - public $writeTimeout; + public ?float $writeTimeout = null; /** * The time RabbitMQ keeps the connection on idle. In seconds. * * @var float|null */ - public $connectionTimeout; + public ?float $connectionTimeout = null; /** * The periods of time PHP pings the broker in order to prolong the connection timeout. In seconds. * * @var float|null */ - public $heartbeat; + public ?float $heartbeat = 0; /** * PHP uses one shared connection if set true. * * @var bool|null */ - public $persisted; + public ?bool $persisted = null; /** * Send keep-alive packets for a socket connection * @var bool * @since 2.3.6 */ - public $keepalive; + public bool $keepalive = false; /** * The connection will be established as later as possible if set true. * * @var bool|null */ - public $lazy; + public ?bool $lazy = null; /** * If false prefetch_count option applied separately to each new consumer on the channel * If true prefetch_count option shared across all consumers on the channel. * * @var bool|null */ - public $qosGlobal; + public ?bool $qosGlobal = null; /** * Defines number of message pre-fetched in advance on a channel basis. * * @var int|null */ - public $qosPrefetchSize; + public ?int $qosPrefetchSize = null; /** * Defines number of message pre-fetched in advance per consumer. * * @var int|null */ - public $qosPrefetchCount; + public ?int $qosPrefetchCount = null; /** * Defines whether secure connection should be used or not. * * @var bool|null */ - public $sslOn; + public ?bool $sslOn = null; /** * Require verification of SSL certificate used. * * @var bool|null */ - public $sslVerify; + public ?bool $sslVerify = null; /** * Location of Certificate Authority file on local filesystem which should be used with the verify_peer context option to authenticate the identity of the remote peer. * * @var string|null */ - public $sslCacert; + public ?string $sslCacert = null; /** * Path to local certificate file on filesystem. * * @var string|null */ - public $sslCert; + public ?string $sslCert = null; /** * Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key. * * @var string|null */ - public $sslKey; + public ?string $sslKey = null; /** * The queue used to consume messages from. * * @var string */ - public $queueName = 'interop_queue'; + public string $queueName = 'interop_queue'; /** * Setting optional arguments for the queue (key-value pairs) * ```php @@ -190,51 +195,51 @@ class Queue extends CliQueue * @since 2.3.3 * @see https://www.rabbitmq.com/queues.html#optional-arguments */ - public $queueOptionalArguments = []; + public array $queueOptionalArguments = []; /** * Set of flags for the queue * @var int * @since 2.3.5 * @see AmqpDestination */ - public $queueFlags = AmqpQueue::FLAG_DURABLE; + public int $queueFlags = AmqpQueue::FLAG_DURABLE; /** * The exchange used to publish messages to. * * @var string */ - public $exchangeName = 'exchange'; + public string $exchangeName = 'exchange'; /** * The exchange type. Can take values: direct, fanout, topic, headers * @var string * @since 2.3.3 */ - public $exchangeType = AmqpTopic::TYPE_DIRECT; + public string $exchangeType = AmqpTopic::TYPE_DIRECT; /** * Set of flags for the exchange * @var int * @since 2.3.5 * @see AmqpDestination */ - public $exchangeFlags = AmqpTopic::FLAG_DURABLE; + public int $exchangeFlags = AmqpTopic::FLAG_DURABLE; /** * Routing key for publishing messages. Default is NULL. * * @var string|null */ - public $routingKey; + public ?string $routingKey = null; /** * Defines the amqp interop transport being internally used. Currently supports lib, ext and bunny values. * * @var string */ - public $driver = self::ENQUEUE_AMQP_LIB; + public string $driver = self::ENQUEUE_AMQP_LIB; /** * The property contains a command class which used in cli. * * @var string command class name */ - public $commandClass = Command::class; + public string $commandClass = Command::class; /** * Headers to send along with the message * ```php @@ -245,35 +250,44 @@ class Queue extends CliQueue * ``` * * @var array - * @since 3.0.0 + * @since 2.3.6 */ - public $setMessageHeaders = []; + public array $setMessageHeaders = []; /** * Amqp interop context. * - * @var AmqpContext + * @var AmqpContext|Context|null */ - protected $context; + protected AmqpContext|null|Context $context = null; /** * List of supported amqp interop drivers. * * @var string[] */ - protected $supportedDrivers = [self::ENQUEUE_AMQP_LIB, self::ENQUEUE_AMQP_EXT, self::ENQUEUE_AMQP_BUNNY]; + protected array $supportedDrivers = [ + self::ENQUEUE_AMQP_LIB, + self::ENQUEUE_AMQP_EXT, + self::ENQUEUE_AMQP_BUNNY + ]; /** * The property tells whether the setupBroker method was called or not. * Having it we can do broker setup only once per process. * * @var bool */ - protected $setupBrokerDone = false; - + protected bool $setupBrokerDone = false; + /** + * Created AmqpQueue instance + * + * @var AmqpQueue + */ + protected AmqpQueue $queue; /** * @inheritdoc */ - public function init() + public function init(): void { parent::init(); Event::on(BaseApp::class, BaseApp::EVENT_AFTER_REQUEST, function () { @@ -306,7 +320,7 @@ public function init() /** * Listens amqp-queue and runs new jobs. */ - public function listen() + public function listen(): void { $this->open(); $this->setupBroker(); @@ -343,9 +357,9 @@ public function listen() } /** - * @return AmqpContext + * @return AmqpContext|null */ - public function getContext() + public function getContext(): ?AmqpContext { $this->open(); @@ -355,13 +369,14 @@ public function getContext() /** * @inheritdoc */ - protected function pushMessage($payload, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { $this->open(); $this->setupBroker(); $topic = $this->context->createTopic($this->exchangeName); + /** @var AmqpMessage $message */ $message = $this->context->createMessage($payload); $message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); $message->setMessageId(uniqid('', true)); @@ -398,7 +413,7 @@ protected function pushMessage($payload, $ttr, $delay, $priority) /** * @inheritdoc */ - public function status($id) + public function status($id): int { throw new NotSupportedException('Status is not supported in the driver.'); } @@ -406,25 +421,24 @@ public function status($id) /** * Opens connection and channel. */ - protected function open() + protected function open(): void { if ($this->context) { return; } - switch ($this->driver) { - case self::ENQUEUE_AMQP_LIB: - $connectionClass = AmqpLibConnectionFactory::class; - break; - case self::ENQUEUE_AMQP_EXT: - $connectionClass = AmqpExtConnectionFactory::class; - break; - case self::ENQUEUE_AMQP_BUNNY: - $connectionClass = AmqpBunnyConnectionFactory::class; - break; - default: - throw new \LogicException(sprintf('The given driver "%s" is not supported. Drivers supported are "%s"', $this->driver, implode('", "', $this->supportedDrivers))); - } + $connectionClass = match ($this->driver) { + self::ENQUEUE_AMQP_LIB => AmqpLibConnectionFactory::class, + self::ENQUEUE_AMQP_EXT => AmqpExtConnectionFactory::class, + self::ENQUEUE_AMQP_BUNNY => AmqpBunnyConnectionFactory::class, + default => throw new \LogicException( + sprintf( + 'The given driver "%s" is not supported. Drivers supported are "%s"', + $this->driver, + implode('", "', $this->supportedDrivers) + ) + ), + }; $config = [ 'dsn' => $this->dsn, @@ -450,7 +464,7 @@ protected function open() 'ssl_key' => $this->sslKey, ]; - $config = array_filter($config, function ($value) { + $config = array_filter($config, static function ($value) { return null !== $value; }); @@ -464,23 +478,24 @@ protected function open() } } - protected function setupBroker() + protected function setupBroker(): void { if ($this->setupBrokerDone) { return; } - $queue = $this->context->createQueue($this->queueName); - $queue->setFlags($this->queueFlags); - $queue->setArguments($this->queueOptionalArguments); - $this->context->declareQueue($queue); + $this->queue = $this->createQueue(); + $this->queue->setFlags($this->queueFlags); + $this->queue->setArguments($this->queueOptionalArguments); + $this->context->declareQueue($this->queue); + /** @var AmqpTopic $topic */ $topic = $this->context->createTopic($this->exchangeName); $topic->setType($this->exchangeType); $topic->setFlags($this->exchangeFlags); $this->context->declareTopic($topic); - $this->context->bind(new AmqpBind($queue, $topic, $this->routingKey)); + $this->context->bind(new AmqpBind($this->queue, $topic, $this->routingKey)); $this->setupBrokerDone = true; } @@ -488,7 +503,7 @@ protected function setupBroker() /** * Closes connection and channel. */ - protected function close() + protected function close(): void { if (!$this->context) { return; @@ -499,14 +514,16 @@ protected function close() $this->setupBrokerDone = false; } - /** - * {@inheritdoc} - */ - protected function redeliver(AmqpMessage $message) + protected function redeliver(AmqpMessage $message): void { $attempt = $message->getProperty(self::ATTEMPT, 1); - $newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders()); + /** @var AmqpMessage $newMessage */ + $newMessage = $this->context->createMessage( + $message->getBody(), + $message->getProperties(), + $message->getHeaders() + ); $newMessage->setDeliveryMode($message->getDeliveryMode()); $newMessage->setProperty(self::ATTEMPT, ++$attempt); @@ -515,4 +532,9 @@ protected function redeliver(AmqpMessage $message) $newMessage ); } + + private function createQueue(): AmqpQueue + { + return $this->context->createQueue($this->queueName); + } } diff --git a/src/drivers/beanstalk/Command.php b/src/drivers/beanstalk/Command.php index d9478cf71e..bcd843cefd 100644 --- a/src/drivers/beanstalk/Command.php +++ b/src/drivers/beanstalk/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -40,7 +43,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen']); } @@ -51,7 +54,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -61,10 +64,10 @@ public function actionRun() * It can be used as daemon process. * * @param int $timeout number of seconds to wait a job. - * @throws Exception when params are invalid. * @return null|int exit code. + *@throws Exception when params are invalid. */ - public function actionListen($timeout = 3) + public function actionListen(int $timeout = 3): ?int { if (!is_numeric($timeout)) { throw new Exception('Timeout must be numeric.'); @@ -83,7 +86,7 @@ public function actionListen($timeout = 3) * @throws Exception when the job is not found. * @since 2.0.1 */ - public function actionRemove($id) + public function actionRemove(int $id): void { if (!$this->queue->remove($id)) { throw new Exception('The job is not found.'); diff --git a/src/drivers/beanstalk/InfoAction.php b/src/drivers/beanstalk/InfoAction.php index 5c88795f20..c8951f2822 100644 --- a/src/drivers/beanstalk/InfoAction.php +++ b/src/drivers/beanstalk/InfoAction.php @@ -1,4 +1,7 @@ format('Statistical information about the tube:', Console::FG_GREEN)); diff --git a/src/drivers/beanstalk/Queue.php b/src/drivers/beanstalk/Queue.php index 49e0c57a39..42288abb58 100644 --- a/src/drivers/beanstalk/Queue.php +++ b/src/drivers/beanstalk/Queue.php @@ -1,4 +1,7 @@ runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { @@ -59,8 +62,8 @@ public function run($repeat, $timeout = 0) if ($this->handleMessage( $payload->getId(), $payload->getData(), - $info->ttr, - $info->reserves + (int)$info->ttr, + (int)$info->reserves )) { $this->getPheanstalk()->delete($payload); } @@ -74,7 +77,7 @@ public function run($repeat, $timeout = 0) /** * @inheritdoc */ - public function status($id) + public function status($id): int { if (!is_numeric($id) || $id <= 0) { throw new InvalidArgumentException("Unknown message ID: $id."); @@ -103,13 +106,13 @@ public function status($id) * @return bool * @since 2.0.1 */ - public function remove($id) + public function remove(int $id): bool { try { $this->getPheanstalk()->delete(new Job($id, null)); return true; } catch (ServerException $e) { - if (strpos($e->getMessage(), 'NOT_FOUND') === 0) { + if (str_starts_with($e->getMessage(), 'NOT_FOUND')) { return false; } @@ -120,11 +123,11 @@ public function remove($id) /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { return $this->getPheanstalk()->putInTube( $this->tube, - $message, + $payload, $priority ?: PheanstalkInterface::DEFAULT_PRIORITY, $delay, $ttr @@ -134,7 +137,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) /** * @return object tube statistics */ - public function getStatsTube() + public function getStatsTube(): object { return $this->getPheanstalk()->statsTube($this->tube); } @@ -142,7 +145,7 @@ public function getStatsTube() /** * @return Pheanstalk */ - protected function getPheanstalk() + protected function getPheanstalk(): Pheanstalk { if (!$this->_pheanstalk) { $this->_pheanstalk = new Pheanstalk($this->host, $this->port); diff --git a/src/drivers/db/Command.php b/src/drivers/db/Command.php index a61e0787da..7ce572a139 100644 --- a/src/drivers/db/Command.php +++ b/src/drivers/db/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -40,7 +43,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen'], true); } @@ -51,7 +54,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -61,14 +64,11 @@ public function actionRun() * It can be used as daemon process. * * @param int $timeout number of seconds to sleep before next reading of the queue. - * @throws Exception when params are invalid. * @return null|int exit code. + *@throws Exception when params are invalid. */ - public function actionListen($timeout = 3) + public function actionListen(int $timeout = 3): ?int { - if (!is_numeric($timeout)) { - throw new Exception('Timeout must be numeric.'); - } if ($timeout < 1) { throw new Exception('Timeout must be greater than zero.'); } @@ -81,7 +81,7 @@ public function actionListen($timeout = 3) * * @since 2.0.1 */ - public function actionClear() + public function actionClear(): void { if ($this->confirm('Are you sure?')) { $this->queue->clear(); @@ -95,7 +95,7 @@ public function actionClear() * @throws Exception when the job is not found. * @since 2.0.1 */ - public function actionRemove($id) + public function actionRemove(int $id): void { if (!$this->queue->remove($id)) { throw new Exception('The job is not found.'); diff --git a/src/drivers/db/InfoAction.php b/src/drivers/db/InfoAction.php index 0510e04968..c0859e0432 100644 --- a/src/drivers/db/InfoAction.php +++ b/src/drivers/db/InfoAction.php @@ -1,4 +1,7 @@ format('Jobs', Console::FG_GREEN)); diff --git a/src/drivers/db/Queue.php b/src/drivers/db/Queue.php index 93f3d98fb3..aade95bda3 100644 --- a/src/drivers/db/Queue.php +++ b/src/drivers/db/Queue.php @@ -1,4 +1,7 @@ db = Instance::ensure($this->db, Connection::class); @@ -71,7 +73,7 @@ public function init() * @internal for worker command only * @since 2.0.2 */ - public function run($repeat, $timeout = 0) + public function run(bool $repeat, int $timeout = 0) { return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { @@ -79,8 +81,8 @@ public function run($repeat, $timeout = 0) if ($this->handleMessage( $payload['id'], $payload['job'], - $payload['ttr'], - $payload['attempt'] + (int)$payload['ttr'], + (int)$payload['attempt'] )) { $this->release($payload); } @@ -96,7 +98,7 @@ public function run($repeat, $timeout = 0) /** * @inheritdoc */ - public function status($id) + public function status($id): int { $payload = (new Query()) ->from($this->tableName) @@ -127,7 +129,7 @@ public function status($id) * * @since 2.0.1 */ - public function clear() + public function clear(): void { $this->db->createCommand() ->delete($this->tableName, ['channel' => $this->channel]) @@ -151,11 +153,11 @@ public function remove($id) /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { $this->db->createCommand()->insert($this->tableName, [ 'channel' => $this->channel, - 'job' => $message, + 'job' => $payload, 'pushed_at' => time(), 'ttr' => $ttr, 'delay' => $delay, diff --git a/src/drivers/db/migrations/M161119140200Queue.php b/src/drivers/db/migrations/M161119140200Queue.php index 30dfa861c0..9db693284a 100644 --- a/src/drivers/db/migrations/M161119140200Queue.php +++ b/src/drivers/db/migrations/M161119140200Queue.php @@ -1,4 +1,7 @@ createTable($this->tableName, [ 'id' => $this->primaryKey(), @@ -35,7 +37,7 @@ public function up() $this->createIndex('started_at', $this->tableName, 'started_at'); } - public function down() + public function down(): void { $this->dropTable($this->tableName); } diff --git a/src/drivers/db/migrations/M170307170300Later.php b/src/drivers/db/migrations/M170307170300Later.php index 3d39d44044..0468806d64 100644 --- a/src/drivers/db/migrations/M170307170300Later.php +++ b/src/drivers/db/migrations/M170307170300Later.php @@ -1,4 +1,7 @@ addColumn($this->tableName, 'timeout', $this->integer()->defaultValue(0)->notNull()->after('created_at')); + $this->addColumn( + $this->tableName, + 'timeout', + $this->integer()->defaultValue(0)->notNull()->after('created_at') + ); } - public function down() + public function down(): void { $this->dropColumn($this->tableName, 'timeout'); } diff --git a/src/drivers/db/migrations/M170509001400Retry.php b/src/drivers/db/migrations/M170509001400Retry.php index 9b232b2542..1d642d293b 100644 --- a/src/drivers/db/migrations/M170509001400Retry.php +++ b/src/drivers/db/migrations/M170509001400Retry.php @@ -1,4 +1,7 @@ db->driverName !== 'sqlite') { $this->renameColumn($this->tableName, 'created_at', 'pushed_at'); @@ -48,7 +50,7 @@ public function up() } } - public function down() + public function down(): void { if ($this->db->driverName !== 'sqlite') { $this->renameColumn($this->tableName, 'done_at', 'finished_at'); diff --git a/src/drivers/db/migrations/M170601155600Priority.php b/src/drivers/db/migrations/M170601155600Priority.php index bfa98c6b13..cf8751e820 100644 --- a/src/drivers/db/migrations/M170601155600Priority.php +++ b/src/drivers/db/migrations/M170601155600Priority.php @@ -1,4 +1,7 @@ addColumn($this->tableName, 'priority', $this->integer()->unsigned()->notNull()->defaultValue(1024)->after('delay')); + $this->addColumn( + $this->tableName, + 'priority', + $this->integer()->unsigned()->notNull()->defaultValue(1024)->after('delay') + ); $this->createIndex('priority', $this->tableName, 'priority'); } - public function down() + public function down(): void { $this->dropIndex('priority', $this->tableName); $this->dropColumn($this->tableName, 'priority'); diff --git a/src/drivers/db/migrations/M211218163000JobQueueSize.php b/src/drivers/db/migrations/M211218163000JobQueueSize.php index 13fda75268..db9e259c60 100644 --- a/src/drivers/db/migrations/M211218163000JobQueueSize.php +++ b/src/drivers/db/migrations/M211218163000JobQueueSize.php @@ -1,4 +1,7 @@ db->driverName === 'mysql') { $this->alterColumn('{{%queue}}', 'job', 'LONGBLOB NOT NULL'); } } - public function down() + public function down(): void { if ($this->db->driverName === 'mysql') { $this->alterColumn('{{%queue}}', 'job', $this->binary()->notNull()); diff --git a/src/drivers/file/Command.php b/src/drivers/file/Command.php index ae6685c31e..b6b8bcd9f2 100644 --- a/src/drivers/file/Command.php +++ b/src/drivers/file/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -40,7 +43,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen']); } @@ -51,7 +54,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -64,11 +67,8 @@ public function actionRun() * @throws Exception when params are invalid. * @return null|int exit code. */ - public function actionListen($timeout = 3) + public function actionListen(int $timeout = 3): ?int { - if (!is_numeric($timeout)) { - throw new Exception('Timeout must be numeric.'); - } if ($timeout < 1) { throw new Exception('Timeout must be greater than zero.'); } @@ -81,7 +81,7 @@ public function actionListen($timeout = 3) * * @since 2.0.1 */ - public function actionClear() + public function actionClear(): void { if ($this->confirm('Are you sure?')) { $this->queue->clear(); diff --git a/src/drivers/file/InfoAction.php b/src/drivers/file/InfoAction.php index 9c996f2a32..cd4b649a45 100644 --- a/src/drivers/file/InfoAction.php +++ b/src/drivers/file/InfoAction.php @@ -1,4 +1,7 @@ format('Jobs', Console::FG_GREEN)); Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); - Console::output($this->getWaitingCount()); + Console::output((string)$this->getWaitingCount()); Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); - Console::output($this->getDelayedCount()); + Console::output((string)$this->getDelayedCount()); Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); - Console::output($this->getReservedCount()); + Console::output((string)$this->getReservedCount()); Console::stdout($this->format('- done: ', Console::FG_YELLOW)); - Console::output($this->getDoneCount()); + Console::output((string)$this->getDoneCount()); } /** * @return int */ - protected function getWaitingCount() + protected function getWaitingCount(): int { $data = $this->getIndexData(); return !empty($data['waiting']) ? count($data['waiting']) : 0; @@ -55,7 +58,7 @@ protected function getWaitingCount() /** * @return int */ - protected function getDelayedCount() + protected function getDelayedCount(): int { $data = $this->getIndexData(); return !empty($data['delayed']) ? count($data['delayed']) : 0; @@ -64,7 +67,7 @@ protected function getDelayedCount() /** * @return int */ - protected function getReservedCount() + protected function getReservedCount(): int { $data = $this->getIndexData(); return !empty($data['reserved']) ? count($data['reserved']) : 0; @@ -73,10 +76,10 @@ protected function getReservedCount() /** * @return int */ - protected function getDoneCount() + protected function getDoneCount(): int { $data = $this->getIndexData(); - $total = isset($data['lastId']) ? $data['lastId'] : 0; + $total = $data['lastId'] ?? 0; return $total - $this->getDelayedCount() - $this->getWaitingCount(); } diff --git a/src/drivers/file/Queue.php b/src/drivers/file/Queue.php index a054bf5579..76326b47be 100644 --- a/src/drivers/file/Queue.php +++ b/src/drivers/file/Queue.php @@ -1,4 +1,7 @@ path = Yii::getAlias($this->path); @@ -68,12 +70,12 @@ public function init() * @internal for worker command only. * @since 2.0.2 */ - public function run($repeat, $timeout = 0) + public function run(bool $repeat, int $timeout = 0): ?int { return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { if (($payload = $this->reserve()) !== null) { - list($id, $message, $ttr, $attempt) = $payload; + [$id, $message, $ttr, $attempt] = $payload; if ($this->handleMessage($id, $message, $ttr, $attempt)) { $this->delete($payload); } @@ -89,7 +91,7 @@ public function run($repeat, $timeout = 0) /** * @inheritdoc */ - public function status($id) + public function status($id): int { if (!is_numeric($id) || $id <= 0) { throw new InvalidArgumentException("Unknown message ID: $id."); @@ -107,7 +109,7 @@ public function status($id) * * @since 2.0.1 */ - public function clear() + public function clear(): void { $this->touchIndex(function (&$data) { $data = []; @@ -188,7 +190,7 @@ protected function reserve() if (!empty($data['delayed']) && $data['delayed'][0][2] <= time()) { list($id, $ttr, $time) = array_shift($data['delayed']); } elseif (!empty($data['waiting'])) { - list($id, $ttr) = array_shift($data['waiting']); + [$id, $ttr] = array_shift($data['waiting']); } if ($id) { $attempt = 1; @@ -208,7 +210,7 @@ protected function reserve() * * @param array $payload */ - protected function delete($payload) + protected function delete(array $payload): void { $id = $payload[0]; $this->touchIndex(function (&$data) use ($id) { @@ -225,19 +227,19 @@ protected function delete($payload) /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { if ($priority !== null) { throw new NotSupportedException('Job priority is not supported in the driver.'); } - $this->touchIndex(function (&$data) use ($message, $ttr, $delay, &$id) { + $this->touchIndex(function (&$data) use ($payload, $ttr, $delay, &$id) { if (!isset($data['lastId'])) { $data['lastId'] = 0; } $id = ++$data['lastId']; $fileName = "$this->path/job$id.data"; - file_put_contents($fileName, $message); + file_put_contents($fileName, $payload); if ($this->fileMode !== null) { chmod($fileName, $this->fileMode); } diff --git a/src/drivers/gearman/Command.php b/src/drivers/gearman/Command.php index 02ffe427ac..745710d4b7 100644 --- a/src/drivers/gearman/Command.php +++ b/src/drivers/gearman/Command.php @@ -1,4 +1,7 @@ queue->run(false); } @@ -47,7 +50,7 @@ public function actionRun() * * @return null|int exit code. */ - public function actionListen() + public function actionListen(): ?int { return $this->queue->run(true); } diff --git a/src/drivers/gearman/Queue.php b/src/drivers/gearman/Queue.php index 54fc4bbd2e..90d276faac 100644 --- a/src/drivers/gearman/Queue.php +++ b/src/drivers/gearman/Queue.php @@ -1,4 +1,7 @@ runWorker(function (callable $canContinue) use ($repeat) { - $worker = new \GearmanWorker(); + $worker = new GearmanWorker(); $worker->addServer($this->host, $this->port); - $worker->addFunction($this->channel, function (\GearmanJob $payload) { - list($ttr, $message) = explode(';', $payload->workload(), 2); - $this->handleMessage($payload->handle(), $message, $ttr, 1); + $worker->addFunction($this->channel, function (GearmanJob $payload) { + [$ttr, $message] = explode(';', $payload->workload(), 2); + $this->handleMessage($payload->handle(), $message, (int)$ttr, 1); }); $worker->setTimeout($repeat ? 1000 : 1); while ($canContinue()) { @@ -56,32 +61,30 @@ public function run($repeat) /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { if ($delay) { throw new NotSupportedException('Delayed work is not supported in the driver.'); } - switch ($priority) { - case 'high': - return $this->getClient()->doHighBackground($this->channel, "$ttr;$message"); - case 'low': - return $this->getClient()->doLowBackground($this->channel, "$ttr;$message"); - default: - return $this->getClient()->doBackground($this->channel, "$ttr;$message"); - } + return match ($priority) { + 'high' => $this->getClient()->doHighBackground($this->channel, "$ttr;$payload"), + 'low' => $this->getClient()->doLowBackground($this->channel, "$ttr;$payload"), + default => $this->getClient()->doBackground($this->channel, "$ttr;$payload"), + }; } /** * @inheritdoc */ - public function status($id) + public function status($id): int { $status = $this->getClient()->jobStatus($id); if ($status[0] && !$status[1]) { return self::STATUS_WAITING; } + /** @psalm-suppress RedundantCondition */ if ($status[0] && $status[1]) { return self::STATUS_RESERVED; } @@ -90,12 +93,12 @@ public function status($id) } /** - * @return \GearmanClient + * @return GearmanClient */ - protected function getClient() + protected function getClient(): GearmanClient { if (!$this->_client) { - $this->_client = new \GearmanClient(); + $this->_client = new GearmanClient(); $this->_client->addServer($this->host, $this->port); } return $this->_client; diff --git a/src/drivers/redis/Command.php b/src/drivers/redis/Command.php index 146aa18d22..8b6e862e17 100644 --- a/src/drivers/redis/Command.php +++ b/src/drivers/redis/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -40,7 +43,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen'], true); } @@ -51,7 +54,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -61,14 +64,11 @@ public function actionRun() * It can be used as daemon process. * * @param int $timeout number of seconds to wait a job. - * @throws Exception when params are invalid. * @return null|int exit code. + * @throws Exception when params are invalid. */ - public function actionListen($timeout = 3) + public function actionListen(int $timeout = 3): ?int { - if (!is_numeric($timeout)) { - throw new Exception('Timeout must be numeric.'); - } if ($timeout < 1) { throw new Exception('Timeout must be greater than zero.'); } @@ -81,7 +81,7 @@ public function actionListen($timeout = 3) * * @since 2.0.1 */ - public function actionClear() + public function actionClear(): void { if ($this->confirm('Are you sure?')) { $this->queue->clear(); @@ -95,7 +95,7 @@ public function actionClear() * @throws Exception when the job is not found. * @since 2.0.1 */ - public function actionRemove($id) + public function actionRemove(int $id): void { if (!$this->queue->remove($id)) { throw new Exception('The job is not found.'); diff --git a/src/drivers/redis/InfoAction.php b/src/drivers/redis/InfoAction.php index 62d410a25a..385a097f26 100644 --- a/src/drivers/redis/InfoAction.php +++ b/src/drivers/redis/InfoAction.php @@ -1,4 +1,7 @@ queue->channel; $waiting = $this->queue->redis->llen("$prefix.waiting"); diff --git a/src/drivers/redis/Queue.php b/src/drivers/redis/Queue.php index b56d1d07f2..5396227ae4 100644 --- a/src/drivers/redis/Queue.php +++ b/src/drivers/redis/Queue.php @@ -1,4 +1,7 @@ redis = Instance::ensure($this->redis, Connection::class); @@ -52,13 +54,13 @@ public function init() * @internal for worker command only. * @since 2.0.2 */ - public function run($repeat, $timeout = 0) + public function run(bool $repeat, int $timeout = 0): ?int { return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { if (($payload = $this->reserve($timeout)) !== null) { - list($id, $message, $ttr, $attempt) = $payload; - if ($this->handleMessage($id, $message, $ttr, $attempt)) { + [$id, $message, $ttr, $attempt] = $payload; + if ($this->handleMessage($id, $message, (int)$ttr, (int)$attempt)) { $this->delete($id); } } elseif (!$repeat) { @@ -71,7 +73,7 @@ public function run($repeat, $timeout = 0) /** * @inheritdoc */ - public function status($id) + public function status($id): int { if (!is_numeric($id) || $id <= 0) { throw new InvalidArgumentException("Unknown message ID: $id."); @@ -93,7 +95,7 @@ public function status($id) * * @since 2.0.1 */ - public function clear() + public function clear(): void { while (!$this->redis->set("$this->channel.moving_lock", true, 'NX')) { usleep(10000); @@ -108,7 +110,7 @@ public function clear() * @return bool * @since 2.0.1 */ - public function remove($id) + public function remove(int $id): bool { while (!$this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) { usleep(10000); @@ -129,7 +131,7 @@ public function remove($id) * @param int $timeout timeout * @return array|null payload */ - protected function reserve($timeout) + protected function reserve(int $timeout): ?array { // Moves delayed and reserved jobs into waiting list with lock for one second if ($this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) { @@ -153,8 +155,8 @@ protected function reserve($timeout) return null; } - list($ttr, $message) = explode(';', $payload, 2); - $this->redis->zadd("$this->channel.reserved", time() + $ttr, $id); + [$ttr, $message] = explode(';', $payload, 2); + $this->redis->zadd("$this->channel.reserved", time() + (int)$ttr, $id); $attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1); return [$id, $message, $ttr, $attempt]; @@ -163,7 +165,7 @@ protected function reserve($timeout) /** * @param string $from */ - protected function moveExpired($from) + protected function moveExpired(string $from): void { $now = time(); if ($expired = $this->redis->zrevrangebyscore($from, $now, '-inf')) { @@ -177,9 +179,9 @@ protected function moveExpired($from) /** * Deletes message by ID. * - * @param int $id of a message + * @param int|string $id of a message */ - protected function delete($id) + protected function delete(int|string $id): void { $this->redis->zrem("$this->channel.reserved", $id); $this->redis->hdel("$this->channel.attempts", $id); @@ -189,14 +191,14 @@ protected function delete($id) /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { if ($priority !== null) { throw new NotSupportedException('Job priority is not supported in the driver.'); } $id = $this->redis->incr("$this->channel.message_id"); - $this->redis->hset("$this->channel.messages", $id, "$ttr;$message"); + $this->redis->hset("$this->channel.messages", $id, "$ttr;$payload"); if (!$delay) { $this->redis->lpush("$this->channel.waiting", $id); } else { diff --git a/src/drivers/sqs/Command.php b/src/drivers/sqs/Command.php index 15474915b7..5bd14c529a 100644 --- a/src/drivers/sqs/Command.php +++ b/src/drivers/sqs/Command.php @@ -1,4 +1,7 @@ queue->run(false); } @@ -40,10 +43,10 @@ public function actionRun() * It can be used as demon process. * * @param int $timeout number of seconds to sleep before next reading of the queue. - * @throws Exception when params are invalid. * @return null|int exit code. + * @throws Exception when params are invalid. */ - public function actionListen($timeout = 3) + public function actionListen(int $timeout = 3): ?int { if (!is_numeric($timeout)) { throw new Exception('Timeout must be numeric.'); @@ -58,7 +61,7 @@ public function actionListen($timeout = 3) /** * Clears the queue. */ - public function actionClear() + public function actionClear(): void { if ($this->confirm('Are you sure?')) { $this->queue->clear(); @@ -69,7 +72,7 @@ public function actionClear() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen']); } diff --git a/src/drivers/sqs/Queue.php b/src/drivers/sqs/Queue.php index 898ea019a7..1618d0e8e5 100644 --- a/src/drivers/sqs/Queue.php +++ b/src/drivers/sqs/Queue.php @@ -1,4 +1,7 @@ runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { @@ -110,7 +104,7 @@ public function run($repeat, $timeout = 0) * @param int $timeout number of seconds for long polling. Must be between 0 and 20. * @return null|array payload. */ - protected function reserve($timeout) + protected function reserve(int $timeout): ?array { $response = $this->getClient()->receiveMessage([ 'QueueUrl' => $this->url, @@ -127,7 +121,7 @@ protected function reserve($timeout) $payload = reset($response['Messages']); $ttr = (int) $payload['MessageAttributes']['TTR']['StringValue']; - if ($ttr != $this->ttr) { + if ($ttr !== $this->ttr) { $this->getClient()->changeMessageVisibility([ 'QueueUrl' => $this->url, 'ReceiptHandle' => $payload['ReceiptHandle'], @@ -143,7 +137,7 @@ protected function reserve($timeout) * * @param array $payload */ - protected function delete($payload) + protected function delete(array $payload): void { $this->getClient()->deleteMessage([ 'QueueUrl' => $this->url, @@ -154,7 +148,7 @@ protected function delete($payload) /** * Clears the queue. */ - public function clear() + public function clear(): void { $this->getClient()->purgeQueue([ 'QueueUrl' => $this->url, @@ -164,7 +158,7 @@ public function clear() /** * @inheritdoc */ - public function status($id) + public function status($id): int { throw new NotSupportedException('Status is not supported in the driver.'); } @@ -179,7 +173,7 @@ public function status($id) * @return bool * @since 2.2.1 */ - public function handle($id, $message, $ttr, $attempt) + public function handle(string $id, string $message, int $ttr, int $attempt) { return $this->handleMessage($id, $message, $ttr, $attempt); } @@ -187,7 +181,7 @@ public function handle($id, $message, $ttr, $attempt) /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { if ($priority) { throw new NotSupportedException('Priority is not supported in this driver'); @@ -195,7 +189,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) $request = [ 'QueueUrl' => $this->url, - 'MessageBody' => $message, + 'MessageBody' => $payload, 'DelaySeconds' => $delay, 'MessageAttributes' => [ 'TTR' => [ @@ -205,9 +199,9 @@ protected function pushMessage($message, $ttr, $delay, $priority) ], ]; - if (substr($this->url, -5) === '.fifo') { + if (str_ends_with($this->url, '.fifo')) { $request['MessageGroupId'] = $this->messageGroupId; - $request['MessageDeduplicationId'] = hash('sha256', $message); + $request['MessageDeduplicationId'] = hash('sha256', $payload); } $response = $this->getClient()->sendMessage($request); @@ -215,9 +209,9 @@ protected function pushMessage($message, $ttr, $delay, $priority) } /** - * @return \Aws\Sqs\SqsClient + * @return SqsClient */ - protected function getClient() + protected function getClient(): SqsClient { if ($this->_client) { return $this->_client; diff --git a/src/drivers/stomp/Command.php b/src/drivers/stomp/Command.php index a51386bbae..87e70bd256 100644 --- a/src/drivers/stomp/Command.php +++ b/src/drivers/stomp/Command.php @@ -1,4 +1,7 @@ queue->run(false); } @@ -52,11 +54,8 @@ public function actionRun() * @throws Exception when params are invalid. * @return null|int exit code. */ - public function actionListen($timeout = 3) + public function actionListen(int $timeout = 3): ?int { - if (!is_numeric($timeout)) { - throw new Exception('Timeout must be numeric.'); - } if ($timeout < 1) { throw new Exception('Timeout must be greater that zero.'); } diff --git a/src/drivers/stomp/Queue.php b/src/drivers/stomp/Queue.php index 87e3b81493..e04ec302c7 100644 --- a/src/drivers/stomp/Queue.php +++ b/src/drivers/stomp/Queue.php @@ -1,4 +1,7 @@ context) { return; @@ -137,7 +143,7 @@ protected function open() 'ssl_on' => $this->sslOn, ]; - $config = array_filter($config, function ($value) { + $config = array_filter($config, static function ($value) { return null !== $value; }); @@ -149,11 +155,11 @@ protected function open() /** * Listens queue and runs each job. * - * @param $repeat + * @param bool $repeat * @param int $timeout * @return int|null */ - public function run($repeat, $timeout = 0) + public function run(bool $repeat, int $timeout = 0): ?int { return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { $this->open(); @@ -161,7 +167,8 @@ public function run($repeat, $timeout = 0) $consumer = $this->context->createConsumer($queue); while ($canContinue()) { - if ($message = ($this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait())) { + $message = $this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait(); + if ($message) { $messageId = $message->getMessageId(); if (!$messageId) { $message = $this->setMessageId($message); @@ -189,7 +196,7 @@ public function run($repeat, $timeout = 0) break; } elseif ($timeout) { sleep($timeout); - $this->context->getStomp()->getConnection()->sendAlive(); + $this->context->getStomp()->getConnection()?->sendAlive(); } } }); @@ -198,9 +205,8 @@ public function run($repeat, $timeout = 0) /** * @param StompMessage $message * @return StompMessage - * @throws \Interop\Queue\Exception */ - protected function setMessageId(StompMessage $message) + protected function setMessageId(Message $message): StompMessage { $message->setMessageId(uniqid('', true)); return $message; @@ -208,15 +214,15 @@ protected function setMessageId(StompMessage $message) /** * @inheritdoc - * @throws \Interop\Queue\Exception + * @throws QueueException * @throws NotSupportedException */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { $this->open(); $queue = $this->createQueue($this->queueName); - $message = $this->context->createMessage($message); + $message = $this->context->createMessage($payload); $message = $this->setMessageId($message); $message->setPersistent(true); $message->setProperty(self::ATTEMPT, 1); @@ -240,7 +246,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) /** * Closes connection. */ - protected function close() + protected function close(): void { if (!$this->context) { return; @@ -254,20 +260,24 @@ protected function close() * @inheritdoc * @throws NotSupportedException */ - public function status($id) + public function status($id): int { throw new NotSupportedException('Status is not supported in the driver.'); } /** * @param StompMessage $message - * @throws \Interop\Queue\Exception + * @throws QueueException */ - protected function redeliver(StompMessage $message) + protected function redeliver(StompMessage $message): void { $attempt = $message->getProperty(self::ATTEMPT, 1); - $newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders()); + $newMessage = $this->context->createMessage( + $message->getBody(), + $message->getProperties(), + $message->getHeaders() + ); $newMessage->setProperty(self::ATTEMPT, ++$attempt); $this->context->createProducer()->send( @@ -277,10 +287,10 @@ protected function redeliver(StompMessage $message) } /** - * @param $name - * @return \Enqueue\Stomp\StompDestination + * @param string $name + * @return InteropQueue|StompDestination */ - private function createQueue($name) + private function createQueue(string $name): InteropQueue|StompDestination { $queue = $this->context->createQueue($name); $queue->setDurable(true); diff --git a/src/drivers/sync/Queue.php b/src/drivers/sync/Queue.php index 28db7e872b..d83d8eee58 100644 --- a/src/drivers/sync/Queue.php +++ b/src/drivers/sync/Queue.php @@ -1,4 +1,7 @@ handle) { @@ -60,10 +61,10 @@ public function init() /** * Runs all jobs from queue. */ - public function run() + public function run(): void { while (($payload = array_shift($this->payloads)) !== null) { - list($ttr, $message) = $payload; + [$ttr, $message] = $payload; $this->startedId = $this->finishedId + 1; $this->handleMessage($this->startedId, $message, $ttr, 1); $this->finishedId = $this->startedId; @@ -74,16 +75,16 @@ public function run() /** * @inheritdoc */ - protected function pushMessage($message, $ttr, $delay, $priority) + protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { - array_push($this->payloads, [$ttr, $message]); + $this->payloads[] = [$ttr, $payload]; return ++$this->pushedId; } /** * @inheritdoc */ - public function status($id) + public function status($id): int { if (!is_int($id) || $id <= 0 || $id > $this->pushedId) { throw new InvalidArgumentException("Unknown messages ID: $id."); diff --git a/src/gii/Generator.php b/src/gii/Generator.php index 7bd1e8002d..a07fd4414e 100644 --- a/src/gii/Generator.php +++ b/src/gii/Generator.php @@ -1,4 +1,7 @@ toArray($job), $this->options); } diff --git a/src/serializers/PhpSerializer.php b/src/serializers/PhpSerializer.php index c521eed78d..d4fb4bc7c5 100644 --- a/src/serializers/PhpSerializer.php +++ b/src/serializers/PhpSerializer.php @@ -1,4 +1,7 @@ - */ -interface Serializer extends SerializerInterface -{ -} diff --git a/src/serializers/SerializerInterface.php b/src/serializers/SerializerInterface.php index a66931a907..d34faad5b7 100644 --- a/src/serializers/SerializerInterface.php +++ b/src/serializers/SerializerInterface.php @@ -1,4 +1,7 @@ on(Queue::EVENT_BEFORE_EXEC, $eventHandler); $queue->on(Queue::EVENT_AFTER_ERROR, $eventHandler); $queue->on(Queue::EVENT_AFTER_ERROR, function (ExecEvent $event) { - $this->assertTrue($event->error instanceof InvalidJobException); + $this->assertInstanceOf(InvalidJobException::class, $event->error); $this->assertFalse($event->retry); }); $jobId = $queue->push('message that cannot be unserialized'); @@ -41,7 +44,7 @@ public function testInvalidJob() $this->assertArrayHasKey(Queue::EVENT_AFTER_ERROR, $eventCounter[$jobId]); } - public function testExecResult() + public function testExecResult(): void { $queue = new SyncQueue(['as closure' => ClosureBehavior::class]); $isTriggered = false; diff --git a/tests/TestCase.php b/tests/TestCase.php index acd2c1101b..c3a4adb02d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,4 +1,7 @@ */ -abstract class TestCase extends \PHPUnit_Framework_TestCase +abstract class TestCase extends \PHPUnit\Framework\TestCase { } diff --git a/tests/app/PriorityJob.php b/tests/app/PriorityJob.php index 2d8c2000f3..7abd876c3e 100644 --- a/tests/app/PriorityJob.php +++ b/tests/app/PriorityJob.php @@ -1,4 +1,7 @@ number, FILE_APPEND); } - public static function getFileName() + public static function getFileName(): bool|string { return Yii::getAlias("@runtime/job-priority.log"); } diff --git a/tests/app/RetryJob.php b/tests/app/RetryJob.php index 8248d4c816..7e372b6f3e 100644 --- a/tests/app/RetryJob.php +++ b/tests/app/RetryJob.php @@ -1,4 +1,7 @@ getFileName(), 'a', FILE_APPEND); throw new \Exception('Planned error.'); } - public function getFileName() + public function getFileName(): bool|string { return Yii::getAlias("@runtime/job-{$this->uid}.lock"); } - public function getTtr() + public function getTtr(): int { return 2; } - public function canRetry($attempt, $error) + public function canRetry($attempt, $error): bool { return $attempt < 2; } diff --git a/tests/app/SimpleJob.php b/tests/app/SimpleJob.php index 4647f4d5f0..5b9ef9651c 100644 --- a/tests/app/SimpleJob.php +++ b/tests/app/SimpleJob.php @@ -1,4 +1,7 @@ getFileName(), ''); } - public function getFileName() + public function getFileName(): bool|string { return Yii::getAlias("@runtime/job-{$this->uid}.lock"); } diff --git a/tests/app/benchmark/Controller.php b/tests/app/benchmark/Controller.php index c5358b6774..265e2ad4f8 100644 --- a/tests/app/benchmark/Controller.php +++ b/tests/app/benchmark/Controller.php @@ -1,4 +1,7 @@ */ -class Controller extends \yii\console\Controller +class Controller extends ConsoleController { - private $startedAt; + private int $startedAt; - public function actions() + public function actions(): array { return [ 'waiting' => waiting\Action::class, @@ -26,7 +31,7 @@ public function actions() /** * @inheritdoc */ - public function beforeAction($action) + public function beforeAction($action): bool { $this->startedAt = time(); return parent::beforeAction($action); diff --git a/tests/app/benchmark/waiting/Action.php b/tests/app/benchmark/waiting/Action.php index d890905157..39b5836df6 100644 --- a/tests/app/benchmark/waiting/Action.php +++ b/tests/app/benchmark/waiting/Action.php @@ -1,4 +1,7 @@ */ -class Action extends \yii\base\Action +class Action extends BaseAction { /** * @var array */ - public $modes = [ + public array $modes = [ // Worker will be run in fast mode 'fast' => [ 'gearmanQueue' => ['gearman-queue/listen' ,'--isolate=0'], 'beanstalkQueue' => ['beanstalk-queue/listen' ,'--isolate=0'], 'redisQueue' => ['redis-queue/listen' ,'--isolate=0'], - 'amqpQueue' => ['amqp-queue/listen' ,'--isolate=0'], 'amqpInteropQueue' => ['amqp-interop-queue/listen' ,'--isolate=0'], 'mysqlQueue' => ['mysql-queue/listen', '1' ,'--isolate=0'], 'fileQueue' => ['file-queue/listen' , '1' ,'--isolate=0'], @@ -40,7 +43,6 @@ class Action extends \yii\base\Action 'gearmanQueue' => ['gearman-queue/listen' ,'--isolate=1'], 'beanstalkQueue' => ['beanstalk-queue/listen' ,'--isolate=1'], 'redisQueue' => ['redis-queue/listen' ,'--isolate=1'], - 'amqpQueue' => ['amqp-queue/listen' ,'--isolate=1'], 'amqpInteropQueue' => ['amqp-interop-queue/listen' ,'--isolate=1'], 'mysqlQueue' => ['mysql-queue/listen', '1' ,'--isolate=1'], 'fileQueue' => ['file-queue/listen' , '1' ,'--isolate=1'], @@ -50,7 +52,7 @@ class Action extends \yii\base\Action /** * @var Process[] */ - private $workers = []; + private array $workers = []; /** * Runs benchmark of job wait time. @@ -61,7 +63,7 @@ class Action extends \yii\base\Action * @param int $payloadSize additional job size * @throws */ - public function run($mode = 'fast', $jobCount = 1000, $workerCount = 10, $payloadSize = 0) + public function run(string $mode = 'fast', int $jobCount = 1000, int $workerCount = 10, int $payloadSize = 0): void { if (!isset($this->modes[$mode])) { throw new ConsoleException("Unknown mode: $mode."); @@ -100,7 +102,7 @@ public function run($mode = 'fast', $jobCount = 1000, $workerCount = 10, $payloa for ($i = 0; $i < $workerCount && $pushedCount < $jobCount; $i++) { $jobs[] = $job = new Job(); $job->resultFileName = $resultFileName; - $lockName = uniqid($queueName); + $lockName = uniqid($queueName, true); $job->lockFileName = Yii::getAlias("@runtime/$lockName.lock"); touch($job->lockFileName); $job->pushedAt = microtime(true); @@ -125,7 +127,7 @@ public function run($mode = 'fast', $jobCount = 1000, $workerCount = 10, $payloa } Console::endProgress(strtr( - 'MEDIAN = {median} s; AVG = {avg} s; MIN = {min} s; MAX = {max} s' . PHP_EOL, + 'COUNT = {count}; MEDIAN = {median} s; AVG = {avg} s; MIN = {min} s; MAX = {max} s' . PHP_EOL, $this->calcResult($resultFileName, 4) )); } finally { @@ -141,7 +143,7 @@ public function run($mode = 'fast', $jobCount = 1000, $workerCount = 10, $payloa * @param int $count * @param callable $callback */ - private function startWorkers($command, $count, callable $callback) + private function startWorkers(array $command, int $count, callable $callback): void { for ($i = 0; $i < $count; $i++) { $this->workers[] = $worker = new Process(array_merge(['php', 'tests/yii'], $command)); @@ -152,10 +154,9 @@ private function startWorkers($command, $count, callable $callback) /** * Stops started workers. */ - private function stopWorkers() + private function stopWorkers(): void { foreach ($this->workers as $worker) { - /** @var Process $worker */ $worker->stop(); } $this->workers = []; @@ -168,7 +169,7 @@ private function stopWorkers() * @param int $scale * @return array of aggregate results in seconds */ - private function calcResult($fileName, $scale = 4) + private function calcResult(string $fileName, int $scale): array { /** @var float[] $times */ $times = explode("\n", trim(file_get_contents($fileName))); @@ -181,8 +182,8 @@ private function calcResult($fileName, $scale = 4) $median = ($median + $times[$middleIndex - 1]) / 2; } $avg = array_sum($times) / $count; - $min = min($times); - $max = max($times); + $min = (float)min($times); + $max = (float)max($times); return [ '{count}' => $count, diff --git a/tests/app/benchmark/waiting/Job.php b/tests/app/benchmark/waiting/Job.php index 670c7cee92..0337ebe2b8 100644 --- a/tests/app/benchmark/waiting/Job.php +++ b/tests/app/benchmark/waiting/Job.php @@ -1,4 +1,7 @@ pushedAt; if (file_exists($this->lockFileName)) { diff --git a/tests/app/config/console.php b/tests/app/config/console.php index 32c337bd47..e8f85e3ee0 100644 --- a/tests/app/config/console.php +++ b/tests/app/config/console.php @@ -1,8 +1,14 @@ [ 'mysql-migrate' => [ - 'class' => \yii\console\controllers\MigrateController::class, + 'class' => MigrateController::class, 'db' => 'mysql', 'migrationPath' => null, 'migrationNamespaces' => [ @@ -10,7 +16,7 @@ ], ], 'sqlite-migrate' => [ - 'class' => \yii\console\controllers\MigrateController::class, + 'class' => MigrateController::class, 'db' => 'sqlite', 'migrationPath' => null, 'migrationNamespaces' => [ @@ -18,13 +24,13 @@ ], ], 'pgsql-migrate' => [ - 'class' => \yii\console\controllers\MigrateController::class, + 'class' => MigrateController::class, 'db' => 'pgsql', 'migrationPath' => null, 'migrationNamespaces' => [ 'yii\queue\db\migrations', ], ], - 'benchmark' => \tests\app\benchmark\Controller::class, + 'benchmark' => Controller::class, ], ]; diff --git a/tests/app/config/main.php b/tests/app/config/main.php index 29b16b8f87..047508603f 100644 --- a/tests/app/config/main.php +++ b/tests/app/config/main.php @@ -1,29 +1,46 @@ 'yii2-queue-app', 'basePath' => dirname(__DIR__), - 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', - 'runtimePath' => dirname(dirname(__DIR__)) . '/runtime', + 'vendorPath' => dirname(__DIR__, 2) . '/vendor', + 'runtimePath' => dirname(__DIR__, 2) . '/runtime', 'bootstrap' => [ 'fileQueue', 'mysqlQueue', 'sqliteQueue', 'pgsqlQueue', 'redisQueue', - 'amqpQueue', 'amqpInteropQueue', 'beanstalkQueue', 'stompQueue', ], 'components' => [ 'syncQueue' => [ - 'class' => \yii\queue\sync\Queue::class, + 'class' => SyncQueue::class, ], 'fileQueue' => [ - 'class' => \yii\queue\file\Queue::class, + 'class' => FileQueue::class, ], 'mysql' => [ - 'class' => \yii\db\Connection::class, + 'class' => Connection::class, 'dsn' => sprintf( 'mysql:host=%s;dbname=%s', getenv('MYSQL_HOST') ?: 'localhost', @@ -37,24 +54,24 @@ ], ], 'mysqlQueue' => [ - 'class' => \yii\queue\db\Queue::class, + 'class' => DbQueue::class, 'db' => 'mysql', 'mutex' => [ - 'class' => \yii\mutex\MysqlMutex::class, + 'class' => MysqlMutex::class, 'db' => 'mysql', ], ], 'sqlite' => [ - 'class' => \yii\db\Connection::class, + 'class' => Connection::class, 'dsn' => 'sqlite:@runtime/yii2_queue_test.db', ], 'sqliteQueue' => [ - 'class' => \yii\queue\db\Queue::class, + 'class' => DbQueue::class, 'db' => 'sqlite', - 'mutex' => \yii\mutex\FileMutex::class, + 'mutex' => FileMutex::class, ], 'pgsql' => [ - 'class' => \yii\db\Connection::class, + 'class' => Connection::class, 'dsn' => sprintf( 'pgsql:host=%s;dbname=%s', getenv('POSTGRES_HOST') ?: 'localhost', @@ -65,45 +82,38 @@ 'charset' => 'utf8', ], 'pgsqlQueue' => [ - 'class' => \yii\queue\db\Queue::class, + 'class' => DbQueue::class, 'db' => 'pgsql', 'mutex' => [ - 'class' => \yii\mutex\PgsqlMutex::class, + 'class' => PgsqlMutex::class, 'db' => 'pgsql', ], 'mutexTimeout' => 0, ], 'redis' => [ - 'class' => \yii\redis\Connection::class, + 'class' => RedisConnection::class, 'hostname' => getenv('REDIS_HOST') ?: 'localhost', 'database' => getenv('REDIS_DB') ?: 1, ], 'redisQueue' => [ - 'class' => \yii\queue\redis\Queue::class, - ], - 'amqpQueue' => [ - 'class' => \yii\queue\amqp\Queue::class, - 'host' => getenv('RABBITMQ_HOST') ?: 'localhost', - 'user' => getenv('RABBITMQ_USER') ?: 'guest', - 'password' => getenv('RABBITMQ_PASSWORD') ?: 'guest', - 'queueName' => 'queue-basic', - 'exchangeName' => 'exchange-basic', + 'class' => RedisQueue::class, ], 'amqpInteropQueue' => [ - 'class' => \yii\queue\amqp_interop\Queue::class, + 'class' => AmqpInteropQueue::class, 'host' => getenv('RABBITMQ_HOST') ?: 'localhost', 'user' => getenv('RABBITMQ_USER') ?: 'guest', 'password' => getenv('RABBITMQ_PASSWORD') ?: 'guest', + 'port' => getenv('RABBITMQ_PORT') ?: 5672, 'queueOptionalArguments' => ['x-max-priority' => 10], 'queueName' => 'queue-interop', 'exchangeName' => 'exchange-interop', ], 'beanstalkQueue' => [ - 'class' => \yii\queue\beanstalk\Queue::class, + 'class' => BeanstalkQueue::class, 'host' => getenv('BEANSTALK_HOST') ?: 'localhost', ], 'stompQueue' => [ - 'class' => \yii\queue\stomp\Queue::class, + 'class' => StompQueue::class, 'host' => getenv('ACTIVEMQ_HOST') ?: 'localhost', ], ], @@ -112,7 +122,7 @@ if (defined('GEARMAN_SUCCESS')) { $config['bootstrap'][] = 'gearmanQueue'; $config['components']['gearmanQueue'] = [ - 'class' => \yii\queue\gearman\Queue::class, + 'class' => GearmanQueue::class, 'host' => getenv('GEARMAN_HOST') ?: 'localhost', ]; } @@ -120,7 +130,7 @@ if (getenv('AWS_SQS_ENABLED')) { $config['bootstrap'][] = 'sqsQueue'; $config['components']['sqsQueue'] = [ - 'class' => \yii\queue\sqs\Queue::class, + 'class' => SqsQueue::class, 'url' => getenv('AWS_SQS_URL'), 'key' => getenv('AWS_KEY'), 'secret' => getenv('AWS_SECRET'), @@ -131,7 +141,7 @@ if (getenv('AWS_SQS_FIFO_ENABLED')) { $config['bootstrap'][] = 'sqsFifoQueue'; $config['components']['sqsFifoQueue'] = [ - 'class' => \yii\queue\sqs\Queue::class, + 'class' => SqsQueue::class, 'url' => getenv('AWS_SQS_FIFO_URL'), 'key' => getenv('AWS_KEY'), 'secret' => getenv('AWS_SECRET'), diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 64cfda50f5..0b388ae229 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,7 @@ closure); } diff --git a/tests/closure/ClosureTest.php b/tests/closure/ClosureTest.php index 3ff41ef3ca..bd4adda7c6 100644 --- a/tests/closure/ClosureTest.php +++ b/tests/closure/ClosureTest.php @@ -1,4 +1,7 @@ getQueue()->push(function () { $fileName = Yii::getAlias('@runtime/job-1.lock'); @@ -29,7 +32,7 @@ public function testPush1() $this->assertFileExists(Yii::getAlias('@runtime/job-1.lock')); } - public function testPush2() + public function testPush2(): void { $fileName = Yii::getAlias('@runtime/job-2.lock'); $this->getQueue()->push(function () use ($fileName) { @@ -39,7 +42,7 @@ public function testPush2() $this->assertFileExists($fileName); } - public function testPush3() + public function testPush3(): void { $job = new ClosureJob([ 'closure' => function () { @@ -52,7 +55,7 @@ public function testPush3() $this->assertFileExists(Yii::getAlias('@runtime/job-3.lock')); } - public function testPush4() + public function testPush4(): void { $fileName = Yii::getAlias('@runtime/job-4.lock'); $job = new ClosureJob([ @@ -68,7 +71,7 @@ public function testPush4() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { if (!$this->_queue) { $this->_queue = new Queue([ @@ -84,7 +87,7 @@ protected function getQueue() /** * @inheritdoc */ - protected function tearDown() + protected function tearDown(): void { foreach (glob(Yii::getAlias("@runtime/job-*.lock")) as $fileName) { unlink($fileName); diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 2b7000c6ab..1f22950d3c 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -5,8 +5,10 @@ services: yii2-queue-php: container_name: yii2-queue-php build: - context: .. - dockerfile: tests/docker/php/${PHP_VERSION:-5.6}/Dockerfile + context: .. + dockerfile: tests/docker/php/Dockerfile + args: + PHP_VERSION: ${PHP_VERSION:-8.1} volumes: - ./runtime/.composer:/root/.composer - ..:/code @@ -54,7 +56,7 @@ services: mysql: image: mysql:5.7 ports: - - 3307:3306 + - "3307:3306" environment: MYSQL_ALLOW_EMPTY_PASSWORD: 1 MYSQL_USER: yii2_queue_test @@ -67,7 +69,7 @@ services: postgres: image: postgres:10.4 ports: - - 5433:5432 + - "5433:5432" environment: POSTGRES_USER: yii2_queue_test POSTGRES_PASSWORD: yii2_queue_test @@ -79,7 +81,7 @@ services: redis: image: redis:4.0 ports: - - 6380:6379 + - "6380:6379" networks: net: {} @@ -96,7 +98,7 @@ services: beanstalk: image: schickling/beanstalkd ports: - - 11301:11300 + - "11301:11300" networks: net: {} @@ -104,7 +106,7 @@ services: gearmand: image: artefactual/gearmand ports: - - 4731:4730 + - "4731:4730" networks: net: {} @@ -112,7 +114,7 @@ services: activemq: image: webcenter/activemq ports: - - 61613:61613 + - "61613:61613" networks: net: {} diff --git a/tests/docker/php/5.6/Dockerfile b/tests/docker/php/5.6/Dockerfile deleted file mode 100644 index ef57421532..0000000000 --- a/tests/docker/php/5.6/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM amd64/php:5.6.40-cli-stretch - -RUN sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list \ - && sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \ - && sed -i '/stretch-updates/d' /etc/apt/sources.list - -RUN apt-get update -qq \ - && apt install -y ca-certificates \ - && sed -i '/^mozilla\/DST_Root_CA_X3.crt$/ s/^/!/' /etc/ca-certificates.conf \ - && update-ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -RUN apt-get update \ - && apt-get install -y unzip curl zlib1g-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql - -RUN pecl install igbinary-2.0.8 gearman-1.1.2 \ - && docker-php-ext-enable gearman igbinary - -RUN rm -rf /var/lib/apt/lists/* - -COPY --from=cytopia/php-cs-fixer:2-php5.6 /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer -COPY --from=composer:lts /usr/bin/composer /usr/local/bin/composer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/7.0/Dockerfile b/tests/docker/php/7.0/Dockerfile deleted file mode 100644 index 7c2902e468..0000000000 --- a/tests/docker/php/7.0/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM amd64/php:7.0.33-cli-stretch - -RUN sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list \ - && sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \ - && sed -i '/stretch-updates/d' /etc/apt/sources.list - -RUN apt update -qq \ - && apt install -y ca-certificates \ - && sed -i '/^mozilla\/DST_Root_CA_X3.crt$/ s/^/!/' /etc/ca-certificates.conf \ - && update-ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -RUN apt-get update \ - && apt-get install -y unzip curl zlib1g-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql - -RUN pecl install igbinary \ - && docker-php-ext-enable igbinary - -RUN TMPDIR=$(mktemp -d) \ - && cd $TMPDIR \ - && curl -L https://github.com/wcgallego/pecl-gearman/archive/gearman-2.0.3.tar.gz | tar xzv --strip 1 \ - && phpize \ - && ./configure \ - && make -j$(nproc) \ - && make install \ - && cd - \ - && rm -r $TMPDIR \ - && docker-php-ext-enable gearman - -COPY --from=cytopia/php-cs-fixer:2-php7.0 /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer -COPY --from=composer:lts /usr/bin/composer /usr/local/bin/composer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/7.1/Dockerfile b/tests/docker/php/7.1/Dockerfile deleted file mode 100644 index 7227164151..0000000000 --- a/tests/docker/php/7.1/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM php:7.1-cli - -RUN apt-get update \ - && apt-get install -y unzip curl zlib1g-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql - -RUN pecl install igbinary \ - && docker-php-ext-enable igbinary - -RUN TMPDIR=$(mktemp -d) \ - && cd $TMPDIR \ - && curl -L https://github.com/wcgallego/pecl-gearman/archive/gearman-2.0.3.tar.gz | tar xzv --strip 1 \ - && phpize \ - && ./configure \ - && make -j$(nproc) \ - && make install \ - && cd - \ - && rm -r $TMPDIR \ - && docker-php-ext-enable gearman - -COPY --from=cytopia/php-cs-fixer:2-php7.1 /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer -COPY --from=composer:lts /usr/bin/composer /usr/local/bin/composer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/7.2/Dockerfile b/tests/docker/php/7.2/Dockerfile deleted file mode 100644 index c2ed7bdaeb..0000000000 --- a/tests/docker/php/7.2/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM php:7.2-cli - -RUN apt-get update \ - && apt-get install -y unzip libbz2-dev curl zlib1g-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql - -RUN pecl install igbinary \ - && docker-php-ext-enable igbinary - -RUN TMPDIR=$(mktemp -d) \ - && cd $TMPDIR \ - && curl -L https://github.com/wcgallego/pecl-gearman/archive/gearman-2.0.3.tar.gz | tar xzv --strip 1 \ - && phpize \ - && ./configure \ - && make -j$(nproc) \ - && make install \ - && cd - \ - && rm -r $TMPDIR \ - && docker-php-ext-enable gearman - -COPY --from=cytopia/php-cs-fixer:2-php7.2 /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer -COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/7.3/Dockerfile b/tests/docker/php/7.3/Dockerfile deleted file mode 100644 index b5c0457912..0000000000 --- a/tests/docker/php/7.3/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM php:7.3-cli - -RUN apt-get update \ - && apt-get install -y unzip curl zlib1g-dev libzip-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql - -RUN pecl install igbinary \ - && docker-php-ext-enable igbinary - -RUN TMPDIR=$(mktemp -d) \ - && cd $TMPDIR \ - && curl -L https://github.com/wcgallego/pecl-gearman/archive/gearman-2.0.3.tar.gz | tar xzv --strip 1 \ - && phpize \ - && ./configure \ - && make -j$(nproc) \ - && make install \ - && cd - \ - && rm -r $TMPDIR \ - && docker-php-ext-enable gearman - -COPY --from=cytopia/php-cs-fixer:2-php7.3 /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer -COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/7.4/Dockerfile b/tests/docker/php/7.4/Dockerfile deleted file mode 100644 index a60bebafe8..0000000000 --- a/tests/docker/php/7.4/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM php:7.4-cli - -RUN apt-get update \ - && apt-get install -y unzip curl zlib1g-dev libzip-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql - -RUN pecl install igbinary gearman \ - && docker-php-ext-enable igbinary gearman - -COPY --from=cytopia/php-cs-fixer:2-php7.4 /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer -COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/8.0/Dockerfile b/tests/docker/php/8.0/Dockerfile deleted file mode 100644 index 8a349d5ad3..0000000000 --- a/tests/docker/php/8.0/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM --platform=linux/amd64 php:8.0.28-cli - -RUN apt-get update \ - && apt-get install -y unzip curl zlib1g-dev libzip-dev libicu-dev libpq-dev libgearman-dev - -RUN docker-php-ext-install zip pcntl bcmath pdo_mysql intl pdo_pgsql sockets -RUN pecl install igbinary gearman -RUN docker-php-ext-enable igbinary gearman - -COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer -COPY --from=cytopia/php-cs-fixer:latest /usr/bin/php-cs-fixer /usr/local/bin/php-cs-fixer - -COPY . /code -WORKDIR /code - -ENTRYPOINT ["tests/docker/php/entrypoint.sh"] -CMD ["sleep", "infinity"] diff --git a/tests/docker/php/Dockerfile b/tests/docker/php/Dockerfile new file mode 100644 index 0000000000..da2ee33bb8 --- /dev/null +++ b/tests/docker/php/Dockerfile @@ -0,0 +1,36 @@ +# Important! Do not use this image in production! +ARG PHP_VERSION + +FROM --platform=linux/amd64 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 \ + && echo https://dl-cdn.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories \ + && apk update + +RUN apk add git icu-dev libpq-dev gearman-dev libcrypto3 openssl-dev autoconf g++ make linux-headers rabbitmq-c-dev + +RUN docker-php-ext-install pcntl bcmath pdo_mysql intl pdo_pgsql sockets opcache +RUN pecl install igbinary pcov amqp-1.11.0 xdebug +RUN docker-php-ext-enable igbinary pcov amqp xdebug + +# Official gearman package not supported PHP 8.1 now +RUN TMPDIR=$(mktemp -d) \ + && cd $TMPDIR \ + && git clone https://github.com/php/pecl-networking-gearman gearman \ + && cd ./gearman \ + && phpize \ + && ./configure \ + && make -j$(nproc) \ + && make install \ + && cd - \ + && rm -r $TMPDIR \ + && docker-php-ext-enable gearman + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +COPY . /code +WORKDIR /code + +ENTRYPOINT ["tests/docker/php/entrypoint.sh"] +CMD ["sleep", "infinity"] diff --git a/tests/docker/php/entrypoint.sh b/tests/docker/php/entrypoint.sh index 641ee5b4d9..2bcde08c9e 100755 --- a/tests/docker/php/entrypoint.sh +++ b/tests/docker/php/entrypoint.sh @@ -5,23 +5,11 @@ set -eu flock tests/runtime/composer-install.lock composer install --prefer-dist --no-interaction tests/yii sqlite-migrate/up --interactive=0 - -tests/docker/wait-for-it.sh mysql:3306 -t 180 +sleep 20 tests/docker/php/mysql-lock.php tests/yii mysql-migrate/up --interactive=0 - -tests/docker/wait-for-it.sh postgres:5432 -t 180 +sleep 20 tests/docker/php/mysql-lock.php tests/yii pgsql-migrate/up --interactive=0 -tests/docker/wait-for-it.sh redis:6379 -t 180 - -tests/docker/wait-for-it.sh rabbitmq:5672 -t 180 - -tests/docker/wait-for-it.sh beanstalk:11300 -t 180 - -tests/docker/wait-for-it.sh gearmand:4730 -t 180 - -tests/docker/wait-for-it.sh activemq:61613 -t 180 - php --version set -x exec "$@" diff --git a/tests/docker/wait-for-it.sh b/tests/docker/wait-for-it.sh deleted file mode 100755 index b444726abc..0000000000 --- a/tests/docker/wait-for-it.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env bash -# Use this script to test if a given TCP host/port are available -# https://github.com/vishnubob/wait-for-it - -cmdname=$(basename $0) - -echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $TIMEOUT -gt 0 ]]; then - echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" - else - echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" - fi - start_ts=$(date +%s) - while : - do - if [[ $ISBUSY -eq 1 ]]; then - nc -z $HOST $PORT - result=$? - else - (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 - result=$? - fi - if [[ $result -eq 0 ]]; then - end_ts=$(date +%s) - echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" - break - fi - sleep 1 - done - return $result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: https://unix.stackexchange.com/a/57692 - if [[ $QUIET -eq 1 ]]; then - timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & - else - timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & - fi - PID=$! - trap "kill -INT -$PID" INT - wait $PID - RESULT=$? - if [[ $RESULT -ne 0 ]]; then - echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" - fi - return $RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - hostport=(${1//:/ }) - HOST=${hostport[0]} - PORT=${hostport[1]} - shift 1 - ;; - --child) - CHILD=1 - shift 1 - ;; - -q | --quiet) - QUIET=1 - shift 1 - ;; - -s | --strict) - STRICT=1 - shift 1 - ;; - -h) - HOST="$2" - if [[ $HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - HOST="${1#*=}" - shift 1 - ;; - -p) - PORT="$2" - if [[ $PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - PORT="${1#*=}" - shift 1 - ;; - -t) - TIMEOUT="$2" - if [[ $TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$HOST" == "" || "$PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -TIMEOUT=${TIMEOUT:-15} -STRICT=${STRICT:-0} -CHILD=${CHILD:-0} -QUIET=${QUIET:-0} - -# check to see if timeout is from busybox? -# check to see if timeout is from busybox? -TIMEOUT_PATH=$(realpath $(which timeout)) -if [[ $TIMEOUT_PATH =~ "busybox" ]]; then - ISBUSY=1 - BUSYTIMEFLAG="-t" -else - ISBUSY=0 - BUSYTIMEFLAG="" -fi - -if [[ $CHILD -gt 0 ]]; then - wait_for - RESULT=$? - exit $RESULT -else - if [[ $TIMEOUT -gt 0 ]]; then - wait_for_wrapper - RESULT=$? - else - wait_for - RESULT=$? - fi -fi - -if [[ $CLI != "" ]]; then - if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then - echoerr "$cmdname: strict mode, refusing to execute subprocess" - exit $RESULT - fi - exec "${CLI[@]}" -else - exit $RESULT -fi diff --git a/tests/drivers/CliTestCase.php b/tests/drivers/CliTestCase.php index 40870565d5..907344766f 100644 --- a/tests/drivers/CliTestCase.php +++ b/tests/drivers/CliTestCase.php @@ -1,4 +1,7 @@ prepareCmd($cmd); $process = new Process($cmd); @@ -43,7 +47,7 @@ protected function runProcess($cmd) * @param array $cmd * @return Process */ - protected function startProcess($cmd) + protected function startProcess(array $cmd): Process { $process = new Process($this->prepareCmd($cmd)); $process->start(); @@ -59,9 +63,9 @@ protected function startProcess($cmd) * @param array $cmd * @return array */ - private function prepareCmd($cmd) + private function prepareCmd(array $cmd): array { - $class = new \ReflectionClass($this->getQueue()); + $class = new ReflectionClass($this->getQueue()); $method = $class->getMethod('getCommandId'); $method->setAccessible(true); @@ -74,7 +78,7 @@ private function prepareCmd($cmd) array_walk( $cmd, static function (&$v) use ($replace) { - $v = strtr($v, $replace); + $v = strtr((string)$v, $replace); } ); @@ -84,7 +88,7 @@ static function (&$v) use ($replace) { /** * @inheritdoc */ - protected function tearDown() + protected function tearDown(): void { if (file_exists(PriorityJob::getFileName())) { unlink(PriorityJob::getFileName()); diff --git a/tests/drivers/TestCase.php b/tests/drivers/TestCase.php index 4d19592d4f..287447ca81 100644 --- a/tests/drivers/TestCase.php +++ b/tests/drivers/TestCase.php @@ -1,4 +1,7 @@ uniqid(), + 'uid' => uniqid('', true), ]); } /** * @param SimpleJob $job */ - protected function assertSimpleJobDone(SimpleJob $job) + protected function assertSimpleJobDone(SimpleJob $job): void { $timeout = 5000000; // 5 sec $step = 50000; @@ -51,7 +54,7 @@ protected function assertSimpleJobDone(SimpleJob $job) * @param SimpleJob $job * @param int $delay */ - protected function assertSimpleJobLaterDone(SimpleJob $job, $delay) + protected function assertSimpleJobLaterDone(SimpleJob $job, int $delay): void { $time = time() + $delay; sleep($delay); @@ -68,7 +71,7 @@ protected function assertSimpleJobLaterDone(SimpleJob $job, $delay) /** * @inheritdoc */ - protected function tearDown() + protected function tearDown(): void { // Removes temp job files foreach (glob(Yii::getAlias("@runtime/job-*.lock")) as $fileName) { diff --git a/tests/drivers/amqp/QueueTest.php b/tests/drivers/amqp/QueueTest.php deleted file mode 100644 index f363d45472..0000000000 --- a/tests/drivers/amqp/QueueTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -class QueueTest extends CliTestCase -{ - /** - * @return Queue - */ - protected function getQueue() - { - return Yii::$app->amqpQueue; - } - - public function testListen() - { - $this->startProcess(['php', 'yii', 'queue/listen']); - $job = $this->createSimpleJob(); - $this->getQueue()->push($job); - - $this->assertSimpleJobDone($job); - } -} diff --git a/tests/drivers/amqp_interop/AmqpTestCase.php b/tests/drivers/amqp_interop/AmqpTestCase.php new file mode 100644 index 0000000000..e8df7c9312 --- /dev/null +++ b/tests/drivers/amqp_interop/AmqpTestCase.php @@ -0,0 +1,125 @@ +routingKey = null; + $this->exchangeName = null; + $this->queueName = null; + + $this->purgeQueue(); + + parent::tearDown(); + } + + /** + * @param bool $createObject + * @return Queue + * @throws InvalidConfigException + */ + protected function getQueue(bool $createObject = false): Queue + { + if ($createObject) { + /** @var Queue $object */ + $object = Yii::createObject(array_merge( + $this->getConnectionConfig(), + [ + 'class' => Queue::class, + 'password' => getenv('RABBITMQ_PASSWORD') ?: 'guest', + 'queueOptionalArguments' => ['x-max-priority' => 10], + 'queueName' => 'queue-interop', + 'exchangeName' => 'exchange-interop', + ] + )); + return $object; + } + return Yii::$app->amqpInteropQueue; + } + + /** + * @return Context|AmqpContext + * @throws Exception + */ + protected function getAMQPContext(): Context|AmqpContext + { + $factory = new AmqpConnectionFactory(array_merge( + $this->getConnectionConfig(), + [ + 'pass' => getenv('RABBITMQ_PASSWORD') ?: 'guest', + ] + )); + $context = $factory->createContext(); + + $queue = $context->createQueue($this->queueName); + $queue->addFlag($this->flags); + $queue->setArguments(['x-max-priority' => 10]); + $context->declareQueue($queue); + + $topic = $context->createTopic($this->exchangeName); + $topic->setType($this->exchangeType); + $topic->addFlag($this->flags); + $context->declareTopic($topic); + + $context->bind(new AmqpBind($queue, $topic, $this->routingKey)); + + return $context; + } + + private function getAmqpConnection(): AMQPStreamConnection + { + return new AMQPStreamConnection( + getenv('RABBITMQ_HOST') ?: 'localhost', + getenv('RABBITMQ_PORT') ?: 5672, + getenv('RABBITMQ_USER') ?: 'guest', + getenv('RABBITMQ_PASSWORD') ?: 'guest' + ); + } + + private function purgeQueue(): void + { + if (null !== $this->queueName) { + $connection = $this->getAmqpConnection(); + $channel = $connection->channel(); + $channel->queue_purge($this->queueName, true); + } + } + + private function getConnectionConfig(): array + { + return [ + 'host' => getenv('RABBITMQ_HOST') ?: 'localhost', + 'user' => getenv('RABBITMQ_USER') ?: 'guest', + 'port' => getenv('RABBITMQ_PORT') ?: 5672, + ]; + } +} diff --git a/tests/drivers/amqp_interop/QueueTest.php b/tests/drivers/amqp_interop/QueueTest.php index 6f5d3f4f33..211ade1371 100644 --- a/tests/drivers/amqp_interop/QueueTest.php +++ b/tests/drivers/amqp_interop/QueueTest.php @@ -1,4 +1,7 @@ */ -class QueueTest extends CliTestCase +class QueueTest extends AmqpTestCase { /** * Test working setter routing key */ - public function testNativeSettingRoutingKey() + public function testNativeSettingRoutingKey(): void { + $this->exchangeName = null; + $this->queueName = null; + $uniqRoutingKey = Yii::$app->security->generateRandomString(12); $message = new InteropAmqpMessage(); $message->setRoutingKey($uniqRoutingKey); @@ -38,80 +40,7 @@ public function testNativeSettingRoutingKey() $this->assertSame($uniqRoutingKey, $message->getRoutingKey()); } - /** - * Sending a message to a queue using RoutingKey - */ - public function testSendMessageWithRoutingKey() - { - $uniqKey = Yii::$app->security->generateRandomString(12); - $receivedRoutingKey = null; - - $yiiQueue = $this->getQueue(); - $yiiQueue->routingKey = $uniqKey; - $yiiQueue->push($this->createSimpleJob()); - - $context = $this->getNativeAMQPContext($yiiQueue); - - $queue = $context->createQueue($yiiQueue->queueName); - $consumer = $context->createConsumer($queue); - $callback = function (AmqpMessage $message) use (&$receivedRoutingKey) { - $receivedRoutingKey = $message->getRoutingKey(); - return true; - }; - $subscriptionConsumer = $context->createSubscriptionConsumer(); - $subscriptionConsumer->subscribe($consumer, $callback); - $subscriptionConsumer->consume(1000); - - sleep(3); - - $this->assertSame($yiiQueue->routingKey, $receivedRoutingKey); - } - - /** - * Test push message with headers - * @return void - */ - public function testPushMessageWithHeaders() - { - $actualHeaders = []; - $messageHeaders = [ - 'header-1' => 'header-value-1', - 'header-2' => 'header-value-2', - ]; - - $yiiQueue = $this->getQueue(); - $yiiQueue->setMessageHeaders = $messageHeaders; - $yiiQueue->push($this->createSimpleJob()); - - $context = $this->getNativeAMQPContext($yiiQueue); - - $queue = $context->createQueue($yiiQueue->queueName); - $consumer = $context->createConsumer($queue); - $callback = function (AmqpMessage $message) use (&$actualHeaders) { - /** - * This not mistake. In original package this function mixed up - * getHeaders() => getProperties() - */ - $actualHeaders = $message->getProperties(); - return true; - }; - $subscriptionConsumer = $context->createSubscriptionConsumer(); - $subscriptionConsumer->subscribe($consumer, $callback); - $subscriptionConsumer->consume(1000); - - sleep(3); - - $expectedHeaders = array_merge( - $messageHeaders, - [ - Queue::ATTEMPT => 1, - Queue::TTR => 300, - ] - ); - $this->assertEquals($expectedHeaders, $actualHeaders); - } - - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen']); $job = $this->createSimpleJob(); @@ -120,7 +49,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen']); $job = $this->createSimpleJob(); @@ -129,10 +58,10 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -140,7 +69,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testPriority() + public function testPriority(): void { $this->getQueue()->priority(3)->push(new PriorityJob(['number' => 1])); $this->getQueue()->priority(1)->push(new PriorityJob(['number' => 5])); @@ -156,7 +85,7 @@ public function testPriority() /** * @requires extension pcntl */ - public function testSignals() + public function testSignals(): void { $signals = [ 1 => 129, // SIGHUP @@ -176,45 +105,84 @@ public function testSignals() } /** - * @return Queue + * Sending a message to a queue using RoutingKey */ - protected function getQueue() + public function testSendMessageWithRoutingKey(): void { - return Yii::$app->amqpInteropQueue; - } + $this->queueName = 'routing-key'; + $this->exchangeName = 'routing-key'; + $this->routingKey = Yii::$app->security->generateRandomString(10); - protected function setUp() - { - if ('true' == getenv('EXCLUDE_AMQP_INTEROP')) { - $this->markTestSkipped('Amqp tests are disabled for php 5.5'); - } + $receivedRoutingKey = null; - parent::setUp(); + $queue = $this->getQueue(true); + $queue->exchangeName = $this->exchangeName; + $queue->queueName = $this->queueName; + $queue->routingKey = $this->routingKey; + $queue->push($this->createSimpleJob()); + + $context = $this->getAMQPContext(); + $consumer = $context->createConsumer( + $context->createQueue($this->queueName) + ); + $callback = function (AmqpMessage $message, AmqpConsumer $consumer) use (&$receivedRoutingKey) { + $receivedRoutingKey = $message->getRoutingKey(); + $consumer->acknowledge($message); + return true; + }; + $subscriptionConsumer = $context->createSubscriptionConsumer(); + $subscriptionConsumer->subscribe($consumer, $callback); + $subscriptionConsumer->consume(1000); + + $this->assertSame($this->routingKey, $receivedRoutingKey); } /** - * @param Queue $yiiQueue - * @return mixed + * Test push message with headers + * @return void */ - private function getNativeAMQPContext($yiiQueue) + public function testPushMessageWithHeaders(): void { - $factory = new AmqpConnectionFactory([ - 'host' => $yiiQueue->host, - ]); - $context = $factory->createContext(); + $this->queueName = 'message-headers'; + $this->exchangeName = 'message-headers'; - $queue = $context->createQueue($yiiQueue->queueName); - $queue->addFlag(AmqpQueue::FLAG_DURABLE); - $queue->setArguments(['x-max-priority' => 10]); - $context->declareQueue($queue); + $actualHeaders = []; + $messageHeaders = [ + 'header-1' => 'header-value-1', + 'header-2' => 'header-value-2', + ]; + + $queue = $this->getQueue(true); + $queue->exchangeName = $this->exchangeName; + $queue->queueName = $this->queueName; + $queue->setMessageHeaders = $messageHeaders; + $queue->push($this->createSimpleJob()); - $topic = $context->createTopic($yiiQueue->exchangeName); - $topic->setType($yiiQueue->exchangeType); - $topic->addFlag(AmqpTopic::FLAG_DURABLE); - $context->declareTopic($topic); + $context = $this->getAMQPContext(); + $consumer = $context->createConsumer( + $context->createQueue($this->queueName) + ); + $callback = function (AmqpMessage $message, AmqpConsumer $consumer) use (&$actualHeaders) { + /** + * This not mistake. In original package this function mixed up + * getHeaders() => getProperties() + */ + $actualHeaders = $message->getProperties(); + $consumer->acknowledge($message); + return true; + }; + $subscriptionConsumer = $context->createSubscriptionConsumer(); + $subscriptionConsumer->subscribe($consumer, $callback); + $subscriptionConsumer->consume(1000); - $context->bind(new AmqpBind($queue, $topic, $yiiQueue->routingKey)); - return $context; + $expectedHeaders = array_merge( + $messageHeaders, + [ + Queue::ATTEMPT => 1, + Queue::TTR => 300, + ] + ); + $this->assertEquals($expectedHeaders, $actualHeaders); } } diff --git a/tests/drivers/beanstalk/QueueTest.php b/tests/drivers/beanstalk/QueueTest.php index a78283ac4d..8ba5bbb7c5 100644 --- a/tests/drivers/beanstalk/QueueTest.php +++ b/tests/drivers/beanstalk/QueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -31,7 +34,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -43,7 +46,7 @@ public function testStatus() $this->assertTrue($isDone); } - public function testPriority() + public function testPriority(): void { $this->getQueue()->priority(100)->push(new PriorityJob(['number' => 1])); $this->getQueue()->priority(300)->push(new PriorityJob(['number' => 5])); @@ -55,7 +58,7 @@ public function testPriority() $this->assertEquals('12345', file_get_contents(PriorityJob::getFileName())); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -64,7 +67,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -73,7 +76,7 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = new RetryJob(['uid' => uniqid()]); @@ -84,7 +87,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testRemove() + public function testRemove(): void { $id = $this->getQueue()->push($this->createSimpleJob()); $this->assertTrue($this->jobIsExists($id)); @@ -96,28 +99,28 @@ public function testRemove() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->beanstalkQueue; } /** - * @param int $id of a job + * @param int|string|null $id of a job * @return bool * @throws */ - protected function jobIsExists($id) + protected function jobIsExists(int|string|null $id): bool { $connection = new Pheanstalk($this->getQueue()->host, $this->getQueue()->port); try { $connection->peek($id); return true; } catch (ServerException $e) { - if (strpos($e->getMessage(), 'NOT_FOUND') === 0) { + if (str_starts_with($e->getMessage(), 'NOT_FOUND')) { return false; - } else { - throw $e; } + + throw $e; } } } diff --git a/tests/drivers/db/MysqlQueueTest.php b/tests/drivers/db/MysqlQueueTest.php index 05e8702aca..87559adb5a 100644 --- a/tests/drivers/db/MysqlQueueTest.php +++ b/tests/drivers/db/MysqlQueueTest.php @@ -1,4 +1,7 @@ mysqlQueue; } diff --git a/tests/drivers/db/PgsqlQueueTest.php b/tests/drivers/db/PgsqlQueueTest.php index 3910d21694..31454c088c 100644 --- a/tests/drivers/db/PgsqlQueueTest.php +++ b/tests/drivers/db/PgsqlQueueTest.php @@ -1,4 +1,7 @@ pgsqlQueue; } diff --git a/tests/drivers/db/SqliteQueueTest.php b/tests/drivers/db/SqliteQueueTest.php index 762a9c3eda..09f27bd901 100644 --- a/tests/drivers/db/SqliteQueueTest.php +++ b/tests/drivers/db/SqliteQueueTest.php @@ -1,4 +1,7 @@ sqliteQueue; } diff --git a/tests/drivers/db/TestCase.php b/tests/drivers/db/TestCase.php index bc57ca7529..4e8e09de7b 100644 --- a/tests/drivers/db/TestCase.php +++ b/tests/drivers/db/TestCase.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -28,7 +31,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -40,7 +43,7 @@ public function testStatus() $this->assertTrue($isDone); } - public function testPriority() + public function testPriority(): void { $this->getQueue()->priority(100)->push(new PriorityJob(['number' => 1])); $this->getQueue()->priority(300)->push(new PriorityJob(['number' => 5])); @@ -52,7 +55,7 @@ public function testPriority() $this->assertEquals('12345', file_get_contents(PriorityJob::getFileName())); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -61,7 +64,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -70,7 +73,7 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = new RetryJob(['uid' => uniqid()]); @@ -81,7 +84,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testClear() + public function testClear(): void { $this->getQueue()->push($this->createSimpleJob()); $this->runProcess(['php', 'yii', 'queue/clear', '--interactive=0']); @@ -93,7 +96,7 @@ public function testClear() $this->assertEquals(0, $actual); } - public function testRemove() + public function testRemove(): void { $id = $this->getQueue()->push($this->createSimpleJob()); $this->runProcess(['php', 'yii', 'queue/remove', $id]); @@ -105,7 +108,7 @@ public function testRemove() $this->assertEquals(0, $actual); } - protected function tearDown() + protected function tearDown(): void { $this->getQueue()->db->createCommand() ->delete($this->getQueue()->tableName) diff --git a/tests/drivers/file/QueueTest.php b/tests/drivers/file/QueueTest.php index 1faea8006f..fb6fe7807a 100644 --- a/tests/drivers/file/QueueTest.php +++ b/tests/drivers/file/QueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -28,7 +31,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -40,7 +43,7 @@ public function testStatus() $this->assertTrue($isDone); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -49,7 +52,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -58,7 +61,7 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = new RetryJob(['uid' => uniqid()]); @@ -69,7 +72,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testClear() + public function testClear(): void { $this->getQueue()->push($this->createSimpleJob()); $this->assertNotEmpty(glob($this->getQueue()->path . '/job*.data')); @@ -78,24 +81,24 @@ public function testClear() $this->assertEmpty(glob($this->getQueue()->path . '/job*.data')); } - public function testRemove() + public function testRemove(): void { $id = $this->getQueue()->push($this->createSimpleJob()); $this->assertFileExists($this->getQueue()->path . "/job$id.data"); $this->runProcess(['php', 'yii', 'queue/remove', $id]); - $this->assertFileNotExists($this->getQueue()->path . "/job$id.data"); + $this->assertFileDoesNotExist($this->getQueue()->path . "/job$id.data"); } /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->fileQueue; } - protected function tearDown() + protected function tearDown(): void { foreach (glob(Yii::getAlias("@runtime/queue/*")) as $fileName) { unlink($fileName); diff --git a/tests/drivers/gearman/QueueTest.php b/tests/drivers/gearman/QueueTest.php index a68c42ed11..c47e30e746 100644 --- a/tests/drivers/gearman/QueueTest.php +++ b/tests/drivers/gearman/QueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -28,7 +31,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testPriority() + public function testPriority(): void { $this->getQueue()->priority('high')->push(new PriorityJob(['number' => 1])); $this->getQueue()->priority('low')->push(new PriorityJob(['number' => 5])); @@ -40,7 +43,7 @@ public function testPriority() $this->assertEquals('12345', file_get_contents(PriorityJob::getFileName())); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -52,7 +55,7 @@ public function testStatus() $this->assertTrue($isDone); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen']); $job = $this->createSimpleJob(); @@ -64,12 +67,12 @@ public function testListen() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->gearmanQueue; } - public function setUp() + public function setUp(): void { if (!defined('GEARMAN_SUCCESS')) { $this->markTestSkipped('Gearman in not installed.'); diff --git a/tests/drivers/redis/QueueTest.php b/tests/drivers/redis/QueueTest.php index 8e6c462e9d..29ca2c6e38 100644 --- a/tests/drivers/redis/QueueTest.php +++ b/tests/drivers/redis/QueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -28,7 +31,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -40,7 +43,7 @@ public function testStatus() $this->assertTrue($isDone); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -49,7 +52,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -58,7 +61,7 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = new RetryJob(['uid' => uniqid()]); @@ -69,7 +72,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testClear() + public function testClear(): void { $this->getQueue()->push($this->createSimpleJob()); $this->assertNotEmpty($this->getQueue()->redis->keys($this->getQueue()->channel . '.*')); @@ -78,7 +81,7 @@ public function testClear() $this->assertEmpty($this->getQueue()->redis->keys($this->getQueue()->channel . '.*')); } - public function testRemove() + public function testRemove(): void { $id = $this->getQueue()->push($this->createSimpleJob()); $this->assertTrue((bool) $this->getQueue()->redis->hexists($this->getQueue()->channel . '.messages', $id)); @@ -90,12 +93,12 @@ public function testRemove() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->redisQueue; } - protected function tearDown() + protected function tearDown(): void { $this->getQueue()->redis->flushdb(); parent::tearDown(); diff --git a/tests/drivers/sqs/FifoQueueTest.php b/tests/drivers/sqs/FifoQueueTest.php index 4cc4b8b556..bdf87582b3 100644 --- a/tests/drivers/sqs/FifoQueueTest.php +++ b/tests/drivers/sqs/FifoQueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -26,7 +29,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -35,7 +38,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testFifoQueueDoesNotSupportPerMessageDelays() + public function testFifoQueueDoesNotSupportPerMessageDelays(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -44,7 +47,7 @@ public function testFifoQueueDoesNotSupportPerMessageDelays() $this->getQueue()->delay(2)->push($job); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = new RetryJob(['uid' => uniqid()]); @@ -55,7 +58,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testClear() + public function testClear(): void { if (!getenv('AWS_SQS_FIFO_CLEAR_TEST_ENABLED')) { $this->markTestSkipped(__METHOD__ . ' is disabled'); @@ -68,12 +71,12 @@ public function testClear() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->sqsFifoQueue; } - protected function setUp() + protected function setUp(): void { if (!getenv('AWS_SQS_FIFO_ENABLED')) { $this->markTestSkipped('AWS SQS FIFO tests are disabled'); diff --git a/tests/drivers/sqs/QueueTest.php b/tests/drivers/sqs/QueueTest.php index 8ef1e518ad..253674326a 100644 --- a/tests/drivers/sqs/QueueTest.php +++ b/tests/drivers/sqs/QueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -28,7 +31,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -37,7 +40,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -46,7 +49,7 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = new RetryJob(['uid' => uniqid()]); @@ -57,7 +60,7 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - public function testClear() + public function testClear(): void { if (!getenv('AWS_SQS_CLEAR_TEST_ENABLED')) { $this->markTestSkipped(__METHOD__ . ' is disabled'); @@ -70,12 +73,12 @@ public function testClear() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->sqsQueue; } - protected function setUp() + protected function setUp(): void { if (!getenv('AWS_SQS_ENABLED')) { $this->markTestSkipped('AWS SQS tests are disabled'); diff --git a/tests/drivers/stomp/QueueTest.php b/tests/drivers/stomp/QueueTest.php index c3aeb72762..105b1aa4bd 100644 --- a/tests/drivers/stomp/QueueTest.php +++ b/tests/drivers/stomp/QueueTest.php @@ -1,4 +1,7 @@ startProcess(['php', 'yii', 'queue/listen']); $job = $this->createSimpleJob(); @@ -24,10 +26,10 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testRetry() + public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -38,19 +40,9 @@ public function testRetry() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->stompQueue; } - - protected function setUp() - { - if ('true' == getenv('EXCLUDE_STOMP')) { - $this->markTestSkipped('Stomp tests are disabled for php 5.5'); - } - - parent::setUp(); - } - } diff --git a/tests/drivers/sync/QueueTest.php b/tests/drivers/sync/QueueTest.php index 65a86e0db0..8b41db3432 100644 --- a/tests/drivers/sync/QueueTest.php +++ b/tests/drivers/sync/QueueTest.php @@ -1,4 +1,7 @@ syncQueue; } - public function testRun() + public function testRun(): void { $job = $this->createSimpleJob(); $this->getQueue()->push($job); @@ -35,7 +38,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); diff --git a/tests/serializers/IgbinarySerializerTest.php b/tests/serializers/IgbinarySerializerTest.php index 8d851e12e5..a7876fc306 100644 --- a/tests/serializers/IgbinarySerializerTest.php +++ b/tests/serializers/IgbinarySerializerTest.php @@ -1,4 +1,7 @@ markTestSkipped('Igbinary extension is not loaded.'); diff --git a/tests/serializers/JsonSerializerTest.php b/tests/serializers/JsonSerializerTest.php index c93067fdd2..d613a1c7ed 100644 --- a/tests/serializers/JsonSerializerTest.php +++ b/tests/serializers/JsonSerializerTest.php @@ -1,4 +1,7 @@ expectException(InvalidConfigException::class); $this->createSerializer()->serialize([ 'class' => 'failed param', ]); diff --git a/tests/serializers/PhpSerializerTest.php b/tests/serializers/PhpSerializerTest.php index 6e2e5cbde8..9237f5d601 100644 --- a/tests/serializers/PhpSerializerTest.php +++ b/tests/serializers/PhpSerializerTest.php @@ -1,4 +1,7 @@ */ -abstract class TestCase extends \tests\TestCase +abstract class TestCase extends BaseTestCase { /** * @return SerializerInterface */ - abstract protected function createSerializer(); + abstract protected function createSerializer(): SerializerInterface; /** * @dataProvider providerSerialize * @param mixed $expected */ - public function testSerialize($expected) + public function testSerialize($expected): void { $serializer = $this->createSerializer(); @@ -37,7 +41,7 @@ public function testSerialize($expected) $this->assertEquals($expected, $actual, "Payload: $serialized"); } - public function providerSerialize() + public static function providerSerialize(): array { return [ // Job object diff --git a/tests/yii b/tests/yii index 8a69da60a6..c7c0732dea 100755 --- a/tests/yii +++ b/tests/yii @@ -9,7 +9,6 @@ require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); Yii::setAlias('@yii/queue', dirname(__DIR__) . '/src'); -Yii::setAlias('@yii/queue/amqp', dirname(__DIR__) . '/src/drivers/amqp'); Yii::setAlias('@yii/queue/amqp_interop', dirname(__DIR__) . '/src/drivers/amqp_interop'); Yii::setAlias('@yii/queue/beanstalk', dirname(__DIR__) . '/src/drivers/beanstalk'); Yii::setAlias('@yii/queue/db', dirname(__DIR__) . '/src/drivers/db'); From 136d99883a68dd24bb9ff7c1e156bea3845f05e7 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 13 Oct 2023 13:43:59 +0400 Subject: [PATCH 02/16] Improve SQS driver and fix tests (#497) --- .env.example | 9 --- .github/workflows/main.yml | 8 --- .github/workflows/static.yml | 8 --- Makefile | 10 +++- docs/guide-ru/driver-sqs.md | 26 ++++++++- docs/guide-zh-CN/driver-sqs.md | 86 +++++++++++++++++++++++++++++ phpunit.xml.dist | 1 + psalm.xml | 1 - src/drivers/sqs/Command.php | 3 - src/drivers/sqs/Queue.php | 17 +++--- tests/app/config/main.php | 40 ++++++-------- tests/docker-compose.yml | 32 ++++++++--- tests/drivers/sqs/FifoQueueTest.php | 18 +----- tests/drivers/sqs/QueueTest.php | 15 +---- 14 files changed, 174 insertions(+), 100 deletions(-) create mode 100644 docs/guide-zh-CN/driver-sqs.md diff --git a/.env.example b/.env.example index 545f0fa141..4ed48f603b 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,2 @@ COMPOSE_PROJECT_NAME=yii2-queue COMPOSE_FILE=tests/docker-compose.yml - -AWS_SQS_ENABLED= -AWS_SQS_URL= -AWS_KEY= -AWS_SECRET= -AWS_REGION= -AWS_SQS_FIFO_ENABLED= -AWS_SQS_FIFO_URL= -AWS_SQS_FIFO_MESSAGE_GROUP_ID= diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 36ec37335c..8dadc45d9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,14 +23,6 @@ on: env: COMPOSE_PROJECT_NAME: yii2-queue COMPOSE_FILE: tests/docker-compose.yml - AWS_SQS_ENABLED: ${{ secrets.AWS_SQS_ENABLED }} - AWS_SQS_URL: ${{ secrets.AWS_SQS_URL }} - AWS_KEY: ${{ secrets.AWS_KEY }} - AWS_SECRET: ${{ secrets.AWS_SECRET }} - AWS_REGION: ${{ secrets.AWS_REGION }} - AWS_SQS_FIFO_ENABLED: ${{ secrets.AWS_SQS_FIFO_ENABLED }} - AWS_SQS_FIFO_URL: ${{ secrets.AWS_SQS_FIFO_URL }} - AWS_SQS_FIFO_MESSAGE_GROUP_ID: ${{ secrets.AWS_SQS_FIFO_MESSAGE_GROUP_ID }} jobs: phpunit: name: PHP ${{ matrix.php }} diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index d9d868552c..be42e8f9a2 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -25,14 +25,6 @@ name: static analysis env: COMPOSE_PROJECT_NAME: yii2-queue COMPOSE_FILE: tests/docker-compose.yml - AWS_SQS_ENABLED: ${{ secrets.AWS_SQS_ENABLED }} - AWS_SQS_URL: ${{ secrets.AWS_SQS_URL }} - AWS_KEY: ${{ secrets.AWS_KEY }} - AWS_SECRET: ${{ secrets.AWS_SECRET }} - AWS_REGION: ${{ secrets.AWS_REGION }} - AWS_SQS_FIFO_ENABLED: ${{ secrets.AWS_SQS_FIFO_ENABLED }} - AWS_SQS_FIFO_URL: ${{ secrets.AWS_SQS_FIFO_URL }} - AWS_SQS_FIFO_MESSAGE_GROUP_ID: ${{ secrets.AWS_SQS_FIFO_MESSAGE_GROUP_ID }} jobs: psalm: name: PHP ${{ matrix.php }} diff --git a/Makefile b/Makefile index 138991334c..7f260cc750 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,11 @@ help: ## Display help information build: ## Build an image from a docker-compose file. Params: {{ v=8.1 }}. Default latest PHP 8.1 @cp -n .env.example .env 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 - PHP_VERSION=$(filter-out $@,$(v)) docker-compose build --pull yii2-queue-php + make build PHP_VERSION=$(filter-out $@,$(v)) docker-compose run yii2-queue-php vendor/bin/phpunit --coverage-clover coverage.xml make down @@ -35,3 +37,9 @@ clean: clean-all: clean sudo rm -rf tests/runtime/.composer* + +create-sqs-queue: ## Create SQS queue + docker exec yii2-queue-localstack sh -c "awslocal sqs create-queue --queue-name yii2-queue" + +create-sqs-fifo-queue: ## Create SQS FIFO queue + docker exec yii2-queue-localstack sh -c 'awslocal sqs create-queue --queue-name yii2-queue.fifo --attributes "FifoQueue=true"' diff --git a/docs/guide-ru/driver-sqs.md b/docs/guide-ru/driver-sqs.md index c7332a9dc5..f99aa818d7 100644 --- a/docs/guide-ru/driver-sqs.md +++ b/docs/guide-ru/driver-sqs.md @@ -10,7 +10,7 @@ ```php return [ 'bootstrap' => [ - 'queue', // The component registers own console commands + 'queue', // Компонент регистрирует собственные консольные команды ], 'components' => [ 'queue' => [ @@ -24,6 +24,30 @@ return [ ]; ``` +Пример настройки для FIFO очередей: + +```php +return [ + 'bootstrap' => [ + 'queue', // Компонент регистрирует собственные консольные команды + ], + 'components' => [ + 'queue' => [ + 'class' => \yii\queue\sqs\Queue::class, + 'url' => '', + 'key' => '', + 'secret' => '', + 'region' => '', + 'messageGroupId' => '', + ], + ], +]; +``` + +Идентификатор группы сообщений требуется SQS для очередей FIFO. Вы можете настроить свои собственные или использовать значение "default". + +Идентификатор дедупликации генерируется автоматически, поэтому независимо от того, активировали ли вы дедупликацию на основе содержимого в очереди SQS или нет, этот идентификатор будет использоваться. + Консоль ------- diff --git a/docs/guide-zh-CN/driver-sqs.md b/docs/guide-zh-CN/driver-sqs.md new file mode 100644 index 0000000000..504050415d --- /dev/null +++ b/docs/guide-zh-CN/driver-sqs.md @@ -0,0 +1,86 @@ +AWS SQS Driver +============ + +The driver uses AWS SQS to store queue data. + +You have to add `aws/aws-sdk-php` extension to your application in order to use it. + +Configuration example for standard queues: + +```php +return [ + 'bootstrap' => [ + 'queue', // The component registers own console commands + ], + 'components' => [ + 'queue' => [ + 'class' => \yii\queue\sqs\Queue::class, + 'url' => '', + 'key' => '', + 'secret' => '', + 'region' => '', + ], + ], +]; +``` + +Configuration example for FIFO queues: + +```php +return [ + 'bootstrap' => [ + 'queue', // The component registers own console commands + ], + 'components' => [ + 'queue' => [ + 'class' => \yii\queue\sqs\Queue::class, + 'url' => '', + 'key' => '', + 'secret' => '', + 'region' => '', + 'messageGroupId' => '', + ], + ], +]; +``` + +The message group ID is required by SQS for FIFO queues. You can configure your own or use the "default" value. + +The deduplication ID is generated automatically, so no matter if you have activated content-based deduplication in the SQS queue or not, this ID will be used. + +Console +------- + +Console command is used to execute tasks. + +```sh +yii queue/listen [timeout] +``` + +`listen` command launches a daemon which infinitely queries the queue. If there are new tasks +they're immediately obtained and executed. `timeout` parameter is number of seconds to wait a job. +It uses SQS "Long Polling" feature, that holds a connection between client and a queue. + +**Important:** `timeout` parameter for SQS driver must be in range between 0 and 20 seconds. + +This method is most efficient when command is properly daemonized via +[supervisor](worker.md#supervisor) or [systemd](worker.md#systemd). + +```sh +yii queue/run +``` + +`run` command obtains and executes tasks in a loop until queue is empty. Works well with +[cron](worker.md#cron). + +`run` and `listen` commands have options: + +- `--verbose`, `-v`: print executing statuses into console. +- `--isolate`: each task is executed in a separate child process. +- `--color`: highlighting for verbose mode. + +```sh +yii queue/clear +``` + +`clear` command clears a queue. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5babaa3c2b..821636bb0d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,6 +8,7 @@ cacheDirectory=".phpunit.cache" displayDetailsOnIncompleteTests="true" displayDetailsOnSkippedTests="true" + executionOrder="random" > diff --git a/psalm.xml b/psalm.xml index 8a030fcb1b..a4f56374a0 100644 --- a/psalm.xml +++ b/psalm.xml @@ -16,7 +16,6 @@ - diff --git a/src/drivers/sqs/Command.php b/src/drivers/sqs/Command.php index 5bd14c529a..3597e0af50 100644 --- a/src/drivers/sqs/Command.php +++ b/src/drivers/sqs/Command.php @@ -48,9 +48,6 @@ public function actionRun(): ?int */ public function actionListen(int $timeout = 3): ?int { - if (!is_numeric($timeout)) { - throw new Exception('Timeout must be numeric.'); - } if ($timeout < 1 || $timeout > 20) { throw new Exception('Timeout must be between 1 and 20'); } diff --git a/src/drivers/sqs/Queue.php b/src/drivers/sqs/Queue.php index 1618d0e8e5..21aad3237d 100644 --- a/src/drivers/sqs/Queue.php +++ b/src/drivers/sqs/Queue.php @@ -15,6 +15,7 @@ use yii\base\NotSupportedException; use yii\queue\cli\Queue as CliQueue; use yii\queue\serializers\JsonSerializer; +use yii\queue\serializers\SerializerInterface; /** * SQS Queue. @@ -64,12 +65,12 @@ class Queue extends CliQueue * Json serializer by default. * @inheritdoc */ - public string|array|\yii\queue\serializers\SerializerInterface $serializer = JsonSerializer::class; + public string|array|SerializerInterface $serializer = JsonSerializer::class; /** - * @var SqsClient + * @var SqsClient|null */ - private SqsClient $_client; + private ?SqsClient $client = null; /** * Listens queue and runs each job. @@ -173,7 +174,7 @@ public function status($id): int * @return bool * @since 2.2.1 */ - public function handle(string $id, string $message, int $ttr, int $attempt) + public function handle(string $id, string $message, int $ttr, int $attempt): bool { return $this->handleMessage($id, $message, $ttr, $attempt); } @@ -213,8 +214,8 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri */ protected function getClient(): SqsClient { - if ($this->_client) { - return $this->_client; + if (null !== $this->client) { + return $this->client; } if ($this->key !== null && $this->secret !== null) { @@ -228,11 +229,11 @@ protected function getClient(): SqsClient $credentials = CredentialProvider::defaultProvider(); } - $this->_client = new SqsClient([ + $this->client = new SqsClient([ 'credentials' => $credentials, 'region' => $this->region, 'version' => $this->version, ]); - return $this->_client; + return $this->client; } } diff --git a/tests/app/config/main.php b/tests/app/config/main.php index 047508603f..7b719c9462 100644 --- a/tests/app/config/main.php +++ b/tests/app/config/main.php @@ -31,6 +31,8 @@ 'amqpInteropQueue', 'beanstalkQueue', 'stompQueue', + 'sqsQueue', + 'sqsFifoQueue', ], 'components' => [ 'syncQueue' => [ @@ -116,6 +118,21 @@ 'class' => StompQueue::class, 'host' => getenv('ACTIVEMQ_HOST') ?: 'localhost', ], + 'sqsQueue' => [ + 'class' => SqsQueue::class, + 'url' => getenv('AWS_SQS_URL'), + 'key' => getenv('AWS_KEY'), + 'secret' => getenv('AWS_SECRET'), + 'region' => getenv('AWS_REGION'), + ], + 'sqsFifoQueue' => [ + 'class' => SqsQueue::class, + 'url' => getenv('AWS_SQS_FIFO_URL'), + 'key' => getenv('AWS_KEY'), + 'secret' => getenv('AWS_SECRET'), + 'region' => getenv('AWS_REGION'), + 'messageGroupId' => getenv('AWS_SQS_FIFO_MESSAGE_GROUP_ID'), + ], ], ]; @@ -127,27 +144,4 @@ ]; } -if (getenv('AWS_SQS_ENABLED')) { - $config['bootstrap'][] = 'sqsQueue'; - $config['components']['sqsQueue'] = [ - 'class' => SqsQueue::class, - 'url' => getenv('AWS_SQS_URL'), - 'key' => getenv('AWS_KEY'), - 'secret' => getenv('AWS_SECRET'), - 'region' => getenv('AWS_REGION'), - ]; -} - -if (getenv('AWS_SQS_FIFO_ENABLED')) { - $config['bootstrap'][] = 'sqsFifoQueue'; - $config['components']['sqsFifoQueue'] = [ - 'class' => SqsQueue::class, - 'url' => getenv('AWS_SQS_FIFO_URL'), - 'key' => getenv('AWS_KEY'), - 'secret' => getenv('AWS_SECRET'), - 'region' => getenv('AWS_REGION'), - 'messageGroupId' => getenv('AWS_SQS_FIFO_MESSAGE_GROUP_ID'), - ]; -} - return $config; diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 1f22950d3c..84fd098128 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -32,15 +32,13 @@ services: BEANSTALK_HOST: beanstalk GEARMAN_HOST: gearmand COMPOSER_ALLOW_SUPERUSER: 1 - AWS_SQS_ENABLED: ${AWS_SQS_ENABLED} - AWS_KEY: ${AWS_KEY} - AWS_SECRET: ${AWS_SECRET} - AWS_REGION: ${AWS_REGION} - AWS_SQS_URL: ${AWS_SQS_URL} ACTIVEMQ_HOST: activemq - AWS_SQS_FIFO_ENABLED: ${AWS_SQS_FIFO_ENABLED} - AWS_SQS_FIFO_URL: ${AWS_SQS_FIFO_URL} - AWS_SQS_FIFO_MESSAGE_GROUP_ID: ${AWS_SQS_FIFO_MESSAGE_GROUP_ID} + AWS_KEY: ${AWS_KEY:-admin} + AWS_SECRET: ${AWS_SECRET:-admin} + AWS_REGION: ${AWS_REGION:-us-east-1} + AWS_SQS_URL: ${AWS_SQS_URL:-http://localstack:4566/000000000000/yii2-queue} + AWS_SQS_FIFO_URL: ${AWS_SQS_FIFO_URL:-http://localstack:4566/000000000000/yii2-queue.fifo} + AWS_SQS_FIFO_MESSAGE_GROUP_ID: ${AWS_SQS_FIFO_MESSAGE_GROUP_ID:-default} depends_on: - mysql - postgres @@ -49,6 +47,7 @@ services: - beanstalk - gearmand - activemq + - localstack networks: net: {} @@ -118,7 +117,22 @@ services: networks: net: {} + # https://hub.docker.com/r/localstack/localstack + localstack: + container_name: yii2-queue-localstack + image: localstack/localstack + restart: always + ports: + - "4566:4566" + environment: + AWS_DEFAULT_REGION: us-east-1 + AWS_ACCESS_KEY_ID: admin + AWS_SECRET_ACCESS_KEY: admin + SERVICES: sqs + DISABLE_CORS_CHECKS: 1 + networks: + net: {} + networks: net: name: yii2_queue_net - diff --git a/tests/drivers/sqs/FifoQueueTest.php b/tests/drivers/sqs/FifoQueueTest.php index bdf87582b3..5e59219b36 100644 --- a/tests/drivers/sqs/FifoQueueTest.php +++ b/tests/drivers/sqs/FifoQueueTest.php @@ -10,6 +10,7 @@ namespace tests\drivers\sqs; +use Aws\Sqs\Exception\SqsException; use tests\app\RetryJob; use tests\drivers\CliTestCase; use Yii; @@ -40,17 +41,17 @@ public function testListen(): void public function testFifoQueueDoesNotSupportPerMessageDelays(): void { + $this->expectException(SqsException::class); $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); - $this->setExpectedException('\Aws\Sqs\Exception\SqsException'); $this->getQueue()->delay(2)->push($job); } public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -60,10 +61,6 @@ public function testRetry(): void public function testClear(): void { - if (!getenv('AWS_SQS_FIFO_CLEAR_TEST_ENABLED')) { - $this->markTestSkipped(__METHOD__ . ' is disabled'); - } - $this->getQueue()->push($this->createSimpleJob()); $this->runProcess(['php', 'yii', 'queue/clear', '--interactive=0']); } @@ -75,13 +72,4 @@ protected function getQueue(): Queue { return Yii::$app->sqsFifoQueue; } - - protected function setUp(): void - { - if (!getenv('AWS_SQS_FIFO_ENABLED')) { - $this->markTestSkipped('AWS SQS FIFO tests are disabled'); - } - - parent::setUp(); - } } diff --git a/tests/drivers/sqs/QueueTest.php b/tests/drivers/sqs/QueueTest.php index 253674326a..acfb14b36f 100644 --- a/tests/drivers/sqs/QueueTest.php +++ b/tests/drivers/sqs/QueueTest.php @@ -52,7 +52,7 @@ public function testLater(): void public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -62,10 +62,6 @@ public function testRetry(): void public function testClear(): void { - if (!getenv('AWS_SQS_CLEAR_TEST_ENABLED')) { - $this->markTestSkipped(__METHOD__ . ' is disabled'); - } - $this->getQueue()->push($this->createSimpleJob()); $this->runProcess(['php', 'yii', 'queue/clear', '--interactive=0']); } @@ -77,13 +73,4 @@ protected function getQueue(): Queue { return Yii::$app->sqsQueue; } - - protected function setUp(): void - { - if (!getenv('AWS_SQS_ENABLED')) { - $this->markTestSkipped('AWS SQS tests are disabled'); - } - - parent::setUp(); - } } From bd3ce764652e0267bacc37adb5e13c6869a6722e Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Fri, 20 Oct 2023 17:32:45 +0400 Subject: [PATCH 03/16] Updated Beanstalk driver to version 5 (#498) Co-authored-by: Bizley --- composer.json | 2 +- psalm.xml | 1 - src/drivers/beanstalk/Command.php | 3 - src/drivers/beanstalk/Queue.php | 139 +++++++++++++++++--------- tests/app/PriorityJob.php | 2 +- tests/app/RetryJob.php | 9 +- tests/app/SimpleJob.php | 2 +- tests/docker-compose.yml | 16 ++- tests/drivers/CliTestCase.php | 1 - tests/drivers/beanstalk/QueueTest.php | 57 +++++++---- 10 files changed, 147 insertions(+), 85 deletions(-) diff --git a/composer.json b/composer.json index 7bec3f66d2..92d208b465 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "enqueue/amqp-bunny": "^0.10.0", "enqueue/amqp-ext": "^0.10.8", "enqueue/stomp": "^0.10.0", - "pda/pheanstalk": "3.2.1", + "pda/pheanstalk": "^v5.0.0", "aws/aws-sdk-php": ">=2.4", "vimeo/psalm": "^5.10.0" }, diff --git a/psalm.xml b/psalm.xml index a4f56374a0..e19fc25555 100644 --- a/psalm.xml +++ b/psalm.xml @@ -15,7 +15,6 @@ - diff --git a/src/drivers/beanstalk/Command.php b/src/drivers/beanstalk/Command.php index bcd843cefd..6360032ac6 100644 --- a/src/drivers/beanstalk/Command.php +++ b/src/drivers/beanstalk/Command.php @@ -69,9 +69,6 @@ public function actionRun(): ?int */ public function actionListen(int $timeout = 3): ?int { - if (!is_numeric($timeout)) { - throw new Exception('Timeout must be numeric.'); - } if ($timeout < 1) { throw new Exception('Timeout must be greater than zero.'); } diff --git a/src/drivers/beanstalk/Queue.php b/src/drivers/beanstalk/Queue.php index 42288abb58..384347fa39 100644 --- a/src/drivers/beanstalk/Queue.php +++ b/src/drivers/beanstalk/Queue.php @@ -10,17 +10,21 @@ namespace yii\queue\beanstalk; -use Pheanstalk\Exception\ServerException; -use Pheanstalk\Job; +use Exception; +use Pheanstalk\Contract\PheanstalkPublisherInterface; +use Pheanstalk\Contract\SocketFactoryInterface; use Pheanstalk\Pheanstalk; -use Pheanstalk\PheanstalkInterface; -use Pheanstalk\Response; +use Pheanstalk\Values\JobId; +use Pheanstalk\Values\Timeout; +use Pheanstalk\Values\TubeName; +use Pheanstalk\Values\TubeStats; use yii\base\InvalidArgumentException; use yii\queue\cli\Queue as CliQueue; /** * Beanstalk Queue. * + * @property-read TubeName $tubeName * @property-read object $statsTube Tube statistics. * * @author Roman Zhuravlev @@ -34,7 +38,15 @@ class Queue extends CliQueue /** * @var int connection port */ - public int $port = PheanstalkInterface::DEFAULT_PORT; + public int $port = SocketFactoryInterface::DEFAULT_PORT; + /** + * @var int|null connection timeout in seconds + */ + public ?int $connectTimeout = null; + /** + * @var int|null receive timeout in seconds + */ + public ?int $receiveTimeout = null; /** * @var string beanstalk tube */ @@ -44,11 +56,13 @@ class Queue extends CliQueue */ public string $commandClass = Command::class; + private ?Pheanstalk $pheanstalk = null; + /** * Listens queue and runs each job. * * @param bool $repeat whether to continue listening when queue is empty. - * @param int $timeout number of seconds to wait for next message. + * @param int<0, max> $timeout number of seconds to wait for next message. * @return null|int exit code. * @internal for worker command only. * @since 2.0.2 @@ -57,15 +71,24 @@ public function run(bool $repeat, int $timeout = 0): ?int { return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { - if ($payload = $this->getPheanstalk()->reserveFromTube($this->tube, $timeout)) { - $info = $this->getPheanstalk()->statsJob($payload); - if ($this->handleMessage( - $payload->getId(), - $payload->getData(), - (int)$info->ttr, - (int)$info->reserves - )) { - $this->getPheanstalk()->delete($payload); + $pheanstalk = $this->getPheanstalk(); + $pheanstalk->watch($this->getTubeName()); + + $job = $pheanstalk->reserveWithTimeout($timeout); + if (null !== $job) { + try { + $info = $pheanstalk->statsJob($job); + + if ($this->handleMessage( + $job->getId(), + $job->getData(), + $info->timeToRelease, + $info->reserves + )) { + $pheanstalk->delete($job); + } + } catch (Exception) { + $pheanstalk->release($job); } } elseif (!$repeat) { break; @@ -84,39 +107,32 @@ public function status($id): int } try { - $stats = $this->getPheanstalk()->statsJob($id); - if ($stats['state'] === 'reserved') { + $stats = $this->getPheanstalk()->statsJob(new JobId($id)); + + if ($stats->state->value === 'reserved') { return self::STATUS_RESERVED; } return self::STATUS_WAITING; - } catch (ServerException $e) { - if ($e->getMessage() === 'Server reported NOT_FOUND') { - return self::STATUS_DONE; - } - - throw $e; + } catch (\Throwable) { + return self::STATUS_DONE; } } /** * Removes a job by ID. * - * @param int $id of a job + * @param int|string $id of a job * @return bool * @since 2.0.1 */ - public function remove(int $id): bool + public function remove(int|string $id): bool { try { - $this->getPheanstalk()->delete(new Job($id, null)); + $this->getPheanstalk()->delete(new JobId($id)); return true; - } catch (ServerException $e) { - if (str_starts_with($e->getMessage(), 'NOT_FOUND')) { - return false; - } - - throw $e; + } catch (\Throwable) { + return false; } } @@ -125,33 +141,58 @@ public function remove(int $id): bool */ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { - return $this->getPheanstalk()->putInTube( - $this->tube, - $payload, - $priority ?: PheanstalkInterface::DEFAULT_PRIORITY, - $delay, - $ttr - ); + $pheanstalk = $this->getPheanstalk(); + $pheanstalk->useTube($this->getTubeName()); + + $result = $pheanstalk + ->put( + $payload, + $priority ?: PheanstalkPublisherInterface::DEFAULT_PRIORITY, + $delay, // Seconds to wait before job becomes ready + $ttr // Time To Run: seconds a job can be reserved for + ); + return $result->getId(); } /** - * @return object tube statistics + * @return TubeStats tube statistics */ - public function getStatsTube(): object + public function getStatsTube(): TubeStats { - return $this->getPheanstalk()->statsTube($this->tube); + return $this->getPheanstalk()->statsTube($this->getTubeName()); } - /** - * @return Pheanstalk - */ protected function getPheanstalk(): Pheanstalk { - if (!$this->_pheanstalk) { - $this->_pheanstalk = new Pheanstalk($this->host, $this->port); + if (null === $this->pheanstalk) { + $this->pheanstalk = Pheanstalk::create( + $this->host, + $this->port, + $this->getConnectTimeout(), + $this->getReceiveTimeout() + ); } - return $this->_pheanstalk; + return $this->pheanstalk; + } + + protected function getTubeName(): TubeName + { + return new TubeName($this->tube); } - private $_pheanstalk; + private function getConnectTimeout(): ?Timeout + { + if (null === $this->connectTimeout) { + return null; + } + return new Timeout($this->connectTimeout); + } + + private function getReceiveTimeout(): ?Timeout + { + if (null === $this->receiveTimeout) { + return null; + } + return new Timeout($this->receiveTimeout); + } } diff --git a/tests/app/PriorityJob.php b/tests/app/PriorityJob.php index 7abd876c3e..a7e31e8f1c 100644 --- a/tests/app/PriorityJob.php +++ b/tests/app/PriorityJob.php @@ -24,7 +24,7 @@ class PriorityJob extends BaseObject implements JobInterface { public int $number; - public function execute(Queue $queue) + public function execute(Queue $queue): void { file_put_contents(self::getFileName(), $this->number, FILE_APPEND); } diff --git a/tests/app/RetryJob.php b/tests/app/RetryJob.php index 7e372b6f3e..2a8c0e8bda 100644 --- a/tests/app/RetryJob.php +++ b/tests/app/RetryJob.php @@ -10,6 +10,7 @@ namespace tests\app; +use Exception; use Yii; use yii\base\BaseObject; use yii\queue\Queue; @@ -22,17 +23,17 @@ */ class RetryJob extends BaseObject implements RetryableJobInterface { - public $uid; + public string $uid; - public function execute(Queue $queue) + public function execute(Queue $queue): void { file_put_contents($this->getFileName(), 'a', FILE_APPEND); - throw new \Exception('Planned error.'); + throw new Exception('Planned error.'); } public function getFileName(): bool|string { - return Yii::getAlias("@runtime/job-{$this->uid}.lock"); + return Yii::getAlias("@runtime/job-$this->uid.lock"); } public function getTtr(): int diff --git a/tests/app/SimpleJob.php b/tests/app/SimpleJob.php index 5b9ef9651c..1e7a669aeb 100644 --- a/tests/app/SimpleJob.php +++ b/tests/app/SimpleJob.php @@ -24,7 +24,7 @@ class SimpleJob extends BaseObject implements JobInterface { public string $uid; - public function execute(Queue $queue) + public function execute(Queue $queue): void { file_put_contents($this->getFileName(), ''); } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 84fd098128..ddcb8e5089 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -16,6 +16,11 @@ services: - 8.8.8.8 - 4.4.4.4 environment: + COMPOSER_ALLOW_SUPERUSER: 1 + XDEBUG_MODE: ${XDEBUG_MODE:-off} # Setup "debug" to enable debugging + XDEBUG_CONFIG: "client_host=host.docker.internal" + XDEBUG_TRIGGER: ${XDEBUG_TRIGGER:-yes} + PHP_IDE_CONFIG: "serverName=yii2-queue" MYSQL_HOST: mysql MYSQL_USER: yii2_queue_test MYSQL_PASSWORD: yii2_queue_test @@ -31,7 +36,6 @@ services: RABBITMQ_PASSWORD: guest BEANSTALK_HOST: beanstalk GEARMAN_HOST: gearmand - COMPOSER_ALLOW_SUPERUSER: 1 ACTIVEMQ_HOST: activemq AWS_KEY: ${AWS_KEY:-admin} AWS_SECRET: ${AWS_SECRET:-admin} @@ -50,6 +54,8 @@ services: - localstack networks: net: {} + extra_hosts: + - host.docker.internal:${HOST_IP:-host-gateway} # https://hub.docker.com/_/mysql/ mysql: @@ -93,9 +99,9 @@ services: networks: net: {} - # https://hub.docker.com/r/schickling/beanstalkd/ + # https://hub.docker.com/r/rayyounghong/beanstalkd/ beanstalk: - image: schickling/beanstalkd + image: rayyounghong/beanstalkd ports: - "11301:11300" networks: @@ -135,4 +141,8 @@ services: networks: net: + driver: bridge name: yii2_queue_net + ipam: + config: + - subnet: 172.18.0.0/16 diff --git a/tests/drivers/CliTestCase.php b/tests/drivers/CliTestCase.php index 907344766f..66a0b60620 100644 --- a/tests/drivers/CliTestCase.php +++ b/tests/drivers/CliTestCase.php @@ -67,7 +67,6 @@ private function prepareCmd(array $cmd): array { $class = new ReflectionClass($this->getQueue()); $method = $class->getMethod('getCommandId'); - $method->setAccessible(true); $replace = [ 'php' => PHP_BINARY, diff --git a/tests/drivers/beanstalk/QueueTest.php b/tests/drivers/beanstalk/QueueTest.php index 8ba5bbb7c5..61491c03df 100644 --- a/tests/drivers/beanstalk/QueueTest.php +++ b/tests/drivers/beanstalk/QueueTest.php @@ -10,10 +10,10 @@ namespace tests\drivers\beanstalk; -use Pheanstalk\Exception\ServerException; +use Exception; use Pheanstalk\Pheanstalk; +use Pheanstalk\Values\JobId; use tests\app\PriorityJob; -use tests\app\RetryJob; use tests\drivers\CliTestCase; use Yii; use yii\queue\beanstalk\Queue; @@ -76,17 +76,6 @@ public function testLater(): void $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry(): void - { - $this->startProcess(['php', 'yii', 'queue/listen', '1']); - $job = new RetryJob(['uid' => uniqid()]); - $this->getQueue()->push($job); - sleep(6); - - $this->assertFileExists($job->getFileName()); - $this->assertEquals('aa', file_get_contents($job->getFileName())); - } - public function testRemove(): void { $id = $this->getQueue()->push($this->createSimpleJob()); @@ -94,6 +83,36 @@ public function testRemove(): void $this->runProcess(['php', 'yii', 'queue/remove', $id]); $this->assertFalse($this->jobIsExists($id)); + + $queue = $this->getQueue(); + $jobId = $queue->push($this->createSimpleJob()); + + $this->assertTrue($queue->remove($jobId)); + $this->assertFalse($queue->remove('007')); + } + + public function testConnect(): void + { + $this->startProcess(['php', 'yii', 'queue/listen', '1']); + + $job = $this->createSimpleJob(); + + $queue = new Queue(['host' => getenv('BEANSTALK_HOST') ?: 'localhost']); + $queue->receiveTimeout = 1; + $queue->connectTimeout = 5; + $queue->push($job); + + $this->assertSimpleJobDone($job); + } + + public function testStatusTube(): void + { + $queue = $this->getQueue(); + $queue->push($this->createSimpleJob()); + + $statusTube = $queue->getStatsTube(); + + $this->assertEquals('queue', $statusTube->name->value); } /** @@ -111,16 +130,12 @@ protected function getQueue(): Queue */ protected function jobIsExists(int|string|null $id): bool { - $connection = new Pheanstalk($this->getQueue()->host, $this->getQueue()->port); + $connection = Pheanstalk::create($this->getQueue()->host, $this->getQueue()->port); try { - $connection->peek($id); + $connection->peek(new JobId($id)); return true; - } catch (ServerException $e) { - if (str_starts_with($e->getMessage(), 'NOT_FOUND')) { - return false; - } - - throw $e; + } catch (\Throwable) { + return false; } } } From f915784f48d6cd3c07bca35a2ef00bb147fa538e Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Thu, 23 Nov 2023 16:52:12 +0300 Subject: [PATCH 04/16] Setup psalm level to 2 (#504) --- .gitattributes | 1 + composer.json | 4 +- phpunit.xml.dist | 30 ++++--- psalm.xml | 6 +- src/ExecEvent.php | 14 ++-- src/InvalidJobException.php | 8 +- src/JobEvent.php | 11 ++- src/LogBehavior.php | 5 +- src/PushEvent.php | 4 +- src/Queue.php | 20 +++-- src/RetryableJobInterface.php | 5 +- src/cli/Action.php | 2 +- src/cli/Command.php | 12 +-- src/cli/Queue.php | 2 - src/cli/VerboseBehavior.php | 81 ++++++++++--------- src/cli/WorkerEvent.php | 6 ++ src/closure/Behavior.php | 2 + src/closure/Job.php | 2 +- src/debug/Panel.php | 20 ++--- src/debug/views/detail.php | 2 +- src/drivers/amqp_interop/Command.php | 2 + src/drivers/amqp_interop/Queue.php | 31 ++++--- src/drivers/beanstalk/Command.php | 3 + src/drivers/beanstalk/InfoAction.php | 20 ++++- src/drivers/beanstalk/Queue.php | 5 +- src/drivers/db/Command.php | 2 + src/drivers/db/InfoAction.php | 32 +++++--- src/drivers/db/Queue.php | 79 +++++++++++------- .../db/migrations/M161119140200Queue.php | 3 +- .../migrations/M211218163000JobQueueSize.php | 4 +- src/drivers/file/Command.php | 6 +- src/drivers/file/InfoAction.php | 18 +++-- src/drivers/file/Queue.php | 32 ++++---- src/drivers/gearman/Command.php | 2 + src/drivers/gearman/Queue.php | 15 ++-- src/drivers/redis/Command.php | 2 + src/drivers/redis/InfoAction.php | 2 + src/drivers/redis/Queue.php | 4 + src/drivers/sqs/Command.php | 2 + src/drivers/sqs/Queue.php | 8 +- src/drivers/stomp/Command.php | 2 + src/drivers/stomp/Queue.php | 54 ++++++++----- src/gii/Generator.php | 23 +++--- src/serializers/IgbinarySerializer.php | 2 +- src/serializers/JsonSerializer.php | 8 +- src/serializers/PhpSerializer.php | 2 +- src/serializers/SerializerInterface.php | 2 +- stubs/psalm/BaseYii.php | 18 +++++ tests/app/RetryJob.php | 3 +- tests/app/SimpleJob.php | 2 +- tests/app/benchmark/Controller.php | 2 +- tests/app/config/main.php | 3 + tests/docker-compose.yml | 10 ++- tests/drivers/beanstalk/QueueTest.php | 4 +- tests/drivers/db/TestCase.php | 2 +- tests/drivers/file/QueueTest.php | 2 +- tests/drivers/redis/QueueTest.php | 2 +- tests/drivers/stomp/QueueTest.php | 15 ++-- tests/serializers/TestCase.php | 6 +- 59 files changed, 417 insertions(+), 254 deletions(-) create mode 100644 stubs/psalm/BaseYii.php diff --git a/.gitattributes b/.gitattributes index 6e08b1d559..8cdf150aeb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,4 @@ /phpunit.xml.dist export-ignore /support export-ignore /psalm.xml export-ignore +/stubs export-ignore diff --git a/composer.json b/composer.json index 92d208b465..0da6de72b5 100644 --- a/composer.json +++ b/composer.json @@ -31,8 +31,8 @@ "enqueue/amqp-bunny": "^0.10.0", "enqueue/amqp-ext": "^0.10.8", "enqueue/stomp": "^0.10.0", - "pda/pheanstalk": "^v5.0.0", - "aws/aws-sdk-php": ">=2.4", + "pda/pheanstalk": "^5.0.0", + "aws/aws-sdk-php": "3.285.0", "vimeo/psalm": "^5.10.0" }, "suggest": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 821636bb0d..3feee9238b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,17 +10,21 @@ displayDetailsOnSkippedTests="true" executionOrder="random" > - - - ./tests - ./tests/app - ./tests/docker - ./tests/runtime - - - - - ./src - - + + + ./tests + ./tests/app + ./tests/docker + ./tests/runtime + + + + + ./src + + + ./src/debug + ./src/gii + + diff --git a/psalm.xml b/psalm.xml index e19fc25555..c9de05bc3b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,6 +1,6 @@ + + + + diff --git a/src/ExecEvent.php b/src/ExecEvent.php index 75945cea71..b59b9dafd0 100644 --- a/src/ExecEvent.php +++ b/src/ExecEvent.php @@ -10,6 +10,8 @@ namespace yii\queue; +use Throwable; + /** * Exec Event. * @@ -23,23 +25,23 @@ class ExecEvent extends JobEvent * @see Queue::EVENT_AFTER_EXEC * @see Queue::EVENT_AFTER_ERROR */ - public int $attempt; + public int $attempt = 0; /** * @var mixed result of a job execution in case job is done. * @see Queue::EVENT_AFTER_EXEC * @since 2.1.1 */ - public $result; + public mixed $result = null; /** - * @var null|\Exception|\Throwable + * @var null|Throwable * @see Queue::EVENT_AFTER_ERROR * @since 2.1.1 */ - public $error; + public ?Throwable $error = null; /** - * @var null|bool + * @var bool * @see Queue::EVENT_AFTER_ERROR * @since 2.1.1 */ - public $retry; + public bool $retry = true; } diff --git a/src/InvalidJobException.php b/src/InvalidJobException.php index a313dd5907..ae345f1d25 100644 --- a/src/InvalidJobException.php +++ b/src/InvalidJobException.php @@ -30,10 +30,10 @@ class InvalidJobException extends Exception * @param Throwable|null $previous */ public function __construct( - private string $serialized, - string $message = '', - int $code = 0, - Throwable $previous = null + private readonly string $serialized, + string $message = '', + int $code = 0, + Throwable $previous = null ) { parent::__construct($message, $code, $previous); diff --git a/src/JobEvent.php b/src/JobEvent.php index 8c56a1db66..e994161efd 100644 --- a/src/JobEvent.php +++ b/src/JobEvent.php @@ -21,14 +21,19 @@ abstract class JobEvent extends Event { /** - * @var Queue * @inheritdoc */ + public $name = ''; + /** + * @var Queue|null|object + * @inheritdoc + * @psalm-suppress PropertyNotSetInConstructor + */ public $sender; /** * @var int|string|null unique id of a job */ - public string|int|null $id; + public string|int|null $id = null; /** * @var Closure|JobInterface|null|mixed */ @@ -36,5 +41,5 @@ abstract class JobEvent extends Event /** * @var int time to reserve in seconds of the job */ - public int $ttr; + public int $ttr = 0; } diff --git a/src/LogBehavior.php b/src/LogBehavior.php index 367cea414e..5f44acf4af 100644 --- a/src/LogBehavior.php +++ b/src/LogBehavior.php @@ -12,6 +12,7 @@ use Yii; use yii\base\Behavior; +use yii\base\Component; /** * Log Behavior. @@ -21,7 +22,7 @@ class LogBehavior extends Behavior { /** - * @var Queue + * @var Queue|null|Component * @inheritdoc */ public $owner; @@ -138,7 +139,7 @@ protected function getExecTitle(ExecEvent $event): string { $title = $this->getJobTitle($event); $extra = "attempt: $event->attempt"; - if ($pid = $event->sender->getWorkerPid()) { + if ($pid = $event->sender?->getWorkerPid()) { $extra .= ", PID: $pid"; } return "$title ($extra)"; diff --git a/src/PushEvent.php b/src/PushEvent.php index 947e0a6bdb..9064836ea3 100644 --- a/src/PushEvent.php +++ b/src/PushEvent.php @@ -20,9 +20,9 @@ class PushEvent extends JobEvent /** * @var int */ - public int $delay; + public int $delay = 0; /** * @var int|string|null */ - public string|int|null $priority; + public string|int|null $priority = null; } diff --git a/src/Queue.php b/src/Queue.php index 280d9918c2..93c01f7448 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -10,6 +10,7 @@ namespace yii\queue; +use Throwable; use yii\base\Component; use yii\base\InvalidArgumentException; use yii\base\InvalidConfigException; @@ -88,8 +89,6 @@ public function init(): void { parent::init(); - $this->serializer = Instance::ensure($this->serializer, SerializerInterface::class); - if ($this->ttr <= 0) { throw new InvalidConfigException('Default TTR must be greater that zero.'); } @@ -141,7 +140,7 @@ public function priority(int|string|null $value): static * @param JobInterface|mixed $job * @return int|string|null id of a job message */ - public function push($job): int|string|null + public function push(mixed $job): int|string|null { $event = new PushEvent([ 'job' => $job, @@ -174,7 +173,7 @@ public function push($job): int|string|null throw new InvalidArgumentException('Job delay must be positive.'); } - $message = $this->serializer->serialize($event->job); + $message = $this->getSerializer()->serialize($event->job); $event->id = $this->pushMessage($message, $event->ttr, $event->delay, $event->priority); $this->trigger(self::EVENT_AFTER_PUSH, $event); @@ -225,8 +224,9 @@ protected function handleMessage(int|string $id, string $message, int $ttr, int return $this->handleError($event); } try { - $event->result = $event->job->execute($this); - } catch (\Exception|\Throwable $error) { + /** @psalm-suppress PossiblyUndefinedMethod */ + $event->result = $event->job?->execute($this); + } catch (Throwable $error) { $event->error = $error; return $this->handleError($event); } @@ -243,7 +243,7 @@ protected function handleMessage(int|string $id, string $message, int $ttr, int public function unserializeMessage(string $serialized): array { try { - $job = $this->serializer->unserialize($serialized); + $job = $this->getSerializer()->unserialize($serialized); } catch (\Exception $e) { return [null, new InvalidJobException($serialized, $e->getMessage(), 0, $e)]; } @@ -307,4 +307,10 @@ public function isDone(int|string $id): bool * @return int status code */ abstract public function status(int|string $id): int; + + private function getSerializer(): SerializerInterface + { + /** @psalm-var SerializerInterface */ + return Instance::ensure($this->serializer, SerializerInterface::class); + } } diff --git a/src/RetryableJobInterface.php b/src/RetryableJobInterface.php index 08791db777..f61ec269b5 100644 --- a/src/RetryableJobInterface.php +++ b/src/RetryableJobInterface.php @@ -10,7 +10,6 @@ namespace yii\queue; -use Exception; use Throwable; /** @@ -27,8 +26,8 @@ public function getTtr(): int; /** * @param int $attempt number - * @param Exception|Throwable $error from last execute of the job + * @param Throwable|null $error from last execute of the job * @return bool */ - public function canRetry(int $attempt, $error): bool; + public function canRetry(int $attempt, ?Throwable $error): bool; } diff --git a/src/cli/Action.php b/src/cli/Action.php index 3ee72af88a..5e90ac7b80 100644 --- a/src/cli/Action.php +++ b/src/cli/Action.php @@ -26,7 +26,7 @@ abstract class Action extends BaseAction */ public Queue $queue; /** - * @var Command|ConsoleController + * @inheritdoc */ public $controller; diff --git a/src/cli/Command.php b/src/cli/Command.php index ef993ba534..f5fd0e9dac 100644 --- a/src/cli/Command.php +++ b/src/cli/Command.php @@ -124,8 +124,9 @@ public function beforeAction($action): bool if ($this->phpBinary === null) { $this->phpBinary = PHP_BINARY; } - $this->queue->messageHandler = function ($id, $message, $ttr, $attempt) { - return $this->handleMessage($id, $message, (int)$ttr, (int)$attempt); + /** @psalm-suppress MissingClosureReturnType */ + $this->queue->messageHandler = function (int|string|null $id, string $message, int $ttr, int $attempt) { + return $this->handleMessage($id, $message, $ttr, $attempt); }; } @@ -136,14 +137,14 @@ public function beforeAction($action): bool * Executes a job. * The command is internal, and used to isolate a job execution. Manual usage is not provided. * - * @param string|null $id of a message + * @param string $id of a message * @param int $ttr time to reserve * @param int $attempt number * @param int $pid of a worker * @return int exit code * @internal It is used with isolate mode. */ - public function actionExec(?string $id, int $ttr, int $attempt, int $pid): int + public function actionExec(string $id, int $ttr, int $attempt, int $pid): int { if ($this->queue->execute($id, file_get_contents('php://stdin'), $ttr, $attempt, $pid ?: null)) { return self::EXEC_DONE; @@ -164,6 +165,7 @@ public function actionExec(?string $id, int $ttr, int $attempt, int $pid): int protected function handleMessage(int|string|null $id, string $message, ?int $ttr, int $attempt): bool { // Child process command: php yii queue/exec "id" "ttr" "attempt" "pid" + /** @psalm-suppress PossiblyUndefinedArrayOffset */ $cmd = [ $this->phpBinary, $_SERVER['SCRIPT_FILENAME'], @@ -185,7 +187,7 @@ protected function handleMessage(int|string|null $id, string $message, ?int $ttr $process = new Process($cmd, null, null, $message, $ttr); try { - $result = $process->run(function ($type, $buffer) { + $result = $process->run(function (string $type, string $buffer) { if ($type === Process::ERR) { $this->stderr($buffer); } else { diff --git a/src/cli/Queue.php b/src/cli/Queue.php index 5dc67a4ec2..9dac8a5bdd 100644 --- a/src/cli/Queue.php +++ b/src/cli/Queue.php @@ -70,7 +70,6 @@ abstract class Queue extends BaseQueue implements BootstrapInterface */ protected function getCommandId(): string { - /** @psalm-suppress UndefinedClass */ foreach (Yii::$app->getComponents(false) as $id => $component) { if ($component === $this) { return Inflector::camel2id($id); @@ -103,7 +102,6 @@ protected function runWorker(callable $handler): ?int { $this->_workerPid = getmypid(); /** @var LoopInterface $loop */ - /** @psalm-suppress UndefinedClass */ $loop = Yii::createObject($this->loopConfig, [$this]); $event = new WorkerEvent(['loop' => $loop]); diff --git a/src/cli/VerboseBehavior.php b/src/cli/VerboseBehavior.php index e25d95b909..184930bc7c 100644 --- a/src/cli/VerboseBehavior.php +++ b/src/cli/VerboseBehavior.php @@ -11,7 +11,9 @@ namespace yii\queue\cli; use yii\base\Behavior; +use yii\base\Component; use yii\console\Controller; +use yii\helpers\BaseConsole; use yii\helpers\Console; use yii\queue\ExecEvent; use yii\queue\JobInterface; @@ -24,22 +26,23 @@ class VerboseBehavior extends Behavior { /** - * @var Queue + * @var Queue|null|Component */ public $owner; /** * @var Controller + * @psalm-suppress PropertyNotSetInConstructor */ public Controller $command; /** - * @var float timestamp + * @var float|null timestamp */ - private float $jobStartedAt; + private ?float $jobStartedAt = null; /** - * @var int timestamp + * @var int|null timestamp */ - private int $workerStartedAt; + private ?int $workerStartedAt = null; /** * @inheritdoc @@ -61,10 +64,10 @@ public function events(): array public function beforeExec(ExecEvent $event): void { $this->jobStartedAt = microtime(true); - $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); - $this->command->stdout($this->jobTitle($event), Console::FG_GREY); - $this->command->stdout(' - ', Console::FG_YELLOW); - $this->command->stdout('Started', Console::FG_GREEN); + Console::ansiFormat(date('Y-m-d H:i:s'), [BaseConsole::FG_YELLOW]); + Console::ansiFormat($this->jobTitle($event), [BaseConsole::FG_GREY]); + Console::ansiFormat(' - ', [BaseConsole::FG_YELLOW]); + Console::ansiFormat('Started', [BaseConsole::FG_GREEN]); $this->command->stdout(PHP_EOL); } @@ -73,13 +76,13 @@ public function beforeExec(ExecEvent $event): void */ public function afterExec(ExecEvent $event): void { - $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); - $this->command->stdout($this->jobTitle($event), Console::FG_GREY); - $this->command->stdout(' - ', Console::FG_YELLOW); - $this->command->stdout('Done', Console::FG_GREEN); + $this->command->stdout(date('Y-m-d H:i:s'), BaseConsole::FG_YELLOW); + $this->command->stdout($this->jobTitle($event), BaseConsole::FG_GREY); + $this->command->stdout(' - ', BaseConsole::FG_YELLOW); + $this->command->stdout('Done', BaseConsole::FG_GREEN); $duration = number_format(round(microtime(true) - $this->jobStartedAt, 3), 3); - $memory = round(memory_get_peak_usage(false)/1024/1024, 2); - $this->command->stdout(" ($duration s, $memory MiB)", Console::FG_YELLOW); + $memory = round(memory_get_peak_usage()/1024/1024, 2); + $this->command->stdout(" ($duration s, $memory MiB)", BaseConsole::FG_YELLOW); $this->command->stdout(PHP_EOL); } @@ -88,23 +91,25 @@ public function afterExec(ExecEvent $event): void */ public function afterError(ExecEvent $event): void { - $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); - $this->command->stdout($this->jobTitle($event), Console::FG_GREY); - $this->command->stdout(' - ', Console::FG_YELLOW); - $this->command->stdout('Error', Console::BG_RED); + $this->command->stdout(date('Y-m-d H:i:s'), BaseConsole::FG_YELLOW); + $this->command->stdout($this->jobTitle($event), BaseConsole::FG_GREY); + $this->command->stdout(' - ', BaseConsole::FG_YELLOW); + $this->command->stdout('Error', BaseConsole::BG_RED); if ($this->jobStartedAt) { $duration = number_format(round(microtime(true) - $this->jobStartedAt, 3), 3); - $this->command->stdout(" ($duration s)", Console::FG_YELLOW); + $this->command->stdout(" ($duration s)", BaseConsole::FG_YELLOW); + } + if (null !== $event->error) { + $this->command->stdout(PHP_EOL); + $this->command->stdout('> ' . get_class($event->error) . ': ', BaseConsole::FG_RED); + $message = explode("\n", ltrim($event->error->getMessage()), 2)[0]; // First line + $this->command->stdout($message, BaseConsole::FG_GREY); + $this->command->stdout(PHP_EOL); + $this->command->stdout('Stack trace:', BaseConsole::FG_GREY); + $this->command->stdout(PHP_EOL); + $this->command->stdout($event->error->getTraceAsString(), BaseConsole::FG_GREY); + $this->command->stdout(PHP_EOL); } - $this->command->stdout(PHP_EOL); - $this->command->stdout('> ' . get_class($event->error) . ': ', Console::FG_RED); - $message = explode("\n", ltrim($event->error->getMessage()), 2)[0]; // First line - $this->command->stdout($message, Console::FG_GREY); - $this->command->stdout(PHP_EOL); - $this->command->stdout('Stack trace:', Console::FG_GREY); - $this->command->stdout(PHP_EOL); - $this->command->stdout($event->error->getTraceAsString(), Console::FG_GREY); - $this->command->stdout(PHP_EOL); } /** @@ -116,7 +121,7 @@ protected function jobTitle(ExecEvent $event): string { $name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job'; $extra = "attempt: $event->attempt"; - if ($pid = $event->sender->getWorkerPid()) { + if ($pid = $event->sender?->getWorkerPid()) { $extra .= ", pid: $pid"; } return " [$event->id] $name ($extra)"; @@ -129,10 +134,10 @@ protected function jobTitle(ExecEvent $event): string public function workerStart(WorkerEvent $event): void { $this->workerStartedAt = time(); - $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); + $this->command->stdout(date('Y-m-d H:i:s'), BaseConsole::FG_YELLOW); $pid = $event->sender->getWorkerPid(); - $this->command->stdout(" [pid: $pid]", Console::FG_GREY); - $this->command->stdout(" - Worker is started\n", Console::FG_GREEN); + $this->command->stdout(" [pid: $pid]", BaseConsole::FG_GREY); + $this->command->stdout(" - Worker is started\n", BaseConsole::FG_GREEN); } /** @@ -141,12 +146,12 @@ public function workerStart(WorkerEvent $event): void */ public function workerStop(WorkerEvent $event): void { - $this->command->stdout(date('Y-m-d H:i:s'), Console::FG_YELLOW); + $this->command->stdout(date('Y-m-d H:i:s'), BaseConsole::FG_YELLOW); $pid = $event->sender->getWorkerPid(); - $this->command->stdout(" [pid: $pid]", Console::FG_GREY); - $this->command->stdout(' - Worker is stopped ', Console::FG_GREEN); + $this->command->stdout(" [pid: $pid]", BaseConsole::FG_GREY); + $this->command->stdout(' - Worker is stopped ', BaseConsole::FG_GREEN); $duration = $this->formatDuration(time() - $this->workerStartedAt); - $this->command->stdout("($duration)\n", Console::FG_YELLOW); + $this->command->stdout("($duration)\n", BaseConsole::FG_YELLOW); } /** @@ -154,7 +159,7 @@ public function workerStop(WorkerEvent $event): void * @return string * @since 2.0.2 */ - protected function formatDuration($value): string + protected function formatDuration(int $value): string { $seconds = $value % 60; $value = ($value - $seconds) / 60; diff --git a/src/cli/WorkerEvent.php b/src/cli/WorkerEvent.php index eb92a5af15..6dc4bf3c60 100644 --- a/src/cli/WorkerEvent.php +++ b/src/cli/WorkerEvent.php @@ -20,13 +20,19 @@ */ class WorkerEvent extends Event { + /** + * @inheritdoc + */ + public $name; /** * @var Queue * @inheritdoc + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public $sender; /** * @var LoopInterface + * @psalm-suppress PropertyNotSetInConstructor */ public LoopInterface $loop; /** diff --git a/src/closure/Behavior.php b/src/closure/Behavior.php index a83f82bdf8..dd0fe3183c 100644 --- a/src/closure/Behavior.php +++ b/src/closure/Behavior.php @@ -33,6 +33,8 @@ class Behavior extends \yii\base\Behavior { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public $owner; diff --git a/src/closure/Job.php b/src/closure/Job.php index ec4ed605e1..5bcab6984d 100644 --- a/src/closure/Job.php +++ b/src/closure/Job.php @@ -24,7 +24,7 @@ class Job implements JobInterface /** * @var string serialized closure */ - public string $serialized; + public string $serialized = ''; /** * Unserializes and executes a closure. diff --git a/src/debug/Panel.php b/src/debug/Panel.php index 11bee1130e..bb35b86d3b 100644 --- a/src/debug/Panel.php +++ b/src/debug/Panel.php @@ -10,6 +10,7 @@ namespace yii\queue\debug; +use Exception; use Yii; use yii\base\NotSupportedException; use yii\base\ViewContextInterface; @@ -25,12 +26,12 @@ */ class Panel extends \yii\debug\Panel implements ViewContextInterface { - private $_jobs = []; + private array $_jobs = []; /** * @inheritdoc */ - public function getName() + public function getName(): string { return 'Queue'; } @@ -38,7 +39,7 @@ public function getName() /** * @inheritdoc */ - public function init() + public function init(): void { parent::init(); PushEvent::on(Queue::class, Queue::EVENT_AFTER_PUSH, function (PushEvent $event) { @@ -50,7 +51,7 @@ public function init() * @param PushEvent $event * @return array */ - protected function getPushData(PushEvent $event) + protected function getPushData(PushEvent $event): array { $data = []; foreach (Yii::$app->getComponents(false) as $id => $component) { @@ -87,7 +88,7 @@ public function save() /** * @inheritdoc */ - public function getViewPath() + public function getViewPath(): string { return __DIR__ . '/views'; } @@ -95,7 +96,7 @@ public function getViewPath() /** * @inheritdoc */ - public function getSummary() + public function getSummary(): string { return Yii::$app->view->render('summary', [ 'url' => $this->getUrl(), @@ -106,9 +107,9 @@ public function getSummary() /** * @inheritdoc */ - public function getDetail() + public function getDetail(): string { - $jobs = isset($this->data['jobs']) ? $this->data['jobs'] : []; + $jobs = $this->data['jobs'] ?? []; foreach ($jobs as &$job) { $job['status'] = 'unknown'; /** @var Queue $queue */ @@ -121,8 +122,7 @@ public function getDetail() } elseif ($queue->isDone($job['id'])) { $job['status'] = 'done'; } - } catch (NotSupportedException $e) { - } catch (\Exception $e) { + } catch (NotSupportedException|Exception $e) { $job['status'] = $e->getMessage(); } } diff --git a/src/debug/views/detail.php b/src/debug/views/detail.php index f031d4e144..2957a45480 100644 --- a/src/debug/views/detail.php +++ b/src/debug/views/detail.php @@ -18,7 +18,7 @@

Pushed jobs

-
+

diff --git a/src/drivers/amqp_interop/Command.php b/src/drivers/amqp_interop/Command.php index e1bbe5ad9d..e2007fd275 100644 --- a/src/drivers/amqp_interop/Command.php +++ b/src/drivers/amqp_interop/Command.php @@ -23,6 +23,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; diff --git a/src/drivers/amqp_interop/Queue.php b/src/drivers/amqp_interop/Queue.php index c7f25b0c91..d38ccaaa90 100644 --- a/src/drivers/amqp_interop/Queue.php +++ b/src/drivers/amqp_interop/Queue.php @@ -325,9 +325,10 @@ public function listen(): void $this->open(); $this->setupBroker(); - $queue = $this->context->createQueue($this->queueName); - $consumer = $this->context->createConsumer($queue); + $queue = $this->getContext()->createQueue($this->queueName); + $consumer = $this->getContext()->createConsumer($queue); + /** @psalm-suppress MissingClosureReturnType */ $callback = function (AmqpMessage $message, AmqpConsumer $consumer) { if ($message->isRedelivered()) { $consumer->acknowledge($message); @@ -339,8 +340,12 @@ public function listen(): void $ttr = $message->getProperty(self::TTR); $attempt = $message->getProperty(self::ATTEMPT, 1); + $messageId = $message->getMessageId(); - if ($this->handleMessage($message->getMessageId(), $message->getBody(), $ttr, $attempt)) { + if ( + null !== $messageId + && $this->handleMessage($messageId, $message->getBody(), $ttr, $attempt) + ) { $consumer->acknowledge($message); } else { $consumer->acknowledge($message); @@ -351,18 +356,19 @@ public function listen(): void return true; }; - $subscriptionConsumer = $this->context->createSubscriptionConsumer(); + $subscriptionConsumer = $this->getContext()->createSubscriptionConsumer(); $subscriptionConsumer->subscribe($consumer, $callback); $subscriptionConsumer->consume(); } /** - * @return AmqpContext|null + * @return AmqpContext */ - public function getContext(): ?AmqpContext + public function getContext(): AmqpContext { $this->open(); + /** @psalm-var AmqpContext */ return $this->context; } @@ -374,10 +380,10 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $this->open(); $this->setupBroker(); - $topic = $this->context->createTopic($this->exchangeName); + $topic = $this->getContext()->createTopic($this->exchangeName); /** @var AmqpMessage $message */ - $message = $this->context->createMessage($payload); + $message = $this->getContext()->createMessage($payload); $message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); $message->setMessageId(uniqid('', true)); $message->setTimestamp(time()); @@ -389,7 +395,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri ] )); - $producer = $this->context->createProducer(); + $producer = $this->getContext()->createProducer(); if ($delay) { $message->setProperty(self::DELAY, $delay); @@ -423,6 +429,7 @@ public function status($id): int */ protected function open(): void { + /** @psalm-suppress RedundantConditionGivenDocblockType */ if ($this->context) { return; } @@ -505,6 +512,9 @@ protected function setupBroker(): void */ protected function close(): void { + /** + * @psalm-suppress DocblockTypeContradiction + */ if (!$this->context) { return; } @@ -535,6 +545,7 @@ protected function redeliver(AmqpMessage $message): void private function createQueue(): AmqpQueue { - return $this->context->createQueue($this->queueName); + /** @psalm-var AmqpQueue */ + return $this->getContext()->createQueue($this->queueName); } } diff --git a/src/drivers/beanstalk/Command.php b/src/drivers/beanstalk/Command.php index 6360032ac6..757a500521 100644 --- a/src/drivers/beanstalk/Command.php +++ b/src/drivers/beanstalk/Command.php @@ -23,6 +23,9 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantPropertyType + * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress NonInvariantDocblockPropertyType */ public CliQueue $queue; /** diff --git a/src/drivers/beanstalk/InfoAction.php b/src/drivers/beanstalk/InfoAction.php index c8951f2822..2d320d9340 100644 --- a/src/drivers/beanstalk/InfoAction.php +++ b/src/drivers/beanstalk/InfoAction.php @@ -10,6 +10,8 @@ namespace yii\queue\beanstalk; +use Throwable; +use yii\helpers\BaseConsole; use yii\helpers\Console; use yii\queue\cli\Action; use yii\queue\cli\Queue as CliQueue; @@ -23,6 +25,7 @@ class InfoAction extends Action { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType */ public CliQueue $queue; @@ -31,11 +34,20 @@ class InfoAction extends Action */ public function run(): void { - Console::output($this->format('Statistical information about the tube:', Console::FG_GREEN)); + Console::output( + $this->format('Statistical information about the tube:', BaseConsole::FG_GREEN) + ); - foreach ($this->queue->getStatsTube() as $key => $value) { - Console::stdout($this->format("- $key: ", Console::FG_YELLOW)); - Console::output($value); + try { + /** @psalm-suppress RawObjectIteration */ + foreach ($this->queue->getStatsTube() as $key => $value) { + Console::stdout($this->format("- $key: ", BaseConsole::FG_YELLOW)); + Console::output($value); + } + } catch (Throwable) { + Console::stdout( + $this->format('Tube not found or empty', BaseConsole::FG_RED) + ); } } } diff --git a/src/drivers/beanstalk/Queue.php b/src/drivers/beanstalk/Queue.php index 384347fa39..e3d7ec4f42 100644 --- a/src/drivers/beanstalk/Queue.php +++ b/src/drivers/beanstalk/Queue.php @@ -18,6 +18,7 @@ use Pheanstalk\Values\Timeout; use Pheanstalk\Values\TubeName; use Pheanstalk\Values\TubeStats; +use Throwable; use yii\base\InvalidArgumentException; use yii\queue\cli\Queue as CliQueue; @@ -114,7 +115,7 @@ public function status($id): int } return self::STATUS_WAITING; - } catch (\Throwable) { + } catch (Throwable) { return self::STATUS_DONE; } } @@ -131,7 +132,7 @@ public function remove(int|string $id): bool try { $this->getPheanstalk()->delete(new JobId($id)); return true; - } catch (\Throwable) { + } catch (Throwable) { return false; } } diff --git a/src/drivers/db/Command.php b/src/drivers/db/Command.php index 7ce572a139..f06af1347d 100644 --- a/src/drivers/db/Command.php +++ b/src/drivers/db/Command.php @@ -23,6 +23,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; /** diff --git a/src/drivers/db/InfoAction.php b/src/drivers/db/InfoAction.php index c0859e0432..43007526f8 100644 --- a/src/drivers/db/InfoAction.php +++ b/src/drivers/db/InfoAction.php @@ -10,7 +10,9 @@ namespace yii\queue\db; +use yii\db\Connection; use yii\db\Query; +use yii\helpers\BaseConsole; use yii\helpers\Console; use yii\queue\cli\Action; use yii\queue\cli\Queue as CliQueue; @@ -24,6 +26,8 @@ class InfoAction extends Action { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; @@ -32,25 +36,27 @@ class InfoAction extends Action */ public function run(): void { - Console::output($this->format('Jobs', Console::FG_GREEN)); + Console::output($this->format('Jobs', BaseConsole::FG_GREEN)); + /** @var Connection $db */ + $db = $this->queue->db; - Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); - Console::output($this->getWaiting()->count('*', $this->queue->db)); + Console::stdout($this->format('- waiting: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getWaiting()->count('*', $db)); - Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); - Console::output($this->getDelayed()->count('*', $this->queue->db)); + Console::stdout($this->format('- delayed: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getDelayed()->count('*', $db)); - Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); - Console::output($this->getReserved()->count('*', $this->queue->db)); + Console::stdout($this->format('- reserved: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getReserved()->count('*', $db)); - Console::stdout($this->format('- done: ', Console::FG_YELLOW)); - Console::output($this->getDone()->count('*', $this->queue->db)); + Console::stdout($this->format('- done: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getDone()->count('*', $db)); } /** * @return Query */ - protected function getWaiting() + protected function getWaiting(): Query { return (new Query()) ->from($this->queue->tableName) @@ -62,7 +68,7 @@ protected function getWaiting() /** * @return Query */ - protected function getDelayed() + protected function getDelayed(): Query { return (new Query()) ->from($this->queue->tableName) @@ -74,7 +80,7 @@ protected function getDelayed() /** * @return Query */ - protected function getReserved() + protected function getReserved(): Query { return (new Query()) ->from($this->queue->tableName) @@ -86,7 +92,7 @@ protected function getReserved() /** * @return Query */ - protected function getDone() + protected function getDone(): Query { return (new Query()) ->from($this->queue->tableName) diff --git a/src/drivers/db/Queue.php b/src/drivers/db/Queue.php index aade95bda3..8b82364daf 100644 --- a/src/drivers/db/Queue.php +++ b/src/drivers/db/Queue.php @@ -28,11 +28,11 @@ class Queue extends CliQueue /** * @var Connection|array|string */ - public $db = 'db'; + public Connection|string|array $db = 'db'; /** * @var Mutex|array|string */ - public $mutex = 'mutex'; + public Mutex|string|array $mutex = 'mutex'; /** * @var int timeout */ @@ -54,13 +54,17 @@ class Queue extends CliQueue */ public string $commandClass = Command::class; + protected int $reserveTime = 0; + /** * @inheritdoc */ public function init(): void { parent::init(); + /** @psalm-suppress PropertyTypeCoercion */ $this->db = Instance::ensure($this->db, Connection::class); + /** @psalm-suppress PropertyTypeCoercion */ $this->mutex = Instance::ensure($this->mutex, Mutex::class); } @@ -68,7 +72,7 @@ public function init(): void * Listens queue and runs each job. * * @param bool $repeat whether to continue listening when queue is empty. - * @param int $timeout number of seconds to sleep before next iteration. + * @param int<0, max> $timeout number of seconds to sleep before next iteration. * @return null|int exit code. * @internal for worker command only * @since 2.0.2 @@ -98,12 +102,12 @@ public function run(bool $repeat, int $timeout = 0) /** * @inheritdoc */ - public function status($id): int + public function status(int|string $id): int { $payload = (new Query()) ->from($this->tableName) ->where(['id' => $id]) - ->one($this->db); + ->one($this->getDb()); if (!$payload) { if ($this->deleteReleased) { @@ -113,11 +117,11 @@ public function status($id): int throw new InvalidArgumentException("Unknown message ID: $id."); } - if (!$payload['reserved_at']) { + if (!isset($payload['reserved_at'])) { return self::STATUS_WAITING; } - if (!$payload['done_at']) { + if (!isset($payload['done_at'])) { return self::STATUS_RESERVED; } @@ -131,7 +135,7 @@ public function status($id): int */ public function clear(): void { - $this->db->createCommand() + $this->getDb()->createCommand() ->delete($this->tableName, ['channel' => $this->channel]) ->execute(); } @@ -143,9 +147,9 @@ public function clear(): void * @return bool * @since 2.0.1 */ - public function remove($id) + public function remove(int $id): bool { - return (bool) $this->db->createCommand() + return (bool) $this->getDb()->createCommand() ->delete($this->tableName, ['channel' => $this->channel, 'id' => $id]) ->execute(); } @@ -155,7 +159,7 @@ public function remove($id) */ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $priority): int|string|null { - $this->db->createCommand()->insert($this->tableName, [ + $this->getDb()->createCommand()->insert($this->tableName, [ 'channel' => $this->channel, 'job' => $payload, 'pushed_at' => time(), @@ -163,8 +167,11 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri 'delay' => $delay, 'priority' => $priority ?: 1024, ])->execute(); - $tableSchema = $this->db->getTableSchema($this->tableName); - return $this->db->getLastInsertID($tableSchema->sequenceName); + $tableSchema = $this->getDb()->getTableSchema($this->tableName); + if (null === $tableSchema) { + return null; + } + return $this->getDb()->getLastInsertID($tableSchema->sequenceName??''); } /** @@ -173,10 +180,10 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri * @return array|false payload * @throws Exception in case it hasn't waited the lock */ - protected function reserve() + protected function reserve(): bool|array { - return $this->db->useMaster(function () { - if (!$this->mutex->acquire(__CLASS__ . $this->channel, $this->mutexTimeout)) { + return $this->getDb()->useMaster(function () { + if (!$this->getMutex()->acquire(__CLASS__ . $this->channel, $this->mutexTimeout)) { throw new Exception('Has not waited the lock.'); } @@ -190,11 +197,11 @@ protected function reserve() ->andWhere('[[pushed_at]] <= :time - [[delay]]', [':time' => time()]) ->orderBy(['priority' => SORT_ASC, 'id' => SORT_ASC]) ->limit(1) - ->one($this->db); + ->one($this->getDb()); if (is_array($payload)) { $payload['reserved_at'] = time(); $payload['attempt'] = (int) $payload['attempt'] + 1; - $this->db->createCommand()->update($this->tableName, [ + $this->getDb()->createCommand()->update($this->tableName, [ 'reserved_at' => $payload['reserved_at'], 'attempt' => $payload['attempt'], ], [ @@ -207,27 +214,25 @@ protected function reserve() } } } finally { - $this->mutex->release(__CLASS__ . $this->channel); + $this->getMutex()->release(__CLASS__ . $this->channel); } return $payload; }); } - - /** * @param array $payload */ - protected function release($payload) + protected function release(array $payload): void { if ($this->deleteReleased) { - $this->db->createCommand()->delete( + $this->getDb()->createCommand()->delete( $this->tableName, ['id' => $payload['id']] )->execute(); } else { - $this->db->createCommand()->update( + $this->getDb()->createCommand()->update( $this->tableName, ['done_at' => time()], ['id' => $payload['id']] @@ -238,11 +243,11 @@ protected function release($payload) /** * Moves expired messages into waiting list. */ - protected function moveExpired() + protected function moveExpired(): void { if ($this->reserveTime !== time()) { $this->reserveTime = time(); - $this->db->createCommand()->update( + $this->getDb()->createCommand()->update( $this->tableName, ['reserved_at' => null], // `reserved_at IS NOT NULL` forces db to use index on column, @@ -253,5 +258,25 @@ protected function moveExpired() } } - protected $reserveTime; + private function getDb(): Connection + { + /** @var Connection $dbConnection */ + $dbConnection = $this->db; + if (is_string($this->db) || is_array($this->db)) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->db = Instance::ensure($this->db, Connection::class); + } + return $dbConnection; + } + + private function getMutex(): Mutex + { + /** @var Mutex $mutex */ + $mutex = $this->mutex; + if (is_string($this->mutex) || is_array($this->mutex)) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->mutex = Instance::ensure($this->mutex, Mutex::class); + } + return $mutex; + } } diff --git a/src/drivers/db/migrations/M161119140200Queue.php b/src/drivers/db/migrations/M161119140200Queue.php index 9db693284a..14b8adfda8 100644 --- a/src/drivers/db/migrations/M161119140200Queue.php +++ b/src/drivers/db/migrations/M161119140200Queue.php @@ -20,7 +20,6 @@ class M161119140200Queue extends Migration { public string $tableName = '{{%queue}}'; - public $tableOptions; public function up(): void { @@ -31,7 +30,7 @@ public function up(): void 'created_at' => $this->integer()->notNull(), 'started_at' => $this->integer(), 'finished_at' => $this->integer(), - ], $this->tableOptions); + ]); $this->createIndex('channel', $this->tableName, 'channel'); $this->createIndex('started_at', $this->tableName, 'started_at'); diff --git a/src/drivers/db/migrations/M211218163000JobQueueSize.php b/src/drivers/db/migrations/M211218163000JobQueueSize.php index db9e259c60..04cd2b897e 100644 --- a/src/drivers/db/migrations/M211218163000JobQueueSize.php +++ b/src/drivers/db/migrations/M211218163000JobQueueSize.php @@ -24,14 +24,14 @@ class M211218163000JobQueueSize extends Migration public function up(): void { if ($this->db->driverName === 'mysql') { - $this->alterColumn('{{%queue}}', 'job', 'LONGBLOB NOT NULL'); + $this->alterColumn($this->tableName, 'job', 'LONGBLOB NOT NULL'); } } public function down(): void { if ($this->db->driverName === 'mysql') { - $this->alterColumn('{{%queue}}', 'job', $this->binary()->notNull()); + $this->alterColumn($this->tableName, 'job', $this->binary()->notNull()); } } } diff --git a/src/drivers/file/Command.php b/src/drivers/file/Command.php index b6b8bcd9f2..222b644b07 100644 --- a/src/drivers/file/Command.php +++ b/src/drivers/file/Command.php @@ -23,6 +23,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; /** @@ -95,9 +97,9 @@ public function actionClear(): void * @throws Exception when the job is not found. * @since 2.0.1 */ - public function actionRemove($id) + public function actionRemove(int $id): void { - if (!$this->queue->remove((int) $id)) { + if (!$this->queue->remove($id)) { throw new Exception('The job is not found.'); } } diff --git a/src/drivers/file/InfoAction.php b/src/drivers/file/InfoAction.php index cd4b649a45..8c786fdecf 100644 --- a/src/drivers/file/InfoAction.php +++ b/src/drivers/file/InfoAction.php @@ -10,6 +10,7 @@ namespace yii\queue\file; +use yii\helpers\BaseConsole; use yii\helpers\Console; use yii\queue\cli\Action; use yii\queue\cli\Queue as CliQueue; @@ -23,6 +24,8 @@ class InfoAction extends Action { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; @@ -31,18 +34,18 @@ class InfoAction extends Action */ public function run(): void { - Console::output($this->format('Jobs', Console::FG_GREEN)); + Console::output($this->format('Jobs', BaseConsole::FG_GREEN)); - Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); + Console::stdout($this->format('- waiting: ', BaseConsole::FG_YELLOW)); Console::output((string)$this->getWaitingCount()); - Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); + Console::stdout($this->format('- delayed: ', BaseConsole::FG_YELLOW)); Console::output((string)$this->getDelayedCount()); - Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); + Console::stdout($this->format('- reserved: ', BaseConsole::FG_YELLOW)); Console::output((string)$this->getReservedCount()); - Console::stdout($this->format('- done: ', Console::FG_YELLOW)); + Console::stdout($this->format('- done: ', BaseConsole::FG_YELLOW)); Console::output((string)$this->getDoneCount()); } @@ -83,7 +86,10 @@ protected function getDoneCount(): int return $total - $this->getDelayedCount() - $this->getWaitingCount(); } - protected function getIndexData() + /** + * @return array|mixed + */ + protected function getIndexData(): mixed { static $data; if ($data === null) { diff --git a/src/drivers/file/Queue.php b/src/drivers/file/Queue.php index 76326b47be..b415972adf 100644 --- a/src/drivers/file/Queue.php +++ b/src/drivers/file/Queue.php @@ -55,9 +55,12 @@ class Queue extends CliQueue public function init(): void { parent::init(); - $this->path = Yii::getAlias($this->path); + $alias = Yii::getAlias($this->path); + if (false !== $alias) { + $this->path = $alias; + } if (!is_dir($this->path)) { - FileHelper::createDirectory($this->path, $this->dirMode, true); + FileHelper::createDirectory($this->path, $this->dirMode); } } @@ -65,7 +68,7 @@ public function init(): void * Listens queue and runs each job. * * @param bool $repeat whether to continue listening when queue is empty. - * @param int $timeout number of seconds to sleep before next iteration. + * @param int<0, max> $timeout number of seconds to sleep before next iteration. * @return null|int exit code. * @internal for worker command only. * @since 2.0.2 @@ -111,8 +114,7 @@ public function status($id): int */ public function clear(): void { - $this->touchIndex(function (&$data) { - $data = []; + $this->touchIndex(function () { foreach (glob("$this->path/job*.data") as $fileName) { unlink($fileName); } @@ -126,10 +128,10 @@ public function clear(): void * @return bool * @since 2.0.1 */ - public function remove($id) + public function remove(int $id): bool { $removed = false; - $this->touchIndex(function (&$data) use ($id, &$removed) { + $this->touchIndex(function (array &$data) use ($id, &$removed) { if (!empty($data['waiting'])) { foreach ($data['waiting'] as $key => $payload) { if ($payload[0] === $id) { @@ -170,16 +172,16 @@ public function remove($id) * * @return array|null payload */ - protected function reserve() + protected function reserve(): ?array { $id = null; $ttr = null; $attempt = null; - $this->touchIndex(function (&$data) use (&$id, &$ttr, &$attempt) { + $this->touchIndex(function (array &$data) use (&$id, &$ttr, &$attempt) { if (!empty($data['reserved'])) { foreach ($data['reserved'] as $key => $payload) { if ($payload[1] + $payload[3] < time()) { - list($id, $ttr, $attempt, $time) = $payload; + [$id, $ttr, $attempt, $time] = $payload; $data['reserved'][$key][2] = ++$attempt; $data['reserved'][$key][3] = time(); return; @@ -188,7 +190,7 @@ protected function reserve() } if (!empty($data['delayed']) && $data['delayed'][0][2] <= time()) { - list($id, $ttr, $time) = array_shift($data['delayed']); + [$id, $ttr, $time] = array_shift($data['delayed']); } elseif (!empty($data['waiting'])) { [$id, $ttr] = array_shift($data['waiting']); } @@ -213,7 +215,7 @@ protected function reserve() protected function delete(array $payload): void { $id = $payload[0]; - $this->touchIndex(function (&$data) use ($id) { + $this->touchIndex(function (array &$data) use ($id) { foreach ($data['reserved'] as $key => $payload) { if ($payload[0] === $id) { unset($data['reserved'][$key]); @@ -233,7 +235,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri throw new NotSupportedException('Job priority is not supported in the driver.'); } - $this->touchIndex(function (&$data) use ($payload, $ttr, $delay, &$id) { + $this->touchIndex(function (array &$data) use ($payload, $ttr, $delay, &$id) { if (!isset($data['lastId'])) { $data['lastId'] = 0; } @@ -247,7 +249,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $data['waiting'][] = [$id, $ttr, 0]; } else { $data['delayed'][] = [$id, $ttr, time() + $delay]; - usort($data['delayed'], function ($a, $b) { + usort($data['delayed'], static function ($a, $b) { if ($a[2] < $b[2]) { return -1; } @@ -272,7 +274,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri * @param callable $callback * @throws InvalidConfigException */ - private function touchIndex($callback) + private function touchIndex(callable $callback): void { $fileName = "$this->path/index.data"; $isNew = !file_exists($fileName); diff --git a/src/drivers/gearman/Command.php b/src/drivers/gearman/Command.php index 745710d4b7..f16c47c4e4 100644 --- a/src/drivers/gearman/Command.php +++ b/src/drivers/gearman/Command.php @@ -22,6 +22,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; diff --git a/src/drivers/gearman/Queue.php b/src/drivers/gearman/Queue.php index 90d276faac..9c47d5162f 100644 --- a/src/drivers/gearman/Queue.php +++ b/src/drivers/gearman/Queue.php @@ -31,6 +31,8 @@ class Queue extends CliQueue */ public string $commandClass = Command::class; + private ?GearmanClient $client = null; + /** * Listens queue and runs each job. * @@ -45,6 +47,7 @@ public function run(bool $repeat): ?int $worker = new GearmanWorker(); $worker->addServer($this->host, $this->port); $worker->addFunction($this->channel, function (GearmanJob $payload) { + /** @psalm-suppress PossiblyUndefinedArrayOffset */ [$ttr, $message] = explode(';', $payload->workload(), 2); $this->handleMessage($payload->handle(), $message, (int)$ttr, 1); }); @@ -79,7 +82,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri */ public function status($id): int { - $status = $this->getClient()->jobStatus($id); + $status = $this->getClient()->jobStatus((string)$id); if ($status[0] && !$status[1]) { return self::STATUS_WAITING; } @@ -97,12 +100,10 @@ public function status($id): int */ protected function getClient(): GearmanClient { - if (!$this->_client) { - $this->_client = new GearmanClient(); - $this->_client->addServer($this->host, $this->port); + if (!$this->client) { + $this->client = new GearmanClient(); + $this->client->addServer($this->host, $this->port); } - return $this->_client; + return $this->client; } - - private $_client; } diff --git a/src/drivers/redis/Command.php b/src/drivers/redis/Command.php index 8b6e862e17..95ed09e45d 100644 --- a/src/drivers/redis/Command.php +++ b/src/drivers/redis/Command.php @@ -23,6 +23,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; /** diff --git a/src/drivers/redis/InfoAction.php b/src/drivers/redis/InfoAction.php index 385a097f26..213e4cc288 100644 --- a/src/drivers/redis/InfoAction.php +++ b/src/drivers/redis/InfoAction.php @@ -23,6 +23,8 @@ class InfoAction extends Action { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; diff --git a/src/drivers/redis/Queue.php b/src/drivers/redis/Queue.php index 5396227ae4..b1ecc9f439 100644 --- a/src/drivers/redis/Queue.php +++ b/src/drivers/redis/Queue.php @@ -25,6 +25,8 @@ class Queue extends CliQueue { /** * @var Connection|array|string + * @psalm-var Connection + * @psalm-suppress InvalidPropertyAssignmentValue */ public Connection|string|array $redis = 'redis'; /** @@ -42,6 +44,7 @@ class Queue extends CliQueue public function init(): void { parent::init(); + /** @psalm-suppress PropertyTypeCoercion */ $this->redis = Instance::ensure($this->redis, Connection::class); } @@ -155,6 +158,7 @@ protected function reserve(int $timeout): ?array return null; } + /** @psalm-suppress PossiblyUndefinedArrayOffset */ [$ttr, $message] = explode(';', $payload, 2); $this->redis->zadd("$this->channel.reserved", time() + (int)$ttr, $id); $attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1); diff --git a/src/drivers/sqs/Command.php b/src/drivers/sqs/Command.php index 3597e0af50..cc9998768e 100644 --- a/src/drivers/sqs/Command.php +++ b/src/drivers/sqs/Command.php @@ -24,6 +24,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; diff --git a/src/drivers/sqs/Queue.php b/src/drivers/sqs/Queue.php index 21aad3237d..c7127db8d4 100644 --- a/src/drivers/sqs/Queue.php +++ b/src/drivers/sqs/Queue.php @@ -29,17 +29,17 @@ class Queue extends CliQueue * The SQS url. * @var string */ - public string $url; + public string $url = 'localhost'; /** * aws access key. * @var string|null */ - public ?string $key; + public ?string $key = null; /** * aws secret. * @var string|null */ - public ?string $secret; + public ?string $secret = null; /** * region where queue is hosted. * @var string @@ -195,7 +195,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri 'MessageAttributes' => [ 'TTR' => [ 'DataType' => 'Number', - 'StringValue' => $ttr, + 'StringValue' => (string) $ttr, ], ], ]; diff --git a/src/drivers/stomp/Command.php b/src/drivers/stomp/Command.php index 87e70bd256..312eac87fb 100644 --- a/src/drivers/stomp/Command.php +++ b/src/drivers/stomp/Command.php @@ -24,6 +24,8 @@ class Command extends CliCommand { /** * @var Queue + * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor */ public CliQueue $queue; diff --git a/src/drivers/stomp/Queue.php b/src/drivers/stomp/Queue.php index e04ec302c7..a98eaad35b 100644 --- a/src/drivers/stomp/Queue.php +++ b/src/drivers/stomp/Queue.php @@ -16,7 +16,7 @@ use Enqueue\Stomp\StompMessage; use Interop\Queue\Exception as QueueException; use Interop\Queue\Message; -use Interop\Queue\Queue as InteropQueue; +use Stomp\Network\Connection; use yii\base\Application as BaseApp; use yii\base\Event; use yii\base\NotSupportedException; @@ -37,7 +37,7 @@ class Queue extends CliQueue * * @var string|null */ - public ?string $host; + public ?string $host = null; /** * The message queue broker's port. * @@ -124,12 +124,8 @@ public function init(): void /** * Opens connection. */ - protected function open(): void + protected function open(): StompContext { - if ($this->context) { - return; - } - $config = [ 'host' => $this->host, 'port' => $this->port, @@ -147,16 +143,14 @@ protected function open(): void return null !== $value; }); - $factory = new StompConnectionFactory($config); - - $this->context = $factory->createContext(); + return (new StompConnectionFactory($config))->createContext(); } /** * Listens queue and runs each job. * * @param bool $repeat - * @param int $timeout + * @param int<0, max> $timeout * @return int|null */ public function run(bool $repeat, int $timeout = 0): ?int @@ -164,11 +158,12 @@ public function run(bool $repeat, int $timeout = 0): ?int return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { $this->open(); $queue = $this->createQueue($this->queueName); - $consumer = $this->context->createConsumer($queue); + $consumer = $this->getContext()->createConsumer($queue); while ($canContinue()) { + /** @var StompMessage|null $message */ $message = $this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait(); - if ($message) { + if (null !== $message) { $messageId = $message->getMessageId(); if (!$messageId) { $message = $this->setMessageId($message); @@ -184,8 +179,9 @@ public function run(bool $repeat, int $timeout = 0): ?int $ttr = $message->getProperty(self::TTR, $this->ttr); $attempt = $message->getProperty(self::ATTEMPT, 1); + $messageId = $message->getMessageId(); - if ($this->handleMessage($message->getMessageId(), $message->getBody(), $ttr, $attempt)) { + if (null !== $messageId && $this->handleMessage($messageId, $message->getBody(), $ttr, $attempt)) { $consumer->acknowledge($message); } else { $consumer->acknowledge($message); @@ -196,7 +192,7 @@ public function run(bool $repeat, int $timeout = 0): ?int break; } elseif ($timeout) { sleep($timeout); - $this->context->getStomp()->getConnection()?->sendAlive(); + $this->getConnection()->sendAlive(); } } }); @@ -222,13 +218,13 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $this->open(); $queue = $this->createQueue($this->queueName); - $message = $this->context->createMessage($payload); + $message = $this->getContext()->createMessage($payload); $message = $this->setMessageId($message); $message->setPersistent(true); $message->setProperty(self::ATTEMPT, 1); $message->setProperty(self::TTR, $ttr); - $producer = $this->context->createProducer(); + $producer = $this->getContext()->createProducer(); if ($delay) { throw new NotSupportedException('Delayed work is not supported in the driver.'); @@ -273,14 +269,14 @@ protected function redeliver(StompMessage $message): void { $attempt = $message->getProperty(self::ATTEMPT, 1); - $newMessage = $this->context->createMessage( + $newMessage = $this->getContext()->createMessage( $message->getBody(), $message->getProperties(), $message->getHeaders() ); $newMessage->setProperty(self::ATTEMPT, ++$attempt); - $this->context->createProducer()->send( + $this->getContext()->createProducer()->send( $this->createQueue($this->queueName), $newMessage ); @@ -288,15 +284,29 @@ protected function redeliver(StompMessage $message): void /** * @param string $name - * @return InteropQueue|StompDestination + * @return StompDestination */ - private function createQueue(string $name): InteropQueue|StompDestination + private function createQueue(string $name): StompDestination { - $queue = $this->context->createQueue($name); + $queue = $this->getContext()->createQueue($name); $queue->setDurable(true); $queue->setAutoDelete(false); $queue->setExclusive(false); return $queue; } + + private function getContext(): StompContext + { + if (null === $this->context) { + $this->context = $this->open(); + } + + return $this->context; + } + + private function getConnection(): Connection + { + return $this->getContext()->getStomp()->getConnection(); + } } diff --git a/src/gii/Generator.php b/src/gii/Generator.php index a07fd4414e..3e67cd5798 100644 --- a/src/gii/Generator.php +++ b/src/gii/Generator.php @@ -13,6 +13,7 @@ use Yii; use yii\base\BaseObject; use yii\gii\CodeFile; +use yii\gii\Generator as BaseGenerator; use yii\queue\JobInterface; use yii\queue\RetryableJobInterface; @@ -21,7 +22,7 @@ * * @author Roman Zhuravlev */ -class Generator extends \yii\gii\Generator +class Generator extends BaseGenerator { public $jobClass; public $properties; @@ -32,7 +33,7 @@ class Generator extends \yii\gii\Generator /** * @inheritdoc */ - public function getName() + public function getName(): string { return 'Job Generator'; } @@ -40,7 +41,7 @@ public function getName() /** * @inheritdoc */ - public function getDescription() + public function getDescription(): string { return 'This generator generates a Job class for the queue.'; } @@ -48,7 +49,7 @@ public function getDescription() /** * @inheritdoc */ - public function rules() + public function rules(): array { return array_merge(parent::rules(), [ [['jobClass', 'properties', 'ns', 'baseClass'], 'trim'], @@ -65,7 +66,7 @@ public function rules() /** * @inheritdoc */ - public function attributeLabels() + public function attributeLabels(): array { return array_merge(parent::attributeLabels(), [ 'jobClass' => 'Job Class', @@ -79,7 +80,7 @@ public function attributeLabels() /** * @inheritdoc */ - public function hints() + public function hints(): array { return array_merge(parent::hints(), [ 'jobClass' => 'This is the name of the Job class to be generated, e.g., SomeJob.', @@ -93,7 +94,7 @@ public function hints() /** * @inheritdoc */ - public function stickyAttributes() + public function stickyAttributes(): array { return array_merge(parent::stickyAttributes(), ['ns', 'baseClass']); } @@ -101,7 +102,7 @@ public function stickyAttributes() /** * @inheritdoc */ - public function requiredTemplates() + public function requiredTemplates(): array { return ['job.php']; } @@ -109,7 +110,7 @@ public function requiredTemplates() /** * @inheritdoc */ - public function generate() + public function generate(): array { $params = []; $params['jobClass'] = $this->jobClass; @@ -140,7 +141,7 @@ public function generate() * * @param string $attribute job attribute name. */ - public function validateJobClass($attribute) + public function validateJobClass(string $attribute): void { if ($this->isReservedKeyword($this->$attribute)) { $this->addError($attribute, 'Class name cannot be a reserved PHP keyword.'); @@ -152,7 +153,7 @@ public function validateJobClass($attribute) * * @param string $attribute Namespace attribute name. */ - public function validateNamespace($attribute) + public function validateNamespace(string $attribute): void { $value = $this->$attribute; $value = ltrim($value, '\\'); diff --git a/src/serializers/IgbinarySerializer.php b/src/serializers/IgbinarySerializer.php index 15fb9bda4a..21db11fdba 100644 --- a/src/serializers/IgbinarySerializer.php +++ b/src/serializers/IgbinarySerializer.php @@ -33,7 +33,7 @@ public function serialize($job): string /** * @inheritdoc */ - public function unserialize($serialized) + public function unserialize(string $serialized) { return igbinary_unserialize($serialized); } diff --git a/src/serializers/JsonSerializer.php b/src/serializers/JsonSerializer.php index 2500263db2..0cc85a91eb 100644 --- a/src/serializers/JsonSerializer.php +++ b/src/serializers/JsonSerializer.php @@ -42,7 +42,7 @@ public function serialize($job): string /** * @inheritdoc */ - public function unserialize($serialized) + public function unserialize(string $serialized) { return $this->fromArray(Json::decode($serialized)); } @@ -52,7 +52,7 @@ public function unserialize($serialized) * @return array|mixed * @throws InvalidConfigException */ - protected function toArray($data) + protected function toArray(mixed $data) { if (is_object($data)) { $result = [$this->classKey => get_class($data)]; @@ -82,10 +82,10 @@ protected function toArray($data) } /** - * @param array $data + * @param mixed $data * @return mixed */ - protected function fromArray($data) + protected function fromArray(mixed $data) { if (!is_array($data)) { return $data; diff --git a/src/serializers/PhpSerializer.php b/src/serializers/PhpSerializer.php index d4fb4bc7c5..ac3e05791b 100644 --- a/src/serializers/PhpSerializer.php +++ b/src/serializers/PhpSerializer.php @@ -30,7 +30,7 @@ public function serialize($job): string /** * @inheritdoc */ - public function unserialize($serialized) + public function unserialize(string $serialized) { return unserialize($serialized); } diff --git a/src/serializers/SerializerInterface.php b/src/serializers/SerializerInterface.php index d34faad5b7..d10b668a5b 100644 --- a/src/serializers/SerializerInterface.php +++ b/src/serializers/SerializerInterface.php @@ -29,5 +29,5 @@ public function serialize($job): string; * @param string $serialized * @return JobInterface */ - public function unserialize($serialized); + public function unserialize(string $serialized); } diff --git a/stubs/psalm/BaseYii.php b/stubs/psalm/BaseYii.php new file mode 100644 index 0000000000..059e8373bd --- /dev/null +++ b/stubs/psalm/BaseYii.php @@ -0,0 +1,18 @@ +|array{class: class-string}|callable(): T $type + * @param array $params + * + * @return T + */ + abstract public static function createObject($type, array $params = []); +} diff --git a/tests/app/RetryJob.php b/tests/app/RetryJob.php index 2a8c0e8bda..1fa8384344 100644 --- a/tests/app/RetryJob.php +++ b/tests/app/RetryJob.php @@ -11,6 +11,7 @@ namespace tests\app; use Exception; +use Throwable; use Yii; use yii\base\BaseObject; use yii\queue\Queue; @@ -41,7 +42,7 @@ public function getTtr(): int return 2; } - public function canRetry($attempt, $error): bool + public function canRetry($attempt, ?Throwable $error): bool { return $attempt < 2; } diff --git a/tests/app/SimpleJob.php b/tests/app/SimpleJob.php index 1e7a669aeb..5fac7a062b 100644 --- a/tests/app/SimpleJob.php +++ b/tests/app/SimpleJob.php @@ -31,6 +31,6 @@ public function execute(Queue $queue): void public function getFileName(): bool|string { - return Yii::getAlias("@runtime/job-{$this->uid}.lock"); + return Yii::getAlias("@runtime/job-$this->uid.lock"); } } diff --git a/tests/app/benchmark/Controller.php b/tests/app/benchmark/Controller.php index 265e2ad4f8..65b66ca4c4 100644 --- a/tests/app/benchmark/Controller.php +++ b/tests/app/benchmark/Controller.php @@ -43,7 +43,7 @@ public function beforeAction($action): bool public function afterAction($action, $result) { $duration = time() - $this->startedAt; - $this->stdout("\nCompleted in {$duration} s.\n"); + $this->stdout("\nCompleted in $duration s.\n"); return parent::afterAction($action, $result); } } diff --git a/tests/app/config/main.php b/tests/app/config/main.php index 7b719c9462..cf47d8faf7 100644 --- a/tests/app/config/main.php +++ b/tests/app/config/main.php @@ -117,6 +117,9 @@ 'stompQueue' => [ 'class' => StompQueue::class, 'host' => getenv('ACTIVEMQ_HOST') ?: 'localhost', + 'port' => getenv('ACTIVEMQ_PORT'), + 'user' => getenv('ACTIVEMQ_USER'), + 'password' => getenv('ACTIVEMQ_PASSWORD'), ], 'sqsQueue' => [ 'class' => SqsQueue::class, diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index ddcb8e5089..3c641de34a 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -37,6 +37,9 @@ services: BEANSTALK_HOST: beanstalk GEARMAN_HOST: gearmand ACTIVEMQ_HOST: activemq + ACTIVEMQ_USER: artemis + ACTIVEMQ_PASSWORD: artemis + ACTIVEMQ_PORT: 61616 AWS_KEY: ${AWS_KEY:-admin} AWS_SECRET: ${AWS_SECRET:-admin} AWS_REGION: ${AWS_REGION:-us-east-1} @@ -115,11 +118,12 @@ services: networks: net: {} - # https://hub.docker.com/r/webcenter/activemq/ + # https://hub.docker.com/r/apache/activemq-artemis activemq: - image: webcenter/activemq + image: apache/activemq-artemis:latest-alpine ports: - - "61613:61613" + - "61616:61616" + - "8161:8161" networks: net: {} diff --git a/tests/drivers/beanstalk/QueueTest.php b/tests/drivers/beanstalk/QueueTest.php index 61491c03df..3837174749 100644 --- a/tests/drivers/beanstalk/QueueTest.php +++ b/tests/drivers/beanstalk/QueueTest.php @@ -10,11 +10,11 @@ namespace tests\drivers\beanstalk; -use Exception; use Pheanstalk\Pheanstalk; use Pheanstalk\Values\JobId; use tests\app\PriorityJob; use tests\drivers\CliTestCase; +use Throwable; use Yii; use yii\queue\beanstalk\Queue; @@ -134,7 +134,7 @@ protected function jobIsExists(int|string|null $id): bool try { $connection->peek(new JobId($id)); return true; - } catch (\Throwable) { + } catch (Throwable) { return false; } } diff --git a/tests/drivers/db/TestCase.php b/tests/drivers/db/TestCase.php index 4e8e09de7b..61b36544ad 100644 --- a/tests/drivers/db/TestCase.php +++ b/tests/drivers/db/TestCase.php @@ -76,7 +76,7 @@ public function testLater(): void public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); diff --git a/tests/drivers/file/QueueTest.php b/tests/drivers/file/QueueTest.php index fb6fe7807a..5fda9c247b 100644 --- a/tests/drivers/file/QueueTest.php +++ b/tests/drivers/file/QueueTest.php @@ -64,7 +64,7 @@ public function testLater(): void public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); diff --git a/tests/drivers/redis/QueueTest.php b/tests/drivers/redis/QueueTest.php index 29ca2c6e38..48ac8ff47e 100644 --- a/tests/drivers/redis/QueueTest.php +++ b/tests/drivers/redis/QueueTest.php @@ -64,7 +64,7 @@ public function testLater(): void public function testRetry(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); - $job = new RetryJob(['uid' => uniqid()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); diff --git a/tests/drivers/stomp/QueueTest.php b/tests/drivers/stomp/QueueTest.php index 105b1aa4bd..f6df3369ce 100644 --- a/tests/drivers/stomp/QueueTest.php +++ b/tests/drivers/stomp/QueueTest.php @@ -13,9 +13,10 @@ use tests\app\RetryJob; use tests\drivers\CliTestCase; use Yii; +use yii\base\NotSupportedException; use yii\queue\stomp\Queue; -class QueueTest extends CliTestCase +final class QueueTest extends CliTestCase { public function testListen(): void { @@ -37,12 +38,16 @@ public function testRetry(): void $this->assertEquals('aa', file_get_contents($job->getFileName())); } - /** - * @return Queue - */ + public function testStatus(): void + { + $this->expectException(NotSupportedException::class); + + $id = $this->getQueue()->push($this->createSimpleJob()); + $this->getQueue()->isWaiting($id); + } + protected function getQueue(): Queue { return Yii::$app->stompQueue; } - } diff --git a/tests/serializers/TestCase.php b/tests/serializers/TestCase.php index 57d5a13523..8e74012a45 100644 --- a/tests/serializers/TestCase.php +++ b/tests/serializers/TestCase.php @@ -31,7 +31,7 @@ abstract protected function createSerializer(): SerializerInterface; * @dataProvider providerSerialize * @param mixed $expected */ - public function testSerialize($expected): void + public function testSerialize(mixed $expected): void { $serializer = $this->createSerializer(); @@ -80,6 +80,6 @@ public static function providerSerialize(): array class TestObject extends BaseObject { - public $foo; - public $bar; + public int $foo; + public array $bar; } From 68bac915195c534a4a20481c8479439b6d465c4c Mon Sep 17 00:00:00 2001 From: Moiseenko Date: Fri, 24 Nov 2023 20:46:49 +0300 Subject: [PATCH 05/16] Added PHP 8.3 to CI runners --- .github/workflows/main.yml | 2 +- .github/workflows/static.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8dadc45d9a..eb0b977231 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.1', '8.2', '8.3' ] steps: - name: Checkout. uses: actions/checkout@v2 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index be42e8f9a2..7e37c591d5 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.1', '8.2', '8.3' ] steps: - name: Checkout. uses: actions/checkout@v2 From 032da0715bac89b5ffaa93ab68d429fa15c3d695 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Mon, 4 Dec 2023 17:42:28 +0300 Subject: [PATCH 06/16] Get Psalm error level to 1 (#505) --- psalm.xml | 10 ++++-- src/JobEvent.php | 4 +-- src/LogBehavior.php | 18 ++++++++-- src/Queue.php | 2 +- src/cli/Action.php | 1 + src/cli/Command.php | 9 +++-- src/cli/Queue.php | 13 +++---- src/cli/SignalLoop.php | 6 ++-- src/cli/VerboseBehavior.php | 11 +++--- src/closure/Behavior.php | 3 +- src/closure/Job.php | 6 +++- src/debug/Panel.php | 14 +++++--- src/debug/views/detail.php | 4 ++- src/debug/views/summary.php | 5 ++- src/drivers/amqp_interop/Command.php | 3 +- src/drivers/amqp_interop/Queue.php | 14 ++++---- src/drivers/beanstalk/Command.php | 4 +-- src/drivers/beanstalk/InfoAction.php | 2 +- src/drivers/beanstalk/Queue.php | 2 +- src/drivers/db/Command.php | 3 +- src/drivers/db/InfoAction.php | 3 +- src/drivers/db/Queue.php | 4 ++- src/drivers/file/Command.php | 3 +- src/drivers/file/InfoAction.php | 7 ++-- src/drivers/file/Queue.php | 33 +++++++++++++++-- src/drivers/gearman/Command.php | 3 +- src/drivers/redis/Command.php | 3 +- src/drivers/redis/InfoAction.php | 13 ++++--- src/drivers/redis/Queue.php | 12 ++++++- src/drivers/sqs/Command.php | 3 +- src/drivers/sqs/Payload.php | 47 +++++++++++++++++++++++++ src/drivers/sqs/Queue.php | 38 +++++++++++--------- src/drivers/stomp/Command.php | 3 +- src/drivers/stomp/Queue.php | 6 ++-- src/drivers/sync/Queue.php | 7 +++- src/gii/Generator.php | 18 ++++++---- src/gii/default/job.php | 8 +++-- src/gii/form.php | 11 ++++-- src/serializers/IgbinarySerializer.php | 2 +- src/serializers/JsonSerializer.php | 2 +- src/serializers/PhpSerializer.php | 2 +- src/serializers/SerializerInterface.php | 2 +- 42 files changed, 253 insertions(+), 111 deletions(-) create mode 100644 src/drivers/sqs/Payload.php diff --git a/psalm.xml b/psalm.xml index c9de05bc3b..d921097324 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,6 +1,6 @@ - - + + + @@ -24,4 +25,7 @@ + + + diff --git a/src/JobEvent.php b/src/JobEvent.php index e994161efd..fab6221506 100644 --- a/src/JobEvent.php +++ b/src/JobEvent.php @@ -25,9 +25,9 @@ abstract class JobEvent extends Event */ public $name = ''; /** - * @var Queue|null|object + * @var Queue * @inheritdoc - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public $sender; /** diff --git a/src/LogBehavior.php b/src/LogBehavior.php index 5f44acf4af..08df14df87 100644 --- a/src/LogBehavior.php +++ b/src/LogBehavior.php @@ -97,9 +97,14 @@ public function afterError(ExecEvent $event): void */ public function workerStart(cli\WorkerEvent $event): void { - $title = 'Worker ' . $event->sender->getWorkerPid(); + $workerPid = $event->sender->getWorkerPid(); + if (null === $workerPid) { + $workerPid = '{PID not found}'; + } + $title = 'Worker ' . $workerPid; Yii::info("$title is started.", Queue::class); Yii::beginProfile($title, Queue::class); + if ($this->autoFlush) { Yii::getLogger()->flush(true); } @@ -111,9 +116,14 @@ public function workerStart(cli\WorkerEvent $event): void */ public function workerStop(cli\WorkerEvent $event): void { - $title = 'Worker ' . $event->sender->getWorkerPid(); + $workerPid = $event->sender->getWorkerPid(); + if (null === $workerPid) { + $workerPid = '{PID not found}'; + } + $title = 'Worker ' . $workerPid; Yii::endProfile($title, Queue::class); Yii::info("$title is stopped.", Queue::class); + if ($this->autoFlush) { Yii::getLogger()->flush(true); } @@ -139,7 +149,9 @@ protected function getExecTitle(ExecEvent $event): string { $title = $this->getJobTitle($event); $extra = "attempt: $event->attempt"; - if ($pid = $event->sender?->getWorkerPid()) { + + $pid = $event->sender->getWorkerPid(); + if (null !== $pid) { $extra .= ", PID: $pid"; } return "$title ($extra)"; diff --git a/src/Queue.php b/src/Queue.php index 93c01f7448..212886fa95 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -224,7 +224,7 @@ protected function handleMessage(int|string $id, string $message, int $ttr, int return $this->handleError($event); } try { - /** @psalm-suppress PossiblyUndefinedMethod */ + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ $event->result = $event->job?->execute($this); } catch (Throwable $error) { $event->error = $error; diff --git a/src/cli/Action.php b/src/cli/Action.php index 5e90ac7b80..081a6a159b 100644 --- a/src/cli/Action.php +++ b/src/cli/Action.php @@ -51,6 +51,7 @@ public function init(): void /** * @param string $string * @return string + * @psalm-suppress MixedInferredReturnType, MixedReturnStatement */ protected function format(string $string): string { diff --git a/src/cli/Command.php b/src/cli/Command.php index f5fd0e9dac..af7cd5e756 100644 --- a/src/cli/Command.php +++ b/src/cli/Command.php @@ -124,8 +124,12 @@ public function beforeAction($action): bool if ($this->phpBinary === null) { $this->phpBinary = PHP_BINARY; } - /** @psalm-suppress MissingClosureReturnType */ - $this->queue->messageHandler = function (int|string|null $id, string $message, int $ttr, int $attempt) { + $this->queue->messageHandler = function ( + int|string|null $id, + string $message, + int $ttr, + int $attempt + ): bool { return $this->handleMessage($id, $message, $ttr, $attempt); }; } @@ -178,6 +182,7 @@ protected function handleMessage(int|string|null $id, string $message, ?int $ttr foreach ($this->getPassedOptions() as $name) { if (in_array($name, $this->options('exec'), true)) { + /** @psalm-suppress MixedOperand */ $cmd[] = '--' . $name . '=' . $this->$name; } } diff --git a/src/cli/Queue.php b/src/cli/Queue.php index 9dac8a5bdd..c041dfff1e 100644 --- a/src/cli/Queue.php +++ b/src/cli/Queue.php @@ -63,7 +63,7 @@ abstract class Queue extends BaseQueue implements BootstrapInterface * @var int|null current process ID of a worker. * @since 2.0.2 */ - private ?int $_workerPid = null; + private ?int $workerPid = null; /** * @return string command id @@ -72,7 +72,7 @@ protected function getCommandId(): string { foreach (Yii::$app->getComponents(false) as $id => $component) { if ($component === $this) { - return Inflector::camel2id($id); + return Inflector::camel2id((string)$id); } } throw new InvalidConfigException('Queue must be an application component.'); @@ -100,7 +100,7 @@ public function bootstrap($app): void */ protected function runWorker(callable $handler): ?int { - $this->_workerPid = getmypid(); + $this->workerPid = getmypid(); /** @var LoopInterface $loop */ $loop = Yii::createObject($this->loopConfig, [$this]); @@ -117,7 +117,7 @@ protected function runWorker(callable $handler): ?int }); } finally { $this->trigger(self::EVENT_WORKER_STOP, $event); - $this->_workerPid = null; + $this->workerPid = null; } return $event->exitCode; @@ -132,11 +132,12 @@ protected function runWorker(callable $handler): ?int */ public function getWorkerPid(): ?int { - return $this->_workerPid; + return $this->workerPid; } /** * @inheritdoc + * @psalm-suppress MixedReturnStatement, MixedInferredReturnType */ protected function handleMessage(int|string $id, string $message, int $ttr, int $attempt): bool { @@ -158,7 +159,7 @@ protected function handleMessage(int|string $id, string $message, int $ttr, int */ public function execute(string $id, string $message, int $ttr, int $attempt, ?int $workerPid): bool { - $this->_workerPid = $workerPid; + $this->workerPid = $workerPid; return parent::handleMessage($id, $message, $ttr, $attempt); } } diff --git a/src/cli/SignalLoop.php b/src/cli/SignalLoop.php index 46d8897ae6..5165117f31 100644 --- a/src/cli/SignalLoop.php +++ b/src/cli/SignalLoop.php @@ -21,7 +21,7 @@ class SignalLoop extends BaseObject implements LoopInterface { /** - * @var array of signals to exit from listening of the queue. + * @var array of signals to exit from listening of the queue. */ public array $exitSignals = [ 15, // SIGTERM @@ -30,12 +30,12 @@ class SignalLoop extends BaseObject implements LoopInterface 1, // SIGHUP ]; /** - * @var array of signals to suspend listening of the queue. + * @var array of signals to suspend listening of the queue. * For example: SIGTSTP */ public array $suspendSignals = []; /** - * @var array of signals to resume listening of the queue. + * @var array of signals to resume listening of the queue. * For example: SIGCONT */ public array $resumeSignals = []; diff --git a/src/cli/VerboseBehavior.php b/src/cli/VerboseBehavior.php index 184930bc7c..e4dd27613e 100644 --- a/src/cli/VerboseBehavior.php +++ b/src/cli/VerboseBehavior.php @@ -36,13 +36,13 @@ class VerboseBehavior extends Behavior public Controller $command; /** - * @var float|null timestamp + * @var float timestamp */ - private ?float $jobStartedAt = null; + private float $jobStartedAt = 0; /** - * @var int|null timestamp + * @var int timestamp */ - private ?int $workerStartedAt = null; + private int $workerStartedAt = 0; /** * @inheritdoc @@ -121,7 +121,8 @@ protected function jobTitle(ExecEvent $event): string { $name = $event->job instanceof JobInterface ? get_class($event->job) : 'unknown job'; $extra = "attempt: $event->attempt"; - if ($pid = $event->sender?->getWorkerPid()) { + $pid = $event->sender->getWorkerPid(); + if (null !== $pid) { $extra .= ", pid: $pid"; } return " [$event->id] $name ($extra)"; diff --git a/src/closure/Behavior.php b/src/closure/Behavior.php index dd0fe3183c..75b051181c 100644 --- a/src/closure/Behavior.php +++ b/src/closure/Behavior.php @@ -33,8 +33,7 @@ class Behavior extends \yii\base\Behavior { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public $owner; diff --git a/src/closure/Job.php b/src/closure/Job.php index 5bcab6984d..eb6897c7dc 100644 --- a/src/closure/Job.php +++ b/src/closure/Job.php @@ -10,6 +10,7 @@ namespace yii\queue\closure; +use Laravel\SerializableClosure\SerializableClosure; use Laravel\SerializableClosure\Serializers\Native; use yii\queue\JobInterface; use yii\queue\Queue; @@ -32,13 +33,16 @@ class Job implements JobInterface */ public function execute(Queue $queue) { - $closure = unserialize($this->serialized)->getClosure(); + /** @var SerializableClosure $unserialize */ + $unserialize = unserialize($this->serialized); + $closure = $unserialize->getClosure(); $nativeClosure = $closure(); if ($nativeClosure instanceof Native) { return $nativeClosure(); } + /** @psalm-var JobInterface $nativeClosure */ return $nativeClosure->execute($queue); } } diff --git a/src/debug/Panel.php b/src/debug/Panel.php index bb35b86d3b..2dcabeb8df 100644 --- a/src/debug/Panel.php +++ b/src/debug/Panel.php @@ -14,6 +14,7 @@ use Yii; use yii\base\NotSupportedException; use yii\base\ViewContextInterface; +use yii\debug\Panel as BasePanel; use yii\helpers\VarDumper; use yii\queue\JobInterface; use yii\queue\PushEvent; @@ -23,10 +24,11 @@ * Debug Panel. * * @author Roman Zhuravlev + * @psalm-suppress PropertyNotSetInConstructor */ -class Panel extends \yii\debug\Panel implements ViewContextInterface +class Panel extends BasePanel implements ViewContextInterface { - private array $_jobs = []; + private array $jobs = []; /** * @inheritdoc @@ -43,7 +45,7 @@ public function init(): void { parent::init(); PushEvent::on(Queue::class, Queue::EVENT_AFTER_PUSH, function (PushEvent $event) { - $this->_jobs[] = $this->getPushData($event); + $this->jobs[] = $this->getPushData($event); }); } @@ -82,7 +84,7 @@ protected function getPushData(PushEvent $event): array */ public function save() { - return ['jobs' => $this->_jobs]; + return ['jobs' => $this->jobs]; } /** @@ -98,6 +100,7 @@ public function getViewPath(): string */ public function getSummary(): string { + /** @psalm-var array{jobs: array} $this->data */ return Yii::$app->view->render('summary', [ 'url' => $this->getUrl(), 'count' => isset($this->data['jobs']) ? count($this->data['jobs']) : 0, @@ -109,12 +112,15 @@ public function getSummary(): string */ public function getDetail(): string { + /** @psalm-var array{jobs: array} $this->data */ $jobs = $this->data['jobs'] ?? []; foreach ($jobs as &$job) { + /** @psalm-var array{sender: string, id: string|int} $job */ $job['status'] = 'unknown'; /** @var Queue $queue */ if ($queue = Yii::$app->get($job['sender'], false)) { try { + /** @psalm-var Queue $queue */ if ($queue->isWaiting($job['id'])) { $job['status'] = 'waiting'; } elseif ($queue->isReserved($job['id'])) { diff --git a/src/debug/views/detail.php b/src/debug/views/detail.php index 2957a45480..fc3480854c 100644 --- a/src/debug/views/detail.php +++ b/src/debug/views/detail.php @@ -3,10 +3,12 @@ declare(strict_types=1); /** - * @var \yii\web\View $this + * @var View $this * @var array $jobs */ + use yii\helpers\Html; +use yii\web\View; $styles = [ 'unknown' => 'default', diff --git a/src/debug/views/summary.php b/src/debug/views/summary.php index c87f88f441..4c25b7ffbc 100644 --- a/src/debug/views/summary.php +++ b/src/debug/views/summary.php @@ -3,10 +3,13 @@ declare(strict_types=1); /** - * @var \yii\web\View $this + * @var View $this * @var string $url * @var int $count */ + +use yii\web\View; + ?>
diff --git a/src/drivers/amqp_interop/Command.php b/src/drivers/amqp_interop/Command.php index e2007fd275..1dcd2b5cdf 100644 --- a/src/drivers/amqp_interop/Command.php +++ b/src/drivers/amqp_interop/Command.php @@ -23,8 +23,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/amqp_interop/Queue.php b/src/drivers/amqp_interop/Queue.php index d38ccaaa90..603e3d1caf 100644 --- a/src/drivers/amqp_interop/Queue.php +++ b/src/drivers/amqp_interop/Queue.php @@ -24,6 +24,7 @@ use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; use Interop\Queue\Context; +use LogicException; use yii\base\Application as BaseApp; use yii\base\Event; use yii\base\NotSupportedException; @@ -328,8 +329,7 @@ public function listen(): void $queue = $this->getContext()->createQueue($this->queueName); $consumer = $this->getContext()->createConsumer($queue); - /** @psalm-suppress MissingClosureReturnType */ - $callback = function (AmqpMessage $message, AmqpConsumer $consumer) { + $callback = function (AmqpMessage $message, AmqpConsumer $consumer): callable|bool { if ($message->isRedelivered()) { $consumer->acknowledge($message); @@ -338,8 +338,8 @@ public function listen(): void return true; } - $ttr = $message->getProperty(self::TTR); - $attempt = $message->getProperty(self::ATTEMPT, 1); + $ttr = (int)$message->getProperty(self::TTR); + $attempt = (int)$message->getProperty(self::ATTEMPT, 1); $messageId = $message->getMessageId(); if ( @@ -382,7 +382,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $topic = $this->getContext()->createTopic($this->exchangeName); - /** @var AmqpMessage $message */ + /** @psalm-var AmqpMessage $message */ $message = $this->getContext()->createMessage($payload); $message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); $message->setMessageId(uniqid('', true)); @@ -402,6 +402,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $producer->setDeliveryDelay($delay * 1000); } + /** @var int|null $priority */ if ($priority) { $message->setProperty(self::PRIORITY, $priority); $producer->setPriority($priority); @@ -438,7 +439,7 @@ protected function open(): void self::ENQUEUE_AMQP_LIB => AmqpLibConnectionFactory::class, self::ENQUEUE_AMQP_EXT => AmqpExtConnectionFactory::class, self::ENQUEUE_AMQP_BUNNY => AmqpBunnyConnectionFactory::class, - default => throw new \LogicException( + default => throw new LogicException( sprintf( 'The given driver "%s" is not supported. Drivers supported are "%s"', $this->driver, @@ -526,6 +527,7 @@ protected function close(): void protected function redeliver(AmqpMessage $message): void { + /** @var int $attempt */ $attempt = $message->getProperty(self::ATTEMPT, 1); /** @var AmqpMessage $newMessage */ diff --git a/src/drivers/beanstalk/Command.php b/src/drivers/beanstalk/Command.php index 757a500521..ebb54b4bf5 100644 --- a/src/drivers/beanstalk/Command.php +++ b/src/drivers/beanstalk/Command.php @@ -23,9 +23,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantPropertyType - * @psalm-suppress PropertyNotSetInConstructor - * @psalm-suppress NonInvariantDocblockPropertyType + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantPropertyType, NonInvariantDocblockPropertyType */ public CliQueue $queue; /** diff --git a/src/drivers/beanstalk/InfoAction.php b/src/drivers/beanstalk/InfoAction.php index 2d320d9340..0445780ad0 100644 --- a/src/drivers/beanstalk/InfoAction.php +++ b/src/drivers/beanstalk/InfoAction.php @@ -42,7 +42,7 @@ public function run(): void /** @psalm-suppress RawObjectIteration */ foreach ($this->queue->getStatsTube() as $key => $value) { Console::stdout($this->format("- $key: ", BaseConsole::FG_YELLOW)); - Console::output($value); + Console::output((string)$value); } } catch (Throwable) { Console::stdout( diff --git a/src/drivers/beanstalk/Queue.php b/src/drivers/beanstalk/Queue.php index e3d7ec4f42..09b76d12c2 100644 --- a/src/drivers/beanstalk/Queue.php +++ b/src/drivers/beanstalk/Queue.php @@ -148,7 +148,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $result = $pheanstalk ->put( $payload, - $priority ?: PheanstalkPublisherInterface::DEFAULT_PRIORITY, + (int)$priority ?: PheanstalkPublisherInterface::DEFAULT_PRIORITY, $delay, // Seconds to wait before job becomes ready $ttr // Time To Run: seconds a job can be reserved for ); diff --git a/src/drivers/db/Command.php b/src/drivers/db/Command.php index f06af1347d..41ca3f4349 100644 --- a/src/drivers/db/Command.php +++ b/src/drivers/db/Command.php @@ -23,8 +23,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; /** diff --git a/src/drivers/db/InfoAction.php b/src/drivers/db/InfoAction.php index 43007526f8..dbc5b4f7aa 100644 --- a/src/drivers/db/InfoAction.php +++ b/src/drivers/db/InfoAction.php @@ -26,8 +26,7 @@ class InfoAction extends Action { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/db/Queue.php b/src/drivers/db/Queue.php index 8b82364daf..a98f88a4c3 100644 --- a/src/drivers/db/Queue.php +++ b/src/drivers/db/Queue.php @@ -82,6 +82,7 @@ public function run(bool $repeat, int $timeout = 0) return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { if ($payload = $this->reserve()) { + /** @psalm-var array{id: int|string, job:string, ttr:int|string, attempt:int|string} $payload */ if ($this->handleMessage( $payload['id'], $payload['job'], @@ -177,7 +178,8 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri /** * Takes one message from waiting list and reserves it for handling. * - * @return array|false payload + * @return array|false + * @psalm-suppress MixedInferredReturnType, MixedReturnStatement * @throws Exception in case it hasn't waited the lock */ protected function reserve(): bool|array diff --git a/src/drivers/file/Command.php b/src/drivers/file/Command.php index 222b644b07..354c373c99 100644 --- a/src/drivers/file/Command.php +++ b/src/drivers/file/Command.php @@ -23,8 +23,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; /** diff --git a/src/drivers/file/InfoAction.php b/src/drivers/file/InfoAction.php index 8c786fdecf..c91d895fd4 100644 --- a/src/drivers/file/InfoAction.php +++ b/src/drivers/file/InfoAction.php @@ -24,8 +24,7 @@ class InfoAction extends Action { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress NonInvariantDocblockPropertyType, PropertyNotSetInConstructor */ public CliQueue $queue; @@ -54,6 +53,7 @@ public function run(): void */ protected function getWaitingCount(): int { + /** @var array{waiting: array} $data */ $data = $this->getIndexData(); return !empty($data['waiting']) ? count($data['waiting']) : 0; } @@ -63,6 +63,7 @@ protected function getWaitingCount(): int */ protected function getDelayedCount(): int { + /** @var array{delayed: array} $data */ $data = $this->getIndexData(); return !empty($data['delayed']) ? count($data['delayed']) : 0; } @@ -72,6 +73,7 @@ protected function getDelayedCount(): int */ protected function getReservedCount(): int { + /** @var array{reserved: array} $data */ $data = $this->getIndexData(); return !empty($data['reserved']) ? count($data['reserved']) : 0; } @@ -81,6 +83,7 @@ protected function getReservedCount(): int */ protected function getDoneCount(): int { + /** @var array{lastId: int} $data */ $data = $this->getIndexData(); $total = $data['lastId'] ?? 0; return $total - $this->getDelayedCount() - $this->getWaitingCount(); diff --git a/src/drivers/file/Queue.php b/src/drivers/file/Queue.php index b415972adf..5a7ee35b43 100644 --- a/src/drivers/file/Queue.php +++ b/src/drivers/file/Queue.php @@ -79,6 +79,12 @@ public function run(bool $repeat, int $timeout = 0): ?int while ($canContinue()) { if (($payload = $this->reserve()) !== null) { [$id, $message, $ttr, $attempt] = $payload; + /** + * @var int|string $id + * @var string $message + * @var int $ttr + * @var int $attempt + */ if ($this->handleMessage($id, $message, $ttr, $attempt)) { $this->delete($payload); } @@ -127,13 +133,16 @@ public function clear(): void * @param int $id of a job * @return bool * @since 2.0.1 + * @psalm-suppress MixedInferredReturnType, MixedReturnStatement */ public function remove(int $id): bool { $removed = false; $this->touchIndex(function (array &$data) use ($id, &$removed) { + /** @var array{waiting: array, delayed: array, reserved: array} $data */ if (!empty($data['waiting'])) { foreach ($data['waiting'] as $key => $payload) { + /** @psalm-var array $payload */ if ($payload[0] === $id) { unset($data['waiting'][$key]); $removed = true; @@ -143,6 +152,7 @@ public function remove(int $id): bool } if (!$removed && !empty($data['delayed'])) { foreach ($data['delayed'] as $key => $payload) { + /** @psalm-var array $payload */ if ($payload[0] === $id) { unset($data['delayed'][$key]); $removed = true; @@ -152,6 +162,7 @@ public function remove(int $id): bool } if (!$removed && !empty($data['reserved'])) { foreach ($data['reserved'] as $key => $payload) { + /** @psalm-var array $payload */ if ($payload[0] === $id) { unset($data['reserved'][$key]); $removed = true; @@ -178,20 +189,30 @@ protected function reserve(): ?array $ttr = null; $attempt = null; $this->touchIndex(function (array &$data) use (&$id, &$ttr, &$attempt) { + /** @var array{reserved: array, delayed: array, waiting: array} $data */ if (!empty($data['reserved'])) { foreach ($data['reserved'] as $key => $payload) { - if ($payload[1] + $payload[3] < time()) { + /** @psalm-var array $payload */ + if ((int)$payload[1] + (int)$payload[3] < time()) { + /** + * @psalm-var int $attempt + */ [$id, $ttr, $attempt, $time] = $payload; + /** @psalm-suppress MixedArrayAssignment */ $data['reserved'][$key][2] = ++$attempt; + /** @psalm-suppress MixedArrayAssignment */ $data['reserved'][$key][3] = time(); return; } } } - if (!empty($data['delayed']) && $data['delayed'][0][2] <= time()) { + /** @psalm-suppress MixedArrayAssignment, MixedArrayAccess */ + if (!empty($data['delayed']) && (int)$data['delayed'][0][2] <= time()) { + /** @psalm-suppress MixedArrayAssignment, MixedArrayAccess */ [$id, $ttr, $time] = array_shift($data['delayed']); } elseif (!empty($data['waiting'])) { + /** @psalm-suppress MixedArrayAssignment, MixedArrayAccess */ [$id, $ttr] = array_shift($data['waiting']); } if ($id) { @@ -216,7 +237,9 @@ protected function delete(array $payload): void { $id = $payload[0]; $this->touchIndex(function (array &$data) use ($id) { + /** @var array{reserved: array} $data */ foreach ($data['reserved'] as $key => $payload) { + /** @psalm-var array $payload */ if ($payload[0] === $id) { unset($data['reserved'][$key]); break; @@ -236,6 +259,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri } $this->touchIndex(function (array &$data) use ($payload, $ttr, $delay, &$id) { + /** @var array{lastId: int, waiting: array, delayed: array} $data */ if (!isset($data['lastId'])) { $data['lastId'] = 0; } @@ -249,7 +273,8 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri $data['waiting'][] = [$id, $ttr, 0]; } else { $data['delayed'][] = [$id, $ttr, time() + $delay]; - usort($data['delayed'], static function ($a, $b) { + /** @psalm-suppress MixedArgumentTypeCoercion */ + usort($data['delayed'], static function (array $a, array $b) { if ($a[2] < $b[2]) { return -1; } @@ -267,6 +292,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri } }); + /** @psalm-var int|string|null $id */ return $id; } @@ -296,6 +322,7 @@ private function touchIndex(callable $callback): void } try { $callback($data); + /** @var string $newContent */ $newContent = call_user_func($this->indexSerializer, $data); if ($newContent !== $content) { ftruncate($file, 0); diff --git a/src/drivers/gearman/Command.php b/src/drivers/gearman/Command.php index f16c47c4e4..3bf256cac1 100644 --- a/src/drivers/gearman/Command.php +++ b/src/drivers/gearman/Command.php @@ -22,8 +22,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/redis/Command.php b/src/drivers/redis/Command.php index 95ed09e45d..0872e319c4 100644 --- a/src/drivers/redis/Command.php +++ b/src/drivers/redis/Command.php @@ -23,8 +23,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; /** diff --git a/src/drivers/redis/InfoAction.php b/src/drivers/redis/InfoAction.php index 213e4cc288..e052feb4a8 100644 --- a/src/drivers/redis/InfoAction.php +++ b/src/drivers/redis/InfoAction.php @@ -23,8 +23,7 @@ class InfoAction extends Action { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; @@ -38,20 +37,20 @@ public function run(): void $delayed = $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf'); $reserved = $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf'); $total = $this->queue->redis->get("$prefix.message_id"); - $done = $total - $waiting - $delayed - $reserved; + $done = (int)$total - (int)$waiting - (int)$delayed - (int)$reserved; Console::output($this->format('Jobs', Console::FG_GREEN)); Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); - Console::output($waiting); + Console::output((string)$waiting); Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); - Console::output($delayed); + Console::output((string)$delayed); Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); - Console::output($reserved); + Console::output((string)$reserved); Console::stdout($this->format('- done: ', Console::FG_YELLOW)); - Console::output($done); + Console::output((string)$done); } } diff --git a/src/drivers/redis/Queue.php b/src/drivers/redis/Queue.php index b1ecc9f439..f9004c9410 100644 --- a/src/drivers/redis/Queue.php +++ b/src/drivers/redis/Queue.php @@ -63,6 +63,10 @@ public function run(bool $repeat, int $timeout = 0): ?int while ($canContinue()) { if (($payload = $this->reserve($timeout)) !== null) { [$id, $message, $ttr, $attempt] = $payload; + /** + * @psalm-var int|string $id + * @psalm-var string $message + */ if ($this->handleMessage($id, $message, (int)$ttr, (int)$attempt)) { $this->delete($id); } @@ -103,6 +107,7 @@ public function clear(): void while (!$this->redis->set("$this->channel.moving_lock", true, 'NX')) { usleep(10000); } + /** @psalm-suppress MixedArgument */ $this->redis->executeCommand('DEL', $this->redis->keys("$this->channel.*")); } @@ -147,6 +152,7 @@ protected function reserve(int $timeout): ?array if (!$timeout) { $id = $this->redis->rpop("$this->channel.waiting"); } elseif ($result = $this->redis->brpop("$this->channel.waiting", $timeout)) { + /** @psalm-var array $result */ $id = $result[1]; } if (!$id) { @@ -158,7 +164,10 @@ protected function reserve(int $timeout): ?array return null; } - /** @psalm-suppress PossiblyUndefinedArrayOffset */ + /** + * @psalm-suppress PossiblyUndefinedArrayOffset + * @psalm-var string $payload + */ [$ttr, $message] = explode(';', $payload, 2); $this->redis->zadd("$this->channel.reserved", time() + (int)$ttr, $id); $attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1); @@ -201,6 +210,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri throw new NotSupportedException('Job priority is not supported in the driver.'); } + /** @var string|int $id */ $id = $this->redis->incr("$this->channel.message_id"); $this->redis->hset("$this->channel.messages", $id, "$ttr;$payload"); if (!$delay) { diff --git a/src/drivers/sqs/Command.php b/src/drivers/sqs/Command.php index cc9998768e..e5f90c490a 100644 --- a/src/drivers/sqs/Command.php +++ b/src/drivers/sqs/Command.php @@ -24,8 +24,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/sqs/Payload.php b/src/drivers/sqs/Payload.php new file mode 100644 index 0000000000..d719fc9d1f --- /dev/null +++ b/src/drivers/sqs/Payload.php @@ -0,0 +1,47 @@ +messages = $response['Messages']??[]; + if (!empty($this->messages)) { + $messages = $this->messages; + /** @var array{ + * MessageAttributes: array, + * Body: string, + * MessageId: string|int, + * ReceiptHandle: mixed, + * Attributes: array + * } $message + */ + $message = reset($messages); + + $this->ttr = (int)$message['MessageAttributes']['TTR']['StringValue']; + $this->body = $message['Body']; + $this->messageId = $message['MessageId']; + $this->attempt = (int)$message['Attributes']['ApproximateReceiveCount']; + $this->receiptHandle = $message['ReceiptHandle']; + } + } +} diff --git a/src/drivers/sqs/Queue.php b/src/drivers/sqs/Queue.php index c7127db8d4..47dbe4b81d 100644 --- a/src/drivers/sqs/Queue.php +++ b/src/drivers/sqs/Queue.php @@ -85,11 +85,14 @@ public function run(bool $repeat, int $timeout = 0): ?int return $this->runWorker(function (callable $canContinue) use ($repeat, $timeout) { while ($canContinue()) { if (($payload = $this->reserve($timeout)) !== null) { - $id = $payload['MessageId']; - $message = $payload['Body']; - $ttr = (int) $payload['MessageAttributes']['TTR']['StringValue']; - $attempt = (int) $payload['Attributes']['ApproximateReceiveCount']; - if ($this->handleMessage($id, $message, $ttr, $attempt)) { + if ( + $this->handleMessage( + $payload->messageId, + $payload->body, + $payload->ttr, + $payload->attempt + ) + ) { $this->delete($payload); } } elseif (!$repeat) { @@ -103,10 +106,11 @@ public function run(bool $repeat, int $timeout = 0): ?int * Gets a single message from SQS queue and sets the visibility to reserve message. * * @param int $timeout number of seconds for long polling. Must be between 0 and 20. - * @return null|array payload. + * @return Payload|null */ - protected function reserve(int $timeout): ?array + protected function reserve(int $timeout): ?Payload { + /** @var array{Messages: array} $response */ $response = $this->getClient()->receiveMessage([ 'QueueUrl' => $this->url, 'AttributeNames' => ['ApproximateReceiveCount'], @@ -114,18 +118,18 @@ protected function reserve(int $timeout): ?array 'MaxNumberOfMessages' => 1, 'VisibilityTimeout' => $this->ttr, 'WaitTimeSeconds' => $timeout, - ]); - if (!$response['Messages']) { + ])->toArray(); + + $payload = new Payload($response); + if (empty($payload->messages)) { return null; } - $payload = reset($response['Messages']); - - $ttr = (int) $payload['MessageAttributes']['TTR']['StringValue']; + $ttr = $payload->ttr; if ($ttr !== $this->ttr) { $this->getClient()->changeMessageVisibility([ 'QueueUrl' => $this->url, - 'ReceiptHandle' => $payload['ReceiptHandle'], + 'ReceiptHandle' => $payload->receiptHandle, 'VisibilityTimeout' => $ttr, ]); } @@ -136,13 +140,13 @@ protected function reserve(int $timeout): ?array /** * Deletes the message after successfully handling. * - * @param array $payload + * @param Payload $payload */ - protected function delete(array $payload): void + protected function delete(Payload $payload): void { $this->getClient()->deleteMessage([ 'QueueUrl' => $this->url, - 'ReceiptHandle' => $payload['ReceiptHandle'], + 'ReceiptHandle' => $payload->receiptHandle, ]); } @@ -206,7 +210,7 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri } $response = $this->getClient()->sendMessage($request); - return $response['MessageId']; + return null === $response['MessageId']?null:(string)$response['MessageId']; } /** diff --git a/src/drivers/stomp/Command.php b/src/drivers/stomp/Command.php index 312eac87fb..71bbfd9d1c 100644 --- a/src/drivers/stomp/Command.php +++ b/src/drivers/stomp/Command.php @@ -24,8 +24,7 @@ class Command extends CliCommand { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/stomp/Queue.php b/src/drivers/stomp/Queue.php index a98eaad35b..16885a136c 100644 --- a/src/drivers/stomp/Queue.php +++ b/src/drivers/stomp/Queue.php @@ -177,8 +177,8 @@ public function run(bool $repeat, int $timeout = 0): ?int continue; } - $ttr = $message->getProperty(self::TTR, $this->ttr); - $attempt = $message->getProperty(self::ATTEMPT, 1); + $ttr = (int)$message->getProperty(self::TTR, $this->ttr); + $attempt = (int)$message->getProperty(self::ATTEMPT, 1); $messageId = $message->getMessageId(); if (null !== $messageId && $this->handleMessage($messageId, $message->getBody(), $ttr, $attempt)) { @@ -267,7 +267,7 @@ public function status($id): int */ protected function redeliver(StompMessage $message): void { - $attempt = $message->getProperty(self::ATTEMPT, 1); + $attempt = (int)$message->getProperty(self::ATTEMPT, 1); $newMessage = $this->getContext()->createMessage( $message->getBody(), diff --git a/src/drivers/sync/Queue.php b/src/drivers/sync/Queue.php index d83d8eee58..6e4bcb6c58 100644 --- a/src/drivers/sync/Queue.php +++ b/src/drivers/sync/Queue.php @@ -27,7 +27,7 @@ class Queue extends BaseQueue */ public bool $handle = false; /** - * @var array of payloads + * @var array $payloads of payloads */ private array $payloads = []; /** @@ -64,6 +64,11 @@ public function init(): void public function run(): void { while (($payload = array_shift($this->payloads)) !== null) { + /** + * @var int $ttr + * @var string $message + * @psalm-suppress MixedArrayAccess + */ [$ttr, $message] = $payload; $this->startedId = $this->finishedId + 1; $this->handleMessage($this->startedId, $message, $ttr, 1); diff --git a/src/gii/Generator.php b/src/gii/Generator.php index 3e67cd5798..1fe0515641 100644 --- a/src/gii/Generator.php +++ b/src/gii/Generator.php @@ -24,11 +24,11 @@ */ class Generator extends BaseGenerator { - public $jobClass; - public $properties; - public $retryable = false; - public $ns = 'app\jobs'; - public $baseClass = BaseObject::class; + public string $jobClass = ''; + public string $properties = ''; + public bool $retryable = false; + public string $ns = 'app\jobs'; + public string $baseClass = BaseObject::class; /** * @inheritdoc @@ -128,8 +128,12 @@ public function generate(): array } $params['properties'] = array_unique(preg_split('/[\s,]+/', $this->properties, -1, PREG_SPLIT_NO_EMPTY)); + $alias = Yii::getAlias('@' . str_replace('\\', '/', $this->ns)); + if (false === $alias) { + return []; + } $jobFile = new CodeFile( - Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $this->jobClass . '.php', + $alias . '/' . $this->jobClass . '.php', $this->render('job.php', $params) ); @@ -143,6 +147,7 @@ public function generate(): array */ public function validateJobClass(string $attribute): void { + /** @psalm-suppress MixedArgument */ if ($this->isReservedKeyword($this->$attribute)) { $this->addError($attribute, 'Class name cannot be a reserved PHP keyword.'); } @@ -155,6 +160,7 @@ public function validateJobClass(string $attribute): void */ public function validateNamespace(string $attribute): void { + /** @psalm-var string $value */ $value = $this->$attribute; $value = ltrim($value, '\\'); $path = Yii::getAlias('@' . str_replace('\\', '/', $value), false); diff --git a/src/gii/default/job.php b/src/gii/default/job.php index ece107a742..d269b4ee74 100644 --- a/src/gii/default/job.php +++ b/src/gii/default/job.php @@ -3,14 +3,18 @@ declare(strict_types=1); /** - * @var \yii\web\View $this - * @var \yii\queue\gii\Generator $generator + * @var View $this + * @var Generator $generator * @var string $jobClass * @var string $ns * @var string $baseClass * @var string[] $interfaces * @var string[] $properties */ + +use yii\web\View; +use yii\queue\gii\Generator; + if ($interfaces) { $implements = 'implements ' . implode(', ', $interfaces); } else { diff --git a/src/gii/form.php b/src/gii/form.php index a28b40b9f5..03a05b710b 100644 --- a/src/gii/form.php +++ b/src/gii/form.php @@ -3,10 +3,15 @@ declare(strict_types=1); /** - * @var \yii\web\View $this - * @var \yii\widgets\ActiveForm $form - * @var \yii\queue\gii\Generator $generator + * @var View $this + * @var ActiveForm $form + * @var Generator $generator */ + +use yii\queue\gii\Generator; +use yii\web\View; +use yii\widgets\ActiveForm; + ?> field($generator, 'jobClass')->textInput(['autofocus' => true]) ?> field($generator, 'properties') ?> diff --git a/src/serializers/IgbinarySerializer.php b/src/serializers/IgbinarySerializer.php index 21db11fdba..98cb58bc35 100644 --- a/src/serializers/IgbinarySerializer.php +++ b/src/serializers/IgbinarySerializer.php @@ -33,7 +33,7 @@ public function serialize($job): string /** * @inheritdoc */ - public function unserialize(string $serialized) + public function unserialize(string $serialized): mixed { return igbinary_unserialize($serialized); } diff --git a/src/serializers/JsonSerializer.php b/src/serializers/JsonSerializer.php index 0cc85a91eb..0483dcbfba 100644 --- a/src/serializers/JsonSerializer.php +++ b/src/serializers/JsonSerializer.php @@ -42,7 +42,7 @@ public function serialize($job): string /** * @inheritdoc */ - public function unserialize(string $serialized) + public function unserialize(string $serialized): mixed { return $this->fromArray(Json::decode($serialized)); } diff --git a/src/serializers/PhpSerializer.php b/src/serializers/PhpSerializer.php index ac3e05791b..95eb0b0adb 100644 --- a/src/serializers/PhpSerializer.php +++ b/src/serializers/PhpSerializer.php @@ -30,7 +30,7 @@ public function serialize($job): string /** * @inheritdoc */ - public function unserialize(string $serialized) + public function unserialize(string $serialized): mixed { return unserialize($serialized); } diff --git a/src/serializers/SerializerInterface.php b/src/serializers/SerializerInterface.php index d10b668a5b..376502e146 100644 --- a/src/serializers/SerializerInterface.php +++ b/src/serializers/SerializerInterface.php @@ -27,7 +27,7 @@ public function serialize($job): string; /** * @param string $serialized - * @return JobInterface + * @return JobInterface|array|mixed */ public function unserialize(string $serialized); } From 9f839ba3934cb9f9770d9fa8c5f51585d59cbe75 Mon Sep 17 00:00:00 2001 From: EAMoiseenko Date: Sat, 6 Jan 2024 12:04:53 +0300 Subject: [PATCH 07/16] Fixed UndefinedVariable for psalm 5.18.0 --- src/drivers/file/Queue.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/drivers/file/Queue.php b/src/drivers/file/Queue.php index 5a7ee35b43..d4212de5ed 100644 --- a/src/drivers/file/Queue.php +++ b/src/drivers/file/Queue.php @@ -257,6 +257,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 */ From 9858a7840c23d479b2cc114364788f8001dc7e4f Mon Sep 17 00:00:00 2001 From: EAMoiseenko Date: Mon, 12 May 2025 17:37:56 +0300 Subject: [PATCH 08/16] Fixed psalm issues --- composer.json | 4 ++-- psalm.xml | 4 ++++ src/JobEvent.php | 2 +- src/cli/VerboseBehavior.php | 1 - src/cli/WorkerEvent.php | 3 +-- src/closure/Behavior.php | 2 +- src/debug/Panel.php | 1 - src/drivers/amqp_interop/Command.php | 2 +- src/drivers/beanstalk/Command.php | 2 +- src/drivers/db/Command.php | 2 +- src/drivers/db/InfoAction.php | 2 +- src/drivers/file/Command.php | 2 +- src/drivers/file/InfoAction.php | 2 +- src/drivers/gearman/Command.php | 2 +- src/drivers/redis/Command.php | 2 +- src/drivers/redis/InfoAction.php | 2 +- src/drivers/sqs/Command.php | 2 +- src/drivers/stomp/Command.php | 2 +- tests/docker-compose.yml | 3 +-- 19 files changed, 21 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 8b378d4318..605c9b6cf7 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,8 @@ }, "require": { "php": ">=8.1", - "yiisoft/yii2": "~2.0.14", - "symfony/process": "^6.3||^7.0", + "yiisoft/yii2": "~2.0.50", + "symfony/process": "^7.0", "laravel/serializable-closure": "^v1.3.0" }, "require-dev": { diff --git a/psalm.xml b/psalm.xml index d921097324..e04d9b391b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -27,5 +27,9 @@ + + + + diff --git a/src/JobEvent.php b/src/JobEvent.php index fab6221506..31968b09ca 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/VerboseBehavior.php b/src/cli/VerboseBehavior.php index e4dd27613e..f9ea59a55f 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 6dc4bf3c60..530c8ebd59 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 75b051181c..f6a2b1dd4d 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 2dcabeb8df..0d63cfddd9 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 1dcd2b5cdf..b24f1cc252 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 ebb54b4bf5..2df4e720a7 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 798a2ca870..4978bdd7f2 100644 --- a/src/drivers/db/Command.php +++ b/src/drivers/db/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/db/InfoAction.php b/src/drivers/db/InfoAction.php index ecbe3d6cc3..1280fa9a06 100644 --- a/src/drivers/db/InfoAction.php +++ b/src/drivers/db/InfoAction.php @@ -28,7 +28,7 @@ class InfoAction extends Action { /** * @var Queue - * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType + * @psalm-suppress NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/file/Command.php b/src/drivers/file/Command.php index 0cce1f9a30..abe94f4d3d 100644 --- a/src/drivers/file/Command.php +++ b/src/drivers/file/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/file/InfoAction.php b/src/drivers/file/InfoAction.php index 8e9a89ce9a..a2bc34d288 100644 --- a/src/drivers/file/InfoAction.php +++ b/src/drivers/file/InfoAction.php @@ -26,7 +26,7 @@ class InfoAction extends Action { /** * @var Queue - * @psalm-suppress NonInvariantDocblockPropertyType, PropertyNotSetInConstructor + * @psalm-suppress NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/gearman/Command.php b/src/drivers/gearman/Command.php index 3bf256cac1..d21c191e49 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 b632a34895..50da9ac51d 100644 --- a/src/drivers/redis/Command.php +++ b/src/drivers/redis/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/redis/InfoAction.php b/src/drivers/redis/InfoAction.php index ec5755b45d..97fe1fa1ea 100644 --- a/src/drivers/redis/InfoAction.php +++ b/src/drivers/redis/InfoAction.php @@ -25,7 +25,7 @@ class InfoAction extends Action { /** * @var Queue - * @psalm-suppress PropertyNotSetInConstructor, NonInvariantDocblockPropertyType + * @psalm-suppress NonInvariantDocblockPropertyType */ public CliQueue $queue; diff --git a/src/drivers/sqs/Command.php b/src/drivers/sqs/Command.php index e5f90c490a..9cb5850adc 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 71bbfd9d1c..974a0d1c31 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/tests/docker-compose.yml b/tests/docker-compose.yml index 01426e3426..67a6fd645f 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 From f5470142e27c5f6404bbafa58479071bf8f0474f Mon Sep 17 00:00:00 2001 From: EAMoiseenko Date: Sun, 18 May 2025 16:34:37 +0300 Subject: [PATCH 09/16] Fixed psalm issues --- .github/workflows/main.yml | 2 +- .github/workflows/static.yml | 2 +- Makefile | 8 +++--- composer.json | 2 +- src/cli/InfoAction.php | 11 +++++--- src/drivers/db/StatisticsProvider.php | 26 ++++++++++++------- src/drivers/file/StatisticsProvider.php | 18 +++++++------ src/drivers/redis/StatisticsProvider.php | 13 ++++++---- src/interfaces/DelayedCountInterface.php | 5 +++- src/interfaces/DoneCountInterface.php | 5 +++- src/interfaces/ReservedCountInterface.php | 5 +++- .../StatisticsProviderInterface.php | 3 +++ src/interfaces/WaitingCountInterface.php | 5 +++- src/serializers/JsonSerializer.php | 2 +- tests/docker-compose.yml | 2 +- tests/docker/php/Dockerfile | 2 +- 16 files changed, 71 insertions(+), 40 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eb0b977231..53abb86e9c 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', '8.3' ] + php: [ '8.2', '8.3', '8.4' ] steps: - name: Checkout. uses: actions/checkout@v2 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 7e37c591d5..5126dc889c 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', '8.3' ] + php: [ '8.2', '8.3', '8.4' ] steps: - name: Checkout. uses: actions/checkout@v2 diff --git a/Makefile b/Makefile index 6fb9191e5e..c01e40b11e 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ 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 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 make down @@ -15,7 +15,7 @@ test: ## Run tests. Params: {{ v=8.1 }}. Default latest PHP 8.1 down: ## Stop and remove containers, networks docker compose down -benchmark: ## Run benchmark. Params: {{ v=8.1 }}. Default latest PHP 8.1 +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 @@ -23,7 +23,7 @@ benchmark: ## Run benchmark. Params: {{ v=8.1 }}. Default latest PHP 8.1 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 +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 diff --git a/composer.json b/composer.json index 605c9b6cf7..84f0a49f19 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "yiisoft/yii2": "~2.0.50", "symfony/process": "^7.0", "laravel/serializable-closure": "^v1.3.0" diff --git a/src/cli/InfoAction.php b/src/cli/InfoAction.php index f857ffe55d..7f3ed669c7 100644 --- a/src/cli/InfoAction.php +++ b/src/cli/InfoAction.php @@ -1,4 +1,7 @@ + * + * @property Controller $controller */ class InfoAction extends Action { /** * @var Queue */ - public $queue; - + public Queue $queue; /** * Info about queue status. */ - public function run() + public function run(): void { if (!($this->queue instanceof StatisticsProviderInterface)) { throw new NotSupportedException('Queue does not support ' . StatisticsProviderInterface::class); diff --git a/src/drivers/db/StatisticsProvider.php b/src/drivers/db/StatisticsProvider.php index ff6020314f..50f05d5fb7 100644 --- a/src/drivers/db/StatisticsProvider.php +++ b/src/drivers/db/StatisticsProvider.php @@ -1,4 +1,7 @@ from($this->queue->tableName) ->andWhere(['channel' => $this->queue->channel]) ->andWhere(['reserved_at' => null]) - ->andWhere(['delay' => 0])->count('*', $this->queue->db); + ->andWhere(['delay' => 0]) + ->count('*', $this->queue->db); } /** * @inheritdoc */ - public function getDelayedCount() + public function getDelayedCount(): int { return (new Query()) ->from($this->queue->tableName) ->andWhere(['channel' => $this->queue->channel]) ->andWhere(['reserved_at' => null]) - ->andWhere(['>', 'delay', 0])->count('*', $this->queue->db); + ->andWhere(['>', 'delay', 0]) + ->count('*', $this->queue->db); } /** * @inheritdoc */ - public function getReservedCount() + public function getReservedCount(): int { return (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); + ->andWhere(['done_at' => null]) + ->count('*', $this->queue->db); } /** * @inheritdoc */ - public function getDoneCount() + public function getDoneCount(): int { return (new Query()) ->from($this->queue->tableName) ->andWhere(['channel' => $this->queue->channel]) - ->andWhere('[[done_at]] is not null')->count('*', $this->queue->db); + ->andWhere('[[done_at]] is not null') + ->count('*', $this->queue->db); } } diff --git a/src/drivers/file/StatisticsProvider.php b/src/drivers/file/StatisticsProvider.php index cedc9520b1..33fdac820d 100644 --- a/src/drivers/file/StatisticsProvider.php +++ b/src/drivers/file/StatisticsProvider.php @@ -1,4 +1,7 @@ getIndexData(); return !empty($data['waiting']) ? count($data['waiting']) : 0; @@ -44,7 +46,7 @@ public function getWaitingCount() /** * @inheritdoc */ - public function getDelayedCount() + public function getDelayedCount(): int { $data = $this->getIndexData(); return !empty($data['delayed']) ? count($data['delayed']) : 0; @@ -53,7 +55,7 @@ public function getDelayedCount() /** * @inheritdoc */ - public function getReservedCount() + public function getReservedCount(): int { $data = $this->getIndexData(); return !empty($data['reserved']) ? count($data['reserved']) : 0; @@ -62,7 +64,7 @@ public function getReservedCount() /** * @inheritdoc */ - public function getDoneCount() + public function getDoneCount(): int { $data = $this->getIndexData(); $total = isset($data['lastId']) ? $data['lastId'] : 0; @@ -74,8 +76,8 @@ protected function getIndexData() $fileName = $this->queue->path . '/index.data'; if (file_exists($fileName)) { return call_user_func($this->queue->indexDeserializer, file_get_contents($fileName)); - } else { - return []; } + + return []; } } diff --git a/src/drivers/redis/StatisticsProvider.php b/src/drivers/redis/StatisticsProvider.php index 78f21d2d37..23ddb45574 100644 --- a/src/drivers/redis/StatisticsProvider.php +++ b/src/drivers/redis/StatisticsProvider.php @@ -1,4 +1,7 @@ queue->channel; return $this->queue->redis->llen("$prefix.waiting"); @@ -44,7 +47,7 @@ public function getWaitingCount() /** * @inheritdoc */ - public function getDelayedCount() + public function getDelayedCount(): int { $prefix = $this->queue->channel; return $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf'); @@ -53,7 +56,7 @@ public function getDelayedCount() /** * @inheritdoc */ - public function getReservedCount() + public function getReservedCount(): int { $prefix = $this->queue->channel; return $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf'); @@ -62,7 +65,7 @@ public function getReservedCount() /** * @inheritdoc */ - public function getDoneCount() + public function getDoneCount(): int { $prefix = $this->queue->channel; $waiting = $this->getWaitingCount(); diff --git a/src/interfaces/DelayedCountInterface.php b/src/interfaces/DelayedCountInterface.php index 41ea005327..fcf19fd222 100644 --- a/src/interfaces/DelayedCountInterface.php +++ b/src/interfaces/DelayedCountInterface.php @@ -1,4 +1,7 @@ > /etc/apk/repositories \ && echo https://dl-cdn.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories \ From f0264bc207c94ef26d6e6328a6dd7510a202a808 Mon Sep 17 00:00:00 2001 From: EAMoiseenko Date: Sun, 18 May 2025 17:02:44 +0300 Subject: [PATCH 10/16] Fixed psalm issues --- src/drivers/db/Queue.php | 13 ++++++----- src/drivers/db/StatisticsProvider.php | 22 ++++++++++++++----- src/drivers/file/Queue.php | 13 ++++++----- src/drivers/file/StatisticsProvider.php | 17 ++++++++++++-- src/drivers/redis/Queue.php | 13 ++++++----- src/drivers/redis/StatisticsProvider.php | 18 ++++++++++----- src/interfaces/StatisticsInterface.php | 18 +++++++++++++++ .../StatisticsProviderInterface.php | 4 ++-- 8 files changed, 84 insertions(+), 34 deletions(-) create mode 100644 src/interfaces/StatisticsInterface.php diff --git a/src/drivers/db/Queue.php b/src/drivers/db/Queue.php index 4007bbc9d8..c26ad79b1a 100644 --- a/src/drivers/db/Queue.php +++ b/src/drivers/db/Queue.php @@ -17,6 +17,7 @@ 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; /** @@ -285,16 +286,16 @@ private function getMutex(): Mutex return $mutex; } - private $_statistcsProvider; + private StatisticsInterface $_statisticsProvider; /** - * @return StatisticsProvider + * @return StatisticsInterface */ - public function getStatisticsProvider() + public function getStatisticsProvider(): StatisticsInterface { - if (!$this->_statistcsProvider) { - $this->_statistcsProvider = new StatisticsProvider($this); + if (!isset($this->_statisticsProvider)) { + $this->_statisticsProvider = new StatisticsProvider($this); } - return $this->_statistcsProvider; + return $this->_statisticsProvider; } } diff --git a/src/drivers/db/StatisticsProvider.php b/src/drivers/db/StatisticsProvider.php index 50f05d5fb7..c5d0f3a9bf 100644 --- a/src/drivers/db/StatisticsProvider.php +++ b/src/drivers/db/StatisticsProvider.php @@ -15,6 +15,7 @@ use yii\queue\interfaces\DelayedCountInterface; use yii\queue\interfaces\DoneCountInterface; use yii\queue\interfaces\ReservedCountInterface; +use yii\queue\interfaces\StatisticsInterface; use yii\queue\interfaces\WaitingCountInterface; /** @@ -22,14 +23,19 @@ * * @author Kalmer Kaurson */ -class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface +class StatisticsProvider extends BaseObject implements + DoneCountInterface, + WaitingCountInterface, + DelayedCountInterface, + ReservedCountInterface, + StatisticsInterface { /** * @var Queue */ protected Queue $queue; - public function __construct(Queue $queue, $config = []) + public function __construct(Queue $queue, array $config = []) { $this->queue = $queue; parent::__construct($config); @@ -40,7 +46,8 @@ public function __construct(Queue $queue, $config = []) */ public function getWaitingCount(): int { - return (new Query()) + /** @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]) @@ -53,7 +60,8 @@ public function getWaitingCount(): int */ public function getDelayedCount(): int { - return (new Query()) + /** @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]) @@ -66,7 +74,8 @@ public function getDelayedCount(): int */ public function getReservedCount(): int { - return (new Query()) + /** @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') @@ -79,7 +88,8 @@ public function getReservedCount(): int */ public function getDoneCount(): int { - return (new Query()) + /** @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') diff --git a/src/drivers/file/Queue.php b/src/drivers/file/Queue.php index 805d3345a6..2bfcade701 100644 --- a/src/drivers/file/Queue.php +++ b/src/drivers/file/Queue.php @@ -16,6 +16,7 @@ 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; /** @@ -340,16 +341,16 @@ private function touchIndex(callable $callback): void } } - private $_statistcsProvider; + private StatisticsInterface $_statisticsProvider; /** - * @return StatisticsProvider + * @return StatisticsInterface */ - public function getStatisticsProvider() + public function getStatisticsProvider(): StatisticsInterface { - if (!$this->_statistcsProvider) { - $this->_statistcsProvider = new StatisticsProvider($this); + if (!isset($this->_statisticsProvider)) { + $this->_statisticsProvider = new StatisticsProvider($this); } - return $this->_statistcsProvider; + return $this->_statisticsProvider; } } diff --git a/src/drivers/file/StatisticsProvider.php b/src/drivers/file/StatisticsProvider.php index 33fdac820d..27cc1a75f7 100644 --- a/src/drivers/file/StatisticsProvider.php +++ b/src/drivers/file/StatisticsProvider.php @@ -14,6 +14,7 @@ use yii\queue\interfaces\DelayedCountInterface; use yii\queue\interfaces\DoneCountInterface; use yii\queue\interfaces\ReservedCountInterface; +use yii\queue\interfaces\StatisticsInterface; use yii\queue\interfaces\WaitingCountInterface; /** @@ -21,14 +22,19 @@ * * @author Kalmer Kaurson */ -class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface +class StatisticsProvider extends BaseObject implements + DoneCountInterface, + WaitingCountInterface, + DelayedCountInterface, + ReservedCountInterface, + StatisticsInterface { /** * @var Queue */ protected Queue $queue; - public function __construct(Queue $queue, $config = []) + public function __construct(Queue $queue, array $config = []) { $this->queue = $queue; parent::__construct($config); @@ -39,6 +45,7 @@ public function __construct(Queue $queue, $config = []) */ public function getWaitingCount(): int { + /** @var array{waiting:array} $data */ $data = $this->getIndexData(); return !empty($data['waiting']) ? count($data['waiting']) : 0; } @@ -48,6 +55,7 @@ public function getWaitingCount(): int */ public function getDelayedCount(): int { + /** @var array{delayed:array} $data */ $data = $this->getIndexData(); return !empty($data['delayed']) ? count($data['delayed']) : 0; } @@ -57,6 +65,7 @@ public function getDelayedCount(): int */ public function getReservedCount(): int { + /** @var array{reserved:array} $data */ $data = $this->getIndexData(); return !empty($data['reserved']) ? count($data['reserved']) : 0; } @@ -66,11 +75,15 @@ public function getReservedCount(): int */ 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'; diff --git a/src/drivers/redis/Queue.php b/src/drivers/redis/Queue.php index 0aa3124986..48232e3c03 100644 --- a/src/drivers/redis/Queue.php +++ b/src/drivers/redis/Queue.php @@ -14,6 +14,7 @@ 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; @@ -225,16 +226,16 @@ protected function pushMessage(string $payload, int $ttr, int $delay, mixed $pri return $id; } - private $_statistcsProvider; + private StatisticsInterface $_statisticsProvider; /** - * @return StatisticsProvider + * @return StatisticsInterface */ - public function getStatisticsProvider() + public function getStatisticsProvider(): StatisticsInterface { - if (!$this->_statistcsProvider) { - $this->_statistcsProvider = new StatisticsProvider($this); + if (!isset($this->_statisticsProvider)) { + $this->_statisticsProvider = new StatisticsProvider($this); } - return $this->_statistcsProvider; + return $this->_statisticsProvider; } } diff --git a/src/drivers/redis/StatisticsProvider.php b/src/drivers/redis/StatisticsProvider.php index 23ddb45574..5df423f334 100644 --- a/src/drivers/redis/StatisticsProvider.php +++ b/src/drivers/redis/StatisticsProvider.php @@ -14,6 +14,7 @@ use yii\queue\interfaces\DelayedCountInterface; use yii\queue\interfaces\DoneCountInterface; use yii\queue\interfaces\ReservedCountInterface; +use yii\queue\interfaces\StatisticsInterface; use yii\queue\interfaces\WaitingCountInterface; /** @@ -21,7 +22,12 @@ * * @author Kalmer Kaurson */ -class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface +class StatisticsProvider extends BaseObject implements + DoneCountInterface, + WaitingCountInterface, + DelayedCountInterface, + ReservedCountInterface, + StatisticsInterface { /** * @var Queue @@ -29,7 +35,7 @@ class StatisticsProvider extends BaseObject implements DoneCountInterface, Waiti protected Queue $queue; - public function __construct(Queue $queue, $config = []) + public function __construct(Queue $queue, array $config = []) { $this->queue = $queue; parent::__construct($config); @@ -41,7 +47,7 @@ public function __construct(Queue $queue, $config = []) public function getWaitingCount(): int { $prefix = $this->queue->channel; - return $this->queue->redis->llen("$prefix.waiting"); + return (int) $this->queue->redis->llen("$prefix.waiting"); } /** @@ -50,7 +56,7 @@ public function getWaitingCount(): int public function getDelayedCount(): int { $prefix = $this->queue->channel; - return $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf'); + return (int) $this->queue->redis->zcount("$prefix.delayed", '-inf', '+inf'); } /** @@ -59,7 +65,7 @@ public function getDelayedCount(): int public function getReservedCount(): int { $prefix = $this->queue->channel; - return $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf'); + return (int) $this->queue->redis->zcount("$prefix.reserved", '-inf', '+inf'); } /** @@ -71,7 +77,7 @@ public function getDoneCount(): int $waiting = $this->getWaitingCount(); $delayed = $this->getDelayedCount(); $reserved = $this->getReservedCount(); - $total = $this->queue->redis->get("$prefix.message_id"); + $total = (int) $this->queue->redis->get("$prefix.message_id"); return $total - $waiting - $delayed - $reserved; } } diff --git a/src/interfaces/StatisticsInterface.php b/src/interfaces/StatisticsInterface.php new file mode 100644 index 0000000000..34cbfde774 --- /dev/null +++ b/src/interfaces/StatisticsInterface.php @@ -0,0 +1,18 @@ + Date: Sun, 18 May 2025 17:41:34 +0300 Subject: [PATCH 11/16] Fixed psalm issues --- composer.json | 2 +- psalm.xml | 9 ++++++ tests/cli/InfoActionTest.php | 29 +++++++++---------- tests/cli/Queue.php | 20 ++++++++----- .../cli/providers/BaseStatisticsProvider.php | 9 ++++-- tests/cli/providers/DelayedCountProvider.php | 4 ++- tests/cli/providers/DoneCountProvider.php | 4 ++- tests/cli/providers/ReservedCountProvider.php | 4 ++- tests/cli/providers/WaitingCountProvider.php | 4 ++- tests/drivers/redis/QueueTest.php | 18 ++++++------ tests/drivers/redis/RedisCrashMock.php | 3 ++ 11 files changed, 67 insertions(+), 39 deletions(-) diff --git a/composer.json b/composer.json index 84f0a49f19..be729e7856 100644 --- a/composer.json +++ b/composer.json @@ -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/psalm.xml b/psalm.xml index e04d9b391b..a937de0658 100644 --- a/psalm.xml +++ b/psalm.xml @@ -31,5 +31,14 @@ + + + + + + + + + diff --git a/tests/cli/InfoActionTest.php b/tests/cli/InfoActionTest.php index 1756e374aa..26ad57f3a8 100644 --- a/tests/cli/InfoActionTest.php +++ b/tests/cli/InfoActionTest.php @@ -1,4 +1,7 @@ getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->with( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -45,8 +47,7 @@ public function testWaitingCount() [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); @@ -67,7 +68,7 @@ public function testWaitingCount() $action->run(); } - public function testDelayedCount() + public function testDelayedCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) @@ -76,7 +77,7 @@ public function testDelayedCount() $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->with( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -110,7 +111,7 @@ public function testDelayedCount() $action->run(); } - public function testReservedCount() + public function testReservedCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) @@ -119,7 +120,7 @@ public function testReservedCount() $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->with( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -131,8 +132,7 @@ public function testReservedCount() [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); @@ -162,7 +162,7 @@ public function testDoneCount() $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->with( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -174,8 +174,7 @@ public function testDoneCount() [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); diff --git a/tests/cli/Queue.php b/tests/cli/Queue.php index e7a6412d74..e7b5b981c5 100644 --- a/tests/cli/Queue.php +++ b/tests/cli/Queue.php @@ -1,4 +1,7 @@ _statistcsProvider) { - $this->_statistcsProvider = new BaseStatisticsProvider($this); + if (!isset($this->_statisticsProvider)) { + $this->_statisticsProvider = new BaseStatisticsProvider($this); } - return $this->_statistcsProvider; + return $this->_statisticsProvider; } } diff --git a/tests/cli/providers/BaseStatisticsProvider.php b/tests/cli/providers/BaseStatisticsProvider.php index a403d52ca0..3e7acce1a4 100644 --- a/tests/cli/providers/BaseStatisticsProvider.php +++ b/tests/cli/providers/BaseStatisticsProvider.php @@ -1,5 +1,7 @@ */ -class BaseStatisticsProvider extends BaseObject +class BaseStatisticsProvider extends BaseObject implements StatisticsInterface { /** * @var Queue */ - protected $queue; + protected Queue $queue; - public function __construct(Queue $queue, $config = []) + 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 index d15beab584..17e7d6dcd5 100644 --- a/tests/cli/providers/DelayedCountProvider.php +++ b/tests/cli/providers/DelayedCountProvider.php @@ -1,5 +1,7 @@ assertFalse((bool) $this->getQueue()->redis->hexists($this->getQueue()->channel . '.messages', $id)); } - public function testWaitingCount() + public function testWaitingCount(): void { $this->getQueue()->push($this->createSimpleJob()); $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getWaitingCount()); } - public function testDelayedCount() + public function testDelayedCount(): void { $this->getQueue()->delay(5)->push($this->createSimpleJob()); $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDelayedCount()); } - public function testReservedCount() + public function testReservedCount(): void { $this->getQueue()->messageHandler = function () { $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount()); }; - $job = $this->createSimpleJob(); - $this->getQueue()->push($job); + $this->getQueue()->push($this->createSimpleJob()); $this->getQueue()->run(false); } - public function testDoneCount() + public function testDoneCount(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -151,13 +151,13 @@ protected function tearDown(): void * 3. Mock Redis to simulate crash during moveExpired * 4. Successfully process job after recovery */ - public function testConsumeDelayedMessageAtLeastOnce() + 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 = function () use(&$messageCount) { + $this->getQueue()->messageHandler = static function () use(&$messageCount) { $messageCount++; }; @@ -171,7 +171,7 @@ public function testConsumeDelayedMessageAtLeastOnce() 'hostname' => getenv('REDIS_HOST') ?: 'localhost', 'database' => getenv('REDIS_DB') ?: 1, 'crashOnCommand' => 'rpush' // Crash when trying to move job to waiting queue. - ], 'yii\redis\Connection'); + ], Connection::class); $queue = $this->getQueue(); $old = $queue->redis; diff --git a/tests/drivers/redis/RedisCrashMock.php b/tests/drivers/redis/RedisCrashMock.php index e463e08a91..b332d6abec 100644 --- a/tests/drivers/redis/RedisCrashMock.php +++ b/tests/drivers/redis/RedisCrashMock.php @@ -1,4 +1,7 @@ Date: Sun, 18 May 2025 18:04:04 +0300 Subject: [PATCH 12/16] Fixed psalm issues --- psalm.xml | 1 + tests/drivers/db/TestCase.php | 4 ++-- tests/drivers/file/QueueTest.php | 11 +++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/psalm.xml b/psalm.xml index a937de0658..ec0f9676e5 100644 --- a/psalm.xml +++ b/psalm.xml @@ -40,5 +40,6 @@ + diff --git a/tests/drivers/db/TestCase.php b/tests/drivers/db/TestCase.php index 6a6a8ebc5d..4a9160bb9d 100644 --- a/tests/drivers/db/TestCase.php +++ b/tests/drivers/db/TestCase.php @@ -136,8 +136,8 @@ public function testReservedCount(): void public function testDoneCount(): void { - $this->getQueue()->messageHandler = function () { - return true; + $this->getQueue()->messageHandler = static function () { + return null; }; $job = $this->createSimpleJob(); diff --git a/tests/drivers/file/QueueTest.php b/tests/drivers/file/QueueTest.php index d9d8cc2287..13a62b0150 100644 --- a/tests/drivers/file/QueueTest.php +++ b/tests/drivers/file/QueueTest.php @@ -90,32 +90,31 @@ public function testRemove(): void $this->assertFileDoesNotExist($this->getQueue()->path . "/job$id.data"); } - public function testWaitingCount() + public function testWaitingCount(): void { $this->getQueue()->push($this->createSimpleJob()); $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getWaitingCount()); } - public function testDelayedCount() + public function testDelayedCount(): void { $this->getQueue()->delay(5)->push($this->createSimpleJob()); $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDelayedCount()); } - public function testReservedCount() + public function testReservedCount(): void { $this->getQueue()->messageHandler = function () { $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount()); }; - $job = $this->createSimpleJob(); - $this->getQueue()->push($job); + $this->getQueue()->push($this->createSimpleJob()); $this->getQueue()->run(false); } - public function testDoneCount() + public function testDoneCount(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); From 51ff720a1a150fa7bff357039948206195a56a0f Mon Sep 17 00:00:00 2001 From: EAMoiseenko Date: Mon, 19 May 2025 11:24:01 +0300 Subject: [PATCH 13/16] Fixed psalm issues --- tests/cli/InfoActionTest.php | 43 ++++++++++++------------------- tests/drivers/db/TestCase.php | 3 ++- tests/drivers/file/QueueTest.php | 1 + tests/drivers/redis/QueueTest.php | 2 ++ 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/tests/cli/InfoActionTest.php b/tests/cli/InfoActionTest.php index 26ad57f3a8..de8a3b876c 100644 --- a/tests/cli/InfoActionTest.php +++ b/tests/cli/InfoActionTest.php @@ -35,7 +35,7 @@ public function testWaitingCount(): void $controller->expects(self::exactly(3)) ->method('stdout') - ->with( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -53,12 +53,10 @@ public function testWaitingCount(): void $provider = $this->getMockBuilder(WaitingCountProvider::class) ->setConstructorArgs([$queue]) - ->getMock() - ; + ->getMock(); $provider->expects(self::once()) ->method('getWaitingCount') - ->willReturn(10) - ; + ->willReturn(10); $queue->method('getStatisticsProvider')->willReturn($provider); @@ -72,12 +70,11 @@ public function testDelayedCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->with( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -89,19 +86,16 @@ public function testDelayedCount(): void [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); $provider = $this->getMockBuilder(DelayedCountProvider::class) ->setConstructorArgs([$queue]) - ->getMock() - ; + ->getMock(); $provider->expects(self::once()) ->method('getDelayedCount') - ->willReturn(10) - ; + ->willReturn(10); $queue->method('getStatisticsProvider')->willReturn($provider); @@ -115,12 +109,11 @@ public function testReservedCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->with( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -142,8 +135,7 @@ public function testReservedCount(): void ; $provider->expects(self::once()) ->method('getReservedCount') - ->willReturn(10) - ; + ->willReturn(10); $queue->method('getStatisticsProvider')->willReturn($provider); @@ -153,16 +145,15 @@ public function testReservedCount(): void $action->run(); } - public function testDoneCount() + public function testDoneCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->with( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -180,12 +171,10 @@ public function testDoneCount() $provider = $this->getMockBuilder(DoneCountProvider::class) ->setConstructorArgs([$queue]) - ->getMock() - ; + ->getMock(); $provider->expects(self::once()) ->method('getDoneCount') - ->willReturn(10) - ; + ->willReturn(10); $queue->method('getStatisticsProvider')->willReturn($provider); diff --git a/tests/drivers/db/TestCase.php b/tests/drivers/db/TestCase.php index 4a9160bb9d..57648e9a7e 100644 --- a/tests/drivers/db/TestCase.php +++ b/tests/drivers/db/TestCase.php @@ -127,6 +127,7 @@ public function testReservedCount(): void { $this->getQueue()->messageHandler = function () { $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount()); + return true; }; $job = $this->createSimpleJob(); @@ -137,7 +138,7 @@ public function testReservedCount(): void public function testDoneCount(): void { $this->getQueue()->messageHandler = static function () { - return null; + return true; }; $job = $this->createSimpleJob(); diff --git a/tests/drivers/file/QueueTest.php b/tests/drivers/file/QueueTest.php index 13a62b0150..cab4694157 100644 --- a/tests/drivers/file/QueueTest.php +++ b/tests/drivers/file/QueueTest.php @@ -108,6 +108,7 @@ public function testReservedCount(): void { $this->getQueue()->messageHandler = function () { $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount()); + return true; }; $this->getQueue()->push($this->createSimpleJob()); diff --git a/tests/drivers/redis/QueueTest.php b/tests/drivers/redis/QueueTest.php index 137761e3c9..6777c0a456 100644 --- a/tests/drivers/redis/QueueTest.php +++ b/tests/drivers/redis/QueueTest.php @@ -110,6 +110,7 @@ public function testReservedCount(): void { $this->getQueue()->messageHandler = function () { $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getReservedCount()); + return true; }; $this->getQueue()->push($this->createSimpleJob()); @@ -159,6 +160,7 @@ public function testConsumeDelayedMessageAtLeastOnce(): void $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. From 5cfd6151cd69f6c7f7af6eb0e8d27814b6360806 Mon Sep 17 00:00:00 2001 From: EAMoiseenko Date: Mon, 19 May 2025 12:16:42 +0300 Subject: [PATCH 14/16] Drop PHP 8.4 --- .github/workflows/main.yml | 2 +- README.md | 2 +- src/InvalidJobException.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53abb86e9c..9f2e1b2c86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.2', '8.3', '8.4' ] + php: [ '8.2', '8.3' ] steps: - name: Checkout. uses: actions/checkout@v2 diff --git a/README.md b/README.md index a28bd8f40b..e336b02c26 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/src/InvalidJobException.php b/src/InvalidJobException.php index ae345f1d25..eb7988fb37 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); From b370e21969a7e9761aa538d8aadd05058a0ea4c7 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Tue, 3 Jun 2025 19:50:34 +0300 Subject: [PATCH 15/16] Fix #528: Prevent multiple execution of aborted jobs (#532) Co-authored-by: Lucas Bartholemy --- CHANGELOG.md | 2 +- src/Queue.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdf0fc758..541d80ee0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Yii2 Queue Extension Change Log ----------------------- - 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) - +- Bug #528: Prevent multiple execution of aborted jobs (luke-) 2.3.7 April 29, 2024 -------------------- diff --git a/src/Queue.php b/src/Queue.php index 212886fa95..27b16e3fc1 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -209,6 +209,12 @@ public function getWorkerPid(): ?int protected function handleMessage(int|string $id, string $message, int $ttr, int $attempt): bool { [$job, $error] = $this->unserializeMessage($message); + // Handle aborted jobs without throwing an error. + if ($attempt > 1 && + (($job instanceof RetryableJobInterface && !$job->canRetry($attempt - 1, $error)) + || (!($job instanceof RetryableJobInterface) && $attempt > $this->attempts))) { + return true; + } $event = new ExecEvent([ 'id' => $id, 'job' => $job, From 24c1b2419333bc1d38f937fa315b1a69388e68f7 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Tue, 10 Jun 2025 16:17:05 +0300 Subject: [PATCH 16/16] Updated CHANGELOG.md (#535) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 541d80ee0a..9e7f2e2cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ Yii2 Queue Extension Change Log =============================== +3.0.0 under development +--- +- The minimum supported PHP version is 8.2 +- Added PSALM for static code analysis. Error level set to 1 +- Added strict typing +- The `pda/pheanstalk` package of the Beanstalk driver has been updated to version 5.* +- Removed deprecated interfaces `RetryableJob`, `Job`, `Serializer` +- Removed deprecated classes `Signal`, `Verbose` +- Deprecated driver amqp has been removed +- Returned tests for the SQS driver +- All dependent packages for supported drivers have been updated to the latest versions +- The `opis/closure` package did not support PHP 8.1 and was replaced by the `laravel/serializable-closure` package + 2.3.8 under development ----------------------- - Enh #516: Ensure Redis driver messages are consumed at least once (soul11201)