├── LICENSE ├── README.md ├── composer.json ├── config ├── Migrations │ ├── 20240307154751_MigrationQueueInitV8.php │ └── 20250508121432_MigrationQueueMemory.php ├── app.example.php └── bootstrap.php ├── docs ├── CONTRIBUTING.md ├── README.md └── sections │ ├── bar_html.png │ ├── bar_text.png │ ├── basic_setup.md │ ├── configuration.md │ ├── cron.md │ ├── custom_tasks.md │ ├── job_statistic.png │ ├── limitations.md │ ├── mailing.md │ ├── misc.md │ ├── queueing_jobs.md │ ├── resources │ └── autocomplete.png │ ├── statistics.md │ ├── tasks │ ├── email.md │ ├── execute.md │ └── mailer.md │ ├── tips.md │ └── upgrading.md ├── phpcs.xml ├── phpstan.neon ├── resources └── locales │ └── queue.pot ├── src ├── Command │ ├── AddCommand.php │ ├── BakeQueueTaskCommand.php │ ├── InfoCommand.php │ ├── JobCommand.php │ ├── RunCommand.php │ └── WorkerCommand.php ├── Config │ └── JobConfig.php ├── Console │ └── Io.php ├── Controller │ └── Admin │ │ ├── LoadHelperTrait.php │ │ ├── QueueController.php │ │ ├── QueueProcessesController.php │ │ └── QueuedJobsController.php ├── Generator │ └── Task │ │ └── QueuedJobTask.php ├── Mailer │ └── Transport │ │ ├── QueueTransport.php │ │ └── SimpleQueueTransport.php ├── Migration │ └── OldTaskFinder.php ├── Model │ ├── Behavior │ │ └── JsonableBehavior.php │ ├── Entity │ │ ├── QueueProcess.php │ │ └── QueuedJob.php │ ├── ProcessEndingException.php │ ├── QueueException.php │ └── Table │ │ ├── QueueProcessesTable.php │ │ └── QueuedJobsTable.php ├── Queue │ ├── AddFromBackendInterface.php │ ├── AddInterface.php │ ├── Config.php │ ├── Processor.php │ ├── ServicesTrait.php │ ├── Task.php │ ├── Task │ │ ├── CostsExampleTask.php │ │ ├── EmailTask.php │ │ ├── ExampleTask.php │ │ ├── ExceptionExampleTask.php │ │ ├── ExecuteTask.php │ │ ├── MailerTask.php │ │ ├── MonitorExampleTask.php │ │ ├── ProgressExampleTask.php │ │ ├── RetryExampleTask.php │ │ ├── SuperExampleTask.php │ │ └── UniqueExampleTask.php │ ├── TaskFinder.php │ └── TaskInterface.php ├── QueuePlugin.php ├── Utility │ └── Memory.php └── View │ └── Helper │ ├── QueueHelper.php │ └── QueueProgressHelper.php ├── templates ├── Admin │ ├── Queue │ │ ├── index.php │ │ └── processes.php │ ├── QueueProcesses │ │ ├── edit.php │ │ ├── index.php │ │ └── view.php │ └── QueuedJobs │ │ ├── data.php │ │ ├── edit.php │ │ ├── execute.php │ │ ├── import.php │ │ ├── index.php │ │ ├── migrate.php │ │ ├── stats.php │ │ ├── test.php │ │ └── view.php ├── bake │ └── Task │ │ └── task.twig └── element │ ├── ok.php │ ├── search.php │ └── yes_no.php └── tests ├── Fixture ├── QueueProcessesFixture.php └── QueuedJobsFixture.php ├── TestCase ├── Command │ ├── AddCommandTest.php │ ├── BakeQueueTaskCommandTest.php │ ├── InfoCommandTest.php │ ├── JobCommandTest.php │ ├── RunCommandTest.php │ └── WorkerCommandTest.php ├── Config │ └── JobConfigTest.php ├── Controller │ └── Admin │ │ ├── QueueControllerTest.php │ │ ├── QueueProcessesControllerTest.php │ │ └── QueuedJobsControllerTest.php ├── Generator │ └── Task │ │ └── QueuedJobGeneratorTest.php ├── Mailer │ └── Transport │ │ ├── QueueTransportTest.php │ │ └── SimpleQueueTransportTest.php ├── Model │ └── Table │ │ ├── QueueProcessesTableTest.php │ │ └── QueuedJobsTableTest.php ├── Queue │ ├── ProcessorTest.php │ ├── Task │ │ ├── CostsExampleTaskTest.php │ │ ├── EmailTaskTest.php │ │ ├── ExampleTaskTest.php │ │ ├── ExceptionExampleTaskTest.php │ │ ├── ExecuteTaskTest.php │ │ ├── MailerTaskTest.php │ │ ├── MonitorExampleTaskTest.php │ │ ├── ProgressExampleTaskTest.php │ │ ├── RetryExampleTaskTest.php │ │ ├── SuperExampleTaskTest.php │ │ └── UniqueExampleTaskTest.php │ ├── TaskFinderTest.php │ └── TaskTest.php └── View │ └── Helper │ ├── QueueHelperTest.php │ └── QueueProgressHelperTest.php ├── config ├── app_queue.php ├── bootstrap.php └── routes.php ├── phpstan.neon └── schema.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 MGriesbach@gmail.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP Queue Plugin 2 | [![CI](https://github.com/dereuromark/cakephp-queue/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dereuromark/cakephp-queue/actions/workflows/ci.yml?query=branch%3Amaster) 3 | [![Coverage Status](https://img.shields.io/codecov/c/github/dereuromark/cakephp-queue/master.svg)](https://codecov.io/github/dereuromark/cakephp-queue/branch/master) 4 | [![Latest Stable Version](https://poser.pugx.org/dereuromark/cakephp-queue/v/stable.svg)](https://packagist.org/packages/dereuromark/cakephp-queue) 5 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) 6 | [![PHPStan](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg?style=flat)](https://phpstan.org/) 7 | [![License](https://poser.pugx.org/dereuromark/cakephp-queue/license.svg)](LICENSE) 8 | [![Total Downloads](https://poser.pugx.org/dereuromark/cakephp-queue/d/total)](https://packagist.org/packages/dereuromark/cakephp-queue) 9 | [![Coding Standards](https://img.shields.io/badge/cs-PSR--2--R-yellow.svg)](https://github.com/php-fig-rectified/fig-rectified-standards) 10 | 11 | This branch is for use with **CakePHP 5.1+**. For details see [version map](https://github.com/dereuromark/cakephp-queue/wiki#cakephp-version-map). 12 | 13 | 14 | ## Background 15 | 16 | This is a very simple and minimalistic job queue (or deferred-task) system for CakePHP. 17 | If you need a very basic PHP internal queue tool, this is definitely an option. 18 | It is also a great tool for demo purposes on how queues work and doesn't have any dependencies. 19 | 20 | Overall functionality is inspired by systems like Gearman, Beanstalk or dropr, but without 21 | any illusion to compete with these more advanced Systems. 22 | 23 | The plugin is an attempt to provide a basic, simple to use method to enable deferred job execution, 24 | without the hassle of setting up or running an extra queue daemon, while integrating nicely into 25 | CakePHP and also simplifying the creation of worker scripts. You can also easily provide progress and status information into your pages. 26 | 27 | Please also read my blog posts about [deferred execution](https://www.dereuromark.de/2013/12/22/queue-deferred-execution-in-cakephp/) and [real-life example usage](https://www.dereuromark.de/2021/07/15/cakephp-queuing-real-life-examples/) [new]. 28 | For more high-volume and sophisticated use cases please see the [awesome list](https://github.com/FriendsOfCake/awesome-cakephp#queue) alternatives. 29 | 30 | ### Why use deferred execution? 31 | 32 | Deferred execution makes sense (especially in PHP) when your page wants to execute tasks, which are not directly related to rendering the current page. 33 | For instance, in a BBS-type system, a new users post might require the creation of multiple personalized email messages, 34 | notifying other users of the new content. 35 | Creating and sending these emails is completely irrelevant to the currently active user, and should not increase page response time. 36 | Another example would be downloading, extraction and/or analyzing an external file per request of the user. 37 | The regular solution to these problems would be to create specialized cronjobs which use specific database states to determine which action should be done. 38 | 39 | The Queue plugin provides a simple method to create and run such non-user-interaction-critical tasks. 40 | 41 | Another important reason is that specific jobs can be (auto)retried if they failed. 42 | So if the email server didn't work the first time, or the API gateway had an issue, the current job to be executed isn't lost but kept for rerun. Most of those external services should be treated as failable once every x calls, and as such a queue implementation can help reducing issues due to such failures. If a job still can't finish despite retries, you still have the option to debug its payload and why this job cannot complete. No data is lost here. 43 | 44 | While you can run multiple workers, and can (to some extent) spread these workers to different machines via a shared database, you should consider using a more advanced system for high volume/high number of workers systems. 45 | 46 | ## Demo 47 | See [Sandbox app](https://sandbox.dereuromark.de/sandbox/queue-examples). 48 | 49 | ## Installation and Usage 50 | See [Documentation](docs/). 51 | 52 | ## Cronjob based background scheduling 53 | If you are looking for scheduling certain background jobs: This plugin works flawlessly with [QueueScheduler plugin](https://github.com/dereuromark/cakephp-queue-scheduler). 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dereuromark/cakephp-queue", 3 | "description": "The Queue plugin for CakePHP provides deferred task execution.", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "cakephp", 8 | "queue", 9 | "deferred tasks", 10 | "background" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Mark Scherer", 15 | "homepage": "https://www.dereuromark.de", 16 | "role": "Maintainer" 17 | }, 18 | { 19 | "name": "Contributors", 20 | "homepage": "https://github.com/dereuromark/cakephp-queue/graphs/contributors", 21 | "role": "Contributor" 22 | } 23 | ], 24 | "homepage": "https://github.com/dereuromark/cakephp-queue", 25 | "support": { 26 | "source": "https://github.com/dereuromark/cakephp-queue" 27 | }, 28 | "require": { 29 | "php": ">=8.1", 30 | "brick/varexporter": "^0.4.0 || ^0.5.0 || ^0.6.0", 31 | "cakephp/cakephp": "^5.1.1" 32 | }, 33 | "require-dev": { 34 | "cakedc/cakephp-phpstan": "^4.0.0", 35 | "cakephp/bake": "^3.0.1", 36 | "cakephp/migrations": "^4.5.1", 37 | "dereuromark/cakephp-ide-helper": "^2.0.0", 38 | "dereuromark/cakephp-templating": "^0.2.1", 39 | "dereuromark/cakephp-tools": "^3.0.0", 40 | "dereuromark/cakephp-dto": "^2.1.0", 41 | "fig-r/psr2r-sniffer": "dev-master", 42 | "friendsofcake/search": "^7.0.0", 43 | "phpunit/phpunit": "^10.5 || ^11.5 || ^12.1" 44 | }, 45 | "suggest": { 46 | "dereuromark/cakephp-ide-helper": "For maximum IDE support, especially around createJob() usage.", 47 | "dereuromark/cakephp-tools": "For the QueueEmailTask (if you don't write your own task here). Also for admin backend.", 48 | "friendsofcake/search": "For admin backend and filtering of current jobs." 49 | }, 50 | "conflict": { 51 | "cakephp/migrations": "<4.5" 52 | }, 53 | "minimum-stability": "stable", 54 | "prefer-stable": true, 55 | "autoload": { 56 | "psr-4": { 57 | "Queue\\": "src/", 58 | "Queue\\Test\\Fixture\\": "tests/Fixture/" 59 | } 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { 63 | "Foo\\": "tests/test_app/plugins/Foo/src/", 64 | "Queue\\Test\\TestCase\\": "tests/TestCase/", 65 | "TestApp\\": "tests/test_app/src/" 66 | } 67 | }, 68 | "config": { 69 | "allow-plugins": { 70 | "cakephp/plugin-installer": true, 71 | "dealerdirect/phpcodesniffer-composer-installer": true 72 | }, 73 | "process-timeout": 600, 74 | "sort-packages": true 75 | }, 76 | "scripts": { 77 | "cs-check": "phpcs --extensions=php", 78 | "cs-fix": "phpcbf --extensions=php", 79 | "lowest": "validate-prefer-lowest", 80 | "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", 81 | "stan": "phpstan analyse", 82 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^2.0.0 && mv composer.backup composer.json", 83 | "stan-tests": "phpstan analyse -c tests/phpstan.neon", 84 | "test": "phpunit", 85 | "test-coverage": "phpunit --log-junit tmp/coverage/unitreport.xml --coverage-html tmp/coverage --coverage-clover tmp/coverage/coverage.xml" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /config/Migrations/20240307154751_MigrationQueueInitV8.php: -------------------------------------------------------------------------------- 1 | execute('DELETE FROM queue_phinxlog WHERE [version] < \'' . $version . '\''); 22 | } else { 23 | $this->execute('DELETE FROM queue_phinxlog WHERE version < \'' . $version . '\''); 24 | } 25 | 26 | if ($this->hasTable('queued_jobs')) { 27 | return; 28 | } 29 | 30 | $this->table('queued_jobs') 31 | ->addColumn('job_task', 'string', [ 32 | 'default' => null, 33 | 'limit' => 90, 34 | 'null' => false, 35 | ]) 36 | ->addColumn('data', 'text', [ 37 | 'default' => null, 38 | 'null' => true, 39 | ]) 40 | ->addColumn('job_group', 'string', [ 41 | 'default' => null, 42 | 'limit' => 190, 43 | 'null' => true, 44 | ]) 45 | ->addColumn('reference', 'string', [ 46 | 'default' => null, 47 | 'limit' => 190, 48 | 'null' => true, 49 | ]) 50 | ->addColumn('created', 'datetime', [ 51 | 'default' => null, 52 | 'null' => false, 53 | ]) 54 | ->addColumn('notbefore', 'datetime', [ 55 | 'default' => null, 56 | 'null' => true, 57 | ]) 58 | ->addColumn('fetched', 'datetime', [ 59 | 'default' => null, 60 | 'null' => true, 61 | ]) 62 | ->addColumn('completed', 'datetime', [ 63 | 'default' => null, 64 | 'null' => true, 65 | ]) 66 | ->addColumn('progress', 'float', [ 67 | 'default' => null, 68 | 'null' => true, 69 | 'signed' => false, 70 | ]) 71 | ->addColumn('attempts', 'tinyinteger', [ 72 | 'default' => '0', 73 | 'null' => true, 74 | 'signed' => false, 75 | ]) 76 | ->addColumn('failure_message', 'text', [ 77 | 'default' => null, 78 | 'null' => true, 79 | ]) 80 | ->addColumn('workerkey', 'string', [ 81 | 'default' => null, 82 | 'limit' => 45, 83 | 'null' => true, 84 | ]) 85 | ->addColumn('status', 'string', [ 86 | 'default' => null, 87 | 'limit' => 190, 88 | 'null' => true, 89 | ]) 90 | ->addColumn('priority', 'integer', [ 91 | 'default' => '5', 92 | 'null' => false, 93 | 'signed' => false, 94 | ]) 95 | ->addIndex( 96 | [ 97 | 'completed', 98 | ], 99 | [ 100 | 'name' => 'completed', 101 | ], 102 | ) 103 | ->addIndex( 104 | [ 105 | 'job_task', 106 | ], 107 | [ 108 | 'name' => 'job_task', 109 | ], 110 | ) 111 | ->create(); 112 | 113 | $this->table('queue_processes') 114 | ->addColumn('pid', 'string', [ 115 | 'default' => null, 116 | 'limit' => 40, 117 | 'null' => false, 118 | ]) 119 | ->addColumn('created', 'datetime', [ 120 | 'default' => null, 121 | 'null' => false, 122 | ]) 123 | ->addColumn('modified', 'datetime', [ 124 | 'default' => null, 125 | 'null' => false, 126 | ]) 127 | ->addColumn('terminate', 'boolean', [ 128 | 'default' => false, 129 | 'null' => false, 130 | ]) 131 | ->addColumn('server', 'string', [ 132 | 'default' => null, 133 | 'limit' => 90, 134 | 'null' => true, 135 | ]) 136 | ->addColumn('workerkey', 'string', [ 137 | 'default' => null, 138 | 'limit' => 45, 139 | 'null' => false, 140 | ]) 141 | ->addIndex( 142 | [ 143 | 'workerkey', 144 | ], 145 | [ 146 | 'name' => 'workerkey', 147 | 'unique' => true, 148 | ], 149 | ) 150 | ->addIndex( 151 | [ 152 | 'pid', 153 | 'server', 154 | ], 155 | [ 156 | 'name' => 'pid', 157 | 'unique' => true, 158 | ], 159 | ) 160 | ->create(); 161 | } 162 | 163 | /** 164 | * Down Method. 165 | * 166 | * More information on this method is available here: 167 | * https://book.cakephp.org/phinx/0/en/migrations.html#the-down-method 168 | * 169 | * @return void 170 | */ 171 | public function down(): void { 172 | $this->table('queue_processes')->drop()->save(); 173 | $this->table('queued_jobs')->drop()->save(); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /config/Migrations/20250508121432_MigrationQueueMemory.php: -------------------------------------------------------------------------------- 1 | table('queued_jobs') 18 | ->addColumn('memory', 'integer', [ 19 | 'default' => null, 20 | 'null' => true, 21 | 'comment' => 'MB', 22 | 'signed' => false, 23 | ]) 24 | ->update(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /config/app.example.php: -------------------------------------------------------------------------------- 1 | [ 12 | // time (in seconds) after which a job is requeued if the worker doesn't report back 13 | 'defaultworkertimeout' => 1800, 14 | 15 | // seconds of running time after which the worker will terminate (0 = unlimited) 16 | 'workermaxruntime' => 120, 17 | 18 | // minimum time (in seconds) which a task remains in the database before being cleaned up. 19 | 'cleanuptimeout' => 2592000, // 30 days 20 | 21 | // number of retries if a job fails or times out. 22 | 'defaultworkerretries' => 1, 23 | 24 | // seconds to sleep() when no executable job is found 25 | 'sleeptime' => 10, 26 | 27 | // probability in percent of a old job cleanup happening 28 | 'gcprob' => 10, 29 | 30 | // set to true for multi server setup, this will affect web backend possibilities to kill/end workers 31 | 'multiserver' => false, 32 | 33 | // set this to a limit that can work with your memory limits and alike, 0 => no limit 34 | 'maxworkers' => 3, 35 | 36 | // instruct a Workerprocess quit when there are no more tasks for it to execute (true = exit, false = keep running) 37 | 'exitwhennothingtodo' => false, 38 | 39 | // seconds of running time after which the PHP process will terminate, null uses workermaxruntime * 100 40 | 'workertimeout' => null, 41 | 42 | // determine whether logging is enabled 43 | 'log' => true, 44 | 45 | // set default Mailer class 46 | 'mailerClass' => 'Cake\Mailer\Email', 47 | 48 | // set default datasource connection 49 | 'connection' => null, 50 | 51 | // enable Search. requires friendsofcake/search 52 | 'isSearchEnabled' => true, 53 | 54 | // enable Search. requires frontend assets 55 | 'isStatisticEnabled' => false, 56 | 57 | // Allow workers to wake up from their "nothing to do, sleeping" state when using QueuedJobs->wakeUpWorkers(). 58 | // This method sends a SIGUSR1 to workers to interrupt any sleep() operation like it was their time to finish. 59 | // This option breaks tasks expecting sleep() to always sleep for the provided duration without interrupting. 60 | 'canInterruptSleep' => false, 61 | 62 | // Skip check for createJob() and if task exists 63 | 'skipExistenceCheck' => false, 64 | 65 | // Additional plugins to include tasks from (if they are not already loaded anyway) 66 | 'plugins' => [], 67 | 68 | // ignores task classes 69 | 'ignoredTasks' => [], 70 | 71 | ], 72 | 'Icon' => [ 73 | 'sets' => [ 74 | 'bs' => BootstrapIcon::class, 75 | ], 76 | ], 77 | ]; 78 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPlugin('Queue', ['bootstrap' => true]);` which will load your `app_queue.php` config file automatically. 98 | 99 | Example `app_queue.php`: 100 | 101 | ```php 102 | return [ 103 | 'Queue' => [ 104 | 'workermaxruntime' => 60, 105 | 'sleeptime' => 15, 106 | ], 107 | ]; 108 | ``` 109 | 110 | You can also drop the configuration into an existing config file (recommended) that is already been loaded. 111 | The values above are the default settings which apply, when no configuration is found. 112 | 113 | ### Backend configuration 114 | 115 | - isSearchEnabled: Set to false if you do not want search/filtering capability. 116 | This is auto-detected based on [Search](https://github.com/FriendsOfCake/search) plugin being available/loaded if not disabled. 117 | 118 | - isStatsEnabled: Set to true to enable. This requires [chart.js](https://github.com/chartjs/Chart.js) asset to be available. 119 | You can also overwrite the template and as such change the asset library as well as the output/chart. 120 | 121 | ### Configuration tips 122 | 123 | For the beginning maybe use not too many runners in parallel, and keep the runtimes rather short while starting new jobs every few minutes. 124 | You can then always increase spawning of runners if there is a shortage. 125 | 126 | ## Task configuration 127 | 128 | You can set two main things on each task as property: timeout and retries. 129 | ```php 130 | /** 131 | * Timeout for this task in seconds, after which the task is reassigned to a new worker. 132 | * 133 | * @var int 134 | */ 135 | public $timeout = 120; 136 | 137 | /** 138 | * Number of times a failed instance of this task should be restarted before giving up. 139 | * 140 | * @var int 141 | */ 142 | public $retries = 1; 143 | ``` 144 | Make sure you set the timeout high enough so that it could never run longer than this, otherwise you risk it being re-run while still being run. 145 | It is recommended setting it to at least 2x the maximum possible execution length. See [Concurrent workers](limitations.md) 146 | 147 | Set the retries to at least 1, otherwise it will never execute again after failure in the first run. 148 | -------------------------------------------------------------------------------- /docs/sections/cron.md: -------------------------------------------------------------------------------- 1 | # Setting up the trigger cronjob 2 | 3 | As outlined in the [book](https://book.cakephp.org/5/en/console-commands/cron-jobs.html#running-shells-as-cron-jobs) you can easily set up a cronjob 4 | to start a new worker. 5 | 6 | The following example uses "crontab": 7 | 8 | */10 * * * * cd /full/path/to/app && bin/cake queue run -q 9 | 10 | Make sure you use `crontab -e -u www-data` to set it up as `www-data` user, and not as root etc. 11 | 12 | This would start a new worker every 10 minutes. If you configure your max lifetime of a worker to 15 minutes, you 13 | got a small overlap where two workers would run simultaneously. If you lower the 10 minutes and raise the lifetime, you 14 | get quite a few overlapping workers and thus more "parallel" processing power. 15 | Play around with it, but just don't shoot over the top. 16 | 17 | Also don't forget to set Configure key `'Queue.maxworkers'` to a reasonable value per server. 18 | If, for any reason, some of the jobs should take way longer, you want to avoid additional x workers to be started. 19 | It will then just not start new ones beyond this count until the already running ones are finished. 20 | This is an important server protection to avoid overloading. 21 | 22 | ## Specific PHP version 23 | 24 | If you have multiple PHP versions running on your server, using the above setup can yield to the default CLI one being used which can be different from the expected one. 25 | You want to select and run the same version as your non-CLI env (nginx/php-fpm). 26 | 27 | To ensure this, specify it by replacing `bin/cake` with `/usr/bin/php8.{n} bin/cake.php`, e.g.: 28 | ``` 29 | 0 * * * * cd /full/path/to/app && /usr/bin/php8.4 bin/cake.php queue run -q 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/sections/custom_tasks.md: -------------------------------------------------------------------------------- 1 | # Creating your own task 2 | 3 | ## Baking new Queue task and test 4 | You can bake a new task and its test via 5 | ``` 6 | bin/cake bake queue_task MyTaskName [-p PluginName] 7 | ``` 8 | 9 | It will generate a `MyTaskNameTask` class in the right namespace. 10 | 11 | It will not overwrite existing classes unless you explicitly force this (after prompting). 12 | 13 | You can use `My/Sub/MyTaskNameTask` to create tasks in sub-namespaces. 14 | 15 | ## Detailed explanation 16 | 17 | In most cases you wouldn't want to use the existing task, but just quickly build your own. 18 | Put it into `src/Queue/Task/` as `{YourNameForIt}Task.php`. 19 | 20 | You need to at least implement the `run()` method: 21 | ```php 22 | namespace App\Queue\Task; 23 | 24 | use Queue\Queue\Task; 25 | 26 | class YourNameForItTask extends Task { 27 | 28 | /** 29 | * @var int 30 | */ 31 | public $timeout = 20; 32 | 33 | /** 34 | * @var int 35 | */ 36 | public $retries = 1; 37 | 38 | /** 39 | * @param array $data The array passed to QueuedJobsTable::createJob() 40 | * @param int $jobId The id of the QueuedJob entity 41 | * @return void 42 | */ 43 | public function run(array $data, int $jobId): void { 44 | $fooBarsTable = $this->fetchTable('FooBars'); 45 | if (!$fooBarsTable->doSth()) { 46 | throw new RuntimeException('Couldnt do sth.'); 47 | } 48 | } 49 | 50 | } 51 | ``` 52 | Make sure it throws an exception with a clear error message in case of failure. 53 | 54 | Note: You can use the provided `Queue\Model\QueueException` if you do not need to include a stack trace. 55 | This is usually the default inside custom tasks. 56 | 57 | ## DI Container Example 58 | 59 | If you use the [Dependency Injection Container](https://book.cakephp.org/5/en/development/dependency-injection.html) provided by CakePHP you can also use 60 | it inside your tasks. 61 | 62 | ```php 63 | use Queue\Queue\ServicesTrait; 64 | 65 | class MyCustomTask extends Task { 66 | 67 | use ServicesTrait; 68 | 69 | public function run(array $data, int $jobId): void { 70 | $myService = $this->getService(MyService::class); 71 | } 72 | } 73 | ``` 74 | 75 | As you see here you have to add the [ServicesTrait](https://github.com/dereuromark/cakephp-queue/blob/master/src/Queue/ServicesTrait.php) to your task which then allows you to use the `$this->getService()` method. 76 | 77 | ## Organize tasks in sub folders 78 | 79 | You can group tasks in sub namespaces. 80 | E.g. `src/Queue/Task/My/Sub/{YourNameForIt}Task.php` would be found and used as `My/Sub/{YourNameForIt}`. 81 | -------------------------------------------------------------------------------- /docs/sections/job_statistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dereuromark/cakephp-queue/3ff4164a2d4a04f72d4f7ffed8455d4bf9fc9dc7/docs/sections/job_statistic.png -------------------------------------------------------------------------------- /docs/sections/limitations.md: -------------------------------------------------------------------------------- 1 | # Known Limitations 2 | 3 | ## Concurrent workers may execute the same job multiple times 4 | 5 | If you want to use multiple workers, please double-check that all jobs have a high enough timeout (>> 2x max possible execution time of a job). Currently it would otherwise risk the jobs being run multiple times! 6 | 7 | ## Concurrent workers may execute the same job type multiple times 8 | 9 | If you need limiting of how many times a specific job type can be run in parallel, you need to find a custom solution here. 10 | -------------------------------------------------------------------------------- /docs/sections/mailing.md: -------------------------------------------------------------------------------- 1 | # Mailing 2 | 3 | ## Using built-in tasks 4 | * [Email](tasks/email.md) using Message class 5 | * [Mailer](tasks/mailer.md) using Mailer class 6 | 7 | ## Using QueueTransport 8 | 9 | Instead of manually adding job every time you want to send mail 10 | you can use existing code ond change only EmailTransport and Email configurations in `app.php`. 11 | 12 | ```php 13 | 'EmailTransport' => [ 14 | 'default' => [ 15 | 'className' => 'Smtp', 16 | // The following keys are used in SMTP transports 17 | 'host' => 'host@gmail.com', 18 | 'port' => 587, 19 | 'timeout' => 30, 20 | 'username' => 'username', 21 | 'password' => 'password', 22 | 'tls' => true, 23 | ], 24 | 'queue' => [ 25 | 'className' => 'Queue.Queue', 26 | 'transport' => 'default', 27 | ], 28 | ], 29 | 30 | 'Email' => [ 31 | 'default' => [ 32 | 'transport' => 'queue', 33 | 'from' => 'no-reply@host.com', 34 | 'charset' => 'utf-8', 35 | 'headerCharset' => 'utf-8', 36 | ], 37 | ], 38 | ``` 39 | 40 | This way each time with `$mailer->deliver()` it will use `QueueTransport` as main to create job and worker will use `'transport'` setting to send mail. 41 | 42 | ### Difference between QueueTransport and SimpleQueueTransport 43 | 44 | * `QueueTransport` serializes whole email into the database and is useful when you have custom `Message` class. 45 | * `SimpleQueueTransport` extracts all data from Message (to, bcc, template etc.) and then uses this to recreate Message inside task, this 46 | is useful when dealing with emails which serialization would overflow database `data` field length. 47 | This can only be used for non-templated emails. 48 | 49 | 50 | ## Manually assembling your emails 51 | 52 | This is the most customizable way to generate your asynchronous emails. 53 | 54 | Don't generate them directly in your code and pass them to the queue, instead just pass the minimum requirements, like non persistent data needed and the primary keys of the records that need to be included. 55 | So let's say someone posted a comment, and you want to get notified. 56 | 57 | Inside your CommentsTable class after saving the data you execute this hook: 58 | 59 | ```php 60 | /** 61 | * @param \App\Model\Entity\Comment $comment 62 | * @return void 63 | */ 64 | protected function _notifyAdmin(Comment $comment) 65 | { 66 | /** @var \Queue\Model\Table\QueuedJobsTable $QueuedJobs */ 67 | $QueuedJobs = TableRegistry::getTableLocator()->get('Queue.QueuedJobs'); 68 | $data = [ 69 | 'settings' => [ 70 | 'subject' => __('New comment submitted by {0}', $comment->name), 71 | ], 72 | 'vars' => [ 73 | 'comment' => $comment->toArray(), 74 | ], 75 | ]; 76 | $QueuedJobs->createJob('CommentNotification', $data); 77 | } 78 | ``` 79 | 80 | And your `QueueAdminEmailTask::run()` method (using `MailerAwareTrait`): 81 | 82 | ```php 83 | $this->getMailer('User'); 84 | $this->Mailer->viewBuilder()->setTemplate('comment_notification'); 85 | // ... 86 | if (!empty($data['vars'])) { 87 | $this->Mailer->setViewVars($data['vars']); 88 | } 89 | 90 | $this->Mailer->deliver(); 91 | ``` 92 | 93 | Make sure you got the template for it then, e.g.: 94 | 95 | ```php 96 | name ?> ( email ?> ) wrote: 97 | 98 | message ?> 99 | 100 | Url->build(['prefix' => 'Admin', 'controller' => 'Comments', 'action'=> 'view', $comment['id']], true) ?> 101 | ``` 102 | 103 | This way all the generation is in the specific task and template and can be tested separately. 104 | -------------------------------------------------------------------------------- /docs/sections/misc.md: -------------------------------------------------------------------------------- 1 | # Misc 2 | 3 | ## Logging 4 | 5 | By default, errors are always logged, and with log enabled also the execution of a job. 6 | Make sure you add this to your config: 7 | ```php 8 | 'Log' => [ 9 | ... 10 | 'queue' => [ 11 | 'className' => ..., 12 | 'type' => 'queue', 13 | 'levels' => ['info'], 14 | 'scopes' => ['queue'], 15 | ], 16 | ], 17 | ``` 18 | 19 | When debugging (not using `--quiet`/`-q`) on "run", it will also log the worker run and end. 20 | 21 | You can disable info logging by setting `Queue.log` to `false` in your config. 22 | 23 | ## Resetting 24 | You can reset all failed jobs from CLI and web backend. 25 | With web backend you can reset specific ones, as well. 26 | 27 | From CLI you run this to reset all at once: 28 | ``` 29 | bin/cake queue reset 30 | ``` 31 | 32 | ## Rerunning 33 | You can rerun successfully run jobs if they are not yet cleaned out. Make sure your cleanup timeout is high enough here. 34 | Usually weeks or months is a good balance to have those still stored for this case. 35 | 36 | This is especially useful for local development or debugging, though. As you would otherwise have to manually trigger or import the job all the time. 37 | 38 | From CLI you run this to rerun all of a specific job type at once: 39 | ``` 40 | bin/cake queue rerun FooBar 41 | ``` 42 | You can add a reference to rerun a specific job. 43 | 44 | ## Using custom finder 45 | You can use a convenience finder for tasks that are still queued, that means not yet finished. 46 | ```php 47 | $queuedJobsTable = $this->fetchTable('Queue.QueuedJobs'); 48 | $query = $queuedJobsTable->find('queued')->...; 49 | ``` 50 | This includes also failed ones if not filtered further using `where()` conditions. 51 | 52 | ## Notes 53 | 54 | `` is the complete class name without the Task suffix (e.g. Example or PluginName.Example). 55 | 56 | Custom tasks should be placed in `src/Queue/Task/`. 57 | Tasks should be named `SomethingTask` and implement the Queue "Task". 58 | 59 | Plugin tasks go in `plugins/PluginName/src/Queue/Task/`. 60 | 61 | A detailed Example task can be found in `src/Queue/Task/QueueExampleTask.php` inside this plugin. 62 | 63 | Some more tips: 64 | - If you copy an example, do not forget to adapt the namespace ("App")! 65 | - For plugin tasks, make sure to load the plugin using the plugin prefix ("MyPlugin.MyName"). 66 | -------------------------------------------------------------------------------- /docs/sections/resources/autocomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dereuromark/cakephp-queue/3ff4164a2d4a04f72d4f7ffed8455d4bf9fc9dc7/docs/sections/resources/autocomplete.png -------------------------------------------------------------------------------- /docs/sections/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | The plugin works completely without it, by just using the CLI shell commands. 4 | But if you want to browse the statistics via URL, you can enable the routing for it (see [Basic Setup](basic_setup.md)) and then access `/admin/queue` 5 | to see how status of your queue, statistics and settings. 6 | Please note that this requires the [Tools plugin](https://github.com/dereuromark/cakephp-tools) to be loaded if you do not customize the view templates on project level. 7 | Also make sure you loaded the helpers needed (Tools.Format, Tools.Time as Time, etc). 8 | 9 | By default, the templates should work fine in both Foundation (v5+) and Bootstrap (v3+). 10 | Copy-and-paste to project level for any customization here. 11 | 12 | Here an example of historical job data for one specific "ProjectBranch" one: 13 | > ![job statistic](job_statistic.png) 14 | 15 | It can be quite valuable to see the runtime (in seconds) over a progression of time. 16 | -------------------------------------------------------------------------------- /docs/sections/tasks/execute.md: -------------------------------------------------------------------------------- 1 | # Using built-in Execute task 2 | 3 | The built-in task directly runs on the same path as your app, so you can use relative paths or absolute ones: 4 | ```php 5 | $data = [ 6 | 'command' => 'bin/cake importer run', 7 | 'content' => $content, 8 | ]; 9 | $queuedJobsTable = TableRegistry::getTableLocator()->get('Queue.QueuedJobs'); 10 | $queuedJobsTable->createJob('Execute', $data); 11 | ``` 12 | 13 | The task automatically captures stderr output into stdout. If you don't want this, set "redirect" to false. 14 | It also escapes by default using "escape" true. Only disable this if you trust the source. 15 | 16 | By default, it only allows return code `0` (success) to pass. If you need different accepted return codes, pass them as "accepted" array. 17 | If you want to disable this check and allow any return code to be successful, pass `[]` (empty array). 18 | 19 | *Warning*: This can essentially execute anything on CLI. Make sure you never expose this directly as free-text input to anyone. 20 | Use only predefined and safe code-snippets here! 21 | -------------------------------------------------------------------------------- /docs/sections/tasks/mailer.md: -------------------------------------------------------------------------------- 1 | # Using built-in Mailer task 2 | 3 | Sending reusable templated emails the easy way: 4 | ```php 5 | $data = [ 6 | 'class' => TestMailer::class, 7 | 'action' => 'testAction', 8 | 'vars' => [...], 9 | ]; 10 | $queuedJobsTable = TableRegistry::getTableLocator()->get('Queue.QueuedJobs'); 11 | $queuedJobsTable->createJob('Queue.Mailer', $data); 12 | ``` 13 | Since we are not passing in an object, but a class string and settings, this is also JsonSerializer safe. 14 | -------------------------------------------------------------------------------- /docs/sections/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading from older versions 2 | 3 | ## Coming from v7 to v8? 4 | - Make sure you ran `bin/cake migrations migrate -p Queue` to migrate DB schema for all previous migrations before upgrading to v8. 5 | - Once upgraded also run it once more, there should be now only 1 migration left. 6 | - Make sure you are not using PHP serialize anymore, it is now all JSON. It is also happening automatically behind the scenes, so remove your 7 | manual calls where they are not needed anymore. 8 | 9 | That includes the config 10 | ``` 11 | 'serializerClass' => ..., // FQCN 12 | ``` 13 | etc 14 | 15 | ## Coming from before v7? 16 | If you are upgrading from Cake3/4 and v5/v6, make sure to install v7 firs, run all migrations, 17 | then jump to v8. 18 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | config/ 7 | src/ 8 | tests/ 9 | 10 | /tests/test_files/ 11 | 12 | 13 | 14 | */config/Migrations/* 15 | 16 | 17 | 18 | */config/Migrations/* 19 | 20 | 21 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src/ 5 | bootstrapFiles: 6 | - tests/bootstrap.php 7 | earlyTerminatingMethodCalls: 8 | Cake\Console\BaseCommand: 9 | - abort 10 | Cake\Console\ConsoleIo: 11 | - abort 12 | treatPhpDocTypesAsCertain: false 13 | ignoreErrors: 14 | - identifier: missingType.iterableValue 15 | - identifier: missingType.generics 16 | - identifier: trait.unused 17 | - '#Access to an undefined property Cake\\ORM\\BehaviorRegistry::\$Search#' 18 | - '#Negated boolean expression is always false.#' 19 | - '#Parameter \#1 \$.+ of function call_user_func_array expects .+, array.+ given.#' 20 | 21 | includes: 22 | - vendor/cakedc/cakephp-phpstan/extension.neon 23 | -------------------------------------------------------------------------------- /src/Command/AddCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('task', [ 29 | 'help' => 'Task name', 30 | 'required' => false, 31 | ]); 32 | $parser->addArgument('data', [ 33 | 'help' => 'Additional data if needed', 34 | 'required' => false, 35 | ]); 36 | $parser->setDescription( 37 | 'Adds a job into the queue. Only tasks that implement AddInterface can be added through CLI.', 38 | ); 39 | 40 | return $parser; 41 | } 42 | 43 | /** 44 | * @param \Cake\Console\Arguments $args Arguments 45 | * @param \Cake\Console\ConsoleIo $io ConsoleIo 46 | * 47 | * @return int|null|void 48 | */ 49 | public function execute(Arguments $args, ConsoleIo $io) { 50 | $tasks = $this->getTasks(); 51 | 52 | $taskName = $args->getArgument('task'); 53 | if (!$taskName) { 54 | $io->out(count($tasks) . ' tasks available:'); 55 | foreach ($tasks as $task => $className) { 56 | $io->out(' - ' . $task); 57 | } 58 | 59 | return; 60 | } 61 | 62 | if (!array_key_exists($taskName, $tasks)) { 63 | $io->abort('Not a supported task.'); 64 | } 65 | 66 | /** @var class-string<\Queue\Queue\AddInterface> $taskClass */ 67 | $taskClass = $tasks[$taskName]; 68 | /** @var \Queue\Queue\AddInterface $task */ 69 | $task = new $taskClass(new Io($io)); 70 | $task->add($args->getArgument('data')); 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | protected function getTasks(): array { 77 | $taskFinder = new TaskFinder(); 78 | 79 | return $taskFinder->allAddable(); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Command/BakeQueueTaskCommand.php: -------------------------------------------------------------------------------- 1 | _name = $name . 'Task'; 42 | 43 | parent::bake($name, $args, $io); 44 | } 45 | 46 | /** 47 | * Generate a test case. 48 | * 49 | * @param string $name The class to bake a test for. 50 | * @param \Cake\Console\Arguments $args The console arguments 51 | * @param \Cake\Console\ConsoleIo $io The console io 52 | * 53 | * @return void 54 | */ 55 | public function bakeTest(string $name, Arguments $args, ConsoleIo $io): void { 56 | if ($args->getOption('no-test')) { 57 | return; 58 | } 59 | 60 | $className = $name . 'Task'; 61 | $io->out('Generating: ' . $className . ' test class'); 62 | 63 | $plugin = (string)$args->getOption('plugin'); 64 | $namespace = $plugin ? str_replace('/', DS, $plugin) : Configure::read('App.namespace'); 65 | 66 | $content = $this->generateTaskTestContent($className, $namespace); 67 | $path = $plugin ? Plugin::path($plugin) : ROOT . DS; 68 | $path .= 'tests/TestCase/Queue/Task/' . $className . 'Test.php'; 69 | 70 | $io->createFile($path, $content, (bool)$args->getOption('force')); 71 | } 72 | 73 | /** 74 | * @param string $name 75 | * @param string $namespace 76 | * 77 | * @return string 78 | */ 79 | protected function generateTaskTestContent(string $name, string $namespace): string { 80 | $testName = $name . 'Test'; 81 | $subNamespace = ''; 82 | $pos = strrpos($testName, '/'); 83 | if ($pos !== false) { 84 | $subNamespace = '\\' . substr($testName, 0, $pos); 85 | $testName = substr($testName, $pos + 1); 86 | } 87 | $taskClassNamespace = $namespace . '\Queue\\Task\\' . str_replace(DS, '\\', $name); 88 | 89 | if (strpos($name, '/') !== false) { 90 | $parts = explode('/', $name); 91 | $name = array_pop($parts); 92 | } 93 | 94 | $content = << 106 | */ 107 | protected array \$fixtures = [ 108 | 'plugin.Queue.QueuedJobs', 109 | 'plugin.Queue.QueueProcesses', 110 | ]; 111 | 112 | /** 113 | * @return void 114 | */ 115 | public function testRun(): void { 116 | \$task = new $name(); 117 | 118 | //TODO 119 | //\$task->run(\$data, \$jobId); 120 | } 121 | 122 | } 123 | 124 | TXT; 125 | 126 | return $content; 127 | } 128 | 129 | /** 130 | * @inheritDoc 131 | */ 132 | public function template(): string { 133 | return 'Queue.Task/task'; 134 | } 135 | 136 | /** 137 | * @inheritDoc 138 | */ 139 | public function templateData(Arguments $arguments): array { 140 | $name = $this->_name; 141 | $namespace = Configure::read('App.namespace'); 142 | $pluginPath = ''; 143 | if ($this->plugin) { 144 | $namespace = $this->_pluginNamespace($this->plugin); 145 | $pluginPath = $this->plugin . '.'; 146 | } 147 | 148 | $namespace .= '\\Queue\\Task'; 149 | 150 | $namespacePart = null; 151 | if (strpos($name, '/') !== false) { 152 | $parts = explode('/', $name); 153 | $name = array_pop($parts); 154 | $namespacePart = implode('\\', $parts); 155 | } 156 | if ($namespacePart) { 157 | $namespace .= '\\' . $namespacePart; 158 | } 159 | 160 | return [ 161 | 'plugin' => $this->plugin, 162 | 'pluginPath' => $pluginPath, 163 | 'namespace' => $namespace, 164 | 'subNamespace' => $namespacePart ? ($namespacePart . '/') : '', 165 | 'name' => $name, 166 | 'add' => $arguments->getOption('add'), 167 | ]; 168 | } 169 | 170 | /** 171 | * @inheritDoc 172 | */ 173 | public function name(): string { 174 | return 'queue_task'; 175 | } 176 | 177 | /** 178 | * @inheritDoc 179 | */ 180 | public function fileName(string $name): string { 181 | return $name . 'Task.php'; 182 | } 183 | 184 | /** 185 | * Gets the option parser instance and configures it. 186 | * 187 | * @param \Cake\Console\ConsoleOptionParser $parser Parser instance 188 | * 189 | * @return \Cake\Console\ConsoleOptionParser 190 | */ 191 | public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { 192 | $parser = parent::buildOptionParser($parser); 193 | $parser->addOption('add', [ 194 | 'help' => 'Task implements AddInterface', 195 | 'boolean' => true, 196 | 'short' => 'a', 197 | ]); 198 | 199 | return $parser; 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/Command/InfoCommand.php: -------------------------------------------------------------------------------- 1 | setDescription( 38 | 'Get list of available tasks as well as current settings and statistics.', 39 | ); 40 | 41 | return $parser; 42 | } 43 | 44 | /** 45 | * @param \Cake\Console\Arguments $args Arguments 46 | * @param \Cake\Console\ConsoleIo $io ConsoleIo 47 | * 48 | * @return int|null|void 49 | */ 50 | public function execute(Arguments $args, ConsoleIo $io) { 51 | $tasks = $this->getTasks(); 52 | $addableTasks = $this->getAddableTasks(); 53 | 54 | $io->out(count($tasks) . ' tasks available:'); 55 | foreach ($tasks as $task => $className) { 56 | if (array_key_exists($task, $addableTasks)) { 57 | $task .= ' [addable via CLI]'; 58 | } 59 | $io->out(' * ' . $task); 60 | } 61 | 62 | $io->out(); 63 | $io->hr(); 64 | $io->out(); 65 | 66 | $io->out('Current Settings:'); 67 | $conf = (array)Configure::read('Queue'); 68 | foreach ($conf as $key => $val) { 69 | if ($val === false) { 70 | $val = 'no'; 71 | } 72 | if ($val === true) { 73 | $val = 'yes'; 74 | } 75 | $io->out('* ' . $key . ': ' . print_r($val, true)); 76 | } 77 | 78 | $io->out(); 79 | $io->hr(); 80 | $io->out(); 81 | 82 | /** @var \Queue\Model\Table\QueuedJobsTable $QueuedJobs */ 83 | $QueuedJobs = $this->fetchTable('Queue.QueuedJobs'); 84 | $QueueProcesses = $this->fetchTable('Queue.QueueProcesses'); 85 | 86 | $io->out('Total unfinished jobs: ' . $QueuedJobs->getLength()); 87 | $status = $QueueProcesses->status(); 88 | $io->out('Current running workers: ' . ($status ? $status['workers'] : '-')); 89 | $io->out('Last run: ' . ($status ? $status['time']->nice() : '-')); 90 | $io->out('Server name: ' . $QueueProcesses->buildServerString()); 91 | 92 | $io->out(); 93 | $io->hr(); 94 | $io->out(); 95 | 96 | $io->out('Jobs currently in the queue:'); 97 | $types = $QueuedJobs->getTypes()->toArray(); 98 | //TODO: refactor using $io->helper table? 99 | foreach ($types as $type) { 100 | $io->out(' - ' . str_pad($type, 20, ' ', STR_PAD_RIGHT) . ': ' . $QueuedJobs->getLength($type)); 101 | } 102 | 103 | $io->out(); 104 | 105 | $scheduled = $QueuedJobs->getScheduledStats()->count(); 106 | $io->out('Jobs currently scheduled to run in the future: ' . $scheduled); 107 | 108 | $io->out(); 109 | $io->hr(); 110 | $io->out(); 111 | 112 | $io->out('Finished job statistics:'); 113 | $data = $QueuedJobs->getStats(); 114 | //TODO: refactor using $io->helper table? 115 | foreach ($data as $item) { 116 | $io->out(' - ' . $item['job_task'] . ': '); 117 | $io->out(' - Finished Jobs in Database: ' . $item['num']); 118 | $io->out(' - Average Job existence : ' . str_pad(Number::precision($item['alltime'], 0), 8, ' ', STR_PAD_LEFT) . 's'); 119 | $io->out(' - Average Execution delay : ' . str_pad(Number::precision($item['fetchdelay'], 0), 8, ' ', STR_PAD_LEFT) . 's'); 120 | $io->out(' - Average Execution time : ' . str_pad(Number::precision($item['runtime'], 0), 8, ' ', STR_PAD_LEFT) . 's'); 121 | } 122 | } 123 | 124 | /** 125 | * @return array 126 | */ 127 | protected function getTasks(): array { 128 | $taskFinder = new TaskFinder(); 129 | 130 | return $taskFinder->all(); 131 | } 132 | 133 | /** 134 | * @return array 135 | */ 136 | protected function getAddableTasks(): array { 137 | $taskFinder = new TaskFinder(); 138 | 139 | return $taskFinder->allAddable(); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/Command/RunCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public static function defaultName(): string { 38 | return 'queue run'; 39 | } 40 | 41 | /** 42 | * @return \Cake\Console\ConsoleOptionParser 43 | */ 44 | public function getOptionParser(): ConsoleOptionParser { 45 | $parser = parent::getOptionParser(); 46 | 47 | $parser->addOption('config', [ 48 | 'default' => 'default', 49 | 'help' => 'Name of a queue config to use', 50 | 'short' => 'c', 51 | ]); 52 | $parser->addOption('logger', [ 53 | 'help' => 'Name of a configured logger', 54 | 'default' => 'stdout', 55 | 'short' => 'l', 56 | ]); 57 | $parser->addOption('max-runtime', [ 58 | 'help' => 'Seconds for max runtime', 59 | 'default' => null, 60 | 'short' => 'r', 61 | ]); 62 | 63 | $parser->addOption('group', [ 64 | 'short' => 'g', 65 | 'help' => 'Group (comma separated list possible)', 66 | 'default' => null, 67 | ]); 68 | $parser->addOption('type', [ 69 | 'short' => 't', 70 | 'help' => 'Type (comma separated list possible)', 71 | 'default' => null, 72 | ]); 73 | 74 | $parser->setDescription( 75 | 'Simple and minimalistic job queue (or deferred-task) system.' 76 | . PHP_EOL 77 | . 'This command runs a queue worker.', 78 | ); 79 | 80 | return $parser; 81 | } 82 | 83 | /** 84 | * @param \Cake\Console\Arguments $args Arguments 85 | * 86 | * @return \Psr\Log\LoggerInterface 87 | */ 88 | protected function getLogger(Arguments $args): LoggerInterface { 89 | $logger = null; 90 | if (!$args->getOption('quiet')) { 91 | $logger = Log::engine((string)$args->getOption('logger')); 92 | } 93 | 94 | return $logger ?? new NullLogger(); 95 | } 96 | 97 | /** 98 | * Run a QueueWorker loop. 99 | * Runs a Queue Worker process which will try to find unassigned jobs in the queue 100 | * which it may run and try to fetch and execute them. 101 | * 102 | * @param \Cake\Console\Arguments $args Arguments 103 | * @param \Cake\Console\ConsoleIo $io ConsoleIo 104 | * 105 | * @return int 106 | */ 107 | public function execute(Arguments $args, ConsoleIo $io): int { 108 | $logger = $this->getLogger($args); 109 | $io = new Io($io); 110 | $processor = new Processor($io, $logger, $this->container); 111 | 112 | return $processor->run($args->getOptions()); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/Command/WorkerCommand.php: -------------------------------------------------------------------------------- 1 | QueueProcesses = $this->fetchTable('Queue.QueueProcesses'); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public static function defaultName(): string { 38 | return 'queue job'; 39 | } 40 | 41 | /** 42 | * @return \Cake\Console\ConsoleOptionParser 43 | */ 44 | public function getOptionParser(): ConsoleOptionParser { 45 | $parser = parent::getOptionParser(); 46 | 47 | $parser->addArgument('action', [ 48 | 'help' => 'Action (end, kill)', 49 | 'required' => false, 50 | ]); 51 | $parser->addArgument('pid', [ 52 | 'help' => 'PID (Process/Worker ID)', 53 | 'required' => false, 54 | ]); 55 | 56 | $parser->setDescription( 57 | 'Display, end or kill running workers.', 58 | ); 59 | 60 | return $parser; 61 | } 62 | 63 | /** 64 | * @param \Cake\Console\Arguments $args Arguments 65 | * @param \Cake\Console\ConsoleIo $io ConsoleIo 66 | * 67 | * @return int|null|void 68 | */ 69 | public function execute(Arguments $args, ConsoleIo $io) { 70 | $action = $args->getArgument('action'); 71 | if (!$action) { 72 | $io->out('Please use with [action] [PID] added.'); 73 | $io->out('Actions are:'); 74 | $io->out('- end: Gracefully end a worker/process, use "all"/"server" for all'); 75 | $io->out('- kill: Kill a worker/process, use "all"/"server" for all'); 76 | $io->out('- clean: '); 77 | $io->out(); 78 | 79 | /** @var array<\Queue\Model\Entity\QueueProcess> $processes */ 80 | $processes = $this->QueueProcesses->find() 81 | ->orderByDesc('modified') 82 | ->limit(10)->all()->toArray(); 83 | if ($processes) { 84 | $io->out('Last jobs are:'); 85 | } else { 86 | $io->out('No workers/processes found'); 87 | } 88 | 89 | foreach ($processes as $worker) { 90 | $io->out('- [' . $worker->pid . '] ' . $worker->server . ':' . $worker->workerkey . ' (' . ($worker->terminate ? 'scheduled to terminate' : 'running') . ')'); 91 | $io->out(' Last run: ' . $worker->modified); 92 | } 93 | 94 | return static::CODE_ERROR; 95 | } 96 | 97 | if (!in_array($action, ['end', 'kill', 'clean'], true)) { 98 | $io->abort('No such action'); 99 | } 100 | $pid = $args->getArgument('pid'); 101 | if (!$pid && $action !== 'clean') { 102 | $io->abort('PID must be given, or "all" used for all.'); 103 | } 104 | if ($action === 'clean' && $pid) { 105 | $io->abort('Clean action does not have a 2nd argument.'); 106 | } 107 | 108 | /** @phpstan-ignore-next-line */ 109 | return $this->$action($io, $pid); 110 | } 111 | 112 | /** 113 | * @param \Cake\Console\ConsoleIo $io 114 | * @param string $pid 115 | * 116 | * @return int 117 | */ 118 | protected function end(ConsoleIo $io, string $pid): int { 119 | if ($pid === 'all' || $pid === 'server') { 120 | $workers = $this->QueueProcesses->getProcesses($pid === 'server'); 121 | foreach ($workers as $worker) { 122 | $this->QueueProcesses->endProcess($worker->pid); 123 | $io->success('Job ' . $worker->pid . ' marked for termination (will finish current job)'); 124 | } 125 | 126 | return static::CODE_SUCCESS; 127 | } 128 | 129 | /** @var \Queue\Model\Entity\QueueProcess $worker */ 130 | $worker = $this->QueueProcesses->find()->where(['pid' => $pid])->first(); 131 | if (!$worker) { 132 | $io->abort('No such worker/process (anymore).'); 133 | } 134 | 135 | $this->QueueProcesses->endProcess($worker->pid); 136 | 137 | $io->success('Job ' . $worker->pid . ' marked for termination (will finish current job)'); 138 | 139 | return static::CODE_SUCCESS; 140 | } 141 | 142 | /** 143 | * @param \Cake\Console\ConsoleIo $io 144 | * @param string $pid 145 | * 146 | * @return int 147 | */ 148 | protected function kill(ConsoleIo $io, string $pid): int { 149 | if ($pid === 'all' || $pid === 'server') { 150 | $workers = $this->QueueProcesses->getProcesses($pid === 'server'); 151 | foreach ($workers as $worker) { 152 | if ($pid === 'all' && Configure::read('Queue.multiserver')) { 153 | $serverString = $this->QueueProcesses->buildServerString(); 154 | if ($serverString !== $worker->workerkey) { 155 | $io->abort('Cannot kill by PID in multiserver environment for this CLI. You need to execute this on the same server.'); 156 | } 157 | } 158 | $this->QueueProcesses->terminateProcess($worker->pid); 159 | $io->success('Job ' . $worker->pid . ' killed'); 160 | } 161 | 162 | return static::CODE_SUCCESS; 163 | } 164 | 165 | /** @var \Queue\Model\Entity\QueueProcess $worker */ 166 | $worker = $this->QueueProcesses->find()->where(['pid' => $pid])->first(); 167 | if (!$worker) { 168 | $io->abort('No such worker/process (anymore).'); 169 | } 170 | 171 | if (Configure::read('Queue.multiserver')) { 172 | $serverString = $this->QueueProcesses->buildServerString(); 173 | if ($serverString !== $worker->workerkey) { 174 | $io->abort('Cannot kill by PID in multiserver environment for this CLI. You need to execute this on the same server.'); 175 | } 176 | } 177 | 178 | $this->QueueProcesses->terminateProcess($worker->pid); 179 | 180 | $io->success('Job ' . $worker->pid . ' killed'); 181 | 182 | return static::CODE_SUCCESS; 183 | } 184 | 185 | /** 186 | * @param \Cake\Console\ConsoleIo $io 187 | * 188 | * @return int 189 | */ 190 | protected function clean(ConsoleIo $io): int { 191 | $timeout = Config::defaultworkertimeout(); 192 | if (!$timeout) { 193 | $io->abort('You disabled `defaultworkertimeout` in config. Aborting.'); 194 | } 195 | $thresholdTime = (new DateTime())->subSeconds($timeout); 196 | 197 | $io->out('Deleting old/outdated processes, that have finished before ' . $thresholdTime); 198 | $result = $this->QueueProcesses->cleanEndedProcesses(); 199 | $io->success('Deleted: ' . $result); 200 | 201 | return static::CODE_SUCCESS; 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/Controller/Admin/LoadHelperTrait.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->addHelpers($helpers); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Controller/Admin/QueueProcessesController.php: -------------------------------------------------------------------------------- 1 | paginate(\Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface|string|null $object = null, array $settings = []) 13 | * @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs 14 | */ 15 | class QueueProcessesController extends AppController { 16 | 17 | use LoadHelperTrait; 18 | 19 | /** 20 | * @var array 21 | */ 22 | protected array $paginate = [ 23 | 'order' => [ 24 | 'created' => 'DESC', 25 | ], 26 | ]; 27 | 28 | /** 29 | * @return void 30 | */ 31 | public function initialize(): void { 32 | parent::initialize(); 33 | 34 | $this->loadHelpers(); 35 | } 36 | 37 | /** 38 | * Index method 39 | * 40 | * @return \Cake\Http\Response|null|void 41 | */ 42 | public function index() { 43 | $queueProcesses = $this->paginate(); 44 | 45 | $this->set(compact('queueProcesses')); 46 | } 47 | 48 | /** 49 | * View method 50 | * 51 | * @param int|null $id Queue Process id. 52 | * 53 | * @return \Cake\Http\Response|null|void 54 | */ 55 | public function view(?int $id = null) { 56 | $queueProcess = $this->QueueProcesses->get($id); 57 | 58 | $this->set(compact('queueProcess')); 59 | } 60 | 61 | /** 62 | * Edit method 63 | * 64 | * @param int|null $id Queue Process id. 65 | * 66 | * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise. 67 | */ 68 | public function edit(?int $id = null) { 69 | $queueProcess = $this->QueueProcesses->get($id); 70 | if ($this->request->is(['patch', 'post', 'put'])) { 71 | $queueProcess = $this->QueueProcesses->patchEntity($queueProcess, $this->request->getData()); 72 | if ($this->QueueProcesses->save($queueProcess)) { 73 | $this->Flash->success(__d('queue', 'The queue process has been saved.')); 74 | 75 | return $this->redirect(['action' => 'index']); 76 | } 77 | 78 | $this->Flash->error(__d('queue', 'The queue process could not be saved. Please, try again.')); 79 | } 80 | 81 | $this->set(compact('queueProcess')); 82 | } 83 | 84 | /** 85 | * @param int|null $id Queue Process id. 86 | * 87 | * @return \Cake\Http\Response|null|void Redirects to index. 88 | */ 89 | public function terminate(?int $id = null) { 90 | $this->request->allowMethod(['post', 'delete']); 91 | 92 | try { 93 | $queueProcess = $this->QueueProcesses->get($id); 94 | $queueProcess->terminate = true; 95 | $this->QueueProcesses->saveOrFail($queueProcess); 96 | $this->Flash->success(__d('queue', 'The queue process has been deleted.')); 97 | } catch (Exception $exception) { 98 | $this->Flash->error(__d('queue', 'The queue process could not be deleted. Please, try again.')); 99 | } 100 | 101 | return $this->redirect(['action' => 'index']); 102 | } 103 | 104 | /** 105 | * @param int|null $id Queue Process id. 106 | * @param int|null $sig Signal (defaults to graceful SIGTERM = 15). 107 | * 108 | * @return \Cake\Http\Response|null|void Redirects to index. 109 | */ 110 | public function delete(?int $id = null, ?int $sig = null) { 111 | $this->request->allowMethod(['post', 'delete']); 112 | $queueProcess = $this->QueueProcesses->get($id); 113 | 114 | if (!Configure::read('Queue.multiserver')) { 115 | $this->QueueProcesses->terminateProcess($queueProcess->pid, $sig ?: SIGTERM); 116 | } 117 | 118 | if ($this->QueueProcesses->delete($queueProcess)) { 119 | $this->Flash->success(__d('queue', 'The queue process has been deleted.')); 120 | } else { 121 | $this->Flash->error(__d('queue', 'The queue process could not be deleted. Please, try again.')); 122 | } 123 | 124 | return $this->redirect(['action' => 'index']); 125 | } 126 | 127 | /** 128 | * @return \Cake\Http\Response|null|void Redirects to index. 129 | */ 130 | public function cleanup() { 131 | $this->request->allowMethod(['post', 'delete']); 132 | 133 | $count = $this->QueueProcesses->cleanEndedProcesses(); 134 | 135 | $this->Flash->success($count . ' leftovers cleaned out.'); 136 | 137 | return $this->redirect($this->referer(['action' => 'index'], true)); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/Generator/Task/QueuedJobTask.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected array $aliases = [ 16 | '\Queue\Model\Table\QueuedJobsTable::createJob()' => 0, 17 | '\Queue\Model\Table\QueuedJobsTable::isQueued()' => 1, 18 | ]; 19 | 20 | /** 21 | * @return array<\IdeHelper\Generator\Directive\BaseDirective> 22 | */ 23 | public function collect(): array { 24 | $list = []; 25 | 26 | $names = $this->collectQueuedJobTasks(); 27 | foreach ($names as $name => $className) { 28 | $list[$name] = "'$name'"; 29 | } 30 | 31 | ksort($list); 32 | 33 | $result = []; 34 | foreach ($this->aliases as $alias => $position) { 35 | $directive = new ExpectedArguments($alias, $position, $list); 36 | $result[$directive->key()] = $directive; 37 | } 38 | 39 | return $result; 40 | } 41 | 42 | /** 43 | * @return array 44 | */ 45 | protected function collectQueuedJobTasks(): array { 46 | $taskFinder = new TaskFinder(); 47 | $tasks = $taskFinder->all(); 48 | 49 | return $tasks; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Mailer/Transport/QueueTransport.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function send(Message $message): array { 32 | if (!empty($this->_config['queue'])) { 33 | $this->_config = $this->_config['queue'] + $this->_config; 34 | $message->setConfig((array)$this->_config['queue'] + ['queue' => []]); 35 | unset($this->_config['queue']); 36 | } 37 | 38 | $transport = $this->_config['transport'] ?? null; 39 | 40 | /** @var \Queue\Model\Table\QueuedJobsTable $QueuedJobs */ 41 | $QueuedJobs = $this->getTableLocator()->get('Queue.QueuedJobs'); 42 | $result = $QueuedJobs->createJob('Queue.Email', ['transport' => $transport, 'settings' => $message]); 43 | $result['headers'] = $message->getHeadersString(); 44 | $result['message'] = $message->getBodyString(); 45 | 46 | return $result->toArray(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Mailer/Transport/SimpleQueueTransport.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function send(Message $message): array { 34 | if (!empty($this->_config['queue'])) { 35 | $this->_config = $this->_config['queue'] + $this->_config; 36 | $message->setConfig((array)$this->_config['queue'] + ['queue' => []]); 37 | unset($this->_config['queue']); 38 | } 39 | 40 | $settings = [ 41 | 'from' => [$message->getFrom()], 42 | 'to' => [$message->getTo()], 43 | 'cc' => [$message->getCc()], 44 | 'bcc' => [$message->getBcc()], 45 | 'charset' => [$message->getCharset()], 46 | 'replyTo' => [$message->getReplyTo()], 47 | 'readReceipt' => [$message->getReadReceipt()], 48 | 'returnPath' => [$message->getReturnPath()], 49 | 'messageId' => [$message->getMessageId()], 50 | 'domain' => [$message->getDomain()], 51 | 'headers' => [$message->getHeaders()], 52 | 'headerCharset' => [$message->getHeaderCharset()], 53 | 'emailFormat' => [$message->getEmailFormat()], 54 | 'subject' => [$message->getOriginalSubject()], 55 | 'transport' => [$this->_config['transport']], 56 | 'attachments' => [$message->getAttachments()], 57 | ]; 58 | 59 | foreach ($settings as $setting => $value) { 60 | /** @phpstan-ignore-next-line */ 61 | if (array_key_exists(0, $value) && ($value[0] === null || $value[0] === [])) { 62 | unset($settings[$setting]); 63 | } 64 | } 65 | 66 | $QueuedJobs = $this->getQueuedJobsModel(); 67 | $result = $QueuedJobs->createJob('Queue.Email', ['settings' => $settings]); 68 | $result['headers'] = $message->getHeadersString(); 69 | $result['message'] = $message->getBodyString(); 70 | 71 | return $result->toArray(); 72 | } 73 | 74 | /** 75 | * @return \Queue\Model\Table\QueuedJobsTable 76 | */ 77 | protected function getQueuedJobsModel(): QueuedJobsTable { 78 | /** @var \Queue\Model\Table\QueuedJobsTable $table */ 79 | $table = $this->getTableLocator()->get('Queue.QueuedJobs'); 80 | 81 | return $table; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Migration/OldTaskFinder.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function all(?string $plugin): array { 20 | $paths = App::classPath('Shell/Task', $plugin); 21 | 22 | $allTasks = []; 23 | foreach ($paths as $path) { 24 | $tasks = $this->getTasks($path, $plugin); 25 | 26 | $allTasks += $tasks; 27 | } 28 | 29 | return $allTasks; 30 | } 31 | 32 | /** 33 | * @param string $path 34 | * @param string|null $plugin 35 | * 36 | * @return array 37 | */ 38 | protected function getTasks(string $path, ?string $plugin): array { 39 | $res = glob($path . '*Task.php') ?: []; 40 | 41 | $tasks = []; 42 | foreach ($res as $r) { 43 | $name = basename($r, 'Task.php'); 44 | $name = substr($name, 5); 45 | 46 | $taskKey = $plugin ? $plugin . '.' . $name : $name; 47 | $tasks[$taskKey] = $path . basename($r); 48 | } 49 | 50 | return $tasks; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Model/Entity/QueueProcess.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected array $_accessible = [ 26 | '*' => true, 27 | 'id' => false, 28 | ]; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Model/Entity/QueuedJob.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected array $_accessible = [ 33 | '*' => true, 34 | 'id' => false, 35 | ]; 36 | 37 | /** 38 | * @return string[] 39 | */ 40 | public static function statusesForSearch(): array { 41 | return [ 42 | 'completed' => 'Completed', 43 | 'in_progress' => 'In Progress', 44 | 'scheduled' => 'Scheduled', 45 | ]; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Model/ProcessEndingException.php: -------------------------------------------------------------------------------- 1 | > 2x) and cannot be zero. 15 | * 16 | * @return int 17 | */ 18 | public static function defaultworkertimeout(): int { 19 | $timeout = Configure::read('Queue.defaultworkertimeout', 600); // 10min 20 | if ($timeout <= 0) { 21 | throw new InvalidArgumentException('Queue.defaultworkertimeout is less or equal than zero. Indefinite running of workers is not supported.'); 22 | } 23 | 24 | return $timeout; 25 | } 26 | 27 | /** 28 | * Seconds of running time after which the worker will terminate (0 = unlimited) 29 | * 30 | * @return int 31 | */ 32 | public static function workermaxruntime(): int { 33 | return Configure::read('Queue.workermaxruntime', 120); 34 | } 35 | 36 | /** 37 | * Minimum number of seconds before a cleanup run will remove a completed task (set to 0 to disable) 38 | * 39 | * @return int 40 | */ 41 | public static function cleanuptimeout(): int { 42 | return Configure::read('Queue.cleanuptimeout', 2592000); // 30 days 43 | } 44 | 45 | /** 46 | * @return int 47 | */ 48 | public static function sleeptime(): int { 49 | return Configure::read('Queue.sleeptime', 10); 50 | } 51 | 52 | /** 53 | * @return int 54 | */ 55 | public static function gcprob(): int { 56 | return Configure::read('Queue.gcprob', 10); 57 | } 58 | 59 | /** 60 | * @return int 61 | */ 62 | public static function defaultworkerretries(): int { 63 | return Configure::read('Queue.defaultworkerretries', 1); 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public static function maxworkers(): int { 70 | return Configure::read('Queue.maxworkers', 1); 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public static function ignoredTasks(): array { 77 | $a = Configure::read('Queue.ignoredTasks', []); 78 | if (!is_array($a)) { 79 | throw new InvalidArgumentException('Queue.ignoredTasks is not an array'); 80 | } 81 | 82 | return $a; 83 | } 84 | 85 | /** 86 | * @param array $tasks 87 | * 88 | * @throws \RuntimeException 89 | * 90 | * @return array> 91 | */ 92 | public static function taskConfig(array $tasks): array { 93 | $config = []; 94 | 95 | foreach ($tasks as $task => $className) { 96 | [$pluginName, $taskName] = pluginSplit($task); 97 | 98 | /** @var \Queue\Queue\Task $taskObject */ 99 | $taskObject = new $className(); 100 | 101 | $config[$task]['class'] = $className; 102 | $config[$task]['name'] = $taskName; 103 | $config[$task]['plugin'] = $pluginName; 104 | $config[$task]['timeout'] = $taskObject->timeout ?? static::defaultworkertimeout(); 105 | $config[$task]['retries'] = $taskObject->retries ?? static::defaultworkerretries(); 106 | $config[$task]['rate'] = $taskObject->rate; 107 | $config[$task]['costs'] = $taskObject->costs; 108 | $config[$task]['unique'] = $taskObject->unique; 109 | 110 | unset($taskObject); 111 | } 112 | 113 | return $config; 114 | } 115 | 116 | /** 117 | * @phpstan-param class-string<\Queue\Queue\Task>|string $class 118 | * 119 | * @param string $class 120 | * 121 | * @return string 122 | */ 123 | public static function taskName(string $class): string { 124 | preg_match('#^(.+?)\\\\Queue\\\\Task\\\\(.+?)Task$#', $class, $matches); 125 | if (!$matches) { 126 | throw new InvalidArgumentException('Invalid class name: ' . $class); 127 | } 128 | 129 | $namespace = str_replace('\\', '/', $matches[1]); 130 | if ($namespace === Configure::read('App.namespace')) { 131 | return $matches[2]; 132 | } 133 | 134 | return $namespace . '.' . $matches[2]; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/Queue/ServicesTrait.php: -------------------------------------------------------------------------------- 1 | $id Classname or identifier of the service you want to retrieve 20 | * 21 | * @throws \Psr\Container\ContainerExceptionInterface 22 | * @throws \Psr\Container\NotFoundExceptionInterface 23 | * 24 | * @return T 25 | */ 26 | protected function getService(string $id) { 27 | if ($this->container === null) { 28 | throw new LogicException( 29 | "The Container has not been set. Hint: getService() must not be called in the Task's constructor.", 30 | ); 31 | } 32 | 33 | return $this->container->get($id); 34 | } 35 | 36 | /** 37 | * @param \Cake\Core\ContainerInterface $container 38 | * 39 | * @return void 40 | */ 41 | public function setContainer(ContainerInterface $container): void { 42 | $this->container = $container; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Queue/Task.php: -------------------------------------------------------------------------------- 1 | > 2x). 35 | * Defaults to Config::defaultworkertimeout(). 36 | * 37 | * @var int|null 38 | */ 39 | public ?int $timeout = null; 40 | 41 | /** 42 | * Number of times a failed instance of this task should be restarted before giving up. 43 | * Defaults to Config::defaultworkerretries(). 44 | * 45 | * @var int|null 46 | */ 47 | public ?int $retries = null; 48 | 49 | /** 50 | * Rate limiting per worker in seconds. 51 | * Activate this if you want to stretch the processing of a specific task per worker. 52 | * 53 | * @var int 54 | */ 55 | public int $rate = 0; 56 | 57 | /** 58 | * Activate this if you want cost management per server to avoid server overloading. 59 | * 60 | * Expensive tasks (CPU, memory, ...) can have 1...100 points here, with higher points 61 | * preventing a similar cost intensive task to be fetched on the same server in parallel. 62 | * Smaller ones can easily still be processed on the same server if some an expensive one is running. 63 | * 64 | * @var int 65 | */ 66 | public int $costs = 0; 67 | 68 | /** 69 | * Set to true if you want to make sure this specific task is never run in parallel, neither 70 | * on the same server, nor any other server. Any worker running will not fetch this task, if any 71 | * job here is already in progress. 72 | * 73 | * @var bool 74 | */ 75 | public bool $unique = false; 76 | 77 | /** 78 | * @var \Queue\Console\Io 79 | */ 80 | protected Io $io; 81 | 82 | /** 83 | * @var \Psr\Log\LoggerInterface|null 84 | */ 85 | protected ?LoggerInterface $logger = null; 86 | 87 | /** 88 | * @param \Queue\Console\Io|null $io IO 89 | * @param \Psr\Log\LoggerInterface|null $logger 90 | */ 91 | public function __construct(?Io $io = null, ?LoggerInterface $logger = null) { 92 | $this->io = $io ?: new Io(new ConsoleIo()); 93 | $this->logger = $logger; 94 | 95 | $tableLocator = $this->getTableLocator(); 96 | 97 | /** @var \Queue\Model\Table\QueuedJobsTable $QueuedJobs */ 98 | $QueuedJobs = $tableLocator->get($this->queueModelClass); 99 | $this->QueuedJobs = $QueuedJobs; 100 | 101 | if (isset($this->defaultTable)) { 102 | $this->fetchTable(); 103 | } 104 | } 105 | 106 | /** 107 | * @throws \InvalidArgumentException 108 | * 109 | * @return string 110 | */ 111 | public static function taskName(): string { 112 | $class = static::class; 113 | 114 | return Config::taskName($class); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/Queue/Task/CostsExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue CostsExample task.'); 35 | $this->io->hr(); 36 | $this->io->out('I will now add an example Job into the Queue.'); 37 | $this->io->out('This job cannot run more than once per server (across all its workers).'); 38 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 39 | $this->io->out(' '); 40 | $this->io->out('To run a Worker use:'); 41 | $this->io->out(' bin/cake queue run'); 42 | $this->io->out(' '); 43 | $this->io->out('You can find the sourcecode of this task in: '); 44 | $this->io->out(__FILE__); 45 | $this->io->out(' '); 46 | 47 | $this->QueuedJobs->createJob('Queue.CostsExample'); 48 | $this->io->success('OK, job created, now run the worker'); 49 | } 50 | 51 | /** 52 | * CostsExample run function. 53 | * This function is executed, when a worker is executing a task. 54 | * 55 | * @param array $data The array passed to QueuedJobsTable::createJob() 56 | * @param int $jobId The id of the QueuedJob entity 57 | * 58 | * @return void 59 | */ 60 | public function run(array $data, int $jobId): void { 61 | $this->io->hr(); 62 | $this->io->out('CakePHP Queue CostsExample task.'); 63 | 64 | sleep($this->sleep); 65 | 66 | $this->io->hr(); 67 | $this->io->success(' -> Success, the CostsExample Job was run. <-'); 68 | } 69 | 70 | /** 71 | * @param int $seconds 72 | * 73 | * @return void 74 | */ 75 | public function setSleep(int $seconds): void { 76 | $this->sleep = $seconds; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Queue/Task/ExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue Example task.'); 38 | $this->io->hr(); 39 | $this->io->out('This is a very simple example of a QueueTask.'); 40 | $this->io->out('I will now add an example Job into the Queue.'); 41 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 42 | $this->io->out(' '); 43 | $this->io->out('To run a Worker use:'); 44 | $this->io->out(' bin/cake queue run'); 45 | $this->io->out(' '); 46 | $this->io->out('You can find the sourcecode of this task in: '); 47 | $this->io->out(__FILE__); 48 | $this->io->out(' '); 49 | 50 | $this->QueuedJobs->createJob('Queue.Example'); 51 | $this->io->success('OK, job created, now run the worker'); 52 | } 53 | 54 | /** 55 | * Example run function. 56 | * This function is executed, when a worker is executing a task. 57 | * The return parameter will determine, if the task will be marked completed, or be requeued. 58 | * 59 | * @param array $data The array passed to QueuedJobsTable::createJob() 60 | * @param int $jobId The id of the QueuedJob entity 61 | * 62 | * @return void 63 | */ 64 | public function run(array $data, int $jobId): void { 65 | $this->io->hr(); 66 | $this->io->out('CakePHP Queue Example task.'); 67 | $this->io->hr(); 68 | $this->io->success(' -> Success, the Example Job was run. <-'); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Queue/Task/ExceptionExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue ExceptionExample task.'); 34 | $this->io->hr(); 35 | $this->io->out('This is a very simple example of a QueueTask and how exceptions are handled.'); 36 | $this->io->out('I will now add an example Job into the Queue.'); 37 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 38 | $this->io->out(' '); 39 | $this->io->out('To run a Worker use:'); 40 | $this->io->out(' bin/cake queue run'); 41 | $this->io->out(' '); 42 | $this->io->out('You can find the sourcecode of this task in: '); 43 | $this->io->out(__FILE__); 44 | $this->io->out(' '); 45 | 46 | $this->QueuedJobs->createJob('Queue.ExceptionExample'); 47 | $this->io->success('OK, job created, now run the worker'); 48 | } 49 | 50 | /** 51 | * Example run function. 52 | * This function is executed, when a worker is executing a task. 53 | * The return parameter will determine, if the task will be marked completed, or be requeued. 54 | * 55 | * @param array $data The array passed to QueuedJobsTable::createJob() 56 | * @param int $jobId The id of the QueuedJob entity 57 | * 58 | * @throws \Queue\Model\QueueException 59 | * 60 | * @return void 61 | */ 62 | public function run(array $data, int $jobId): void { 63 | $this->io->hr(); 64 | $this->io->out('CakePHP Queue ExceptionExample task.'); 65 | $this->io->hr(); 66 | 67 | throw new QueueException('Exception demo :-)'); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/Queue/Task/ExecuteTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue Execute task.'); 37 | $this->io->hr(); 38 | if (!$data) { 39 | $this->io->out('This will run an shell command on the Server.'); 40 | $this->io->out('The task is mainly intended to serve as a kind of buffer for program calls from a CakePHP application.'); 41 | $this->io->out(' '); 42 | $this->io->out('Call like this:'); 43 | $this->io->out(' bin/cake queue add Execute "*command* *param1* *param2*" ...'); 44 | $this->io->out(' '); 45 | $this->io->out('For commands with spaces use " around it. E.g. `bin/cake queue add Execute "sleep 10s"`.'); 46 | $this->io->out(' '); 47 | 48 | return; 49 | } 50 | 51 | $command = $data; 52 | $params = null; 53 | if (strpos($data, ' ') !== false) { 54 | [$command, $params] = explode(' ', $data, 2); 55 | } 56 | 57 | $data = [ 58 | 'command' => $command, 59 | 'params' => $params ? [$params] : [], 60 | ]; 61 | 62 | $this->QueuedJobs->createJob('Queue.Execute', $data); 63 | $this->io->success('OK, job created, now run the worker'); 64 | } 65 | 66 | /** 67 | * Run function. 68 | * This function is executed, when a worker is executing a task. 69 | * The return parameter will determine, if the task will be marked completed, or be requeued. 70 | * 71 | * @param array $data The array passed to QueuedJobsTable::createJob() 72 | * @param int $jobId The id of the QueuedJob entity 73 | * 74 | * @throws \Queue\Model\QueueException 75 | * 76 | * @return void 77 | */ 78 | public function run(array $data, int $jobId): void { 79 | $data += [ 80 | 'command' => null, 81 | 'params' => [], 82 | 'redirect' => true, 83 | 'escape' => true, 84 | 'log' => false, 85 | 'accepted' => [CommandInterface::CODE_SUCCESS], 86 | ]; 87 | 88 | $command = $data['command']; 89 | if ($data['escape']) { 90 | $command = escapeshellcmd($data['command']); 91 | } 92 | 93 | if ($data['params']) { 94 | $params = $data['params']; 95 | if ($data['escape']) { 96 | foreach ($params as $key => $value) { 97 | $params[$key] = escapeshellcmd($value); 98 | } 99 | } 100 | $command .= ' ' . implode(' ', $params); 101 | } 102 | 103 | $this->io->out('Executing: `' . $command . '`'); 104 | 105 | if ($data['redirect']) { 106 | $command .= ' 2>&1'; 107 | } 108 | 109 | exec($command, $output, $exitCode); 110 | $this->io->nl(); 111 | $this->io->out($output); 112 | 113 | if ($data['log']) { 114 | $queueProcesses = $this->getTableLocator()->get('Queue.QueueProcesses'); 115 | $server = $queueProcesses->buildServerString(); 116 | $this->log($server . ': `' . $command . '` exits with `' . $exitCode . '` and returns `' . print_r($output, true) . '`' . PHP_EOL . 'Data : ' . print_r($data, true), 'info'); 117 | } 118 | 119 | $acceptedReturnCodes = $data['accepted']; 120 | $success = !$acceptedReturnCodes || in_array($exitCode, $acceptedReturnCodes, true); 121 | if (!$success) { 122 | $this->io->err('Error (code ' . $exitCode . ')', ConsoleIo::VERBOSE); 123 | } else { 124 | $this->io->success('Success (code ' . $exitCode . ')', ConsoleIo::VERBOSE); 125 | } 126 | 127 | if (!$success) { 128 | throw new QueueException('Failed with error code ' . $exitCode . ': `' . $command . '`'); 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/Queue/Task/MailerTask.php: -------------------------------------------------------------------------------- 1 | $data The array passed to QueuedJobsTable::createJob() 35 | * @param int $jobId The id of the QueuedJob entity 36 | * 37 | * @throws \Queue\Model\QueueException 38 | * @throws \Cake\Mailer\Exception\MissingMailerException 39 | * @throws \Throwable 40 | * 41 | * @return void 42 | */ 43 | public function run(array $data, int $jobId): void { 44 | if (!isset($data['class'])) { 45 | throw new QueueException('Queue Mailer task called without valid `mailer` class.'); 46 | } 47 | if (!isset($data['action'])) { 48 | throw new QueueException('Queue Mailer task called without `action` data.'); 49 | } 50 | 51 | $this->mailer = $this->getMailer($data['class']); 52 | 53 | try { 54 | $this->mailer->setTransport($data['transport'] ?? 'default'); 55 | $result = $this->mailer->send($data['action'], $data['vars'] ?? []); 56 | } catch (Throwable $e) { 57 | $error = $e->getMessage(); 58 | $error .= ' (line ' . $e->getLine() . ' in ' . $e->getFile() . ')' . PHP_EOL . $e->getTraceAsString(); 59 | Log::write('error', $error); 60 | 61 | throw $e; 62 | } 63 | 64 | if (!$result) { 65 | throw new QueueException('Could not send email.'); 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Queue/Task/MonitorExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue MonitorExample task.'); 40 | $this->io->hr(); 41 | $this->io->out('This is an example of doing some server monitor tasks and logging.'); 42 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 43 | $this->io->out(' '); 44 | $this->io->out('To run a Worker use:'); 45 | $this->io->out(' bin/cake queue run'); 46 | $this->io->out(' '); 47 | $this->io->out('You can find the sourcecode of this task in: '); 48 | $this->io->out(__FILE__); 49 | $this->io->out(' '); 50 | 51 | $this->QueuedJobs->createJob('Queue.MonitorExample'); 52 | $this->io->success('OK, job created, now run the worker'); 53 | } 54 | 55 | /** 56 | * MonitorExample run function. 57 | * This function is executed, when a worker is executing a task. 58 | * The return parameter will determine, if the task will be marked completed, or be requeued. 59 | * 60 | * @param array $data The array passed to QueuedJobsTable::createJob() 61 | * @param int $jobId The id of the QueuedJob entity 62 | * 63 | * @return void 64 | */ 65 | public function run(array $data, int $jobId): void { 66 | $this->io->hr(); 67 | $this->io->out('CakePHP Queue MonitorExample task.'); 68 | $this->io->hr(); 69 | 70 | $this->doMonitoring(); 71 | 72 | $this->io->success(' -> Success, the MonitorExample Job was run. <-'); 73 | } 74 | 75 | /** 76 | * @return void 77 | */ 78 | protected function doMonitoring(): void { 79 | $memory = $this->getSystemMemInfo(); 80 | 81 | $array = [ 82 | '[PHP] ' . PHP_VERSION, 83 | '[PHP Memory Limit] ' . ini_get('memory_limit'), 84 | '[Server Memory] Total: ' . $memory['MemTotal'] . ', Free: ' . $memory['MemFree'], 85 | ]; 86 | 87 | $message = implode(PHP_EOL, $array); 88 | $this->log($message, 'info'); 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | protected function getSystemMemInfo(): array { 95 | $data = explode("\n", file_get_contents('/proc/meminfo') ?: ''); 96 | $meminfo = []; 97 | foreach ($data as $line) { 98 | if (strpos($line, ':') === false) { 99 | continue; 100 | } 101 | [$key, $val] = explode(':', $line); 102 | $meminfo[$key] = trim($val); 103 | } 104 | 105 | return $meminfo; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Queue/Task/ProgressExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue ProgressExample task.'); 38 | $this->io->hr(); 39 | $this->io->out('This is a very simple but long running example of a QueueTask.'); 40 | $this->io->out('I will now add the Job into the Queue.'); 41 | $this->io->out('This job will need at least 2 minutes to complete.'); 42 | $this->io->out(' '); 43 | $this->io->out('To run a Worker use:'); 44 | $this->io->out(' bin/cake queue run'); 45 | $this->io->out(' '); 46 | $this->io->out('You can find the sourcecode of this task in:'); 47 | $this->io->out(__FILE__); 48 | $this->io->out(' '); 49 | 50 | $data = [ 51 | 'duration' => 2 * static::MINUTE, 52 | ]; 53 | $this->QueuedJobs->createJob('Queue.ProgressExample', $data); 54 | $this->io->success('OK, job created, now run the worker'); 55 | } 56 | 57 | /** 58 | * Example run function. 59 | * This function is executed, when a worker is executing a task. 60 | * The return parameter will determine, if the task will be marked completed, or be requeued. 61 | * 62 | * Defaults to 120 seconds 63 | * 64 | * @param array $data The array passed to QueuedJobsTable::createJob() 65 | * @param int $jobId The id of the QueuedJob entity 66 | * 67 | * @return void 68 | */ 69 | public function run(array $data, int $jobId): void { 70 | $this->io->hr(); 71 | $this->io->out('CakePHP Queue ProgressExample task.'); 72 | $seconds = !empty($data['duration']) ? (int)$data['duration'] : 2 * static::MINUTE; 73 | 74 | $this->io->out('A total of ' . $seconds . ' seconds need to pass...'); 75 | for ($i = 0; $i < $seconds; $i++) { 76 | sleep(1); 77 | $this->QueuedJobs->updateProgress($jobId, ($i + 1) / $seconds, 'Status Test ' . ($i + 1) . 's'); 78 | } 79 | $this->QueuedJobs->updateProgress($jobId, 1, 'Status Test Done'); 80 | 81 | $this->io->hr(); 82 | $this->io->success(' -> Success, the ProgressExample Job was run. <-'); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/Queue/Task/RetryExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue RetryExample task.'); 70 | $this->io->hr(); 71 | $this->io->out('This is a very simple example of a QueueTask and how retries work.'); 72 | $this->io->out('I will now add an example Job into the Queue.'); 73 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 74 | $this->io->out(' '); 75 | $this->io->out('To run a Worker use:'); 76 | $this->io->out(' bin/cake queue run'); 77 | $this->io->out(' '); 78 | $this->io->out('You can find the sourcecode of this task in: '); 79 | $this->io->out(__FILE__); 80 | $this->io->out(' '); 81 | 82 | $init = static::init(); 83 | if (!$init) { 84 | $this->io->warn('File seems to already exist. Make sure you run this task standalone. You cannot run it multiple times in parallel!'); 85 | } 86 | 87 | $this->QueuedJobs->createJob('Queue.RetryExample'); 88 | $this->io->success('OK, job created, now run the worker'); 89 | } 90 | 91 | /** 92 | * Example run function. 93 | * This function is executed, when a worker is executing a task. 94 | * The return parameter will determine, if the task will be marked completed, or be requeued. 95 | * 96 | * @param array $data The array passed to QueuedJobsTable::createJob() 97 | * @param int $jobId The id of the QueuedJob entity 98 | * 99 | * @return void 100 | */ 101 | public function run(array $data, int $jobId): void { 102 | if (!file_exists(static::$file)) { 103 | $this->io->abort(' -> No demo file found. Aborting. <-'); 104 | } 105 | 106 | $count = (int)file_get_contents(static::$file); 107 | 108 | $this->io->hr(); 109 | $this->io->out('CakePHP Queue RetryExample task.'); 110 | $this->io->hr(); 111 | 112 | // Let's fake 3 fails before it actually runs successfully 113 | if ($count < 3) { 114 | $count++; 115 | file_put_contents(static::$file, (string)$count); 116 | $this->io->abort(' -> Sry, the RetryExample Job failed. Try again. <-'); 117 | } 118 | 119 | unlink(static::$file); 120 | $this->io->success(' -> Success, the RetryExample Job was run. <-'); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/Queue/Task/SuperExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue SuperExample task.'); 38 | $this->io->hr(); 39 | $this->io->out('This is a very superb example of a QueueTask.'); 40 | $this->io->out('I will now add an example Job into the Queue.'); 41 | $this->io->out('It will also create another Example job upon successful execution.'); 42 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 43 | $this->io->out(' '); 44 | $this->io->out('To run a Worker use:'); 45 | $this->io->out(' bin/cake queue run'); 46 | $this->io->out(' '); 47 | $this->io->out('You can find the sourcecode of this task in: '); 48 | $this->io->out(__FILE__); 49 | $this->io->out(' '); 50 | 51 | $this->QueuedJobs->createJob('Queue.SuperExample'); 52 | $this->io->success('OK, job created, now run the worker'); 53 | } 54 | 55 | /** 56 | * SuperExample run function. 57 | * This function is executed, when a worker is executing a task. 58 | * The return parameter will determine, if the task will be marked completed, or be requeued. 59 | * 60 | * @param array $data The array passed to QueuedJobsTable::createJob() 61 | * @param int $jobId The id of the QueuedJob entity 62 | * 63 | * @return void 64 | */ 65 | public function run(array $data, int $jobId): void { 66 | $this->io->hr(); 67 | $this->io->out('CakePHP Queue SuperExample task.'); 68 | 69 | // Lets create an Example task on successful execution 70 | $this->QueuedJobs->createJob('Queue.Example'); 71 | $this->io->out('... New Example task has been scheduled.'); 72 | 73 | $this->io->hr(); 74 | $this->io->success(' -> Success, the SuperExample Job was run. <-'); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Queue/Task/UniqueExampleTask.php: -------------------------------------------------------------------------------- 1 | io->out('CakePHP Queue UniqueExample task.'); 35 | $this->io->hr(); 36 | $this->io->out('I will now add an example Job into the Queue.'); 37 | $this->io->out('This job cannot run more than once across all workers.'); 38 | $this->io->out('This job will only produce some console output on the worker that it runs on.'); 39 | $this->io->out(' '); 40 | $this->io->out('To run a Worker use:'); 41 | $this->io->out(' bin/cake queue run'); 42 | $this->io->out(' '); 43 | $this->io->out('You can find the sourcecode of this task in: '); 44 | $this->io->out(__FILE__); 45 | $this->io->out(' '); 46 | 47 | $this->QueuedJobs->createJob('Queue.UniqueExample'); 48 | $this->io->success('OK, job created, now run the worker'); 49 | } 50 | 51 | /** 52 | * UniqueExample run function. 53 | * This function is executed, when a worker is executing a task. 54 | * The return parameter will determine, if the task will be marked completed, or be requeued. 55 | * 56 | * @param array $data The array passed to QueuedJobsTable::createJob() 57 | * @param int $jobId The id of the QueuedJob entity 58 | * 59 | * @return void 60 | */ 61 | public function run(array $data, int $jobId): void { 62 | $this->io->hr(); 63 | $this->io->out('CakePHP Queue UniqueExample task.'); 64 | 65 | sleep($this->sleep); 66 | 67 | $this->io->hr(); 68 | $this->io->success(' -> Success, the UniqueExample Job was run. <-'); 69 | } 70 | 71 | /** 72 | * @param int $seconds 73 | * 74 | * @return void 75 | */ 76 | public function setSleep(int $seconds): void { 77 | $this->sleep = $seconds; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Queue/TaskFinder.php: -------------------------------------------------------------------------------- 1 | >|null 19 | * @var array|null 20 | */ 21 | protected ?array $tasks = null; 22 | 23 | /** 24 | * @phpstan-return array> 25 | * 26 | * @param string $type Type of interface. 27 | * 28 | * @return array 29 | */ 30 | public function allAddable(string $type = AddInterface::class): array { 31 | $all = $this->all(); 32 | foreach ($all as $task => $class) { 33 | if (!is_subclass_of($class, $type, true)) { 34 | unset($all[$task]); 35 | } 36 | } 37 | 38 | return $all; 39 | } 40 | 41 | /** 42 | * Returns all possible Queue tasks. 43 | * 44 | * Makes sure that app tasks are prioritized over plugin ones. 45 | * 46 | * @phpstan-return array> 47 | * 48 | * @return array 49 | */ 50 | public function all(): array { 51 | if ($this->tasks !== null) { 52 | return $this->tasks; 53 | } 54 | 55 | $paths = App::classPath('Queue/Task'); 56 | $this->tasks = []; 57 | 58 | foreach ($paths as $path) { 59 | $this->tasks += $this->getTasks($path); 60 | } 61 | $plugins = array_merge((array)Configure::read('Queue.plugins'), Plugin::loaded()); 62 | $plugins = array_unique($plugins); 63 | foreach ($plugins as $plugin) { 64 | $pluginPaths = App::classPath('Queue/Task', $plugin); 65 | foreach ($pluginPaths as $pluginPath) { 66 | $pluginTasks = $this->getTasks($pluginPath, $plugin); 67 | $this->tasks += $pluginTasks; 68 | } 69 | } 70 | 71 | ksort($this->tasks); 72 | 73 | return $this->tasks; 74 | } 75 | 76 | /** 77 | * @phpstan-return array> 78 | * 79 | * @param string $path 80 | * @param string|null $plugin 81 | * 82 | * @return array 83 | */ 84 | protected function getTasks(string $path, ?string $plugin = null): array { 85 | if (!is_dir($path)) { 86 | return []; 87 | } 88 | 89 | $tasks = []; 90 | $ignoredTasks = Config::ignoredTasks(); 91 | 92 | $directoryIterator = new RecursiveDirectoryIterator($path); 93 | $recursiveIterator = new RecursiveIteratorIterator($directoryIterator); 94 | $iterator = new RegexIterator($recursiveIterator, '#.+\b(\w+)Task\.php$#', RecursiveRegexIterator::GET_MATCH); 95 | /** @var array $file */ 96 | foreach ($iterator as $file) { 97 | $path = str_replace(DS, '/', $file[0]); 98 | $pos = strpos($path, 'src/Queue/Task/'); 99 | if ($pos) { 100 | $name = substr($path, $pos + strlen('src/Queue/Task/'), -8); 101 | } else { 102 | $pos = strpos($path, APP_DIR . '/Queue/Task/'); 103 | if (!$pos) { 104 | continue; 105 | } 106 | $name = substr($path, $pos + strlen(APP_DIR . '/Queue/Task/'), -8); 107 | } 108 | 109 | $namespace = $plugin ? str_replace('/', '\\', $plugin) : Configure::read('App.namespace'); 110 | 111 | /** @phpstan-var class-string<\Queue\Queue\Task> $className */ 112 | $className = $namespace . '\Queue\Task\\' . str_replace('/', '\\', $name) . 'Task'; 113 | $key = $plugin ? $plugin . '.' . $name : $name; 114 | 115 | if (!in_array($className, $ignoredTasks, true)) { 116 | $tasks[$key] = $className; 117 | } 118 | } 119 | 120 | return $tasks; 121 | } 122 | 123 | /** 124 | * Resolves FQCN to a task name. 125 | * 126 | * @param class-string<\Queue\Queue\Task>|string $jobTask 127 | * 128 | * @return string 129 | */ 130 | public function resolve(string $jobTask): string { 131 | if (Configure::read('Queue.skipExistenceCheck')) { 132 | if (!str_contains($jobTask, '\\')) { 133 | return $jobTask; 134 | } 135 | 136 | return Config::taskName($jobTask); 137 | } 138 | 139 | $all = $this->all(); 140 | foreach ($all as $name => $className) { 141 | if ($jobTask === $className || $jobTask === $name) { 142 | return $name; 143 | } 144 | } 145 | 146 | if (!str_contains($jobTask, '\\')) { 147 | // Let's try matching without plugin prefix 148 | foreach ($all as $name => $className) { 149 | if (!str_contains($name, '.')) { 150 | continue; 151 | } 152 | [$plugin, $name] = explode('.', $name, 2); 153 | if ($jobTask === $name) { 154 | $message = 'You seem to be adding a plugin job without plugin syntax (' . $jobTask . '), migrate to using ' . $plugin . '.' . $name . ' instead.'; 155 | trigger_error($message, E_USER_DEPRECATED); 156 | 157 | return $plugin . '.' . $name; 158 | } 159 | } 160 | } 161 | 162 | throw new RuntimeException('No job type can be resolved for ' . $jobTask); 163 | } 164 | 165 | /** 166 | * @phpstan-return class-string<\Queue\Queue\Task> 167 | * 168 | * @param string $name 169 | * 170 | * @return string 171 | */ 172 | public function getClass(string $name): string { 173 | $all = $this->all(); 174 | foreach ($all as $taskName => $className) { 175 | if ($name === $taskName) { 176 | return $className; 177 | } 178 | } 179 | 180 | throw new RuntimeException('No such task: ' . $name); 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/Queue/TaskInterface.php: -------------------------------------------------------------------------------- 1 | abort('My message'); to fail a job. 11 | * 12 | * @author Mark Scherer 13 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 14 | */ 15 | interface TaskInterface { 16 | 17 | /** 18 | * Main execution of the task. 19 | * 20 | * @param array $data The array passed to QueuedJobsTable::createJob() 21 | * @param int $jobId The id of the QueuedJob entity 22 | * 23 | * @return void 24 | */ 25 | public function run(array $data, int $jobId): void; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/QueuePlugin.php: -------------------------------------------------------------------------------- 1 | add('queue add', AddCommand::class); 36 | $commands->add('queue info', InfoCommand::class); 37 | $commands->add('queue run', RunCommand::class); 38 | $commands->add('queue worker', WorkerCommand::class); 39 | $commands->add('queue job', JobCommand::class); 40 | if (class_exists('Bake\Command\SimpleBakeCommand')) { 41 | $commands->add('bake queue_task', BakeQueueTaskCommand::class); 42 | } 43 | 44 | return $commands; 45 | } 46 | 47 | /** 48 | * @param \Cake\Routing\RouteBuilder $routes The route builder to update. 49 | * 50 | * @return void 51 | */ 52 | public function routes(RouteBuilder $routes): void { 53 | $routes->prefix('Admin', function (RouteBuilder $routes): void { 54 | $routes->plugin('Queue', function (RouteBuilder $routes): void { 55 | $routes->connect('/', ['controller' => 'Queue', 'action' => 'index']); 56 | 57 | $routes->fallbacks(); 58 | }); 59 | }); 60 | 61 | $routes->plugin('Queue', ['path' => '/queue'], function (RouteBuilder $routes): void { 62 | $routes->connect('/{controller}'); 63 | }); 64 | } 65 | 66 | /** 67 | * @param \Cake\Core\ContainerInterface $container The DI container instance 68 | * 69 | * @return void 70 | */ 71 | public function services(ContainerInterface $container): void { 72 | $container->add(ContainerInterface::class, $container); 73 | $container 74 | ->add(RunCommand::class) 75 | ->addArgument(ContainerInterface::class); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/Utility/Memory.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | protected array $taskConfig = []; 20 | 21 | /** 22 | * @param \Queue\Model\Entity\QueuedJob $queuedJob 23 | * 24 | * @return bool 25 | */ 26 | public function hasFailed(QueuedJob $queuedJob): bool { 27 | if ($queuedJob->completed || !$queuedJob->fetched || !$queuedJob->attempts) { 28 | return false; 29 | } 30 | 31 | // Restarted 32 | if (!$queuedJob->failure_message) { 33 | return false; 34 | } 35 | 36 | // Requeued 37 | $taskConfig = $this->taskConfig($queuedJob->job_task); 38 | if ($taskConfig && $queuedJob->attempts <= $taskConfig['retries']) { 39 | return false; 40 | } 41 | 42 | return true; 43 | } 44 | 45 | /** 46 | * @param \Queue\Model\Entity\QueuedJob $queuedJob 47 | * 48 | * @return string|null 49 | */ 50 | public function attempts(QueuedJob $queuedJob): ?string { 51 | if ($queuedJob->attempts < 1) { 52 | return '0x'; 53 | } 54 | 55 | $taskConfig = $this->taskConfig($queuedJob->job_task); 56 | if ($taskConfig) { 57 | $maxFails = $taskConfig['retries'] + 1; 58 | 59 | return $queuedJob->attempts . '/' . $maxFails; 60 | } 61 | 62 | return $queuedJob->attempts . 'x'; 63 | } 64 | 65 | /** 66 | * Returns failure status (message) if applicable. 67 | * 68 | * @param \Queue\Model\Entity\QueuedJob $queuedJob 69 | * 70 | * @return string|null 71 | */ 72 | public function failureStatus(QueuedJob $queuedJob): ?string { 73 | if ($queuedJob->completed || !$queuedJob->fetched || !$queuedJob->attempts) { 74 | return null; 75 | } 76 | 77 | if (!$queuedJob->failure_message) { 78 | return __d('queue', 'Restarted'); 79 | } 80 | 81 | $taskConfig = $this->taskConfig($queuedJob->job_task); 82 | if ($taskConfig && $queuedJob->attempts <= $taskConfig['retries']) { 83 | return __d('queue', 'Requeued'); 84 | } 85 | 86 | return __d('queue', 'Aborted'); 87 | } 88 | 89 | /** 90 | * @param string $jobTask 91 | * 92 | * @return array 93 | */ 94 | protected function taskConfig(string $jobTask): array { 95 | if (!$this->taskConfig) { 96 | $tasks = (new TaskFinder())->all(); 97 | $this->taskConfig = Config::taskConfig($tasks); 98 | } 99 | 100 | return $this->taskConfig[$jobTask] ?? []; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /templates/Admin/Queue/processes.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 21 | 22 |
23 |

24 | 25 |

26 |

:

27 | 28 |
    29 | ' . $process->pid . ':'; 32 | echo '
      '; 33 | echo '
    • Current active job: ' . ($process->active_job ? $this->Html->link($process->active_job->job_task, [ 34 | 'controller' => 'QueuedJobs', 35 | 'action' => 'view', 36 | $process->active_job->id 37 | ]) : 'Currently no job is being processed by this worker') . '
    • '; 38 | echo '
    • Last run: ' . $this->Time->nice(new DateTime($process->modified)) . '
    • '; 39 | 40 | echo '
    • End: ' . $this->Form->postLink(__d('queue', 'Finish current job and end'), ['action' => 'processes', '?' => ['end' => $process->pid]], ['confirm' => 'Sure?', 'class' => 'button secondary btn margin btn-secondary']) . ' (next loop run)
    • '; 41 | if ($process->workerkey === $key || !$this->Configure->read('Queue.multiserver')) { 42 | echo '
    • ' . __d('queue', 'Kill') . ': ' . $this->Form->postLink(__d('queue', 'Soft kill'), ['action' => 'processes', '?' => ['kill' => $process->pid]], ['confirm' => 'Sure?']) . ' (termination SIGTERM = 15)
    • '; 43 | } 44 | 45 | echo '
    '; 46 | echo ''; 47 | } 48 | if (empty($processes)) { 49 | echo 'n/a'; 50 | } 51 | ?> 52 |
53 | 54 | 55 |

56 |

:

57 |
    58 | ' . $queuedJob->pid; 61 | echo ''; 62 | } 63 | ?> 64 |
65 | 66 | 67 |
68 | -------------------------------------------------------------------------------- /templates/Admin/QueueProcesses/edit.php: -------------------------------------------------------------------------------- 1 | 7 | 14 |
15 | 16 |

pid); ?>

17 | 18 | Form->create($queueProcess) ?> 19 |
20 | 21 | Form->control('server'); 23 | ?> 24 |
25 | Form->button(__d('queue', 'Submit')) ?> 26 | Form->end() ?> 27 |
28 | -------------------------------------------------------------------------------- /templates/Admin/QueueProcesses/index.php: -------------------------------------------------------------------------------- 1 | $queueProcesses 5 | */ 6 | use Queue\Queue\Config; 7 | ?> 8 | 16 |
17 |

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 44 | 53 | 54 | 55 | 62 | 63 | 64 | 65 |
Paginator->sort('pid') ?>Paginator->sort('created', __d('queue', 'Started'), ['direction' => 'desc']) ?>Paginator->sort('modified', __d('queue', 'Last Run'), ['direction' => 'desc']) ?>Paginator->sort('terminate', __d('queue', 'Active')) ?>Paginator->sort('server') ?>
33 | pid) ?> 34 | workerkey && $queueProcess->workerkey !== $queueProcess->pid) { ?> 35 |
workerkey); ?>
36 | 37 |
39 | Time->nice($queueProcess->created) ?> 40 | created->addSeconds(Config::workermaxruntime())->isFuture()) { 41 | echo $this->Icon->render('exclamation-triangle', [], ['title' => 'Long running (!)']); 42 | } ?> 43 | 45 | Time->nice($queueProcess->modified); 47 | if (!$queueProcess->created->addSeconds(Config::defaultworkertimeout())->isFuture()) { 48 | $modified = '' . $modified . ''; 49 | } 50 | echo $modified; 51 | ?> 52 | element('Queue.yes_no', ['value' => !$queueProcess->terminate]) ?>server) ?> 56 | Html->link($this->Icon->render('view'), ['action' => 'view', $queueProcess->id], ['escapeTitle' => false]); ?> 57 | terminate) { ?> 58 | Form->postLink($this->Icon->render('times', [], ['title' => __d('queue', 'Terminate')]), ['action' => 'terminate', $queueProcess->id], ['escapeTitle' => false, 'confirm' => __d('queue', 'Are you sure you want to terminate # {0}?', $queueProcess->id)]); ?> 59 | 60 | 61 |
66 | 67 | element('Tools.pagination'); ?> 68 |
69 | -------------------------------------------------------------------------------- /templates/Admin/QueueProcesses/view.php: -------------------------------------------------------------------------------- 1 | 8 | 22 |
23 |

PID pid) ?>

