├── .gitignore ├── .editorconfig ├── tests ├── TestCase.php └── EnsureProcessesCommandTest.php ├── .php_cs ├── phpunit.xml ├── .scrutinizer.yml ├── LICENSE.md ├── config └── queue-ensurer.php ├── src ├── ServiceProvider.php ├── Processes │ └── ProcessManager.php ├── PidsFile │ └── ContentsManager.php ├── Config │ └── ConfigReader.php └── Commands │ └── EnsureProcesses.php ├── composer.json ├── .travis.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .php_cs.cache 3 | .phpunit.result.cache 4 | composer.lock 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Documentation: http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.php] 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected function getPackageProviders($app): array 18 | { 19 | return [ServiceProvider::class]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@Symfony' => true, 6 | '@Symfony:risky' => true, 7 | 'array_syntax' => ['syntax' => 'short'], 8 | 'protected_to_private' => false, 9 | 'compact_nullable_typehint' => true, 10 | 'concat_space' => ['spacing' => 'one'], 11 | 'phpdoc_separation' => false, 12 | 'yoda_style' => null, 13 | ]) 14 | ->setRiskyAllowed(true) 15 | ->setFinder( 16 | PhpCsFixer\Finder::create() 17 | ->in([ 18 | __DIR__ . '/src', 19 | __DIR__ . '/tests', 20 | ]) 21 | ->append([__FILE__]) 22 | ); 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | 4 | checks: 5 | php: 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | code_rating: true 20 | duplication: true 21 | 22 | build: 23 | nodes: 24 | php71: 25 | environment: 26 | php: 27 | version: 7.1.12 28 | tests: 29 | override: 30 | - php-scrutinizer-run 31 | - 32 | command: vendor/bin/phpunit --coverage-clover=coverage71 33 | coverage: 34 | file: coverage71 35 | format: php-clover 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Webparking BV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /config/queue-ensurer.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'specify-queue' => true, // Should the --queue parameter be used? 7 | 'timeout' => 0, // The timeout for the worker process. 8 | 'sleep' => 10, // The sleep time when there are no jobs. 9 | 'tries' => 5, // The maximum number of tries 10 | ], 11 | 12 | // Configure the number of processes you want to run per queue or 13 | // alternatively, define a more in depth configuration. 14 | 'queues' => [ 15 | 'default' => 1, 16 | // 'another' => [ 17 | // 'amount' => 1, // The number of processes you want to run. 18 | // 'connection' => 'second-connection', // Optional: the connection you'd like to use. 19 | // // Override any of the default queue:workerk options here. 20 | // ], 21 | ], 22 | 23 | // Should we schedule the ensurer command to run every minute? 24 | 'schedule' => true, 25 | 26 | // The default php executable is called, if you have multiple PHP versions or intalled PHP on different location you can insert the full path top it here 27 | //'php-path' => '/usr/local/php73/bin/php73', 28 | ]; 29 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 14 | __DIR__ . '/../config/queue-ensurer.php', 15 | 'queue-ensurer' 16 | ); 17 | } 18 | 19 | public function boot(): void 20 | { 21 | $this->publishes([ 22 | __DIR__ . '/../config/queue-ensurer.php' => config_path('queue-ensurer.php'), 23 | ], 'config'); 24 | 25 | if ($this->app->runningInConsole()) { 26 | $this->commands([ 27 | EnsureProcesses::class, 28 | ]); 29 | 30 | if (config('queue-ensurer.schedule')) { 31 | $this->app->booted(function () { 32 | /** @var Schedule $schedule */ 33 | $schedule = app(Schedule::class); 34 | $schedule->command(EnsureProcesses::class)->everyMinute(); 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Processes/ProcessManager.php: -------------------------------------------------------------------------------- 1 | /dev/null & echo $!'; 38 | 39 | return (int) exec( 40 | $command 41 | ); 42 | } 43 | 44 | public function killProcess(int $processId): void 45 | { 46 | posix_kill($processId, 9); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webparking/laravel-queue-ensurer", 3 | "description": "This composer package provides a Laravel queue ensurer.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "php", 8 | "queue", 9 | "ensurer", 10 | "queues" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Remko Brenters", 15 | "email": "remko.brenters@webparking.nl" 16 | }, 17 | { 18 | "name": "Jeroen van Oort", 19 | "email": "jeroen.vanoort@webparking.nl" 20 | }, 21 | { 22 | "name": "Peter Klooster", 23 | "email": "peter.klooster@webparking.nl" 24 | } 25 | ], 26 | "require": { 27 | "php": ">=7.1.0", 28 | "ext-json": "*", 29 | "ext-posix": "*", 30 | "illuminate/support": "^5.5|^6.0|^7.0|^8.0", 31 | "illuminate/console": "^5.5|^6.0|^7.0|^8.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Webparking\\QueueEnsurer\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Webparking\\QueueEnsurer\\Tests\\": "tests/" 41 | } 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Webparking\\QueueEnsurer\\ServiceProvider" 47 | ] 48 | } 49 | }, 50 | "scripts": { 51 | "phpstan": "@php vendor/bin/phpstan analyse config src tests -l max --no-progress --ansi", 52 | "php-cs-fixer": "vendor/bin/php-cs-fixer fix . --config=.php_cs", 53 | "phpmd": "vendor/bin/phpmd config,src,tests text cleancode,codesize,controversial,design,unusedcode", 54 | "phpunit": "vendor/bin/phpunit", 55 | "test": "composer php-cs-fixer && composer phpstan && composer phpmd && composer phpunit" 56 | }, 57 | "require-dev": { 58 | "friendsofphp/php-cs-fixer": "^2.12", 59 | "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0", 60 | "orchestra/testbench": "^3.0|^4.0|^5.0|^6", 61 | "phpmd/phpmd": "^2.6", 62 | "phpstan/phpstan": "^0.12.18" 63 | }, 64 | "prefer-stable": true 65 | } 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | cache: 4 | directories: 5 | - $HOME/.composer/cache 6 | 7 | matrix: 8 | fast_finish: true 9 | include: 10 | - php: 7.1 11 | env: LARAVEL='5.5.*' TESTBENCH='3.5.*' COMPOSER_FLAGS='--prefer-lowest' 12 | - php: 7.1 13 | env: LARAVEL='5.5.*' TESTBENCH='3.5.*' 14 | - php: 7.1 15 | env: LARAVEL='5.6.*' TESTBENCH='3.6.*' 16 | - php: 7.1 17 | env: LARAVEL='5.7.*' TESTBENCH='3.7.*' 18 | - php: 7.1 19 | env: LARAVEL='5.8.*' TESTBENCH='3.8.*' 20 | - php: 7.2 21 | env: LARAVEL='5.5.*' TESTBENCH='3.5.*' 22 | - php: 7.2 23 | env: LARAVEL='5.6.*' TESTBENCH='3.6.*' 24 | - php: 7.2 25 | env: LARAVEL='5.7.*' TESTBENCH='3.7.*' 26 | - php: 7.2 27 | env: LARAVEL='5.8.*' TESTBENCH='3.8.*' 28 | - php: 7.2 29 | env: LARAVEL='^6' TESTBENCH='^4' 30 | - php: 7.2 31 | env: LARAVEL='^7' TESTBENCH='^5' 32 | - php: 7.3 33 | env: LARAVEL='5.5.*' TESTBENCH='3.5.*' 34 | - php: 7.3 35 | env: LARAVEL='5.6.*' TESTBENCH='3.6.*' 36 | - php: 7.3 37 | env: LARAVEL='5.7.*' TESTBENCH='3.7.*' 38 | - php: 7.3 39 | env: LARAVEL='5.8.*' TESTBENCH='3.8.*' 40 | - php: 7.3 41 | env: LARAVEL='^6' TESTBENCH='^4' 42 | - php: 7.3 43 | env: LARAVEL='^7' TESTBENCH='^5' 44 | - php: 7.3 45 | env: LARAVEL='^8' TESTBENCH='^6' 46 | - php: 7.4 47 | env: LARAVEL='5.5.*' TESTBENCH='3.5.*' 48 | - php: 7.4 49 | env: LARAVEL='5.6.*' TESTBENCH='3.6.*' 50 | - php: 7.4 51 | env: LARAVEL='5.7.*' TESTBENCH='3.7.*' 52 | - php: 7.4 53 | env: LARAVEL='5.8.*' TESTBENCH='3.8.*' 54 | - php: 7.4 55 | env: LARAVEL='^6' TESTBENCH='^4' 56 | - php: 7.4 57 | env: LARAVEL='^7' TESTBENCH='^5' 58 | - php: 7.4 59 | env: LARAVEL='^8' TESTBENCH='^6' 60 | 61 | before_install: 62 | - travis_retry composer self-update 63 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer require --no-update --no-interaction "illuminate/support:${LARAVEL}" "illuminate/console:${LARAVEL}" "orchestra/testbench:${TESTBENCH}" 64 | 65 | install: 66 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --no-interaction 67 | 68 | script: 69 | - composer test 70 | 71 | notifications: 72 | email: false 73 | -------------------------------------------------------------------------------- /src/PidsFile/ContentsManager.php: -------------------------------------------------------------------------------- 1 | getFileContents()); 15 | } 16 | 17 | /** @return int[] */ 18 | public function getPids(string $queueName): array 19 | { 20 | $fileContents = $this->getFileContents(); 21 | 22 | if (!isset($fileContents[$queueName])) { 23 | return []; 24 | } 25 | 26 | return $fileContents[$queueName]; 27 | } 28 | 29 | public function addPid(string $queueName, int $processId): void 30 | { 31 | $pids = $this->getPids($queueName); 32 | 33 | $pids[] = $processId; 34 | 35 | $this->updateFileForQueue($queueName, $pids); 36 | } 37 | 38 | public function removePid(string $queueName, int $processId): void 39 | { 40 | $pids = $this->getPids($queueName); 41 | 42 | unset($pids[array_search($processId, $pids)]); 43 | 44 | $this->updateFileForQueue($queueName, $pids); 45 | } 46 | 47 | public function removeQueue(string $queueName): void 48 | { 49 | $fileContents = $this->getFileContents(); 50 | 51 | unset($fileContents[$queueName]); 52 | 53 | $this->writeFile($fileContents); 54 | } 55 | 56 | /** @param array $contents */ 57 | private function updateFileForQueue(string $queueName, array $contents): void 58 | { 59 | $fileContents = $this->getFileContents(); 60 | 61 | $fileContents[$queueName] = $contents; 62 | 63 | $this->writeFile($fileContents); 64 | } 65 | 66 | /** @return array> */ 67 | private function getFileContents(): array 68 | { 69 | if (!file_exists($this->getPidsFilePath())) { 70 | return []; 71 | } 72 | 73 | $fileContents = json_decode( 74 | (string) file_get_contents($this->getPidsFilePath()), 75 | true 76 | ); 77 | 78 | if (!\is_array($fileContents)) { 79 | return []; 80 | } 81 | 82 | return $fileContents; 83 | } 84 | 85 | /** @param array> $contents */ 86 | private function writeFile(array $contents): void 87 | { 88 | file_put_contents( 89 | $this->getPidsFilePath(), 90 | json_encode($contents) 91 | ); 92 | } 93 | 94 | private function getPidsFilePath(): string 95 | { 96 | return storage_path(self::PIDS_FILE_NAME); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Config/ConfigReader.php: -------------------------------------------------------------------------------- 1 | */ 8 | public function getQueueNames(): array 9 | { 10 | /** @var array $res */ 11 | $res = array_keys(config('queue-ensurer.queues')); 12 | 13 | return $res; 14 | } 15 | 16 | public function getAmount(string $queueName): int 17 | { 18 | $queueConfig = $this->getQueueConfig($queueName); 19 | 20 | if (\is_array($queueConfig)) { 21 | return (int) $queueConfig['amount']; 22 | } 23 | 24 | return $queueConfig; 25 | } 26 | 27 | public function getPhpPath(): string 28 | { 29 | return config('queue-ensurer.php-path', 'php'); 30 | } 31 | 32 | public function getConnection(string $queueName): ?string 33 | { 34 | $queueConfig = $this->getQueueConfig($queueName); 35 | 36 | if (\is_array($queueConfig) && \array_key_exists('connection', $queueConfig)) { 37 | return (string) $queueConfig['connection']; 38 | } 39 | 40 | return null; 41 | } 42 | 43 | public function specifyQueue(string $queueName): bool 44 | { 45 | $queueConfig = $this->getQueueConfig($queueName); 46 | 47 | if (\is_array($queueConfig) && \array_key_exists('specify-queue', $queueConfig)) { 48 | return (bool) $queueConfig['specify-queue']; 49 | } 50 | 51 | return config('queue-ensurer.defaults.specify-queue'); 52 | } 53 | 54 | public function getTimeout(string $queueName): int 55 | { 56 | $queueConfig = $this->getQueueConfig($queueName); 57 | 58 | if (\is_array($queueConfig) && \array_key_exists('timeout', $queueConfig)) { 59 | return (int) $queueConfig['timeout']; 60 | } 61 | 62 | return config('queue-ensurer.defaults.timeout'); 63 | } 64 | 65 | public function getSleep(string $queueName): int 66 | { 67 | $queueConfig = $this->getQueueConfig($queueName); 68 | 69 | if (\is_array($queueConfig) && \array_key_exists('sleep', $queueConfig)) { 70 | return (int) $queueConfig['sleep']; 71 | } 72 | 73 | return config('queue-ensurer.defaults.sleep'); 74 | } 75 | 76 | public function getTries(string $queueName): int 77 | { 78 | $queueConfig = $this->getQueueConfig($queueName); 79 | 80 | if (\is_array($queueConfig) && \array_key_exists('tries', $queueConfig)) { 81 | return (int) $queueConfig['tries']; 82 | } 83 | 84 | return config('queue-ensurer.defaults.tries'); 85 | } 86 | 87 | /** 88 | * @return array|int 89 | */ 90 | private function getQueueConfig(string $queueName) 91 | { 92 | return config('queue-ensurer.queues')[$queueName]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Laravel Queue Ensurer 3 |

4 | 5 |

6 | 7 | Build Status 8 | 9 | 10 | Quality score 11 | 12 | 13 | Code coverage 14 | 15 |

16 | 17 | This package provides a command (`queue:ensure-processes`) to allow running the Laravel queue worker (`queue:work`) from the Laravel schedule. This enables a cronjob to ensure that configured queue workers are running. It eliminates the need for a process manager like [supervisord](http://supervisord.org/), which is not available in all production environments (like when working with DirectAdmin or most other server control panels). 18 | 19 | Multiple queues can be configured and the number of desired processes can be configured per queue (which makes it possible to run multiple jobs in parallel). Doing so, allows having the queue configuration in your project's codebase. 20 | 21 | This package doesn't care about which queue driver(s) you use and `queue:restart` still works as normal. 22 | 23 | ## Installation 24 | ``` 25 | composer require webparking/laravel-queue-ensurer 26 | ``` 27 | 28 | By default, the `queue:ensure-processes` command is configured to run once a minute, ensuring one worker for the default queue. So if that's all you desire, you're good to go. 29 | 30 | ## Configuration 31 | You can publish the configuration file to your project by running `php artisan vendor:publish --provider="Webparking\QueueEnsurer\ServiceProvider" --tag="config"`. 32 | 33 | The documentation for the configurable options can be found in the config file. 34 | 35 | ## Working 36 | The queue ensurer works by keeping a cache of process id's (PID's) it has started. Every time the ensurer runs, it does this: 37 | 38 | 1. Remove any PID's of stopped processes from the cache. 39 | These processes may have been stopped by a server reboot, `queue:restart` or for any other reason. 40 | 2. Kill processes belonging to no longer configured queue's and remove their PID's from the cache. 41 | When a queue was configured to have processes before, but is not configured now. 42 | 3. Kill processes that are no longer required and remove their PID's from the cache. 43 | When the number of configured processes is lower than the number of running processes. 44 | 4. Start new processes and add their PID's to the cache. 45 | When the number of configured processes is higher than the number of running processes. 46 | 47 | This means that the ensurer will not take in account any processes it has not started itself. 48 | 49 | For the PID cache, the ensurer uses a JSON file (`storage/app/queue-listener-pids.json`) instead of the Laravel cache mechanism. If the queue ensurer were to use the Laravel cache and the cache were to be cleared (`php artisan cache:clear`), the running processes would not be known to the ensurer any longer. Resulting in it starting new processes, without every killing the old ones. 50 | 51 | ## Contribution and development 52 | We're happy to receive pull requests or issues. 53 | 54 | When developing, you can run `composer test` to execute all code quality checks and tests. 55 | 56 | ## Future features 57 | These are features we may add. We don't have a specific need for them now, but we acknowledge their usefulness and we will add them when we have some down time. Should you or your project require one or more of these future features earlier, please submit a PR or create an issue. 58 | 59 | * Testing compatibility with Lumen 60 | 61 | ## Licence and Postcardware 62 | 63 | This software is open source and licensed under the [MIT license](LICENSE.md). 64 | 65 | If you use this software in your daily development we would appreciate to receive a postcard of your hometown. 66 | 67 | Please send it to: Webparking BV, Cypresbaan 31a, 2908 LT Capelle aan den IJssel, The Netherlands 68 | -------------------------------------------------------------------------------- /src/Commands/EnsureProcesses.php: -------------------------------------------------------------------------------- 1 | contentsManager = $contentsManager; 34 | $this->processManager = $processManager; 35 | $this->configReader = $configReader; 36 | 37 | parent::__construct(); 38 | } 39 | 40 | public function handle(): void 41 | { 42 | $this->removeStoppedProcessesFromFile(); 43 | $this->killNoLongerConfiguredQueueProcesses(); 44 | $this->killNoLongerNeededProcesses(); 45 | $this->startNeededProcesses(); 46 | } 47 | 48 | private function removeStoppedProcessesFromFile(): void 49 | { 50 | foreach ($this->contentsManager->getQueueNames() as $queueName) { 51 | foreach ($this->contentsManager->getPids($queueName) as $processId) { 52 | if (!$this->processManager->isStillRunning($processId)) { 53 | $this->contentsManager->removePid($queueName, $processId); 54 | } 55 | } 56 | } 57 | } 58 | 59 | private function killNoLongerConfiguredQueueProcesses(): void 60 | { 61 | $queuesToKill = array_diff( 62 | $this->contentsManager->getQueueNames(), 63 | $this->configReader->getQueueNames() 64 | ); 65 | 66 | foreach ($queuesToKill as $queueName) { 67 | foreach ($this->contentsManager->getPids($queueName) as $processId) { 68 | $this->processManager->killProcess($processId); 69 | $this->contentsManager->removePid($queueName, $processId); 70 | } 71 | 72 | $this->contentsManager->removeQueue($queueName); 73 | } 74 | } 75 | 76 | private function killNoLongerNeededProcesses(): void 77 | { 78 | foreach ($this->configReader->getQueueNames() as $queueName) { 79 | $configuredAmount = $this->configReader->getAmount($queueName); 80 | $amountOfProcesses = \count($this->contentsManager->getPids($queueName)); 81 | 82 | if ($configuredAmount < $amountOfProcesses) { 83 | foreach (\array_slice( 84 | $this->contentsManager->getPids($queueName), 85 | $configuredAmount - $amountOfProcesses 86 | ) as $processId) { 87 | $this->processManager->killProcess($processId); 88 | $this->contentsManager->removePid($queueName, $processId); 89 | } 90 | } 91 | } 92 | } 93 | 94 | private function startNeededProcesses(): void 95 | { 96 | foreach ($this->configReader->getQueueNames() as $queueName) { 97 | $configuredAmount = $this->configReader->getAmount($queueName); 98 | $amountOfProcesses = \count($this->contentsManager->getPids($queueName)); 99 | 100 | if ($configuredAmount > $amountOfProcesses) { 101 | for ($i = 1; $i <= ($configuredAmount - $amountOfProcesses); ++$i) { 102 | $this->contentsManager->addPid( 103 | $queueName, 104 | $this->processManager->startProcess( 105 | $this->configReader->getPhpPath(), 106 | $queueName, 107 | $this->configReader->getConnection($queueName), 108 | $this->configReader->specifyQueue($queueName), 109 | $this->configReader->getTimeout($queueName), 110 | $this->configReader->getSleep($queueName), 111 | $this->configReader->getTries($queueName) 112 | ) 113 | ); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/EnsureProcessesCommandTest.php: -------------------------------------------------------------------------------- 1 | setQueueConfig([ 12 | 'default' => 0, 13 | ]); 14 | 15 | $process = $this->startProcess(); 16 | $pid = (int) $process->getPid(); 17 | $process->stop(); 18 | 19 | $this->assertNoLongerRunning($process); 20 | 21 | $this->writePidsFile([ 22 | 'default' => [ 23 | $pid, 24 | ], 25 | ]); 26 | 27 | $this->artisan('queue:ensure-processes'); 28 | 29 | $this->assertPidsFileEquals([ 30 | 'default' => [], 31 | ]); 32 | } 33 | 34 | public function testKillNoLongerConfiguredQueues(): void 35 | { 36 | $this->setQueueConfig([]); 37 | 38 | $process = $this->startProcess(); 39 | 40 | $this->writePidsFile([ 41 | 'test-queue' => [ 42 | (int) $process->getPid(), 43 | ], 44 | ]); 45 | 46 | $this->artisan('queue:ensure-processes'); 47 | 48 | $this->assertPidsFileEquals([]); 49 | $this->assertNoLongerRunning($process); 50 | } 51 | 52 | public function testKillsNoLongerNeededProcess(): void 53 | { 54 | $this->setQueueConfig([ 55 | 'default' => 1, 56 | ]); 57 | 58 | $process1 = $this->startProcess(); 59 | $process2 = $this->startProcess(); 60 | 61 | $this->writePidsFile([ 62 | 'default' => [ 63 | (int) $process1->getPid(), 64 | (int) $process2->getPid(), 65 | ], 66 | ]); 67 | 68 | $this->artisan('queue:ensure-processes'); 69 | 70 | $this->assertPidsFileEquals([ 71 | 'default' => [ 72 | (int) $process1->getPid(), 73 | ], 74 | ]); 75 | $this->assertStillRunning($process1); 76 | $this->assertNoLongerRunning($process2); 77 | $process1->stop(); 78 | } 79 | 80 | public function testStartsNewProcesses(): void 81 | { 82 | $this->setQueueConfig([ 83 | 'default' => 1, 84 | ]); 85 | 86 | $this->writePidsFile([]); 87 | 88 | $this->artisan('queue:ensure-processes'); 89 | 90 | $this->assertNumberOfProcessesForQueue('default', 1); 91 | } 92 | 93 | public function testStartsNewProcessesWithArrayConfig(): void 94 | { 95 | $this->setQueueConfig([ 96 | 'default' => [ 97 | 'amount' => 2, 98 | 'connection' => 'altcon', 99 | 'specify-queue' => true, 100 | 'timeout' => 120, 101 | 'sleep' => 2, 102 | 'tries' => 2, 103 | ], 104 | ]); 105 | 106 | $this->writePidsFile([]); 107 | 108 | $this->artisan('queue:ensure-processes'); 109 | 110 | $this->assertNumberOfProcessesForQueue('default', 2); 111 | } 112 | 113 | public function testLeaveCorrectProcessesAsIs(): void 114 | { 115 | $this->setQueueConfig([ 116 | 'default' => 1, 117 | ]); 118 | 119 | $process = $this->startProcess(); 120 | 121 | $this->writePidsFile([ 122 | 'default' => [ 123 | (int) $process->getPid(), 124 | ], 125 | ]); 126 | 127 | $this->artisan('queue:ensure-processes'); 128 | 129 | $this->assertPidsFileEquals([ 130 | 'default' => [ 131 | (int) $process->getPid(), 132 | ], 133 | ]); 134 | $this->assertStillRunning($process); 135 | $process->stop(); 136 | } 137 | 138 | public function testWorkWhenNoFileExists(): void 139 | { 140 | $this->setQueueConfig([ 141 | 'default' => 1, 142 | ]); 143 | 144 | unlink(storage_path('app/queue-listener-pids.json')); 145 | 146 | $this->artisan('queue:ensure-processes'); 147 | 148 | $this->assertNumberOfProcessesForQueue('default', 1); 149 | } 150 | 151 | /** @param array> $contents */ 152 | private function writePidsFile(array $contents): void 153 | { 154 | file_put_contents( 155 | storage_path('app/queue-listener-pids.json'), 156 | json_encode($contents) 157 | ); 158 | } 159 | 160 | /** @param array> $contents */ 161 | private function assertPidsFileEquals(array $contents): void 162 | { 163 | $this->assertJsonStringEqualsJsonString( 164 | (string) json_encode($contents), 165 | (string) file_get_contents(storage_path('app/queue-listener-pids.json')) 166 | ); 167 | } 168 | 169 | private function assertNumberOfProcessesForQueue(string $queueName, int $expected): void 170 | { 171 | $contents = json_decode( 172 | (string) file_get_contents(storage_path('app/queue-listener-pids.json')), 173 | true 174 | ); 175 | 176 | $this->assertArrayHasKey($queueName, $contents); 177 | $this->assertCount($expected, $contents[$queueName]); 178 | } 179 | 180 | /** @return Process */ 181 | private function startProcess(): Process 182 | { 183 | $process = new Process(['sleep', '10']); 184 | $process->start(); 185 | 186 | return $process; 187 | } 188 | 189 | /** @param Process $process */ 190 | private function assertNoLongerRunning(Process $process): void 191 | { 192 | $this->assertFalse( 193 | $process->isRunning() 194 | ); 195 | } 196 | 197 | /** @param Process $process */ 198 | private function assertStillRunning(Process $process): void 199 | { 200 | $this->assertTrue( 201 | $process->isRunning() 202 | ); 203 | } 204 | 205 | /** @param array> $config */ 206 | private function setQueueConfig(array $config): void 207 | { 208 | config()->set('queue-ensurer.queues', $config); 209 | } 210 | } 211 | --------------------------------------------------------------------------------