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/.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/.gitattributes b/.gitattributes index 0e3c6ca146..8cdf150aeb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,8 @@ /.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 +/stubs export-ignore diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ffa601f0aa..9f2e1b2c86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,18 +1,28 @@ 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 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 }} @@ -20,9 +30,15 @@ jobs: strategy: fail-fast: false matrix: - php: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0' ] + php: [ '8.2', '8.3' ] 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..5126dc889c --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,41 @@ +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 +jobs: + psalm: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ '8.2', '8.3', '8.4' ] + 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/CHANGELOG.md b/CHANGELOG.md index 4bdf0fc758..9e7f2e2cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,24 @@ 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) - 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/Makefile b/Makefile index 0292d4b498..c01e40b11e 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,32 @@ 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.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=5.6 }}. Default latest PHP 5.6 - 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 +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 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.2 }}. Default latest PHP 8.2 PHP_VERSION=$(filter-out $@,$(v)) docker compose build --pull yii2-queue-php PHP_VERSION=$(filter-out $@,$(v)) docker compose run yii2-queue-php tests/yii benchmark/waiting make down sh: ## Enter the container with the application - docker exec -it yii2-queue-php 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.2 }}. Default latest PHP 8.2 + PHP_VERSION=$(filter-out $@,$(v)) docker compose build --pull yii2-queue-php + PHP_VERSION=$(filter-out $@,$(v)) docker compose run yii2-queue-php vendor/bin/psalm --config=psalm.xml --shepherd --stats --php-version=$(v) + make down clean: docker compose down @@ -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/README.md b/README.md index 8b5fed3a63..e336b02c26 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.2 or higher. + Installation ------------ diff --git a/composer.json b/composer.json index 0197ca1398..be729e7856 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", - "yiisoft/yii2": "~2.0.14", - "symfony/process": "^3.3||^4.0||^5.0||^6.0||^7.0" + "php": ">=8.2", + "yiisoft/yii2": "~2.0.50", + "symfony/process": "^7.0", + "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", - "aws/aws-sdk-php": ">=2.4", - "enqueue/stomp": "^0.8.39||^0.10.0", - "cweagans/composer-patches": "^1.7" + "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": "^5.0.0", + "aws/aws-sdk-php": "3.285.0", + "vimeo/psalm": "^6.0.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,23 +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/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 d40b0c8eda..3feee9238b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,30 @@ - + - + ./tests ./tests/app ./tests/docker ./tests/runtime + + + ./src + + + ./src/debug + ./src/gii + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000000..ec0f9676e5 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ExecEvent.php b/src/ExecEvent.php index 0689a6ff12..b59b9dafd0 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 readonly 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..31968b09ca 100644 --- a/src/JobEvent.php +++ b/src/JobEvent.php @@ -1,4 +1,7 @@ 'afterPush', @@ -46,7 +49,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 +58,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 +68,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 +81,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,11 +95,16 @@ 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(); + $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); } @@ -106,11 +114,16 @@ 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(); + $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); } @@ -121,7 +134,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,11 +145,13 @@ 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"; - if ($pid = $event->sender->getWorkerPid()) { + + $pid = $event->sender->getWorkerPid(); + if (null !== $pid) { $extra .= ", PID: $pid"; } return "$title ($extra)"; diff --git a/src/PushEvent.php b/src/PushEvent.php index b4a4949f24..9064836ea3 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 +101,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 +113,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 +125,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 +138,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(mixed $job): int|string|null { $event = new PushEvent([ 'job' => $job, @@ -173,23 +165,15 @@ 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.'); } - $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); @@ -197,34 +181,40 @@ 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); + // 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, @@ -240,11 +230,9 @@ protected function handleMessage($id, $message, $ttr, $attempt) return $this->handleError($event); } try { - $event->result = $event->job->execute($this); - } catch (\Exception $error) { - $event->error = $error; - return $this->handleError($event); - } catch (\Throwable $error) { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $event->result = $event->job?->execute($this); + } catch (Throwable $error) { $event->error = $error; return $this->handleError($event); } @@ -255,14 +243,13 @@ 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); + $job = $this->getSerializer()->unserialize($serialized); } catch (\Exception $e) { return [null, new InvalidJobException($serialized, $e->getMessage(), 0, $e)]; } @@ -282,7 +269,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 +282,41 @@ 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; + + private function getSerializer(): SerializerInterface + { + /** @psalm-var SerializerInterface */ + return Instance::ensure($this->serializer, SerializerInterface::class); + } } 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..f61ec269b5 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)) { @@ -49,8 +51,9 @@ public function init() /** * @param string $string * @return string + * @psalm-suppress MixedInferredReturnType, MixedReturnStatement */ - 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..af7cd5e756 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); @@ -122,7 +124,12 @@ public function beforeAction($action) if ($this->phpBinary === null) { $this->phpBinary = PHP_BINARY; } - $this->queue->messageHandler = function ($id, $message, $ttr, $attempt) { + $this->queue->messageHandler = function ( + int|string|null $id, + string $message, + int $ttr, + int $attempt + ): bool { return $this->handleMessage($id, $message, $ttr, $attempt); }; } @@ -134,14 +141,14 @@ public function beforeAction($action) * 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($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,17 +159,17 @@ 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" + /** @psalm-suppress PossiblyUndefinedArrayOffset */ $cmd = [ $this->phpBinary, $_SERVER['SCRIPT_FILENAME'], @@ -175,6 +182,7 @@ protected function handleMessage($id, $message, $ttr, $attempt) foreach ($this->getPassedOptions() as $name) { if (in_array($name, $this->options('exec'), true)) { + /** @psalm-suppress MixedOperand */ $cmd[] = '--' . $name . '=' . $this->$name; } } @@ -184,7 +192,7 @@ protected function handleMessage($id, $message, $ttr, $attempt) $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 { @@ -196,7 +204,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/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/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); + return Inflector::camel2id((string)$id); } } throw new InvalidConfigException('Queue must be an application component.'); @@ -80,7 +81,7 @@ protected function getCommandId() /** * @inheritdoc */ - public function bootstrap($app) + public function bootstrap($app): void { if ($app instanceof ConsoleApp) { $app->controllerMap[$this->getCommandId()] = [ @@ -97,9 +98,9 @@ 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(); + $this->workerPid = getmypid(); /** @var LoopInterface $loop */ $loop = Yii::createObject($this->loopConfig, [$this]); @@ -109,15 +110,14 @@ 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(); }); } finally { $this->trigger(self::EVENT_WORKER_STOP, $event); - $this->_workerPid = null; + $this->workerPid = null; } return $event->exitCode; @@ -130,15 +130,16 @@ protected function runWorker(callable $handler) * @return int|null * @since 2.0.2 */ - public function getWorkerPid() + public function getWorkerPid(): ?int { - return $this->_workerPid; + return $this->workerPid; } /** * @inheritdoc + * @psalm-suppress MixedReturnStatement, MixedInferredReturnType */ - 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,9 +157,9 @@ 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; + $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..5165117f31 100644 --- a/src/cli/SignalLoop.php +++ b/src/cli/SignalLoop.php @@ -1,4 +1,7 @@ of signals to exit from listening of the queue. */ - public $exitSignals = [ + public array $exitSignals = [ 15, // SIGTERM 3, // SIGQUIT 2, // SIGINT 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 $suspendSignals = []; + 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 $resumeSignals = []; + public array $resumeSignals = []; /** * @var Queue */ - protected $queue; + protected Queue $queue; /** * @var bool status when exit signal was got. */ - private static $exit = false; + private static bool $exit = false; /** * @var bool status when suspend or resume signal was got. */ - private static $pause = false; - + private static bool $pause = false; /** * @param Queue $queue @@ -67,22 +69,22 @@ public function __construct($queue, array $config = []) * * @inheritdoc */ - public function init() + public function init(): void { parent::init(); if (extension_loaded('pcntl') && function_exists('pcntl_signal')) { foreach ($this->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..f9ea59a55f 100644 --- a/src/cli/VerboseBehavior.php +++ b/src/cli/VerboseBehavior.php @@ -1,4 +1,7 @@ 'beforeExec', @@ -56,53 +60,55 @@ 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); - $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); } /** * @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); - $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); } /** * @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); - $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); } /** @@ -110,11 +116,12 @@ 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"; - if ($pid = $event->sender->getWorkerPid()) { + $pid = $event->sender->getWorkerPid(); + if (null !== $pid) { $extra .= ", pid: $pid"; } return " [$event->id] $name ($extra)"; @@ -124,27 +131,27 @@ 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); + $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); } /** * @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); + $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); } /** @@ -152,7 +159,7 @@ public function workerStop(WorkerEvent $event) * @return string * @since 2.0.2 */ - protected function formatDuration($value) + 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 e1ff4cbe2a..530c8ebd59 100644 --- a/src/cli/WorkerEvent.php +++ b/src/cli/WorkerEvent.php @@ -1,4 +1,7 @@ 'beforePush', @@ -48,9 +51,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..eb6897c7dc 100644 --- a/src/closure/Job.php +++ b/src/closure/Job.php @@ -1,4 +1,7 @@ serialized); - if ($unserialized instanceof \Closure) { - return $unserialized(); + /** @var SerializableClosure $unserialize */ + $unserialize = unserialize($this->serialized); + $closure = $unserialize->getClosure(); + $nativeClosure = $closure(); + + if ($nativeClosure instanceof Native) { + return $nativeClosure(); } - return $unserialized->execute($queue); + + /** @psalm-var JobInterface $nativeClosure */ + return $nativeClosure->execute($queue); } } diff --git a/src/debug/Panel.php b/src/debug/Panel.php index 7cb0511391..0d63cfddd9 100644 --- a/src/debug/Panel.php +++ b/src/debug/Panel.php @@ -1,4 +1,7 @@ */ -class Panel extends \yii\debug\Panel implements ViewContextInterface +class Panel extends BasePanel implements ViewContextInterface { - private $_jobs = []; - + private array $jobs = []; /** * @inheritdoc */ - public function getName() + public function getName(): string { return 'Queue'; } @@ -36,11 +40,11 @@ public function getName() /** * @inheritdoc */ - public function init() + 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); }); } @@ -48,7 +52,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) { @@ -79,13 +83,13 @@ protected function getPushData(PushEvent $event) */ public function save() { - return ['jobs' => $this->_jobs]; + return ['jobs' => $this->jobs]; } /** * @inheritdoc */ - public function getViewPath() + public function getViewPath(): string { return __DIR__ . '/views'; } @@ -93,8 +97,9 @@ public function getViewPath() /** * @inheritdoc */ - public function getSummary() + 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, @@ -104,14 +109,17 @@ public function getSummary() /** * @inheritdoc */ - public function getDetail() + public function getDetail(): string { - $jobs = isset($this->data['jobs']) ? $this->data['jobs'] : []; + /** @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'])) { @@ -119,8 +127,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 18eaffd867..fc3480854c 100644 --- a/src/debug/views/detail.php +++ b/src/debug/views/detail.php @@ -1,9 +1,14 @@ 'default', @@ -15,7 +20,7 @@