24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
28 | Time->nice($queueProcess->created) ?> 29 | created->addSeconds(Config::defaultworkertimeout())->isFuture()) { 30 | echo $this->Icon->render('exclamation-triangle', [], ['title' => 'Long running (!)']); 31 | } ?> 32 |
Time->nice($queueProcess->modified) ?>
41 | element('Queue.yes_no', ['value' => !$queueProcess->terminate]) ?> 42 | terminate ? 'Yes' : 'No' ?> 43 |
server) ?>
workerkey) ?>
54 | 55 |
56 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/data.php: -------------------------------------------------------------------------------- 1 | 7 | 14 |
15 |

16 | 17 | Form->create($queuedJob) ?> 18 |
19 | 20 | Form->control('data_string', ['rows' => 20]); 22 | ?> 23 |
24 | Form->button(__d('queue', 'Submit')) ?> 25 | Form->end() ?> 26 |
27 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/edit.php: -------------------------------------------------------------------------------- 1 | 7 | 21 |
22 |

23 | 24 | Form->create($queuedJob) ?> 25 |
26 | 27 | Form->control('notbefore', ['empty' => true]); 29 | echo $this->Form->control('priority'); 30 | ?> 31 |
32 | Form->button(__d('queue', 'Submit')) ?> 33 | Form->end() ?> 34 |
35 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/execute.php: -------------------------------------------------------------------------------- 1 | 6 | 13 |
14 |

15 | 16 | Form->create(null) ?> 17 |
18 | 19 | Form->control('command', ['placeholder' => 'bin/cake foo bar --baz']); 21 | echo $this->Form->control('escape', ['type' => 'checkbox', 'default' => true]); 22 | echo $this->Form->control('log', ['type' => 'checkbox', 'default' => true]); 23 | echo $this->Form->control('exit_code', ['placeholder' => 'Defaults to 0 (success)', 'default' => '0']); 24 | 25 | echo '

Escaping is recommended to keep on.

'; 26 | 27 | echo $this->Form->control('amount', ['default' => 1, 'label' => 'Amount of jobs to spawn']); 28 | ?> 29 |
30 | Form->button(__d('queue', 'Submit')) ?> 31 | Form->end() ?> 32 |
33 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/import.php: -------------------------------------------------------------------------------- 1 | 6 | 12 |
13 |

Import

14 | 15 | Form->create(null, ['type' => 'file']) ?> 16 |
17 | 18 | Form->control('file', ['type' => 'file', 'required' => true, 'accept' => '.json']); 20 | echo $this->Form->control('reset', ['type' => 'checkbox', 'default' => true]); 21 | ?> 22 |
23 | Form->button(__d('queue', 'Submit')) ?> 24 | Form->end() ?> 25 |
26 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/index.php: -------------------------------------------------------------------------------- 1 | $queuedJobs 5 | */ 6 | 7 | use Brick\VarExporter\VarExporter; 8 | use Cake\Core\Configure; 9 | use Cake\Core\Plugin; 10 | 11 | ?> 12 | 28 |
29 | 30 | element('Queue.search'); 33 | } 34 | ?> 35 | 36 |