Pushed jobs

-
+

diff --git a/src/debug/views/summary.php b/src/debug/views/summary.php index a98218afce..4c25b7ffbc 100644 --- a/src/debug/views/summary.php +++ b/src/debug/views/summary.php @@ -1,9 +1,15 @@ - 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..b24f1cc252 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..603e3d1caf 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 +196,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 +251,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,15 +321,15 @@ public function init() /** * Listens amqp-queue and runs new jobs. */ - public function listen() + 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); - $callback = function (AmqpMessage $message, AmqpConsumer $consumer) { + $callback = function (AmqpMessage $message, AmqpConsumer $consumer): callable|bool { if ($message->isRedelivered()) { $consumer->acknowledge($message); @@ -323,10 +338,14 @@ public function listen() 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 ($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); @@ -337,7 +356,7 @@ public function listen() return true; }; - $subscriptionConsumer = $this->context->createSubscriptionConsumer(); + $subscriptionConsumer = $this->getContext()->createSubscriptionConsumer(); $subscriptionConsumer->subscribe($consumer, $callback); $subscriptionConsumer->consume(); } @@ -345,24 +364,26 @@ public function listen() /** * @return AmqpContext */ - public function getContext() + public function getContext(): AmqpContext { $this->open(); + /** @psalm-var AmqpContext */ return $this->context; } /** * @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); + $topic = $this->getContext()->createTopic($this->exchangeName); - $message = $this->context->createMessage($payload); + /** @psalm-var AmqpMessage $message */ + $message = $this->getContext()->createMessage($payload); $message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); $message->setMessageId(uniqid('', true)); $message->setTimestamp(time()); @@ -374,13 +395,14 @@ protected function pushMessage($payload, $ttr, $delay, $priority) ] )); - $producer = $this->context->createProducer(); + $producer = $this->getContext()->createProducer(); if ($delay) { $message->setProperty(self::DELAY, $delay); $producer->setDeliveryDelay($delay * 1000); } + /** @var int|null $priority */ if ($priority) { $message->setProperty(self::PRIORITY, $priority); $producer->setPriority($priority); @@ -398,7 +420,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 +428,25 @@ public function status($id) /** * Opens connection and channel. */ - protected function open() + protected function open(): void { + /** @psalm-suppress RedundantConditionGivenDocblockType */ 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 +472,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 +486,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,8 +511,11 @@ protected function setupBroker() /** * Closes connection and channel. */ - protected function close() + protected function close(): void { + /** + * @psalm-suppress DocblockTypeContradiction + */ if (!$this->context) { return; } @@ -499,14 +525,17 @@ protected function close() $this->setupBrokerDone = false; } - /** - * {@inheritdoc} - */ - protected function redeliver(AmqpMessage $message) + protected function redeliver(AmqpMessage $message): void { + /** @var int $attempt */ $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 +544,10 @@ protected function redeliver(AmqpMessage $message) $newMessage ); } + + private function createQueue(): AmqpQueue + { + /** @psalm-var AmqpQueue */ + return $this->getContext()->createQueue($this->queueName); + } } diff --git a/src/drivers/beanstalk/Command.php b/src/drivers/beanstalk/Command.php index d9478cf71e..2df4e720a7 100644 --- a/src/drivers/beanstalk/Command.php +++ b/src/drivers/beanstalk/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -40,7 +44,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen']); } @@ -51,7 +55,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -61,14 +65,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.'); } @@ -83,7 +84,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..0445780ad0 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)); + 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((string)$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 49e0c57a39..09b76d12c2 100644 --- a/src/drivers/beanstalk/Queue.php +++ b/src/drivers/beanstalk/Queue.php @@ -1,4 +1,7 @@ @@ -26,43 +35,61 @@ class Queue extends CliQueue /** * @var string connection host */ - public $host = 'localhost'; + public string $host = 'localhost'; /** * @var int connection port */ - public $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 */ - public $tube = 'queue'; + public string $tube = 'queue'; /** * @var string command class name */ - public $commandClass = Command::class; + 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 */ - 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->getPheanstalk()->reserveFromTube($this->tube, $timeout)) { - $info = $this->getPheanstalk()->statsJob($payload); - if ($this->handleMessage( - $payload->getId(), - $payload->getData(), - $info->ttr, - $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; @@ -74,81 +101,99 @@ 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."); } 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($id) + 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 (strpos($e->getMessage(), 'NOT_FOUND') === 0) { - return false; - } - - throw $e; + } catch (Throwable) { + return false; } } /** * @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, - $priority ?: PheanstalkInterface::DEFAULT_PRIORITY, - $delay, - $ttr - ); + $pheanstalk = $this->getPheanstalk(); + $pheanstalk->useTube($this->getTubeName()); + + $result = $pheanstalk + ->put( + $payload, + (int)$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() + public function getStatsTube(): TubeStats { - return $this->getPheanstalk()->statsTube($this->tube); + return $this->getPheanstalk()->statsTube($this->getTubeName()); } - /** - * @return Pheanstalk - */ - protected function getPheanstalk() + 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; } - private $_pheanstalk; + protected function getTubeName(): TubeName + { + return new TubeName($this->tube); + } + + 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/src/drivers/db/Command.php b/src/drivers/db/Command.php index 141fe9754b..4978bdd7f2 100644 --- a/src/drivers/db/Command.php +++ b/src/drivers/db/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -41,7 +45,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen'], true); } @@ -52,7 +56,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -62,14 +66,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.'); } @@ -82,7 +83,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(); @@ -96,7 +97,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 a91db65789..1280fa9a06 100644 --- a/src/drivers/db/InfoAction.php +++ b/src/drivers/db/InfoAction.php @@ -1,4 +1,7 @@ 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) @@ -61,7 +69,7 @@ protected function getWaiting() /** * @return Query */ - protected function getDelayed() + protected function getDelayed(): Query { return (new Query()) ->from($this->queue->tableName) @@ -73,7 +81,7 @@ protected function getDelayed() /** * @return Query */ - protected function getReserved() + protected function getReserved(): Query { return (new Query()) ->from($this->queue->tableName) @@ -85,7 +93,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 4d18266ad0..c26ad79b1a 100644 --- a/src/drivers/db/Queue.php +++ b/src/drivers/db/Queue.php @@ -1,4 +1,7 @@ db = Instance::ensure($this->db, Connection::class); + /** @psalm-suppress PropertyTypeCoercion */ $this->mutex = Instance::ensure($this->mutex, Mutex::class); } @@ -69,21 +76,22 @@ public function init() * 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 */ - 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()) { 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'], - $payload['ttr'], - $payload['attempt'] + (int) $payload['ttr'], + (int) $payload['attempt'] )) { $this->release($payload); } @@ -99,12 +107,12 @@ public function run($repeat, $timeout = 0) /** * @inheritdoc */ - public function status($id) + 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) { @@ -114,11 +122,11 @@ public function status($id) 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; } @@ -130,9 +138,9 @@ public function status($id) * * @since 2.0.1 */ - public function clear() + public function clear(): void { - $this->db->createCommand() + $this->getDb()->createCommand() ->delete($this->tableName, ['channel' => $this->channel]) ->execute(); } @@ -144,9 +152,9 @@ public function clear() * @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(); } @@ -154,30 +162,34 @@ 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, [ + $this->getDb()->createCommand()->insert($this->tableName, [ 'channel' => $this->channel, - 'job' => $message, + 'job' => $payload, 'pushed_at' => time(), 'ttr' => $ttr, '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??''); } /** * 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() + 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.'); } @@ -191,11 +203,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'], ], [ @@ -208,27 +220,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']] @@ -236,16 +246,14 @@ protected function release($payload) } } - protected $reserveTime; - /** * 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, @@ -256,16 +264,38 @@ protected function moveExpired() } } - private $_statistcsProvider; + 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; + } + + 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 ff6020314f..c5d0f3a9bf 100644 --- a/src/drivers/db/StatisticsProvider.php +++ b/src/drivers/db/StatisticsProvider.php @@ -1,4 +1,7 @@ */ -class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface +class StatisticsProvider extends BaseObject implements + DoneCountInterface, + WaitingCountInterface, + DelayedCountInterface, + ReservedCountInterface, + 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); @@ -36,47 +44,55 @@ public function __construct(Queue $queue, $config = []) /** * @inheritdoc */ - public function getWaitingCount() + 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]) - ->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()) + /** @psalm-var \yii\db\Connection $this->queue->db */ + return (int) (new Query()) ->from($this->queue->tableName) ->andWhere(['channel' => $this->queue->channel]) ->andWhere(['reserved_at' => null]) - ->andWhere(['>', 'delay', 0])->count('*', $this->queue->db); + ->andWhere(['>', 'delay', 0]) + ->count('*', $this->queue->db); } /** * @inheritdoc */ - public function getReservedCount() + 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') - ->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()) + /** @psalm-var \yii\db\Connection $this->queue->db */ + return (int) (new Query()) ->from($this->queue->tableName) ->andWhere(['channel' => $this->queue->channel]) - ->andWhere('[[done_at]] is not null')->count('*', $this->queue->db); + ->andWhere('[[done_at]] is not null') + ->count('*', $this->queue->db); } } diff --git a/src/drivers/db/migrations/M161119140200Queue.php b/src/drivers/db/migrations/M161119140200Queue.php index 30dfa861c0..14b8adfda8 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(), @@ -29,13 +30,13 @@ public function up() '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'); } - 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 a9d9fa54b1..04cd2b897e 100644 --- a/src/drivers/db/migrations/M211218163000JobQueueSize.php +++ b/src/drivers/db/migrations/M211218163000JobQueueSize.php @@ -1,4 +1,7 @@ db->driverName === 'mysql') { $this->alterColumn($this->tableName, 'job', 'LONGBLOB NOT NULL'); } } - public function down() + public function down(): void { if ($this->db->driverName === 'mysql') { $this->alterColumn($this->tableName, 'job', $this->binary()->notNull()); diff --git a/src/drivers/file/Command.php b/src/drivers/file/Command.php index edfdd5dbd5..abe94f4d3d 100644 --- a/src/drivers/file/Command.php +++ b/src/drivers/file/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -41,7 +45,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen']); } @@ -52,7 +56,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -65,11 +69,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.'); } @@ -82,7 +83,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(); @@ -96,9 +97,9 @@ 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((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 6a168f7fbe..a2bc34d288 100644 --- a/src/drivers/file/InfoAction.php +++ b/src/drivers/file/InfoAction.php @@ -1,4 +1,7 @@ format('Jobs', Console::FG_GREEN)); + Console::output($this->format('Jobs', BaseConsole::FG_GREEN)); - Console::stdout($this->format('- waiting: ', Console::FG_YELLOW)); - Console::output($this->getWaitingCount()); + Console::stdout($this->format('- waiting: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getWaitingCount()); - Console::stdout($this->format('- delayed: ', Console::FG_YELLOW)); - Console::output($this->getDelayedCount()); + Console::stdout($this->format('- delayed: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getDelayedCount()); - Console::stdout($this->format('- reserved: ', Console::FG_YELLOW)); - Console::output($this->getReservedCount()); + Console::stdout($this->format('- reserved: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getReservedCount()); - Console::stdout($this->format('- done: ', Console::FG_YELLOW)); - Console::output($this->getDoneCount()); + Console::stdout($this->format('- done: ', BaseConsole::FG_YELLOW)); + Console::output((string)$this->getDoneCount()); } /** * @return int */ - protected function getWaitingCount() + protected function getWaitingCount(): int { + /** @var array{waiting: array} $data */ $data = $this->getIndexData(); return !empty($data['waiting']) ? count($data['waiting']) : 0; } @@ -57,8 +63,9 @@ protected function getWaitingCount() /** * @return int */ - protected function getDelayedCount() + protected function getDelayedCount(): int { + /** @var array{delayed: array} $data */ $data = $this->getIndexData(); return !empty($data['delayed']) ? count($data['delayed']) : 0; } @@ -66,8 +73,9 @@ protected function getDelayedCount() /** * @return int */ - protected function getReservedCount() + protected function getReservedCount(): int { + /** @var array{reserved: array} $data */ $data = $this->getIndexData(); return !empty($data['reserved']) ? count($data['reserved']) : 0; } @@ -75,14 +83,18 @@ protected function getReservedCount() /** * @return int */ - protected function getDoneCount() + protected function getDoneCount(): int { + /** @var array{lastId: int} $data */ $data = $this->getIndexData(); - $total = isset($data['lastId']) ? $data['lastId'] : 0; + $total = $data['lastId'] ?? 0; 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 d92c9eb13a..2bfcade701 100644 --- a/src/drivers/file/Queue.php +++ b/src/drivers/file/Queue.php @@ -1,4 +1,7 @@ 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); } } @@ -66,17 +72,23 @@ public function init() * 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 */ - 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; + /** + * @var int|string $id + * @var string $message + * @var int $ttr + * @var int $attempt + */ if ($this->handleMessage($id, $message, $ttr, $attempt)) { $this->delete($payload); } @@ -92,7 +104,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."); @@ -110,10 +122,9 @@ public function status($id) * * @since 2.0.1 */ - public function clear() + public function clear(): void { - $this->touchIndex(function (&$data) { - $data = []; + $this->touchIndex(function () { foreach (glob("$this->path/job*.data") as $fileName) { unlink($fileName); } @@ -126,13 +137,16 @@ public function clear() * @param int $id of a job * @return bool * @since 2.0.1 + * @psalm-suppress MixedInferredReturnType, MixedReturnStatement */ - 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) { + /** @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; @@ -142,6 +156,7 @@ public function remove($id) } 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; @@ -151,6 +166,7 @@ public function remove($id) } 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; @@ -171,27 +187,37 @@ 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) { + /** @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()) { - list($id, $ttr, $attempt, $time) = $payload; + /** @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()) { - list($id, $ttr, $time) = array_shift($data['delayed']); + /** @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'])) { - list($id, $ttr) = array_shift($data['waiting']); + /** @psalm-suppress MixedArrayAssignment, MixedArrayAccess */ + [$id, $ttr] = array_shift($data['waiting']); } if ($id) { $attempt = 1; @@ -211,11 +237,13 @@ 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) { + $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; @@ -228,19 +256,21 @@ 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.'); } + $id = 0; - $this->touchIndex(function (&$data) use ($message, $ttr, $delay, &$id) { + $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; } $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); } @@ -248,7 +278,8 @@ protected function pushMessage($message, $ttr, $delay, $priority) $data['waiting'][] = [$id, $ttr, 0]; } else { $data['delayed'][] = [$id, $ttr, time() + $delay]; - usort($data['delayed'], function ($a, $b) { + /** @psalm-suppress MixedArgumentTypeCoercion */ + usort($data['delayed'], static function (array $a, array $b) { if ($a[2] < $b[2]) { return -1; } @@ -266,6 +297,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) } }); + /** @psalm-var int|string|null $id */ return $id; } @@ -273,7 +305,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) * @param callable $callback * @throws InvalidConfigException */ - private function touchIndex($callback) + private function touchIndex(callable $callback): void { $fileName = "$this->path/index.data"; $isNew = !file_exists($fileName); @@ -295,6 +327,7 @@ private function touchIndex($callback) } try { $callback($data); + /** @var string $newContent */ $newContent = call_user_func($this->indexSerializer, $data); if ($newContent !== $content) { ftruncate($file, 0); @@ -308,16 +341,16 @@ private function touchIndex($callback) } } - 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 cedc9520b1..27cc1a75f7 100644 --- a/src/drivers/file/StatisticsProvider.php +++ b/src/drivers/file/StatisticsProvider.php @@ -1,4 +1,7 @@ */ -class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface +class StatisticsProvider extends BaseObject implements + DoneCountInterface, + WaitingCountInterface, + DelayedCountInterface, + ReservedCountInterface, + 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); @@ -35,8 +43,9 @@ public function __construct(Queue $queue, $config = []) /** * @inheritdoc */ - public function getWaitingCount() + public function getWaitingCount(): int { + /** @var array{waiting:array} $data */ $data = $this->getIndexData(); return !empty($data['waiting']) ? count($data['waiting']) : 0; } @@ -44,8 +53,9 @@ public function getWaitingCount() /** * @inheritdoc */ - public function getDelayedCount() + public function getDelayedCount(): int { + /** @var array{delayed:array} $data */ $data = $this->getIndexData(); return !empty($data['delayed']) ? count($data['delayed']) : 0; } @@ -53,8 +63,9 @@ public function getDelayedCount() /** * @inheritdoc */ - public function getReservedCount() + public function getReservedCount(): int { + /** @var array{reserved:array} $data */ $data = $this->getIndexData(); return !empty($data['reserved']) ? count($data['reserved']) : 0; } @@ -62,20 +73,24 @@ public function getReservedCount() /** * @inheritdoc */ - public function getDoneCount() + public function getDoneCount(): int { + /** @var array{lastId:int} $data */ $data = $this->getIndexData(); $total = isset($data['lastId']) ? $data['lastId'] : 0; return $total - $this->getDelayedCount() - $this->getWaitingCount(); } + /** + * @psalm-suppress MissingReturnType + */ protected function getIndexData() { $fileName = $this->queue->path . '/index.data'; if (file_exists($fileName)) { return call_user_func($this->queue->indexDeserializer, file_get_contents($fileName)); - } else { - return []; } + + return []; } } diff --git a/src/drivers/gearman/Command.php b/src/drivers/gearman/Command.php index 02ffe427ac..d21c191e49 100644 --- a/src/drivers/gearman/Command.php +++ b/src/drivers/gearman/Command.php @@ -1,4 +1,7 @@ queue->run(false); } @@ -47,7 +51,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..9c47d5162f 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) { + /** @psalm-suppress PossiblyUndefinedArrayOffset */ + [$ttr, $message] = explode(';', $payload->workload(), 2); + $this->handleMessage($payload->handle(), $message, (int)$ttr, 1); }); $worker->setTimeout($repeat ? 1000 : 1); while ($canContinue()) { @@ -56,32 +64,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); + $status = $this->getClient()->jobStatus((string)$id); if ($status[0] && !$status[1]) { return self::STATUS_WAITING; } + /** @psalm-suppress RedundantCondition */ if ($status[0] && $status[1]) { return self::STATUS_RESERVED; } @@ -90,16 +96,14 @@ public function status($id) } /** - * @return \GearmanClient + * @return GearmanClient */ - protected function getClient() + 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 0d152389ca..50da9ac51d 100644 --- a/src/drivers/redis/Command.php +++ b/src/drivers/redis/Command.php @@ -1,4 +1,7 @@ InfoAction::class, @@ -41,7 +45,7 @@ public function actions() /** * @inheritdoc */ - protected function isWorkerAction($actionID) + protected function isWorkerAction($actionID): bool { return in_array($actionID, ['run', 'listen'], true); } @@ -52,7 +56,7 @@ protected function isWorkerAction($actionID) * * @return null|int exit code. */ - public function actionRun() + public function actionRun(): ?int { return $this->queue->run(false); } @@ -62,14 +66,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.'); } @@ -82,7 +83,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(); @@ -96,7 +97,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 b9f1cd5078..97fe1fa1ea 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"); $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 1da8238795..48232e3c03 100644 --- a/src/drivers/redis/Queue.php +++ b/src/drivers/redis/Queue.php @@ -1,4 +1,7 @@ redis = Instance::ensure($this->redis, Connection::class); } @@ -55,13 +61,17 @@ 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; + /** + * @psalm-var int|string $id + * @psalm-var string $message + */ + if ($this->handleMessage($id, $message, (int)$ttr, (int)$attempt)) { $this->delete($id); } } elseif (!$repeat) { @@ -74,7 +84,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."); @@ -96,11 +106,12 @@ 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); } + /** @psalm-suppress MixedArgument */ $this->redis->executeCommand('DEL', $this->redis->keys("$this->channel.*")); } @@ -111,7 +122,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); @@ -132,7 +143,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)) { @@ -145,6 +156,7 @@ protected function reserve($timeout) 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) { @@ -156,8 +168,12 @@ protected function reserve($timeout) return null; } - list($ttr, $message) = explode(';', $payload, 2); - $this->redis->zadd("$this->channel.reserved", time() + $ttr, $id); + /** + * @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); return [$id, $message, $ttr, $attempt]; @@ -166,7 +182,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')) { @@ -180,9 +196,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); @@ -192,14 +208,15 @@ 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.'); } + /** @var string|int $id */ $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 { @@ -209,16 +226,16 @@ protected function pushMessage($message, $ttr, $delay, $priority) 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 78f21d2d37..5df423f334 100644 --- a/src/drivers/redis/StatisticsProvider.php +++ b/src/drivers/redis/StatisticsProvider.php @@ -1,4 +1,7 @@ */ -class StatisticsProvider extends BaseObject implements DoneCountInterface, WaitingCountInterface, DelayedCountInterface, ReservedCountInterface +class StatisticsProvider extends BaseObject implements + DoneCountInterface, + WaitingCountInterface, + DelayedCountInterface, + ReservedCountInterface, + 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); @@ -35,40 +44,40 @@ public function __construct(Queue $queue, $config = []) /** * @inheritdoc */ - public function getWaitingCount() + public function getWaitingCount(): int { $prefix = $this->queue->channel; - return $this->queue->redis->llen("$prefix.waiting"); + return (int) $this->queue->redis->llen("$prefix.waiting"); } /** * @inheritdoc */ - public function getDelayedCount() + 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'); } /** * @inheritdoc */ - public function getReservedCount() + 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'); } /** * @inheritdoc */ - public function getDoneCount() + public function getDoneCount(): int { $prefix = $this->queue->channel; $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/drivers/sqs/Command.php b/src/drivers/sqs/Command.php index d2b5e288bd..9cb5850adc 100644 --- a/src/drivers/sqs/Command.php +++ b/src/drivers/sqs/Command.php @@ -1,4 +1,7 @@ queue->run(false); } @@ -40,16 +44,11 @@ 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.'); - } - $timeout = (int) $timeout; - if ($timeout < 1 || $timeout > 20) { throw new Exception('Timeout must be between 1 and 20'); } @@ -60,7 +59,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(); @@ -71,7 +70,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/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 486c381473..47dbe4b81d 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()) { 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) { @@ -108,10 +106,11 @@ public function run($repeat, $timeout = 0) * 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($timeout) + protected function reserve(int $timeout): ?Payload { + /** @var array{Messages: array} $response */ $response = $this->getClient()->receiveMessage([ 'QueueUrl' => $this->url, 'AttributeNames' => ['ApproximateReceiveCount'], @@ -119,18 +118,18 @@ protected function reserve($timeout) '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']; - if ($ttr != $this->ttr) { + $ttr = $payload->ttr; + if ($ttr !== $this->ttr) { $this->getClient()->changeMessageVisibility([ 'QueueUrl' => $this->url, - 'ReceiptHandle' => $payload['ReceiptHandle'], + 'ReceiptHandle' => $payload->receiptHandle, 'VisibilityTimeout' => $ttr, ]); } @@ -141,20 +140,20 @@ protected function reserve($timeout) /** * Deletes the message after successfully handling. * - * @param array $payload + * @param Payload $payload */ - protected function delete($payload) + protected function delete(Payload $payload): void { $this->getClient()->deleteMessage([ 'QueueUrl' => $this->url, - 'ReceiptHandle' => $payload['ReceiptHandle'], + 'ReceiptHandle' => $payload->receiptHandle, ]); } /** * Clears the queue. */ - public function clear() + public function clear(): void { $this->getClient()->purgeQueue([ 'QueueUrl' => $this->url, @@ -164,7 +163,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 +178,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): bool { return $this->handleMessage($id, $message, $ttr, $attempt); } @@ -187,7 +186,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 +194,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) $request = [ 'QueueUrl' => $this->url, - 'MessageBody' => $message, + 'MessageBody' => $payload, 'DelaySeconds' => $delay, 'MessageAttributes' => [ 'TTR' => [ @@ -205,22 +204,22 @@ 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); - return $response['MessageId']; + return null === $response['MessageId']?null:(string)$response['MessageId']; } /** - * @return \Aws\Sqs\SqsClient + * @return SqsClient */ - protected function getClient() + protected function getClient(): SqsClient { - if ($this->_client) { - return $this->_client; + if (null !== $this->client) { + return $this->client; } if ($this->key !== null && $this->secret !== null) { @@ -234,11 +233,11 @@ protected function getClient() $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/src/drivers/stomp/Command.php b/src/drivers/stomp/Command.php index a51386bbae..974a0d1c31 100644 --- a/src/drivers/stomp/Command.php +++ b/src/drivers/stomp/Command.php @@ -1,4 +1,7 @@ queue->run(false); } @@ -52,11 +55,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..16885a136c 100644 --- a/src/drivers/stomp/Queue.php +++ b/src/drivers/stomp/Queue.php @@ -1,4 +1,7 @@ context) { - return; - } - $config = [ 'host' => $this->host, 'port' => $this->port, @@ -137,31 +139,31 @@ protected function open() 'ssl_on' => $this->sslOn, ]; - $config = array_filter($config, function ($value) { + $config = array_filter($config, static function ($value) { return null !== $value; }); - $factory = new StompConnectionFactory($config); - - $this->context = $factory->createContext(); + return (new StompConnectionFactory($config))->createContext(); } /** * Listens queue and runs each job. * - * @param $repeat - * @param int $timeout + * @param bool $repeat + * @param int<0, max> $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(); $queue = $this->createQueue($this->queueName); - $consumer = $this->context->createConsumer($queue); + $consumer = $this->getContext()->createConsumer($queue); while ($canContinue()) { - if ($message = ($this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait())) { + /** @var StompMessage|null $message */ + $message = $this->readTimeOut > 0 ? $consumer->receive($this->readTimeOut) : $consumer->receiveNoWait(); + if (null !== $message) { $messageId = $message->getMessageId(); if (!$messageId) { $message = $this->setMessageId($message); @@ -175,10 +177,11 @@ public function run($repeat, $timeout = 0) 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 ($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); @@ -189,7 +192,7 @@ public function run($repeat, $timeout = 0) break; } elseif ($timeout) { sleep($timeout); - $this->context->getStomp()->getConnection()->sendAlive(); + $this->getConnection()->sendAlive(); } } }); @@ -198,9 +201,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,21 +210,21 @@ 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->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.'); @@ -240,7 +242,7 @@ protected function pushMessage($message, $ttr, $delay, $priority) /** * Closes connection. */ - protected function close() + protected function close(): void { if (!$this->context) { return; @@ -254,39 +256,57 @@ 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); + $attempt = (int)$message->getProperty(self::ATTEMPT, 1); - $newMessage = $this->context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders()); + $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 ); } /** - * @param $name - * @return \Enqueue\Stomp\StompDestination + * @param string $name + * @return StompDestination */ - private function createQueue($name) + 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/drivers/sync/Queue.php b/src/drivers/sync/Queue.php index 28db7e872b..6e4bcb6c58 100644 --- a/src/drivers/sync/Queue.php +++ b/src/drivers/sync/Queue.php @@ -1,4 +1,7 @@ handle) { @@ -60,10 +61,15 @@ 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; + /** + * @var int $ttr + * @var string $message + * @psalm-suppress MixedArrayAccess + */ + [$ttr, $message] = $payload; $this->startedId = $this->finishedId + 1; $this->handleMessage($this->startedId, $message, $ttr, 1); $this->finishedId = $this->startedId; @@ -74,16 +80,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..1fe0515641 100644 --- a/src/gii/Generator.php +++ b/src/gii/Generator.php @@ -1,4 +1,7 @@ */ -class Generator extends \yii\gii\Generator +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 */ - public function getName() + public function getName(): string { return 'Job Generator'; } @@ -38,7 +41,7 @@ public function getName() /** * @inheritdoc */ - public function getDescription() + public function getDescription(): string { return 'This generator generates a Job class for the queue.'; } @@ -46,7 +49,7 @@ public function getDescription() /** * @inheritdoc */ - public function rules() + public function rules(): array { return array_merge(parent::rules(), [ [['jobClass', 'properties', 'ns', 'baseClass'], 'trim'], @@ -63,7 +66,7 @@ public function rules() /** * @inheritdoc */ - public function attributeLabels() + public function attributeLabels(): array { return array_merge(parent::attributeLabels(), [ 'jobClass' => 'Job Class', @@ -77,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.', @@ -91,7 +94,7 @@ public function hints() /** * @inheritdoc */ - public function stickyAttributes() + public function stickyAttributes(): array { return array_merge(parent::stickyAttributes(), ['ns', 'baseClass']); } @@ -99,7 +102,7 @@ public function stickyAttributes() /** * @inheritdoc */ - public function requiredTemplates() + public function requiredTemplates(): array { return ['job.php']; } @@ -107,7 +110,7 @@ public function requiredTemplates() /** * @inheritdoc */ - public function generate() + public function generate(): array { $params = []; $params['jobClass'] = $this->jobClass; @@ -125,8 +128,12 @@ public function generate() } $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) ); @@ -138,8 +145,9 @@ public function generate() * * @param string $attribute job attribute name. */ - public function validateJobClass($attribute) + 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.'); } @@ -150,8 +158,9 @@ public function validateJobClass($attribute) * * @param string $attribute Namespace attribute name. */ - public function validateNamespace($attribute) + 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 898d5c1249..d269b4ee74 100644 --- a/src/gii/default/job.php +++ b/src/gii/default/job.php @@ -1,13 +1,20 @@ field($generator, 'jobClass')->textInput(['autofocus' => true]) ?> field($generator, 'properties') ?> 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 @@ toArray($job), $this->options); } @@ -40,7 +42,7 @@ public function serialize($job) /** * @inheritdoc */ - public function unserialize($serialized) + public function unserialize(string $serialized): mixed { return $this->fromArray(Json::decode($serialized)); } @@ -50,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)]; @@ -80,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 c521eed78d..95eb0b0adb 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..376502e146 100644 --- a/src/serializers/SerializerInterface.php +++ b/src/serializers/SerializerInterface.php @@ -1,4 +1,7 @@ |array{class: class-string}|callable(): T $type + * @param array $params + * + * @return T + */ + abstract public static function createObject($type, array $params = []); +} diff --git a/support/ide-helper.php b/support/ide-helper.php new file mode 100644 index 0000000000..dd0a459e97 --- /dev/null +++ b/support/ide-helper.php @@ -0,0 +1,65 @@ +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..a7e31e8f1c 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..1fa8384344 100644 --- a/tests/app/RetryJob.php +++ b/tests/app/RetryJob.php @@ -1,4 +1,7 @@ getFileName(), 'a', FILE_APPEND); - throw new \Exception('Planned error.'); + throw new Exception('Planned error.'); } - public function getFileName() + public function getFileName(): bool|string { - return Yii::getAlias("@runtime/job-{$this->uid}.lock"); + 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, ?Throwable $error): bool { return $attempt < 2; } diff --git a/tests/app/SimpleJob.php b/tests/app/SimpleJob.php index 4647f4d5f0..5fac7a062b 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"); + return Yii::getAlias("@runtime/job-$this->uid.lock"); } } diff --git a/tests/app/benchmark/Controller.php b/tests/app/benchmark/Controller.php index c5358b6774..65b66ca4c4 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); @@ -38,7 +43,7 @@ public function beforeAction($action) 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/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 a4cb30fd13..66f104ed05 100644 --- a/tests/app/config/main.php +++ b/tests/app/config/main.php @@ -1,29 +1,48 @@ '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', + 'sqsQueue', + 'sqsFifoQueue', ], '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,26 +56,26 @@ ], ], 'mysqlQueue' => [ - 'class' => \yii\queue\db\Queue::class, + 'class' => DbQueue::class, 'db' => 'mysql', 'mutex' => [ - 'class' => \yii\mutex\MysqlMutex::class, + 'class' => MysqlMutex::class, 'db' => 'mysql', ], 'deleteReleased' => false, ], '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, 'deleteReleased' => false, ], 'pgsql' => [ - 'class' => \yii\db\Connection::class, + 'class' => Connection::class, 'dsn' => sprintf( 'pgsql:host=%s;dbname=%s', getenv('POSTGRES_HOST') ?: 'localhost', @@ -67,47 +86,58 @@ '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, 'deleteReleased' => false, ], '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', + 'port' => getenv('ACTIVEMQ_PORT'), + 'user' => getenv('ACTIVEMQ_USER'), + 'password' => getenv('ACTIVEMQ_PASSWORD'), + ], + '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'), ], ], ]; @@ -115,32 +145,9 @@ if (defined('GEARMAN_SUCCESS')) { $config['bootstrap'][] = 'gearmanQueue'; $config['components']['gearmanQueue'] = [ - 'class' => \yii\queue\gearman\Queue::class, + 'class' => GearmanQueue::class, 'host' => getenv('GEARMAN_HOST') ?: 'localhost', ]; } -if (getenv('AWS_SQS_ENABLED')) { - $config['bootstrap'][] = 'sqsQueue'; - $config['components']['sqsQueue'] = [ - 'class' => \yii\queue\sqs\Queue::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' => \yii\queue\sqs\Queue::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/bootstrap.php b/tests/bootstrap.php index 64cfda50f5..0b388ae229 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,7 @@ getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -45,19 +47,16 @@ public function testWaitingCount() [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); $provider = $this->getMockBuilder(WaitingCountProvider::class) ->setConstructorArgs([$queue]) - ->getMock() - ; + ->getMock(); $provider->expects(self::once()) ->method('getWaitingCount') - ->willReturn(10) - ; + ->willReturn(10); $queue->method('getStatisticsProvider')->willReturn($provider); @@ -67,16 +66,15 @@ public function testWaitingCount() $action->run(); } - public function testDelayedCount() + public function testDelayedCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -88,19 +86,16 @@ public function testDelayedCount() [ 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); @@ -110,16 +105,15 @@ public function testDelayedCount() $action->run(); } - public function testReservedCount() + public function testReservedCount(): void { $controller = $this->getMockBuilder(Controller::class) ->setConstructorArgs(['testController', new Module('testModule')]) - ->getMock() - ; + ->getMock(); $controller->expects(self::exactly(3)) ->method('stdout') - ->withConsecutive( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -131,8 +125,7 @@ public function testReservedCount() [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); @@ -142,8 +135,7 @@ public function testReservedCount() ; $provider->expects(self::once()) ->method('getReservedCount') - ->willReturn(10) - ; + ->willReturn(10); $queue->method('getStatisticsProvider')->willReturn($provider); @@ -153,16 +145,15 @@ public function testReservedCount() $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') - ->withConsecutive( + ->willReturnOnConsecutiveCalls( [ 'Jobs' . PHP_EOL, Console::FG_GREEN, @@ -174,19 +165,16 @@ public function testDoneCount() [ 10 . PHP_EOL ] - ) - ; + ); $queue = $this->getMockBuilder(Queue::class)->getMock(); $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/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 @@ 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 6e78de144c..0efa422190 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,12 +1,13 @@ -version: "3.5" +--- services: - # https://hub.docker.com/_/php/ 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.2} volumes: - ./runtime/.composer:/root/.composer - ..:/code @@ -14,6 +15,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 @@ -29,16 +35,16 @@ services: RABBITMQ_PASSWORD: guest 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} + 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} + 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 @@ -47,14 +53,17 @@ services: - beanstalk - gearmand - activemq + - localstack networks: net: {} + extra_hosts: + - host.docker.internal:${HOST_IP:-host-gateway} # https://hub.docker.com/_/mysql/ mysql: image: mysql:5.7 ports: - - 3307:3306 + - "3307:3306" environment: MYSQL_ALLOW_EMPTY_PASSWORD: 1 MYSQL_USER: yii2_queue_test @@ -71,7 +80,7 @@ services: postgres: image: postgres:10.4 ports: - - 5433:5432 + - "5433:5432" environment: POSTGRES_USER: yii2_queue_test POSTGRES_PASSWORD: yii2_queue_test @@ -83,7 +92,7 @@ services: redis: image: redis:4.0 ports: - - 6380:6379 + - "6380:6379" networks: net: {} @@ -96,11 +105,11 @@ 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 + - "11301:11300" networks: net: {} @@ -108,19 +117,39 @@ services: gearmand: image: artefactual/gearmand ports: - - 4731:4730 + - "4731:4730" networks: net: {} - # https://hub.docker.com/r/webcenter/activemq/ + # https://hub.docker.com/r/apache/activemq-artemis activemq: - image: islandora/activemq:3 + image: apache/activemq-artemis:latest-alpine ports: - - 61613:61613 + - "61616:61616" + - "8161:8161" + 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: + driver: bridge name: yii2_queue_net - + ipam: + config: + - subnet: 172.18.0.0/16 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..c76d6726d3 --- /dev/null +++ b/tests/docker/php/Dockerfile @@ -0,0 +1,36 @@ +# Important! Do not use this image in production! +ARG PHP_VERSION + +FROM php:${PHP_VERSION}-cli-alpine + +RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories \ + && echo https://dl-cdn.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories \ + && 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..66a0b60620 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,11 +63,10 @@ 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); $replace = [ 'php' => PHP_BINARY, @@ -74,7 +77,7 @@ private function prepareCmd($cmd) array_walk( $cmd, static function (&$v) use ($replace) { - $v = strtr($v, $replace); + $v = strtr((string)$v, $replace); } ); @@ -84,7 +87,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..3837174749 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,51 +76,66 @@ public function testLater() $this->assertSimpleJobLaterDone($job, 2); } - public function testRetry() + public function testRemove(): void + { + $id = $this->getQueue()->push($this->createSimpleJob()); + $this->assertTrue($this->jobIsExists($id)); + $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 = new RetryJob(['uid' => uniqid()]); - $this->getQueue()->push($job); - sleep(6); - $this->assertFileExists($job->getFileName()); - $this->assertEquals('aa', file_get_contents($job->getFileName())); + $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 testRemove() + public function testStatusTube(): void { - $id = $this->getQueue()->push($this->createSimpleJob()); - $this->assertTrue($this->jobIsExists($id)); - $this->runProcess(['php', 'yii', 'queue/remove', $id]); + $queue = $this->getQueue(); + $queue->push($this->createSimpleJob()); - $this->assertFalse($this->jobIsExists($id)); + $statusTube = $queue->getStatsTube(); + + $this->assertEquals('queue', $statusTube->name->value); } /** * @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); + $connection = Pheanstalk::create($this->getQueue()->host, $this->getQueue()->port); try { - $connection->peek($id); + $connection->peek(new JobId($id)); return true; - } catch (ServerException $e) { - if (strpos($e->getMessage(), 'NOT_FOUND') === 0) { - return false; - } else { - throw $e; - } + } catch (Throwable) { + return false; } } } 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 181f95b85c..57648e9a7e 100644 --- a/tests/drivers/db/TestCase.php +++ b/tests/drivers/db/TestCase.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -29,7 +32,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -41,7 +44,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])); @@ -53,7 +56,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(); @@ -62,7 +65,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -71,10 +74,10 @@ 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()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -82,7 +85,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']); @@ -94,7 +97,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]); @@ -106,24 +109,25 @@ public function testRemove() $this->assertEquals(0, $actual); } - 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()); + return true; }; $job = $this->createSimpleJob(); @@ -131,9 +135,9 @@ public function testReservedCount() $this->getQueue()->run(false); } - public function testDoneCount() + public function testDoneCount(): void { - $this->getQueue()->messageHandler = function () { + $this->getQueue()->messageHandler = static function () { return true; }; @@ -144,8 +148,7 @@ public function testDoneCount() $this->assertEquals(1, $this->getQueue()->getStatisticsProvider()->getDoneCount()); } - - protected function tearDown() + protected function tearDown(): void { $this->getQueue()->messageHandler = null; $this->getQueue()->db->createCommand() diff --git a/tests/drivers/file/QueueTest.php b/tests/drivers/file/QueueTest.php index 42bc68050c..cab4694157 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,10 +61,10 @@ 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()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -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,41 +81,41 @@ 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"); } - 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()); + return true; }; - $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(); @@ -126,12 +129,12 @@ public function testDoneCount() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->fileQueue; } - protected function tearDown() + protected function tearDown(): void { $this->getQueue()->messageHandler = null; foreach (glob(Yii::getAlias("@runtime/queue/*")) as $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 92200fd630..6777c0a456 100644 --- a/tests/drivers/redis/QueueTest.php +++ b/tests/drivers/redis/QueueTest.php @@ -1,4 +1,7 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -29,7 +33,7 @@ public function testRun() $this->assertSimpleJobDone($job); } - public function testStatus() + public function testStatus(): void { $job = $this->createSimpleJob(); $id = $this->getQueue()->push($job); @@ -41,7 +45,7 @@ public function testStatus() $this->assertTrue($isDone); } - public function testListen() + public function testListen(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -50,7 +54,7 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testLater() + public function testLater(): void { $this->startProcess(['php', 'yii', 'queue/listen', '1']); $job = $this->createSimpleJob(); @@ -59,10 +63,10 @@ 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()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -70,7 +74,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 . '.*')); @@ -79,7 +83,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)); @@ -88,32 +92,32 @@ public function testRemove() $this->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()); + return true; }; - $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(); @@ -127,12 +131,12 @@ public function testDoneCount() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->redisQueue; } - protected function tearDown() + protected function tearDown(): void { $this->getQueue()->messageHandler = null; $this->getQueue()->redis->flushdb(); @@ -148,14 +152,15 @@ protected function tearDown() * 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++; + return true; }; // Ensure the delayed message can be consumed when more time passed than the delay is. @@ -168,7 +173,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 @@ createSimpleJob(); $this->getQueue()->push($job); @@ -26,7 +30,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,19 +39,19 @@ public function testListen() $this->assertSimpleJobDone($job); } - public function testFifoQueueDoesNotSupportPerMessageDelays() + 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() + 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); @@ -55,12 +59,8 @@ 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'); - } - $this->getQueue()->push($this->createSimpleJob()); $this->runProcess(['php', 'yii', 'queue/clear', '--interactive=0']); } @@ -68,17 +68,8 @@ public function testClear() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->sqsFifoQueue; } - - protected function setUp() - { - 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 8ef1e518ad..acfb14b36f 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,10 +49,10 @@ 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()]); + $job = new RetryJob(['uid' => uniqid('', true)]); $this->getQueue()->push($job); sleep(6); @@ -57,12 +60,8 @@ 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'); - } - $this->getQueue()->push($this->createSimpleJob()); $this->runProcess(['php', 'yii', 'queue/clear', '--interactive=0']); } @@ -70,17 +69,8 @@ public function testClear() /** * @return Queue */ - protected function getQueue() + protected function getQueue(): Queue { return Yii::$app->sqsQueue; } - - protected function setUp() - { - if (!getenv('AWS_SQS_ENABLED')) { - $this->markTestSkipped('AWS SQS tests are disabled'); - } - - parent::setUp(); - } } diff --git a/tests/drivers/stomp/QueueTest.php b/tests/drivers/stomp/QueueTest.php index c3aeb72762..f6df3369ce 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 +27,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); @@ -35,22 +38,16 @@ public function testRetry() $this->assertEquals('aa', file_get_contents($job->getFileName())); } - /** - * @return Queue - */ - protected function getQueue() + public function testStatus(): void { - return Yii::$app->stompQueue; - } + $this->expectException(NotSupportedException::class); + $id = $this->getQueue()->push($this->createSimpleJob()); + $this->getQueue()->isWaiting($id); + } - protected function setUp() + protected function getQueue(): Queue { - if ('true' == getenv('EXCLUDE_STOMP')) { - $this->markTestSkipped('Stomp tests are disabled for php 5.5'); - } - - parent::setUp(); + return Yii::$app->stompQueue; } - } 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(mixed $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 @@ -76,6 +80,6 @@ public function providerSerialize() class TestObject extends BaseObject { - public $foo; - public $bar; + public int $foo; + public array $bar; } 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');