37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 71 | 72 | 82 | 95 | 105 | 108 | 133 | 134 | 142 | 143 | 144 | 145 |
Paginator->sort('job_task') ?>Paginator->sort('job_group') ?>Paginator->sort('reference') ?>Paginator->sort('created', null, ['direction' => 'desc']) ?>Paginator->sort('notbefore', null, ['direction' => 'desc']) ?>Paginator->sort('fetched', null, ['direction' => 'desc']) ?>Paginator->sort('completed', null, ['direction' => 'desc']) ?>Paginator->sort('attempts') ?>Paginator->sort('status') ?>Paginator->sort('priority', null, ['direction' => 'desc']) ?>
job_task) ?>job_group) ?: '---' ?> 60 | reference) ?: '---' ?> 61 | data) { 62 | $data = $queuedJob->data; 63 | if ($data && !is_array($data)) { 64 | $data = json_decode($queuedJob->data, true); 65 | } 66 | $data = VarExporter::export($data, VarExporter::TRAILING_COMMA_IN_ARRAY); 67 | echo $this->Icon->render('cubes', [], ['title' => $this->Text->truncate($data, 1000)]); 68 | } 69 | ?> 70 | Time->nice($queuedJob->created) ?> 73 | Time->nice($queuedJob->notbefore) ?> 74 |
75 | QueueProgress->timeoutProgressBar($queuedJob, 8); ?> 76 | notbefore && $queuedJob->notbefore->isFuture()) { 77 | echo '
'; 78 | echo $this->Time->relLengthOfTime($queuedJob->notbefore); 79 | echo '
'; 80 | } ?> 81 |
83 | Time->nice($queuedJob->fetched) ?> 84 | 85 | fetched) { 86 | echo '
'; 87 | echo $this->Time->relLengthOfTime($queuedJob->fetched); 88 | echo '
'; 89 | } ?> 90 | 91 | workerkey) { ?> 92 |
workerkey); ?>
93 | 94 |
96 | Format->ok($this->Time->nice($queuedJob->completed), (bool)$queuedJob->completed) ?> 97 | completed) { ?> 98 |
99 | ' . $this->Time->duration($queuedJob->completed->diff($queuedJob->fetched)) . ''; 101 | ?> 102 |
103 | 104 |
106 | element('Queue.ok', ['value' => $this->Queue->attempts($queuedJob), 'ok' => $queuedJob->completed || $queuedJob->attempts < 1]); ?> 107 | 109 | status) ?> 110 | completed && $queuedJob->fetched) { ?> 111 |
112 | failure_message) { ?> 113 | QueueProgress->progress($queuedJob) ?> 114 |
115 | QueueProgress->progressBar($queuedJob, 8); 117 | echo $this->QueueProgress->htmlProgressBar($queuedJob, $textProgressBar); 118 | ?> 119 | 120 | Queue->failureStatus($queuedJob); ?> 121 | 122 |
123 | 124 | 125 | completed) { ?> 126 | 127 | 128 | 129 | memory) { ?> 130 |
Number->format($queuedJob->memory); ?> MB
131 | 132 |
Number->format($queuedJob->priority) ?> 135 | Html->link($this->Icon->render('view'), ['action' => 'view', $queuedJob->id], ['escapeTitle' => false]); ?> 136 | 137 | completed) { ?> 138 | Html->link($this->Icon->render('edit'), ['action' => 'edit', $queuedJob->id], ['escapeTitle' => false]); ?> 139 | 140 | Form->postLink($this->Icon->render('delete'), ['action' => 'delete', $queuedJob->id], ['escapeTitle' => false, 'confirm' => __d('queue', 'Are you sure you want to delete # {0}?', $queuedJob->id)]); ?> 141 |
146 | 147 | element('Tools.pagination'); ?> 148 |
149 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/migrate.php: -------------------------------------------------------------------------------- 1 | 7 | 13 |
14 |

15 | 16 | Form->create() ?> 17 |
18 | 19 | $fullName) { 21 | echo $this->Form->control('tasks.' . $name, ['type' => 'checkbox', 'label' => $name . ' => ' . $fullName, 'default' => true]); 22 | } 23 | ?> 24 |
25 | Form->button(__d('queue', 'Submit')) ?> 26 | Form->end() ?> 27 |
28 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/stats.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 |
21 |

22 | 23 |
24 |
25 | 26 |

27 | 28 |

For already processed jobs - in average seconds per timeframe.

29 | 30 | 31 | 32 | 33 |

Select a specific job type

34 |
    35 | 36 |
  • Html->link($jobType, ['action' => 'stats', $jobType]); ?>
  • 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 | $days) { 47 | $labels = array_keys($days); 48 | break; 49 | } 50 | 51 | $dataSets = []; 52 | foreach ($stats as $type => $days) { 53 | $data = implode(', ', $days); 54 | 55 | $dataSets[] = << 71 | 72 | append('script');?> 73 | 76 | 92 | end();?> 93 | -------------------------------------------------------------------------------- /templates/Admin/QueuedJobs/test.php: -------------------------------------------------------------------------------- 1 | 8 | 14 |
15 |

16 | 17 | Form->create($queuedJob) ?> 18 |
19 | 20 | Form->control('job_task', ['options' => $tasks, 'empty' => true]); 22 | 23 | echo '

Current (server) time: ' . (new \Cake\I18n\DateTime()) . ''; 24 | 25 | echo $this->Form->control('notbefore', ['default' => (new \Cake\I18n\DateTime())->addMinutes(5)]); 26 | 27 | echo '

The target time must also be in that (server) time(zone).

'; 28 | ?> 29 |
30 | Form->button(__d('queue', 'Submit')) ?> 31 | Form->end() ?> 32 |
33 | -------------------------------------------------------------------------------- /templates/bake/Task/task.twig: -------------------------------------------------------------------------------- 1 | $data The array passed to QueuedJobsTable::createJob() 14 | * @param int $jobId The id of the QueuedJob entity 15 | * @return void 16 | */ 17 | public function run(array $data, int $jobId): void { 18 | } 19 | {% if add %} 20 | 21 | /** 22 | * @param string|null $data Optional data for the task, make sure to "quote multi words" 23 | * 24 | * @return void 25 | */ 26 | public function add(?string $data): void { 27 | $this->QueuedJobs->createJob('{{ subNamespace }}{{ name }}'); 28 | } 29 | {% endif %} 30 | 31 | } -------------------------------------------------------------------------------- /templates/element/ok.php: -------------------------------------------------------------------------------- 1 | 10 | helpers()->has('Templating')) { 12 | echo $this->Templating->ok($value, $ok); 13 | } elseif ($this->helpers()->has('Format')) { 14 | echo $this->Format->ok($value, $ok); 15 | } else { 16 | echo $ok ? '' . h($value) . '' : '' . h($value) . ''; 17 | } 18 | ?> 19 | -------------------------------------------------------------------------------- /templates/element/search.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | Form->create(null, ['valueSources' => 'query']); 10 | echo $this->Form->control('search', ['placeholder' => 'Auto-wildcard mode', 'label' => 'Search (Jobgroup/Reference)']); 11 | echo $this->Form->control('job_task', ['empty' => ' - no filter - ']); 12 | echo $this->Form->control('status', ['options' => \Queue\Model\Entity\QueuedJob::statusesForSearch(), 'empty' => ' - no filter - ']); 13 | echo $this->Form->button('Filter', ['type' => 'submit']); 14 | if (!empty($_isSearch)) { 15 | echo $this->Html->link('Reset', ['action' => 'index'], ['class' => 'button']); 16 | } 17 | echo $this->Form->end(); 18 | ?> 19 |
20 | -------------------------------------------------------------------------------- /templates/element/yes_no.php: -------------------------------------------------------------------------------- 1 | 9 | helpers()->has('IconSnippet')) { 11 | echo $this->IconSnippet->yesNo($value); 12 | } elseif ($this->helpers()->has('Format')) { 13 | echo $this->Format->yesNo($value); 14 | } else { 15 | echo $value ? 'Yes' : 'No'; 16 | } 17 | ?> 18 | -------------------------------------------------------------------------------- /tests/Fixture/QueueProcessesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 20 | 'pid' => ['type' => 'string', 'length' => 40, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 21 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], 22 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], 23 | 'terminate' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => '0', 'comment' => '', 'precision' => null], 24 | 'server' => ['type' => 'string', 'length' => 90, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 25 | 'workerkey' => ['type' => 'string', 'length' => 45, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 26 | '_constraints' => [ 27 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 28 | 'workerkey' => ['type' => 'unique', 'columns' => ['workerkey'], 'length' => []], 29 | 'pid' => ['type' => 'unique', 'columns' => ['pid', 'server'], 'length' => []], 30 | ], 31 | '_options' => [ 32 | 'engine' => 'InnoDB', 33 | ], 34 | ]; 35 | // @codingStandardsIgnoreEnd 36 | 37 | /** 38 | * Init method 39 | * 40 | * @return void 41 | */ 42 | public function init(): void { 43 | $this->records = [ 44 | [ 45 | 'pid' => '0', 46 | 'created' => '2019-01-04 17:27:40', 47 | 'modified' => '2019-01-04 17:27:40', 48 | 'terminate' => 1, 49 | 'workerkey' => 'key', 50 | ], 51 | ]; 52 | parent::init(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/Fixture/QueuedJobsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 22 | 'job_task' => ['type' => 'string', 'length' => 90, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 23 | 'data' => ['type' => 'text', 'length' => 16777215, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 24 | 'job_group' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 25 | 'reference' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 26 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], 27 | 'notbefore' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 28 | 'fetched' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 29 | 'completed' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 30 | 'progress' => ['type' => 'float', 'length' => null, 'precision' => null, 'unsigned' => false, 'null' => true, 'default' => null, 'comment' => ''], 31 | 'attempts' => ['type' => 'integer', 'length' => 12, 'unsigned' => true, 'null' => false, 'default' => 0, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 32 | 'failure_message' => ['type' => 'text', 'length' => 16777215, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 33 | 'workerkey' => ['type' => 'string', 'length' => 45, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 34 | 'status' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 35 | 'priority' => ['type' => 'integer', 'length' => 3, 'unsigned' => true, 'null' => false, 'default' => 5, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 36 | 'memory' => ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => true, 'default' => null, 'comment' => 'MB'], 37 | '_constraints' => [ 38 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 39 | ], 40 | '_options' => [ 41 | 'engine' => 'InnoDB', 42 | ], 43 | ]; 44 | // @codingStandardsIgnoreEnd 45 | 46 | /** 47 | * Init method 48 | * 49 | * @return void 50 | */ 51 | public function init(): void { 52 | $this->records = [ 53 | ]; 54 | parent::init(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/TestCase/Command/AddCommandTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | protected array $fixtures = [ 20 | 'plugin.Queue.QueuedJobs', 21 | ]; 22 | 23 | /** 24 | * @return void 25 | */ 26 | public function setUp(): void { 27 | parent::setUp(); 28 | 29 | //$this->useCommandRunner(); 30 | $this->loadPlugins(['Queue']); 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | public function testExecute(): void { 37 | $this->exec('queue add'); 38 | 39 | $output = $this->_out->output(); 40 | $this->assertStringContainsString('11 tasks available:', $output); 41 | $this->assertExitCode(0); 42 | } 43 | 44 | /** 45 | * @return void 46 | */ 47 | public function testExecuteAddExample(): void { 48 | $this->exec('queue add Queue.Example'); 49 | 50 | $output = $this->_out->output(); 51 | $this->assertStringContainsString('OK, job created, now run the worker', $output); 52 | $this->assertExitCode(0); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/TestCase/Command/BakeQueueTaskCommandTest.php: -------------------------------------------------------------------------------- 1 | loadPlugins(['Queue']); 35 | 36 | $this->removeFiles(); 37 | } 38 | 39 | /** 40 | * @return void 41 | */ 42 | public function tearDown(): void { 43 | parent::tearDown(); 44 | 45 | $this->removeFiles(); 46 | } 47 | 48 | /** 49 | * Test execute method 50 | * 51 | * @return void 52 | */ 53 | public function testExecute(): void { 54 | $this->exec('bake queue_task FooBarBaz -a -f'); 55 | 56 | $output = $this->_out->output(); 57 | $this->assertStringContainsString('Creating file', $output); 58 | $this->assertStringContainsString('Wrote', $output); 59 | 60 | $file = $this->filePath . 'FooBarBazTask.php'; 61 | $expected = TESTS . 'test_files' . DS . 'bake' . DS . 'task.php'; 62 | $this->assertFileEquals($expected, $file); 63 | 64 | $file = $this->testFilePath . 'FooBarBazTaskTest.php'; 65 | $expected = TESTS . 'test_files' . DS . 'bake' . DS . 'task_test.php'; 66 | $this->assertFileEquals($expected, $file); 67 | } 68 | 69 | /** 70 | * @return void 71 | */ 72 | public function testExecuteWithSubFolder(): void { 73 | $this->exec('bake queue_task Sub/FooBarBaz -a -f'); 74 | 75 | $output = $this->_out->output(); 76 | $this->assertStringContainsString('Creating file', $output); 77 | $this->assertStringContainsString('Wrote', $output); 78 | 79 | $file = $this->filePath . 'Sub' . DS . 'FooBarBazTask.php'; 80 | $expected = TESTS . 'test_files' . DS . 'bake' . DS . 'Sub' . DS . 'task.php'; 81 | $this->assertFileEquals($expected, $file); 82 | 83 | $file = $this->testFilePath . 'Sub' . DS . 'FooBarBazTaskTest.php'; 84 | $expected = TESTS . 'test_files' . DS . 'bake' . DS . 'Sub' . DS . 'task_test.php'; 85 | $this->assertFileEquals($expected, $file); 86 | } 87 | 88 | /** 89 | * @return void 90 | */ 91 | protected function removeFiles(): void { 92 | if ($this->isDebug()) { 93 | return; 94 | } 95 | 96 | $file = $this->filePath . 'FooBarBazTask.php'; 97 | if (file_exists($file)) { 98 | unlink($file); 99 | } 100 | $file = $this->filePath . 'Sub' . DS . 'FooBarBazTask.php'; 101 | if (file_exists($file)) { 102 | unlink($file); 103 | } 104 | 105 | $testFile = $this->testFilePath . 'FooBarBazTaskTest.php'; 106 | if (file_exists($testFile)) { 107 | unlink($testFile); 108 | } 109 | $testFile = $this->testFilePath . 'Sub' . DS . 'FooBarBazTaskTest.php'; 110 | if (file_exists($testFile)) { 111 | unlink($testFile); 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /tests/TestCase/Command/InfoCommandTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | protected array $fixtures = [ 20 | 'plugin.Queue.QueueProcesses', 21 | 'plugin.Queue.QueuedJobs', 22 | ]; 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function setUp(): void { 28 | parent::setUp(); 29 | 30 | $this->loadPlugins(['Queue']); 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | public function testExecute(): void { 37 | $this->exec('queue info'); 38 | 39 | $output = $this->_out->output(); 40 | $this->assertStringContainsString('15 tasks available:', $output); 41 | $this->assertExitCode(0); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/TestCase/Command/JobCommandTest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected array $fixtures = [ 23 | 'plugin.Queue.QueuedJobs', 24 | ]; 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function setUp(): void { 30 | parent::setUp(); 31 | 32 | $this->loadPlugins(['Queue']); 33 | 34 | Configure::write('Queue.cleanuptimeout', 10); 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | public function tearDown(): void { 41 | parent::tearDown(); 42 | 43 | Configure::delete('Queue.cleanuptimeout'); 44 | } 45 | 46 | /** 47 | * @return void 48 | */ 49 | public function testExecute(): void { 50 | $this->exec('queue job'); 51 | 52 | $output = $this->_out->output(); 53 | $this->assertStringContainsString('Please use with [action] [ID] added', $output); 54 | $this->assertExitCode(1); 55 | } 56 | 57 | /** 58 | * @return void 59 | */ 60 | public function testExecuteView(): void { 61 | $job = $this->createJob(); 62 | $this->exec('queue job view ' . $job->id); 63 | 64 | $output = $this->_out->output(); 65 | $this->assertStringContainsString('Task: Example', $output); 66 | } 67 | 68 | /** 69 | * @return void 70 | */ 71 | public function testExecuteRemove(): void { 72 | $job = $this->createJob(); 73 | $this->exec('queue job remove ' . $job->id); 74 | 75 | $output = $this->_out->output(); 76 | $this->assertStringContainsString('removed', $output); 77 | } 78 | 79 | /** 80 | * @return void 81 | */ 82 | public function testExecuteRemoveAll(): void { 83 | $job = $this->createJob(); 84 | $this->exec('queue job remove all'); 85 | 86 | $output = $this->_out->output(); 87 | $this->assertStringContainsString('removed', $output); 88 | } 89 | 90 | /** 91 | * @return void 92 | */ 93 | public function testExecuteRerun(): void { 94 | $job = $this->createJob(['completed' => new DateTime()]); 95 | $this->exec('queue job rerun ' . $job->id); 96 | 97 | $output = $this->_out->output(); 98 | $this->assertStringContainsString('queued for rerun', $output); 99 | } 100 | 101 | /** 102 | * @return void 103 | */ 104 | public function testExecuteRerunAll(): void { 105 | $this->createJob(['completed' => new DateTime()]); 106 | $this->exec('queue job rerun all'); 107 | 108 | $output = $this->_out->output(); 109 | $this->assertStringContainsString('queued for rerun', $output); 110 | $this->assertExitCode(0); 111 | } 112 | 113 | /** 114 | * @return void 115 | */ 116 | public function testExecuteClean(): void { 117 | $this->exec('queue job clean'); 118 | 119 | $output = $this->_out->output(); 120 | $this->assertStringContainsString('Deleted: ', $output); 121 | } 122 | 123 | /** 124 | * @return void 125 | */ 126 | public function testExecuteFlush(): void { 127 | $this->exec('queue job flush'); 128 | 129 | $output = $this->_out->output(); 130 | $this->assertStringContainsString('Deleted: ', $output); 131 | } 132 | 133 | /** 134 | * @param array $data 135 | * 136 | * @return \Queue\Model\Entity\QueuedJob 137 | */ 138 | protected function createJob(array $data = []): QueuedJob { 139 | $data += [ 140 | 'job_task' => 'Example', 141 | ]; 142 | /** @var \Queue\Model\Entity\QueuedJob $job */ 143 | $job = $this->getTableLocator()->get('Queue.QueuedJobs')->newEntity($data); 144 | $this->getTableLocator()->get('Queue.QueuedJobs')->saveOrFail($job); 145 | 146 | return $job; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /tests/TestCase/Command/RunCommandTest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $fixtures = [ 22 | 'plugin.Queue.QueueProcesses', 23 | 'plugin.Queue.QueuedJobs', 24 | ]; 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function setUp(): void { 30 | parent::setUp(); 31 | 32 | $this->loadPlugins(['Queue']); 33 | 34 | Configure::write('Queue', [ 35 | 'sleeptime' => 1, 36 | 'defaultworkertimeout' => 3, 37 | 'workermaxruntime' => 3, 38 | 'cleanuptimeout' => 10, 39 | 'exitwhennothingtodo' => false, 40 | ]); 41 | } 42 | 43 | /** 44 | * @return void 45 | */ 46 | public function testExecute(): void { 47 | $this->_needsConnection(); 48 | 49 | $this->exec('queue run'); 50 | 51 | $output = $this->_out->output(); 52 | $this->assertStringContainsString('Looking for Job', $output); 53 | $this->assertExitCode(0); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function testServiceInjection(): void { 60 | $this->_needsConnection(); 61 | 62 | $this->exec('queue add Foo'); 63 | $this->exec('queue run'); 64 | 65 | $output = $this->_out->output(); 66 | $this->assertStringContainsString('Looking for Job', $output); 67 | $this->assertStringContainsString('CakePHP Foo Example.', $output); 68 | $this->assertStringContainsString('My TestService', $output); 69 | $this->assertExitCode(0); 70 | } 71 | 72 | /** 73 | * Helper method for skipping tests that need a real connection. 74 | * 75 | * @return void 76 | */ 77 | protected function _needsConnection() { 78 | $config = ConnectionManager::getConfig('test'); 79 | $skip = strpos($config['driver'], 'Mysql') === false && strpos($config['driver'], 'Postgres') === false; 80 | $this->skipIf($skip, 'Only Mysql/Postgres is working yet for this.'); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /tests/TestCase/Command/WorkerCommandTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | protected array $fixtures = [ 20 | 'plugin.Queue.QueueProcesses', 21 | ]; 22 | 23 | /** 24 | * @return void 25 | */ 26 | public function setUp(): void { 27 | parent::setUp(); 28 | 29 | $this->loadPlugins(['Queue']); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function testExecute(): void { 36 | $this->exec('queue worker'); 37 | 38 | $output = $this->_out->output(); 39 | $this->assertStringContainsString('Please use with [action] [PID] added', $output); 40 | $this->assertExitCode(1); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /tests/TestCase/Config/JobConfigTest.php: -------------------------------------------------------------------------------- 1 | setPriority(1); 17 | $jobConfig->setGroup('mygroup'); 18 | $jobConfig->setReferenceOrFail('reference'); 19 | $jobConfig->setNotBefore('+1 hour'); 20 | 21 | $array = $jobConfig->toArray(); 22 | 23 | $expected = [ 24 | 'priority' => 1, 25 | 'notbefore' => '+1 hour', 26 | 'job_group' => 'mygroup', 27 | 'reference' => 'reference', 28 | 'status' => null, 29 | ]; 30 | $this->assertSame($expected, $array); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Admin/QueueProcessesControllerTest.php: -------------------------------------------------------------------------------- 1 | loadPlugins(['Queue']); 33 | 34 | $this->disableErrorHandlerMiddleware(); 35 | } 36 | 37 | /** 38 | * Test index method 39 | * 40 | * @return void 41 | */ 42 | public function testIndex() { 43 | $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueueProcesses', 'action' => 'index']); 44 | 45 | $this->assertResponseCode(200); 46 | } 47 | 48 | /** 49 | * Test view method 50 | * 51 | * @return void 52 | */ 53 | public function testView() { 54 | $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueueProcesses', 'action' => 'view', 1]); 55 | 56 | $this->assertResponseCode(200); 57 | } 58 | 59 | /** 60 | * Test edit method 61 | * 62 | * @return void 63 | */ 64 | public function testEdit() { 65 | $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueueProcesses', 'action' => 'edit', 1]); 66 | 67 | $this->assertResponseCode(200); 68 | } 69 | 70 | /** 71 | * @return void 72 | */ 73 | public function testTerminate() { 74 | /** @var \Queue\Model\Entity\QueueProcess $queueProcess */ 75 | $queueProcess = $this->getTableLocator()->get('Queue.QueueProcesses')->find()->firstOrFail(); 76 | $queueProcess->terminate = false; 77 | $this->getTableLocator()->get('Queue.QueueProcesses')->saveOrFail($queueProcess); 78 | 79 | $this->post(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueueProcesses', 'action' => 'terminate', 1]); 80 | 81 | $this->assertResponseCode(302); 82 | 83 | $queueProcess = $this->getTableLocator()->get('Queue.QueueProcesses')->find()->firstOrFail(); 84 | $this->assertTrue($queueProcess->terminate); 85 | } 86 | 87 | /** 88 | * @return void 89 | */ 90 | public function testDelete() { 91 | $this->post(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueueProcesses', 'action' => 'delete', 1]); 92 | 93 | $this->assertResponseCode(302); 94 | 95 | $count = $this->getTableLocator()->get('Queue.QueueProcesses')->find()->count(); 96 | $this->assertSame(0, $count); 97 | } 98 | 99 | /** 100 | * @return void 101 | */ 102 | public function testCleanup() { 103 | /** @var \Queue\Model\Entity\QueueProcess $queueProcess */ 104 | $queueProcess = $this->getTableLocator()->get('Queue.QueueProcesses')->find()->firstOrFail(); 105 | $queueProcess->modified = new DateTime(time() - 4 * QueuedJobsTable::DAY); 106 | $this->getTableLocator()->get('Queue.QueueProcesses')->saveOrFail($queueProcess); 107 | 108 | $this->post(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueueProcesses', 'action' => 'cleanup']); 109 | 110 | $this->assertResponseCode(302); 111 | 112 | $count = $this->getTableLocator()->get('Queue.QueueProcesses')->find()->count(); 113 | $this->assertSame(0, $count); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /tests/TestCase/Generator/Task/QueuedJobGeneratorTest.php: -------------------------------------------------------------------------------- 1 | task = new QueuedJobTask(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testCollect() { 28 | $result = $this->task->collect(); 29 | 30 | $this->assertCount(2, $result); 31 | 32 | /** @var \IdeHelper\Generator\Directive\ExpectedArguments $directive */ 33 | $directive = array_shift($result); 34 | $this->assertSame('\Queue\Model\Table\QueuedJobsTable::createJob()', $directive->toArray()['method']); 35 | 36 | $list = $directive->toArray()['list']; 37 | $expected = [ 38 | 'Queue.Execute' => "'Queue.Execute'", 39 | 'Queue.ProgressExample' => "'Queue.ProgressExample'", 40 | ]; 41 | foreach ($expected as $name => $value) { 42 | $this->assertSame($value, $list[$name]); 43 | } 44 | 45 | /** @var \IdeHelper\Generator\Directive\ExpectedArguments $directive */ 46 | $directive = array_shift($result); 47 | $this->assertSame('\Queue\Model\Table\QueuedJobsTable::isQueued()', $directive->toArray()['method']); 48 | 49 | $list = $directive->toArray()['list']; 50 | $this->assertNotEmpty($list); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/TestCase/Mailer/Transport/QueueTransportTest.php: -------------------------------------------------------------------------------- 1 | QueueTransport = new QueueTransport(); 35 | } 36 | 37 | /** 38 | * TestSend method 39 | * 40 | * @return void 41 | */ 42 | public function testSendWithEmail() { 43 | $message = new Message(); 44 | $message->setFrom('noreply@cakephp.org', 'CakePHP Test'); 45 | $message->setTo('cake@cakephp.org', 'CakePHP'); 46 | $message->setCc(['mark@cakephp.org' => 'Mark Story', 'juan@cakephp.org' => 'Juan Basso']); 47 | $message->setBcc('phpnut@cakephp.org'); 48 | $message->setSubject('Testing Message'); 49 | 50 | $result = $this->QueueTransport->send($message); 51 | $this->assertSame('Queue.Email', $result['job_task']); 52 | $this->assertNotEmpty($result['data']); 53 | 54 | $output = $result['data']; 55 | $this->assertInstanceOf(Message::class, $output['settings']); 56 | 57 | /** @var \Cake\Mailer\Message $message */ 58 | $message = $output['settings']; 59 | $this->assertSame('Testing Message', $message->getSubject()); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /tests/TestCase/Mailer/Transport/SimpleQueueTransportTest.php: -------------------------------------------------------------------------------- 1 | QueueTransport = new SimpleQueueTransport(); 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | public function testSendWithEmail() { 41 | $config = [ 42 | 'transport' => 'queue', 43 | 'charset' => 'utf-8', 44 | 'headerCharset' => 'utf-8', 45 | ]; 46 | 47 | $this->QueueTransport->setConfig($config); 48 | $mailer = new Mailer($config); 49 | 50 | $mailer->setFrom('noreply@cakephp.org', 'CakePHP Test'); 51 | $mailer->setTo('cake@cakephp.org', 'CakePHP'); 52 | $mailer->setCc(['mark@cakephp.org' => 'Mark Story', 'juan@cakephp.org' => 'Juan Basso']); 53 | $mailer->setBcc('phpnut@cakephp.org'); 54 | $mailer->setSubject('Testing Message'); 55 | $mailer->setAttachments([ 56 | 'wow.txt' => [ 57 | 'data' => 'much wow!', 58 | 'mimetype' => 'text/plain', 59 | 'contentId' => 'important', 60 | ], 61 | ]); 62 | 63 | $mailer->render('Foo Bar Content'); 64 | $mailer->setSubject("L'utilisateur n'a pas pu être enregistré"); 65 | $mailer->setReplyTo('noreply@cakephp.org'); 66 | $mailer->setReadReceipt('noreply2@cakephp.org'); 67 | $mailer->setReturnPath('noreply3@cakephp.org'); 68 | $mailer->setDomain('cakephp.org'); 69 | $mailer->setEmailFormat('both'); 70 | 71 | $result = $this->QueueTransport->send($mailer->getMessage()); 72 | $this->assertSame('Queue.Email', $result['job_task']); 73 | $this->assertNotEmpty($result['data']); 74 | 75 | $output = $result['data']; 76 | 77 | $settings = $output['settings']; 78 | $this->assertSame([['noreply@cakephp.org' => 'CakePHP Test']], $settings['from']); 79 | $this->assertSame(['L\'utilisateur n\'a pas pu être enregistré'], $settings['subject']); 80 | $this->assertSame(['queue'], $settings['transport']); 81 | $this->assertNotEmpty($settings['attachments']); 82 | 83 | $this->assertNotEmpty($result['headers']); 84 | $this->assertTextContains('Foo Bar Content', $result['message']); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/QueueProcessesTableTest.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected array $fixtures = [ 28 | 'plugin.Queue.QueueProcesses', 29 | 'plugin.Queue.QueuedJobs', 30 | ]; 31 | 32 | /** 33 | * setUp method 34 | * 35 | * @return void 36 | */ 37 | public function setUp(): void { 38 | parent::setUp(); 39 | $config = TableRegistry::getTableLocator()->exists('QueueProcesses') ? [] : ['className' => QueueProcessesTable::class]; 40 | $this->QueueProcesses = $this->getTableLocator()->get('QueueProcesses', $config); 41 | 42 | Configure::delete('Queue.maxworkers'); 43 | } 44 | 45 | /** 46 | * tearDown method 47 | * 48 | * @return void 49 | */ 50 | public function tearDown(): void { 51 | unset($this->QueueProcesses); 52 | 53 | parent::tearDown(); 54 | 55 | Configure::delete('Queue.maxworkers'); 56 | } 57 | 58 | /** 59 | * @return void 60 | */ 61 | public function testAdd() { 62 | $pid = '123'; 63 | $id = $this->QueueProcesses->add($pid, '456'); 64 | $this->assertNotEmpty($id); 65 | 66 | $queueProcess = $this->QueueProcesses->get($id); 67 | $this->assertSame($pid, $queueProcess->pid); 68 | 69 | $this->assertFalse($queueProcess->terminate); 70 | $this->assertNotEmpty($queueProcess->server); 71 | $this->assertNotEmpty($queueProcess->workerkey); 72 | } 73 | 74 | /** 75 | * @return void 76 | */ 77 | public function testAddMaxCount() { 78 | Configure::write('Queue.maxworkers', 2); 79 | 80 | $pid = '123'; 81 | $id = $this->QueueProcesses->add($pid, '123123'); 82 | $this->assertNotEmpty($id); 83 | 84 | $pid = '234'; 85 | $id = $this->QueueProcesses->add($pid, '234234'); 86 | $this->assertNotEmpty($id); 87 | 88 | $this->expectException(PersistenceFailedException::class); 89 | $pid = '345'; 90 | $this->QueueProcesses->add($pid, '345345'); 91 | } 92 | 93 | /** 94 | * @return void 95 | */ 96 | public function testUpdate() { 97 | $pid = '123'; 98 | $id = $this->QueueProcesses->add($pid, '456'); 99 | $this->assertNotEmpty($id); 100 | 101 | $this->QueueProcesses->update($pid); 102 | 103 | $queueProcess = $this->QueueProcesses->get($id); 104 | $this->assertFalse($queueProcess->terminate); 105 | } 106 | 107 | /** 108 | * @return void 109 | */ 110 | public function testRemove() { 111 | $pid = '123'; 112 | $queueProcessId = $this->QueueProcesses->add($pid, '456'); 113 | $this->assertNotEmpty($queueProcessId); 114 | 115 | $this->QueueProcesses->remove($pid); 116 | 117 | $result = $this->QueueProcesses->find()->where(['id' => $queueProcessId])->first(); 118 | $this->assertNull($result); 119 | } 120 | 121 | /** 122 | * @return void 123 | */ 124 | public function testWakeUpWorkersSendsSigUsr1() { 125 | /** @var \Queue\Model\Table\QueueProcessesTable $queuedProcessesTable */ 126 | $queuedProcessesTable = $this->getTableLocator()->get('Queue.QueueProcesses'); 127 | $queuedProcess = $queuedProcessesTable->newEntity([ 128 | 'pid' => (string)getmypid(), 129 | 'workerkey' => $queuedProcessesTable->buildServerString(), 130 | 'server' => $queuedProcessesTable->buildServerString(), 131 | ]); 132 | $queuedProcessesTable->saveOrFail($queuedProcess); 133 | 134 | $gotSignal = false; 135 | pcntl_signal(SIGUSR1, function () use (&$gotSignal) { 136 | $gotSignal = true; 137 | }); 138 | 139 | $queuedProcessesTable->wakeUpWorkers(); 140 | pcntl_signal_dispatch(); 141 | 142 | $this->assertTrue($gotSignal); 143 | pcntl_signal(SIGUSR1, SIG_DFL); 144 | } 145 | 146 | /** 147 | * @return void 148 | */ 149 | public function testEndProcess() { 150 | /** @var \Queue\Model\Table\QueueProcessesTable $queuedProcessesTable */ 151 | $queuedProcessesTable = $this->getTableLocator()->get('Queue.QueueProcesses'); 152 | 153 | $queuedProcess = $queuedProcessesTable->newEntity([ 154 | 'pid' => '1', 155 | 'workerkey' => '123', 156 | ]); 157 | $queuedProcessesTable->saveOrFail($queuedProcess); 158 | 159 | $queuedProcessesTable->endProcess('1'); 160 | 161 | $queuedProcess = $queuedProcessesTable->get($queuedProcess->id); 162 | $this->assertTrue($queuedProcess->terminate); 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/ProcessorTest.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected array $fixtures = [ 25 | 'plugin.Queue.QueueProcesses', 26 | 'plugin.Queue.QueuedJobs', 27 | ]; 28 | 29 | /** 30 | * @var \Queue\Queue\Processor 31 | */ 32 | protected $Processor; 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function setUp(): void { 38 | parent::setUp(); 39 | 40 | Configure::write('Queue', [ 41 | 'sleeptime' => 1, 42 | 'defaultworkertimeout' => 3, 43 | 'workermaxruntime' => 3, 44 | 'cleanuptimeout' => 10, 45 | 'exitwhennothingtodo' => false, 46 | ]); 47 | } 48 | 49 | /** 50 | * @return void 51 | */ 52 | public function testStringToArray() { 53 | $this->Processor = new Processor(new Io(new ConsoleIo()), new NullLogger()); 54 | 55 | $string = 'Foo,Bar,'; 56 | $result = $this->invokeMethod($this->Processor, 'stringToArray', [$string]); 57 | 58 | $expected = [ 59 | 'Foo', 60 | 'Bar', 61 | ]; 62 | $this->assertSame($expected, $result); 63 | } 64 | 65 | /** 66 | * @return void 67 | */ 68 | public function testTimeNeeded() { 69 | $this->Processor = new Processor(new Io(new ConsoleIo()), new NullLogger()); 70 | 71 | $result = $this->invokeMethod($this->Processor, 'timeNeeded'); 72 | $this->assertMatchesRegularExpression('/\d+s/', $result); 73 | } 74 | 75 | /** 76 | * @return void 77 | */ 78 | public function testMemoryUsage() { 79 | $this->Processor = new Processor(new Io(new ConsoleIo()), new NullLogger()); 80 | 81 | $result = $this->invokeMethod($this->Processor, 'memoryUsage'); 82 | $this->assertMatchesRegularExpression('/^\d+MB/', $result, 'Should be e.g. `17MB` or `17MB/1GB` etc.'); 83 | } 84 | 85 | /** 86 | * @return void 87 | */ 88 | public function testRun() { 89 | $this->_needsConnection(); 90 | 91 | $out = new ConsoleOutput(); 92 | $err = new ConsoleOutput(); 93 | $this->Processor = new Processor(new Io(new ConsoleIo($out, $err)), new NullLogger()); 94 | 95 | $config = [ 96 | 'verbose' => true, 97 | ]; 98 | $result = $this->Processor->run($config); 99 | 100 | $this->assertSame(CommandInterface::CODE_SUCCESS, $result); 101 | } 102 | 103 | /** 104 | * Helper method for skipping tests that need a real connection. 105 | * 106 | * @return void 107 | */ 108 | protected function _needsConnection() { 109 | $config = ConnectionManager::getConfig('test'); 110 | $skip = strpos($config['driver'], 'Mysql') === false && strpos($config['driver'], 'Postgres') === false; 111 | $this->skipIf($skip, 'Only Mysql/Postgres is working yet for this.'); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/CostsExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 48 | $this->err = new ConsoleOutput(); 49 | $io = new Io(new ConsoleIo($this->out, $this->err)); 50 | 51 | $this->Task = new CostsExampleTask($io); 52 | } 53 | 54 | /** 55 | * @return void 56 | */ 57 | public function testRun() { 58 | $this->Task->setSleep(0); 59 | $this->Task->run([], 0); 60 | 61 | $this->assertTextContains('Success, the CostsExample Job was run', $this->out->output()); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/ExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 48 | $this->err = new ConsoleOutput(); 49 | $io = new Io(new ConsoleIo($this->out, $this->err)); 50 | 51 | $this->Task = new ExampleTask($io); 52 | } 53 | 54 | /** 55 | * @return void 56 | */ 57 | public function testRun() { 58 | $this->Task->run([], 0); 59 | 60 | $this->assertTextContains('Success, the Example Job was run', $this->out->output()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/ExceptionExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 49 | $this->err = new ConsoleOutput(); 50 | $io = new Io(new ConsoleIo($this->out, $this->err)); 51 | 52 | $this->Task = new ExceptionExampleTask($io); 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | public function testAdd() { 59 | $before = $this->getTableLocator()->get('Queue.QueuedJobs')->find()->count(); 60 | 61 | $this->Task->add(null); 62 | 63 | $after = $this->getTableLocator()->get('Queue.QueuedJobs')->find()->count(); 64 | $this->assertSame($before + 1, $after); 65 | } 66 | 67 | /** 68 | * @return void 69 | */ 70 | public function testRun() { 71 | $this->expectException(RuntimeException::class); 72 | 73 | $this->Task->run([], 0); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/ExecuteTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 50 | $this->err = new ConsoleOutput(); 51 | $io = new Io(new ConsoleIo($this->out, $this->err)); 52 | 53 | $this->Task = new ExecuteTask($io); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function testRun() { 60 | $this->Task->run(['command' => 'php -v'], 0); 61 | 62 | $this->assertTextContains('PHP ', $this->out->output()); 63 | } 64 | 65 | /** 66 | * @return void 67 | */ 68 | public function testRunFailureWithRedirect() { 69 | $exception = null; 70 | try { 71 | $this->Task->run(['command' => 'fooooobbbaraar -eeee'], 0); 72 | } catch (Exception $e) { 73 | $exception = $e; 74 | } 75 | 76 | $this->assertInstanceOf(Exception::class, $exception); 77 | $this->assertSame('Failed with error code 127: `fooooobbbaraar -eeee 2>&1`', $exception->getMessage()); 78 | 79 | $this->assertTextContains('Error (code 127)', $this->err->output()); 80 | $this->assertTextContains('fooooobbbaraar: not found', $this->out->output()); 81 | } 82 | 83 | /** 84 | * @return void 85 | */ 86 | public function testRunFailureWithRedirectAndIgnoreCode() { 87 | $this->Task->run(['command' => 'fooooobbbaraar -eeee', 'accepted' => []], 0); 88 | 89 | $this->assertTextContains('Success (code 127)', $this->out->output()); 90 | $this->assertTextContains('fooooobbbaraar: not found', $this->out->output()); 91 | } 92 | 93 | /** 94 | * @return void 95 | */ 96 | public function testRunFailureWithoutRedirect() { 97 | $this->skipIf((bool)getenv('TRAVIS'), 'Not redirecting stderr to stdout prints noise to the CLI output in between test runs.'); 98 | 99 | $this->expectException(RuntimeException::class); 100 | $this->expectExceptionMessage('Failed with error code 127: `fooooobbbaraar -eeee`'); 101 | 102 | $this->Task->run(['command' => 'fooooobbbaraar -eeee', 'redirect' => false], 0); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/MailerTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 51 | $this->err = new ConsoleOutput(); 52 | $io = new Io(new ConsoleIo($this->out, $this->err)); 53 | 54 | $this->Task = new MailerTask($io); 55 | } 56 | 57 | /** 58 | * @return void 59 | */ 60 | public function testRunToolsMailerConfig() { 61 | $this->Task->run([ 62 | 'class' => TestMailer::class, 63 | 'action' => 'testAction', 64 | 'vars' => [true], 65 | ], 0); 66 | 67 | $reflection = new ReflectionClass($this->Task); 68 | $property = $reflection->getProperty('mailer'); 69 | $property->setAccessible(true); 70 | $mailer = $property->getValue($this->Task); 71 | 72 | $this->assertInstanceOf(TestMailer::class, $mailer); 73 | 74 | $transportConfig = $mailer->getTransport()->getConfig(); 75 | $this->assertSame('Debug', $transportConfig['className']); 76 | 77 | $result = $mailer->getDebug(); 78 | $this->assertTextContains('bool(true)', $result['message']); 79 | } 80 | 81 | /** 82 | * @return void 83 | */ 84 | public function testRunMissingMailerException() { 85 | $this->expectException(QueueException::class); 86 | $this->expectExceptionMessage('Queue Mailer task called without valid `mailer` class.'); 87 | 88 | $this->Task->run([], 0); 89 | } 90 | 91 | /** 92 | * @return void 93 | */ 94 | public function testRunMissingActionException() { 95 | $this->expectException(QueueException::class); 96 | $this->expectExceptionMessage('Queue Mailer task called without `action` data.'); 97 | 98 | $this->Task->run([ 99 | 'class' => TestMailer::class, 100 | ], 0); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/MonitorExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 41 | $this->err = new ConsoleOutput(); 42 | $io = new Io(new ConsoleIo($this->out, $this->err)); 43 | 44 | $this->Task = new MonitorExampleTask($io); 45 | } 46 | 47 | /** 48 | * @return void 49 | */ 50 | public function testRun() { 51 | $this->Task->run([], 0); 52 | 53 | $this->assertTextContains('Success, the MonitorExample Job was run', $this->out->output()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/ProgressExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 41 | $this->err = new ConsoleOutput(); 42 | $io = new Io(new ConsoleIo($this->out, $this->err)); 43 | 44 | $this->Task = new ProgressExampleTask($io); 45 | } 46 | 47 | /** 48 | * @return void 49 | */ 50 | public function testRun() { 51 | $this->Task->run(['duration' => 1], 0); 52 | 53 | $this->assertTextContains('Success, the ProgressExample Job was run', $this->out->output()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/RetryExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 43 | $this->err = new ConsoleOutput(); 44 | $io = new Io(new ConsoleIo($this->out, $this->err)); 45 | 46 | $this->Task = new RetryExampleTask($io); 47 | } 48 | 49 | /** 50 | * @return void 51 | */ 52 | public function testRun() { 53 | $file = TMP . 'task_retry.txt'; 54 | file_put_contents($file, '0'); 55 | 56 | $exception = null; 57 | try { 58 | $this->Task->run([], 0); 59 | } catch (Exception $e) { 60 | $exception = $e; 61 | } 62 | 63 | $this->assertInstanceOf(StopException::class, $exception); 64 | $this->assertTextContains('Sry, the RetryExample Job failed. Try again.', $this->err->output()); 65 | } 66 | 67 | /** 68 | * @return void 69 | */ 70 | public function testRunSuccess() { 71 | $file = TMP . 'task_retry.txt'; 72 | file_put_contents($file, '3'); 73 | 74 | $this->Task->run([], 0); 75 | 76 | $this->assertTextContains('Success, the RetryExample Job was run', $this->out->output()); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/SuperExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 48 | $this->err = new ConsoleOutput(); 49 | $io = new Io(new ConsoleIo($this->out, $this->err)); 50 | 51 | $this->Task = new SuperExampleTask($io); 52 | } 53 | 54 | /** 55 | * @return void 56 | */ 57 | public function testRun() { 58 | $this->Task->run([], 0); 59 | 60 | $this->assertTextContains('Success, the SuperExample Job was run', $this->out->output()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/Task/UniqueExampleTaskTest.php: -------------------------------------------------------------------------------- 1 | out = new ConsoleOutput(); 48 | $this->err = new ConsoleOutput(); 49 | $io = new Io(new ConsoleIo($this->out, $this->err)); 50 | 51 | $this->Task = new UniqueExampleTask($io); 52 | } 53 | 54 | /** 55 | * @return void 56 | */ 57 | public function testRun() { 58 | $this->Task->setSleep(0); 59 | $this->Task->run([], 0); 60 | 61 | $this->assertTextContains('Success, the UniqueExample Job was run', $this->out->output()); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/TaskFinderTest.php: -------------------------------------------------------------------------------- 1 | taskFinder = new TaskFinder(); 23 | 24 | $result = $this->taskFinder->all(); 25 | 26 | $this->assertArrayHasKey('Queue.Example', $result); 27 | $this->assertArrayHasKey('Foo', $result); 28 | $this->assertArrayHasKey('Foo.Foo', $result); 29 | 30 | $this->assertSame('TestApp\Queue\Task\Sub\SubFooTask', $result['Sub/SubFoo']); 31 | $this->assertSame('Foo\Queue\Task\Sub\SubFooTask', $result['Foo.Sub/SubFoo']); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function testResolve(): void { 38 | $this->taskFinder = new TaskFinder(); 39 | 40 | $result = $this->taskFinder->resolve('Foo'); 41 | $this->assertSame('Foo', $result); 42 | 43 | $result = $this->taskFinder->resolve(FooTask::class); 44 | $this->assertSame('Foo', $result); 45 | 46 | $result = $this->taskFinder->resolve('Queue.Example'); 47 | $this->assertSame('Queue.Example', $result); 48 | 49 | $result = $this->taskFinder->resolve(ExampleTask::class); 50 | $this->assertSame('Queue.Example', $result); 51 | 52 | $result = $this->taskFinder->resolve(ExampleTask::taskName()); 53 | $this->assertSame('Queue.Example', $result); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function testClassName(): void { 60 | $this->taskFinder = new TaskFinder(); 61 | 62 | $class = $this->taskFinder->getClass('Queue.Example'); 63 | $this->assertSame(ExampleTask::class, $class); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/TestCase/Queue/TaskTest.php: -------------------------------------------------------------------------------- 1 | assertSame('Foo', $name); 18 | 19 | $name = ExampleTask::taskName(); 20 | $this->assertSame('Queue.Example', $name); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/QueueHelperTest.php: -------------------------------------------------------------------------------- 1 | QueueHelper = new QueueHelper(new View(null)); 25 | } 26 | 27 | /** 28 | * @return void 29 | */ 30 | public function tearDown(): void { 31 | parent::tearDown(); 32 | 33 | unset($this->QueueHelper); 34 | } 35 | 36 | /** 37 | * @return void 38 | */ 39 | public function testHasFailed() { 40 | $queuedJob = new QueuedJob([ 41 | 'job_task' => 'Queue.Example', 42 | 'fetched' => '2019', 43 | 'attempts' => 0, 44 | ]); 45 | $result = $this->QueueHelper->hasFailed($queuedJob); 46 | $this->assertFalse($result); 47 | 48 | $queuedJob->attempts = 1; 49 | $queuedJob->failure_message = 'Foo'; 50 | $result = $this->QueueHelper->hasFailed($queuedJob); 51 | $this->assertFalse($result); 52 | 53 | $queuedJob->attempts = 999; 54 | $result = $this->QueueHelper->hasFailed($queuedJob); 55 | $this->assertTrue($result); 56 | 57 | $queuedJob->attempts = 999; 58 | $queuedJob->failure_message = null; 59 | $result = $this->QueueHelper->hasFailed($queuedJob); 60 | $this->assertFalse($result); 61 | } 62 | 63 | /** 64 | * @return void 65 | */ 66 | public function testFails() { 67 | $queuedJob = new QueuedJob([ 68 | 'job_task' => 'Queue.Example', 69 | 'attempts' => 0, 70 | ]); 71 | $result = $this->QueueHelper->attempts($queuedJob); 72 | $this->assertSame('0x', $result); 73 | 74 | $queuedJob->attempts = 1; 75 | $result = $this->QueueHelper->attempts($queuedJob); 76 | $this->assertSame('1/2', $result); 77 | 78 | $queuedJob->attempts = 2; 79 | $result = $this->QueueHelper->attempts($queuedJob); 80 | $this->assertSame('2/2', $result); 81 | 82 | $queuedJob->job_task = 'Queue.ExampleInvalid'; 83 | $result = $this->QueueHelper->attempts($queuedJob); 84 | $this->assertSame('2x', $result); 85 | } 86 | 87 | /** 88 | * @return void 89 | */ 90 | public function testFailureStatus() { 91 | $queuedJob = new QueuedJob([ 92 | 'job_task' => 'Queue.Example', 93 | 'fetched' => '2019', 94 | 'attempts' => 0, 95 | ]); 96 | $result = $this->QueueHelper->failureStatus($queuedJob); 97 | $this->assertNull($result); 98 | 99 | $queuedJob->attempts = 1; 100 | $queuedJob->failure_message = 'Foo'; 101 | $result = $this->QueueHelper->failureStatus($queuedJob); 102 | $this->assertSame(__d('queue', 'Requeued'), $result); 103 | 104 | $queuedJob->failure_message = null; 105 | $result = $this->QueueHelper->failureStatus($queuedJob); 106 | $this->assertSame('Restarted', $result); 107 | 108 | $queuedJob->attempts = 999; 109 | $queuedJob->failure_message = 'Foo'; 110 | $result = $this->QueueHelper->failureStatus($queuedJob); 111 | $this->assertSame('Aborted', $result); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /tests/config/app_queue.php: -------------------------------------------------------------------------------- 1 | [ 5 | // time (in seconds) after which a job is requeued if the worker doesn't report back 6 | 'defaultworkertimeout' => 1800, 7 | 8 | // seconds of running time after which the worker will terminate (0 = unlimited) 9 | 'workermaxruntime' => 120, 10 | 11 | // minimum time (in seconds) which a task remains in the database before being cleaned up. 12 | 'cleanuptimeout' => 2592000, // 30 days 13 | 14 | /* Optional */ 15 | 16 | 'isSearchEnabled' => true, 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /tests/config/bootstrap.php: -------------------------------------------------------------------------------- 1 | $iterator 10 | */ 11 | $iterator = new DirectoryIterator(__DIR__ . DS . 'Fixture'); 12 | foreach ($iterator as $file) { 13 | if (!preg_match('/(\w+)Fixture.php$/', (string)$file, $matches)) { 14 | continue; 15 | } 16 | 17 | $name = $matches[1]; 18 | $tableName = null; 19 | $class = 'Queue\\Test\\Fixture\\' . $name . 'Fixture'; 20 | try { 21 | $fieldsObject = (new ReflectionClass($class))->getProperty('fields'); 22 | $tableObject = (new ReflectionClass($class))->getProperty('table'); 23 | $tableName = $tableObject->getDefaultValue(); 24 | } catch (ReflectionException $e) { 25 | continue; 26 | } 27 | 28 | if (!$tableName) { 29 | $tableName = Inflector::underscore($name); 30 | } 31 | 32 | $array = $fieldsObject->getDefaultValue(); 33 | $constraints = $array['_constraints'] ?? []; 34 | $indexes = $array['_indexes'] ?? []; 35 | unset($array['_constraints'], $array['_indexes'], $array['_options']); 36 | $table = [ 37 | 'table' => $tableName, 38 | 'columns' => $array, 39 | 'constraints' => $constraints, 40 | 'indexes' => $indexes, 41 | ]; 42 | $tables[$tableName] = $table; 43 | } 44 | 45 | return $tables; 46 | --------------------------------------------------------------------------------