├── .github └── workflows │ ├── main.yml │ └── phpcs.yml ├── CHANGELOG.md ├── LICENSE ├── Module.php ├── README.md ├── UPGRADE.md ├── composer.json ├── config └── module.config.php ├── data ├── DefaultQueue.php └── queue_default.sql ├── phpcs.xml ├── phpunit.xml └── src ├── Command └── RecoverJobsCommand.php ├── ConfigProvider.php ├── Exception ├── ExceptionInterface.php ├── JobNotFoundException.php ├── LogicException.php └── RuntimeException.php ├── Factory └── DoctrineQueueFactory.php ├── Job └── Exception │ ├── BuryableException.php │ └── ReleasableException.php ├── Module.php ├── Options └── DoctrineOptions.php ├── Persistence └── ObjectManagerAwareInterface.php ├── Queue ├── DoctrineQueue.php └── DoctrineQueueInterface.php ├── Strategy ├── ClearObjectManagerStrategy.php └── IdleNapStrategy.php └── Worker └── DoctrineWorker.php /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Unit tests" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "master" 7 | push: 8 | branches: 9 | - "master" 10 | 11 | jobs: 12 | unit-test: 13 | name: "Unit tests" 14 | runs-on: "ubuntu-20.04" 15 | strategy: 16 | matrix: 17 | php-version: 18 | - "7.4" 19 | - "8.0" 20 | - "8.1" 21 | - "8.2" 22 | - "8.3" 23 | dependencies: 24 | - "highest" 25 | - "lowest" 26 | steps: 27 | - name: "Checkout" 28 | uses: "actions/checkout@v2" 29 | 30 | - name: "Install PHP" 31 | uses: "shivammathur/setup-php@v2" 32 | with: 33 | php-version: "${{ matrix.php-version }}" 34 | env: 35 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: "Install dependencies with Composer" 38 | uses: "ramsey/composer-install@v1" 39 | with: 40 | dependency-versions: "${{ matrix.dependencies }}" 41 | env: 42 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: "Run tests" 45 | run: "composer test" 46 | env: 47 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/phpcs.yml: -------------------------------------------------------------------------------- 1 | name: "PHPCS" 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "**.php" 7 | - "phpcs.xml" 8 | - ".github/workflows/phpcs.yml" 9 | 10 | jobs: 11 | phpcs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # important! 17 | 18 | # we may use whatever way to install phpcs, just specify the path on the next step 19 | # however, curl seems to be the fastest 20 | - name: Install PHP_CodeSniffer 21 | run: | 22 | curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar 23 | php phpcs.phar --version 24 | 25 | - uses: tinovyatkin/action-php-codesniffer@v1 26 | with: 27 | files: "**.php" # you may customize glob as needed 28 | phpcs_path: php phpcs.phar 29 | standard: phpcs.xml 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.3 And later 2 | 3 | Changelog for these releases can be found on the releases page, see https://github.com/juriansluiman/SlmQueueDoctrine/releases. 4 | 5 | # 2.0.2 6 | 7 | - fixed incorrect run priority for the ClearObjectManagerStrategy 8 | - correctly acquire lock for update to avoid race condition when using multiple workers 9 | 10 | # 2.0.1 11 | 12 | - fixes incorrect entity for migration purposes 13 | 14 | # 2.0.0 15 | 16 | - introduces concept job prioritization (you will need to update you schema) 17 | 18 | # 1.0.0 19 | 20 | - identical to 0.7.1 21 | 22 | # 0.7.1 23 | 24 | - We now use querybuilder for poping job from the queue to better support "Hardcoded LIMIT 1 doesn't work wih Oracle, as Oracle doesn't know LIMIT". This means we also require doctrine/dbal:^2.5 minimal, which could be an issue if you're using older packages. 25 | - Synchronized the indexes between the provided entity and the sql script. You may recreate the tables if you do don't have the correct indexes present. 26 | 27 | # 0.7.0 28 | 29 | - [BC] Synchronize with SlmQueue release 0.7.0 which adds the ability to store binary data in job content 30 | 31 | # 0.6.1 32 | 33 | - Fixes an issue with timezones 34 | - Adds support for microtimes 35 | 36 | # 0.6.0 37 | 38 | - [BC] Synchronize with SlmQueue release 0.6.0 which adds compatibility with PHP7, zend-servicemanager 3 and zend-eventmanager 3 39 | 40 | # 0.5.0 41 | 42 | - [BC] Minimum dependency has been raised to PHP 5.5 43 | - Project has been migrated to PSR-4 44 | 45 | # 0.4.0 46 | 47 | - First stable release in 0.4.x branch 48 | 49 | # 0.4.0-beta3 50 | 51 | * [BC] Due to changes in SlmQueue ([changelog](https://github.com/juriansluiman/SlmQueue/blob/master/CHANGELOG.md)) existing jobs won't be able to be executed correctly. 52 | 53 | # 0.4.0-beta1 54 | 55 | * [BC] SlmQueueDoctrine has been upgraded to SlmQueue 0.4. This feature includes a new, flexible and modular event system. 56 | 57 | # 0.3.0 58 | 59 | * Initial release for 0.3.* branch 60 | 61 | # 0.3.0-beta1 62 | 63 | First release of SlmQueueDoctrine which bring it on par with its parent SlmQueue 0.3.0-beta1 64 | 65 | Comes with unittests, bug fixes and general happiness but please be aware of the following, if you are upgrading from an earlier version... 66 | 67 | - Existing jobs are not compatible (Job content is now serialized). Make sure you empty the queue before upgrading. 68 | - It is suggested that you recreate the table from data/queue_default.sql as the schema changed in minor ways. 69 | - The ability to configure the queue defaults has been removed. Any queue specific configuration now goes into the queues configuration of slm_queue.local.php 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | New BSD License 2 | =============== 3 | 4 | Copyright (c) 2012-2014, Jurian Sluiman 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the names of the copyright holders nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | [ 48 | 'connection' => [ 49 | // default connection name 50 | 'orm_default' => [ 51 | 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', 52 | 'params' => [ 53 | 'host' => 'localhost', 54 | 'port' => '3306', 55 | 'user' => 'username', 56 | 'password' => 'password', 57 | 'dbname' => 'database', 58 | ] 59 | ] 60 | ] 61 | ], 62 | ]; 63 | ``` 64 | 65 | ### Creating the table from SQL file 66 | 67 | You must create the required table that will contain the queue's you may use the schema located in 'data/queue_default.sql'. If you change the table name look at [Configuring queues](./#configuring-queues) 68 | 69 | ``` 70 | >mysql database < data/queue_default.sql 71 | ``` 72 | ### Creating the table from Doctrine Entity 73 | There is an alternative way to create 'queue_default' table in your database by copying Doctrine Entity 'date/DefaultQueue.php' to your entity folder ('Application\Entity' in our example) and executing Doctrine's 'orm:schema-tool:update' command which should create the table for you. Notice that DefaultQueue entity is only used for table creation and is not used by this module internally. 74 | 75 | 76 | ### Adding queues 77 | 78 | ```php 79 | return [ 80 | 'slm_queue' => [ 81 | 'queue_manager' => [ 82 | 'factories' => [ 83 | 'foo' => 'SlmQueueDoctrine\Factory\DoctrineQueueFactory' 84 | ] 85 | ] 86 | ] 87 | ]; 88 | ``` 89 | ### Adding jobs 90 | 91 | ```php 92 | return [ 93 | 'slm_queue' => [ 94 | 'job_manager' => [ 95 | 'factories' => [ 96 | 'My\Job' => 'My\JobFactory' 97 | ] 98 | ] 99 | ] 100 | ]; 101 | 102 | ``` 103 | ### Configuring queues 104 | 105 | The following options can be set per queue ; 106 | 107 | - connection (defaults to 'doctrine.connection.orm_default') : Name of the registered doctrine connection service 108 | - table_name (defaults to 'queue_default') : Table name which should be used to store jobs 109 | - deleted_lifetime (defaults to 0) : How long to keep deleted (successful) jobs (in minutes) 110 | - buried_lifetime (defaults to 0) : How long to keep buried (failed) jobs (in minutes) 111 | 112 | 113 | ```php 114 | return [ 115 | 'slm_queue' => [ 116 | 'queues' => [ 117 | 'foo' => [ 118 | // ... 119 | ] 120 | ] 121 | ] 122 | ]; 123 | ``` 124 | 125 | Provided Worker Strategies 126 | -------------------------- 127 | 128 | In addition to the provided strategies by [SlmQueue](https://github.com/JouwWeb/SlmQueue/blob/master/docs/6.Events.md) SlmQueueDoctrine comes with these strategies; 129 | 130 | #### ClearObjectManagerStrategy 131 | 132 | This strategy will clear the ObjectManager before execution of individual jobs. The job must implement the [DoctrineModule\Persistence\ObjectManagerAwareInterface](https://github.com/doctrine/DoctrineModule/blob/master/src/DoctrineModule/Persistence/ObjectManagerAwareInterface.php) or [SlmQueueDoctrine\Persistence\ObjectManagerAwareInterface](https://github.com/JouwWeb/SlmQueueDoctrine/blob/master/src/Persistence/ObjectManagerAwareInterface.php). 133 | 134 | listens to: 135 | 136 | - `process.job` event at priority 1000 137 | 138 | options: 139 | 140 | - none 141 | 142 | This strategy is enabled by default. 143 | 144 | #### IdleNapStrategy 145 | 146 | When no jobs are available in the queue this strategy will make the worker wait for a specific amount time before quering the database again. 147 | 148 | listens to: 149 | 150 | - `process.idle` event at priority 1 151 | 152 | options: 153 | 154 | - `nap_duration` defaults to 1 (second) 155 | 156 | This strategy is enabled by default. 157 | 158 | ### Operations on queues 159 | 160 | #### push 161 | 162 | Valid options are: 163 | 164 | * scheduled: the time when the job will be scheduled to run next 165 | * numeric string or integer - interpreted as a timestamp 166 | * string parserable by the DateTime object 167 | * DateTime instance 168 | * delay: the delay before a job become available to be popped (defaults to 0 - no delay -) 169 | * numeric string or integer - interpreted as seconds 170 | * string parserable (ISO 8601 duration) by DateTimeInterval::__construct 171 | * string parserable (relative parts) by DateTimeInterval::createFromDateString 172 | * DateTimeInterval instance 173 | * priority: the lower the priority is, the sooner the job get popped from the queue (default to 1024) 174 | 175 | Examples: 176 | ```php 177 | // scheduled for execution asap 178 | $queue->push($job); 179 | 180 | // will get executed before jobs that have higher priority 181 | $queue->push($job, [ 182 | 'priority' => 200, 183 | ]); 184 | 185 | // scheduled for execution 2015-01-01 00:00:00 (system timezone applies) 186 | $queue->push($job, [ 187 | 'scheduled' => 1420070400, 188 | ]); 189 | 190 | // scheduled for execution 2015-01-01 00:00:00 (system timezone applies) 191 | $queue->push($job, [ 192 | 'scheduled' => '2015-01-01 00:00:00' 193 | ]); 194 | 195 | // scheduled for execution at 2015-01-01 01:00:00 196 | $queue->push($job, [ 197 | 'scheduled' => '2015-01-01 00:00:00', 198 | 'delay' => 3600 199 | ]); 200 | 201 | // scheduled for execution at now + 300 seconds 202 | $queue->push($job, [ 203 | 'delay' => 'PT300S' 204 | ]); 205 | 206 | // scheduled for execution at now + 2 weeks (1209600 seconds) 207 | $queue->push($job, [ 208 | 'delay' => '2 weeks' 209 | ]); 210 | 211 | // scheduled for execution at now + 300 seconds 212 | $queue->push($job, [ 213 | 'delay' => new DateInterval("PT300S")) 214 | ]); 215 | ``` 216 | 217 | 218 | ### Worker actions 219 | 220 | Interact with workers from the command line from within the public folder of your Laminas Framework 2 application 221 | 222 | #### Starting a worker 223 | Start a worker that will keep monitoring a specific queue for jobs scheduled to be processed. This worker will continue until it has reached certain criteria (exceeds a memory limit or has processed a specified number of jobs). 224 | 225 | `vendor/bin/laminas slm-queue:start ` 226 | 227 | A worker will exit when you press cntr-C *after* it has finished the current job it is working on. (PHP doesn't support signal handling on Windows) 228 | 229 | #### Recovering jobs 230 | 231 | To recover jobs which are in the 'running' state for prolonged period of time (specified in minutes) use the following command. 232 | 233 | `vendor/bin/laminas slm-queue:doctrine:recover [--executionTime=]` 234 | 235 | *Note : Workers that are processing a job that is being recovered are NOT stopped.* 236 | 237 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Ugrade to 4.0 2 | 3 | This release adds support for PHP 8.0. 4 | 5 | ## BC BREAK: require `slm\queue` >= 3.0 6 | 7 | Upgrading to `slm\queue` 3+ will require some changes, see [upgrade notes](https://github.com/JouwWeb/SlmQueue/blob/master/UPGRADE.md). 8 | 9 | ## BC BREAK: changed recover CLI command 10 | 11 | The recover CLI can now be invoked with the following command: 12 | 13 | ```sh 14 | vendor/bin/laminas slm-queue:doctrine:recover [--executionTime=] 15 | ``` 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slm/queue-doctrine", 3 | "description": "Laminas Framework module that integrates Doctrine as queuing system", 4 | "license": "BSD-3-Clause", 5 | "type": "library", 6 | "keywords": [ 7 | "laminas", 8 | "mezzio", 9 | "queue", 10 | "job", 11 | "doctrine", 12 | "db", 13 | "database" 14 | ], 15 | "homepage": "https://github.com/Webador/SlmQueueDoctrine", 16 | "authors": [ 17 | { 18 | "name": "Stefan Kleff", 19 | "email": "s.kleff@goalio.de", 20 | "homepage": "https://www.goalio.de" 21 | }, 22 | { 23 | "name": "Bas Kamer", 24 | "email": "baskamer@gmail.com", 25 | "homepage": "https://bushbaby.nl" 26 | } 27 | ], 28 | "require": { 29 | "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", 30 | "laminas/laminas-eventmanager": "^3.4", 31 | "laminas/laminas-router": "^3.5", 32 | "slm/queue": "^3.1", 33 | "doctrine/dbal": "^3.1.2", 34 | "doctrine/annotations": "^1.8", 35 | "laminas/laminas-config": "^3.7" 36 | }, 37 | "require-dev": { 38 | "ext-sqlite3": "*", 39 | "doctrine/doctrine-orm-module": "^4.1 || ^5.0 || ^6.0", 40 | "laminas/laminas-modulemanager": "^2.11", 41 | "laminas/laminas-view": "^2.13", 42 | "laminas/laminas-log": "^2.15", 43 | "laminas/laminas-i18n": "^2.12", 44 | "laminas/laminas-serializer": "^2.11", 45 | "laminas/laminas-mvc": "^3.3", 46 | "doctrine/orm": "^2.11.1", 47 | "phpunit/phpunit": "^9.3", 48 | "squizlabs/php_codesniffer": "^3.6.2" 49 | }, 50 | "conflict": { 51 | "doctrine/cache": ">=2.0" 52 | }, 53 | "suggest": { 54 | "doctrine/doctrine-orm-module": "If you use Doctrine in combination with Laminas", 55 | "roave/psr-container-doctrine": "Configures Doctrine services automatically." 56 | }, 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "3.1.x-dev" 60 | }, 61 | "laminas": { 62 | "module": "SlmQueueDoctrine\\Module", 63 | "config-provider": "SlmQueueDoctrine\\ConfigProvider", 64 | "component-whitelist": [ 65 | "slm/queue" 66 | ] 67 | } 68 | }, 69 | "autoload": { 70 | "psr-4": { 71 | "SlmQueueDoctrine\\": "src/" 72 | } 73 | }, 74 | "autoload-dev": { 75 | "psr-4": { 76 | "SlmQueueDoctrineTest\\": "tests/src" 77 | } 78 | }, 79 | "scripts": { 80 | "cs-check": "phpcs", 81 | "cs-fix": "phpcbf", 82 | "test": [ 83 | "phpunit", 84 | "@composer test --working-dir=tests/laminas-runner", 85 | "@composer test --working-dir=tests/mezzio-runner" 86 | ] 87 | }, 88 | "config": { 89 | "platform": { 90 | "php": "7.4.0" 91 | }, 92 | "allow-plugins": { 93 | "composer/package-versions-deprecated": true 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /config/module.config.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'factories' => [ 10 | RecoverJobsCommand::class => \Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory::class, 11 | ] 12 | ], 13 | 'laminas-cli' => [ 14 | 'commands' => [ 15 | 'slm-queue:doctrine:recover' => RecoverJobsCommand::class, 16 | ], 17 | ], 18 | 'slm_queue' => [ 19 | /** 20 | * Worker Strategies 21 | */ 22 | 'worker_strategies' => [ 23 | 'default' => [ 24 | IdleNapStrategy::class => ['nap_duration' => 1], 25 | ClearObjectManagerStrategy::class 26 | ], 27 | 'queues' => [ 28 | ], 29 | ], 30 | /** 31 | * Strategy manager configuration 32 | */ 33 | 'strategy_manager' => [ 34 | 'factories' => [ 35 | IdleNapStrategy::class => \Laminas\ServiceManager\Factory\InvokableFactory::class, 36 | ClearObjectManagerStrategy::class => \Laminas\ServiceManager\Factory\InvokableFactory::class, 37 | ], 38 | ], 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /data/DefaultQueue.php: -------------------------------------------------------------------------------- 1 | id; 112 | } 113 | 114 | /** 115 | * Set queue 116 | * 117 | * @param string $queue 118 | * @return DefaultQueue 119 | */ 120 | public function setQueue($queue) 121 | { 122 | $this->queue = $queue; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Get queue 129 | * 130 | * @return string 131 | */ 132 | public function getQueue() 133 | { 134 | return $this->queue; 135 | } 136 | 137 | /** 138 | * Set data 139 | * 140 | * @param string $data 141 | * @return DefaultQueue 142 | */ 143 | public function setData($data) 144 | { 145 | $this->data = $data; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Get data 152 | * 153 | * @return string 154 | */ 155 | public function getData() 156 | { 157 | return $this->data; 158 | } 159 | 160 | /** 161 | * Set status 162 | * 163 | * @param int $status 164 | * @return DefaultQueue 165 | */ 166 | public function setStatus($status) 167 | { 168 | $this->status = $status; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Get status 175 | * 176 | * @return int 177 | */ 178 | public function getStatus() 179 | { 180 | return $this->status; 181 | } 182 | 183 | /** 184 | * Set created 185 | * 186 | * @param \DateTime $created 187 | * @return DefaultQueue 188 | */ 189 | public function setCreated(DateTime $created) 190 | { 191 | $this->created = clone $created; 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Get created 198 | * 199 | * @return \DateTime 200 | */ 201 | public function getCreated() 202 | { 203 | if ($this->created) { 204 | return clone $this->created; 205 | } 206 | 207 | return null; 208 | } 209 | 210 | /** 211 | * Set scheduled 212 | * 213 | * @param \DateTime $scheduled 214 | * @return DefaultQueue 215 | */ 216 | public function setScheduled(DateTime $scheduled) 217 | { 218 | $this->scheduled = clone $scheduled; 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Get scheduled 225 | * 226 | * @return \DateTime 227 | */ 228 | public function getScheduled() 229 | { 230 | if ($this->scheduled) { 231 | return clone $this->scheduled; 232 | } 233 | 234 | return null; 235 | } 236 | 237 | /** 238 | * Set executed 239 | * 240 | * @param \DateTime $executed 241 | * @return DefaultQueue 242 | */ 243 | public function setExecuted(DateTime $executed = null) 244 | { 245 | $this->executed = $executed ? clone $executed : null; 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * Get executed 252 | * 253 | * @return \DateTime 254 | */ 255 | public function getExecuted() 256 | { 257 | if ($this->executed) { 258 | return clone $this->executed; 259 | } 260 | 261 | return null; 262 | } 263 | 264 | /** 265 | * Set finished 266 | * 267 | * @param \DateTime $finished 268 | * @return DefaultQueue 269 | */ 270 | public function setFinished(DateTime $finished = null) 271 | { 272 | $this->finished = $finished ? clone $finished : null; 273 | 274 | return $this; 275 | } 276 | 277 | /** 278 | * Get finished 279 | * 280 | * @return \DateTime 281 | */ 282 | public function getFinished() 283 | { 284 | if ($this->finished) { 285 | return clone $this->finished; 286 | } 287 | 288 | return null; 289 | } 290 | 291 | /** 292 | * Set message 293 | * 294 | * @param string $message 295 | * @return DefaultQueue 296 | */ 297 | public function setMessage($message) 298 | { 299 | $this->message = $message; 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Get message 306 | * 307 | * @return string 308 | */ 309 | public function getMessage() 310 | { 311 | return $this->message; 312 | } 313 | 314 | /** 315 | * Set trace 316 | * 317 | * @param string $trace 318 | * @return DefaultQueue 319 | */ 320 | public function setTrace($trace) 321 | { 322 | $this->trace = $trace; 323 | 324 | return $this; 325 | } 326 | 327 | /** 328 | * Get trace 329 | * 330 | * @return string 331 | */ 332 | public function getTrace() 333 | { 334 | return $this->trace; 335 | } 336 | 337 | /** 338 | * @return int 339 | */ 340 | public function getPriority() 341 | { 342 | return $this->priority; 343 | } 344 | 345 | /*** 346 | * @param int $priority 347 | * @return $this 348 | */ 349 | public function setPriority($priority) 350 | { 351 | $this->priority = $priority; 352 | 353 | return $this; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /data/queue_default.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `queue_default` ( 2 | `id` int(11) NOT NULL AUTO_INCREMENT, 3 | `queue` varchar(64) NOT NULL, 4 | `data` mediumtext NOT NULL, 5 | `status` smallint(1) NOT NULL, 6 | `created` datetime(6) NOT NULL, 7 | `scheduled` datetime(6) NOT NULL, 8 | `executed` datetime(6) DEFAULT NULL, 9 | `finished` datetime(6) DEFAULT NULL, 10 | `priority` int DEFAULT 1024 NOT NULL, 11 | `message` text, 12 | `trace` text, 13 | PRIMARY KEY (`id`), 14 | KEY `pop` (`status`,`queue`,`scheduled`,`priority`), 15 | KEY `prune` (`status`,`queue`,`finished`) 16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 17 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | config 9 | src 10 | tests 11 | 12 | tests/*/vendor/*$ 13 | 14 | 15 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | ./tests/src 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Command/RecoverJobsCommand.php: -------------------------------------------------------------------------------- 1 | queuePluginManager = $queuePluginManager; 30 | } 31 | 32 | protected function configure(): void 33 | { 34 | $this 35 | ->addArgument('queue', InputArgument::REQUIRED) 36 | ->addOption('executionTime', null, InputOption::VALUE_REQUIRED, '', 0); 37 | } 38 | 39 | /** 40 | * Recover long running jobs 41 | */ 42 | protected function execute(InputInterface $input, OutputInterface $output): int 43 | { 44 | $queueName = $input->getArgument('queue'); 45 | $executionTime = $input->getOption('executionTime'); 46 | $queue = $this->queuePluginManager->get($queueName); 47 | 48 | if (! $queue instanceof DoctrineQueueInterface) { 49 | return sprintf("\nQueue % does not support the recovering of job\n\n", $queueName); 50 | } 51 | 52 | try { 53 | $count = $queue->recover($executionTime); 54 | } catch (ExceptionInterface $exception) { 55 | throw new WorkerProcessException("An error occurred", $exception->getCode(), $exception); 56 | } 57 | 58 | $output->writeln(sprintf( 59 | "\nWork for queue %s is done, %s jobs were recovered\n\n", 60 | $queueName, 61 | $count 62 | )); 63 | 64 | return 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | getConfig(); 14 | 15 | return [ 16 | 'dependencies' => $config['service_manager'], 17 | 'slm_queue' => $config['slm_queue'], 18 | 'laminas-cli' => $config['laminas-cli'], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | get('config'); 22 | $queuesOptions = $config['slm_queue']['queues']; 23 | $options = isset($queuesOptions[$requestedName]) ? $queuesOptions[$requestedName] : []; 24 | $queueOptions = new DoctrineOptions($options); 25 | 26 | /** @var $connection \Doctrine\DBAL\Connection */ 27 | $connection = $container->get($queueOptions->getConnection()); 28 | $jobPluginManager = $container->get(JobPluginManager::class); 29 | 30 | return new DoctrineQueue( 31 | $connection, 32 | $queueOptions, 33 | $requestedName, 34 | $jobPluginManager 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Job/Exception/BuryableException.php: -------------------------------------------------------------------------------- 1 | options = $options; 28 | } 29 | 30 | /** 31 | * Get the options 32 | */ 33 | public function getOptions(): array 34 | { 35 | return $this->options; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Job/Exception/ReleasableException.php: -------------------------------------------------------------------------------- 1 | options = $options; 29 | } 30 | 31 | /** 32 | * Get the options 33 | */ 34 | public function getOptions(): array 35 | { 36 | return $this->options; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 50 | } 51 | 52 | /** 53 | * Get the connection service name 54 | */ 55 | public function getConnection(): string 56 | { 57 | return $this->connection; 58 | } 59 | 60 | public function setBuriedLifetime(int $buriedLifetime): void 61 | { 62 | $this->buriedLifetime = (int) $buriedLifetime; 63 | } 64 | 65 | public function getBuriedLifetime(): int 66 | { 67 | return $this->buriedLifetime; 68 | } 69 | 70 | public function setDeletedLifetime(int $deletedLifetime): void 71 | { 72 | $this->deletedLifetime = (int) $deletedLifetime; 73 | } 74 | 75 | public function getDeletedLifetime(): int 76 | { 77 | return $this->deletedLifetime; 78 | } 79 | 80 | public function setTableName(string $tableName): void 81 | { 82 | $this->tableName = $tableName; 83 | } 84 | 85 | public function getTableName(): string 86 | { 87 | return $this->tableName; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Persistence/ObjectManagerAwareInterface.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 57 | $this->options = clone $options; 58 | 59 | parent::__construct($name, $jobPluginManager); 60 | } 61 | 62 | public function getOptions(): DoctrineOptions 63 | { 64 | return $this->options; 65 | } 66 | 67 | /** 68 | * Valid options are: 69 | * - priority: the lower the priority is, the sooner the job get popped from the queue (default to 1024) 70 | * 71 | * Note : see DoctrineQueue::parseOptionsToDateTime for schedule and delay options 72 | */ 73 | public function push(JobInterface $job, array $options = []): void 74 | { 75 | $time = microtime(true); 76 | $micro = sprintf("%06d", ($time - floor($time)) * 1000000); 77 | $this->now = new DateTime( 78 | date('Y-m-d H:i:s.' . $micro, (int)$time), 79 | new DateTimeZone(date_default_timezone_get()) 80 | ); 81 | $scheduled = $this->parseOptionsToDateTime($options); 82 | 83 | $this->connection->insert($this->options->getTableName(), [ 84 | 'queue' => $this->getName(), 85 | 'status' => self::STATUS_PENDING, 86 | 'created' => $this->now->format('Y-m-d H:i:s.u'), 87 | 'data' => $this->serializeJob($job), 88 | 'scheduled' => $scheduled->format('Y-m-d H:i:s.u'), 89 | 'priority' => isset($options['priority']) ? $options['priority'] : self::DEFAULT_PRIORITY, 90 | ], [ 91 | Types::STRING, 92 | Types::SMALLINT, 93 | Types::STRING, 94 | Types::TEXT, 95 | Types::STRING, 96 | Types::INTEGER, 97 | ]); 98 | 99 | if (self::DATABASE_PLATFORM_POSTGRES == $this->connection->getDatabasePlatform()->getName()) { 100 | $id = $this->connection->lastInsertId($this->options->getTableName() . '_id_seq'); 101 | } else { 102 | $id = $this->connection->lastInsertId(); 103 | } 104 | 105 | $job->setId($id); 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function pop(array $options = []): ?JobInterface 112 | { 113 | // First run garbage collection 114 | $this->purge(); 115 | 116 | $conn = $this->connection; 117 | $conn->beginTransaction(); 118 | 119 | $time = microtime(true); 120 | $micro = sprintf("%06d", ($time - floor($time)) * 1000000); 121 | $this->now = new DateTime( 122 | date('Y-m-d H:i:s.' . $micro, (int)$time), 123 | new DateTimeZone(date_default_timezone_get()) 124 | ); 125 | 126 | try { 127 | $platform = $conn->getDatabasePlatform(); 128 | 129 | $queryBuilder = $conn->createQueryBuilder(); 130 | 131 | $queryBuilder 132 | ->select('*') 133 | ->from($platform->appendLockHint($this->options->getTableName(), LockMode::PESSIMISTIC_WRITE)) 134 | ->where('status = ?') 135 | ->andWhere('queue = ?') 136 | ->andWhere('scheduled <= ?') 137 | ->addOrderBy('priority', 'ASC') 138 | ->addOrderBy('scheduled', 'ASC') 139 | ->setParameter(0, static::STATUS_PENDING) 140 | ->setParameter(1, $this->getName()) 141 | ->setParameter(2, $this->now->format('Y-m-d H:i:s.u')) 142 | ->setMaxResults(1); 143 | 144 | // Modify the query so it supports row locking (if applicable for database provider) 145 | // @see https://github.com/doctrine/doctrine2/blob/5f3afa4c4ffb8cb49870d794cc7daf6a49406966/lib/Doctrine/ORM/Query/SqlWalker.php#L556-L558 146 | $sql = $queryBuilder->getSQL() . ' ' . $platform->getWriteLockSQL(); 147 | 148 | $query = $this->connection->executeQuery( 149 | $sql, 150 | $queryBuilder->getParameters(), 151 | $queryBuilder->getParameterTypes() 152 | ); 153 | 154 | if ($row = $query->fetch()) { 155 | $update = 'UPDATE ' . $this->options->getTableName() . ' ' . 156 | 'SET status = ?, executed = ? ' . 157 | 'WHERE id = ? AND status = ?'; 158 | 159 | $rows = $conn->executeUpdate( 160 | $update, 161 | [ 162 | static::STATUS_RUNNING, 163 | $this->now->format('Y-m-d H:i:s.u'), 164 | $row['id'], 165 | static::STATUS_PENDING 166 | ], 167 | [Types::SMALLINT, Types::STRING, Types::INTEGER, Types::SMALLINT] 168 | ); 169 | 170 | if ($rows !== 1) { 171 | throw new LogicException("Race-condition detected while updating item in queue."); 172 | } 173 | } 174 | 175 | $conn->commit(); 176 | } catch (DBALException $e) { 177 | $conn->rollback(); 178 | $conn->close(); 179 | throw new RuntimeException($e->getMessage(), $e->getCode(), $e); 180 | } 181 | 182 | if ($row === false) { 183 | return null; 184 | } 185 | 186 | // Add job ID to meta data 187 | return $this->unserializeJob($row['data'], ['__id__' => $row['id']]); 188 | } 189 | 190 | /** 191 | * {@inheritDoc} 192 | * 193 | * Note: When $deletedLifetime === 0 the job will be deleted immediately 194 | */ 195 | public function delete(JobInterface $job, array $options = []): void 196 | { 197 | if ($this->options->getDeletedLifetime() === static::LIFETIME_DISABLED) { 198 | $this->connection->delete($this->options->getTableName(), ['id' => $job->getId()]); 199 | } else { 200 | $update = 'UPDATE ' . $this->options->getTableName() . ' ' . 201 | 'SET status = ?, finished = ? ' . 202 | 'WHERE id = ? AND status = ?'; 203 | 204 | $time = microtime(true); 205 | $micro = sprintf("%06d", ($time - floor($time)) * 1000000); 206 | $this->now = new DateTime( 207 | date('Y-m-d H:i:s.' . $micro, (int)$time), 208 | new DateTimeZone(date_default_timezone_get()) 209 | ); 210 | 211 | $rows = $this->connection->executeUpdate( 212 | $update, 213 | [ 214 | static::STATUS_DELETED, 215 | $this->now->format('Y-m-d H:i:s.u'), 216 | $job->getId(), 217 | static::STATUS_RUNNING 218 | ], 219 | [Types::SMALLINT, Types::STRING, Types::INTEGER, Types::SMALLINT] 220 | ); 221 | 222 | if ($rows !== 1) { 223 | throw new LogicException("Race-condition detected while updating item in queue."); 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * {@inheritDoc} 230 | * 231 | * Note: When $buriedLifetime === 0 the job will be deleted immediately 232 | */ 233 | public function bury(JobInterface $job, array $options = []): void 234 | { 235 | if ($this->options->getBuriedLifetime() === static::LIFETIME_DISABLED) { 236 | $this->connection->delete($this->options->getTableName(), ['id' => $job->getId()]); 237 | } else { 238 | $message = isset($options['message']) ? $options['message'] : null; 239 | $trace = isset($options['trace']) ? $options['trace'] : null; 240 | 241 | $update = 'UPDATE ' . $this->options->getTableName() . ' ' . 242 | 'SET status = ?, finished = ?, message = ?, trace = ? ' . 243 | 'WHERE id = ? AND status = ?'; 244 | 245 | $time = microtime(true); 246 | $micro = sprintf("%06d", ($time - floor($time)) * 1000000); 247 | $this->now = new DateTime( 248 | date('Y-m-d H:i:s.' . $micro, (int)$time), 249 | new DateTimeZone(date_default_timezone_get()) 250 | ); 251 | 252 | $rows = $this->connection->executeUpdate( 253 | $update, 254 | [ 255 | static::STATUS_BURIED, 256 | $this->now->format('Y-m-d H:i:s.u'), 257 | $message, 258 | $trace, 259 | $job->getId(), 260 | static::STATUS_RUNNING 261 | ], 262 | [Types::SMALLINT, Types::STRING, Types::STRING, Types::TEXT, Types::INTEGER, Types::SMALLINT] 263 | ); 264 | 265 | if ($rows !== 1) { 266 | throw new LogicException("Race-condition detected while updating item in queue."); 267 | } 268 | } 269 | } 270 | 271 | /** 272 | * {@inheritDoc} 273 | */ 274 | public function recover(int $executionTime): int 275 | { 276 | $executedLifetime = $this->parseOptionsToDateTime(['delay' => - ($executionTime * 60)]); 277 | 278 | $update = 'UPDATE ' . $this->options->getTableName() . ' ' . 279 | 'SET status = ? ' . 280 | 'WHERE executed < ? AND status = ? AND queue = ? AND finished IS NULL'; 281 | 282 | return $this->connection->executeUpdate( 283 | $update, 284 | [ 285 | static::STATUS_PENDING, 286 | $executedLifetime->format('Y-m-d H:i:s.u'), 287 | static::STATUS_RUNNING, 288 | $this->getName() 289 | ], 290 | [Types::SMALLINT, Types::STRING, Types::SMALLINT, Types::STRING] 291 | ); 292 | } 293 | 294 | /** 295 | * Create a concrete instance of a job from the queue 296 | */ 297 | public function peek(int $id): JobInterface 298 | { 299 | $sql = 'SELECT * FROM ' . $this->options->getTableName() . ' WHERE id = ?'; 300 | $row = $this->connection->fetchAssociative($sql, [$id], [Types::SMALLINT]); 301 | 302 | if (!$row) { 303 | throw new JobNotFoundException(sprintf("Job with id '%s' does not exists.", $id)); 304 | } 305 | 306 | // Add job ID to meta data 307 | return $this->unserializeJob($row['data'], ['__id__' => $row['id']]); 308 | } 309 | 310 | /** 311 | * Reschedules a specific running job 312 | * 313 | * Note : see DoctrineQueue::parseOptionsToDateTime for schedule and delay options 314 | */ 315 | public function release(JobInterface $job, array $options = []): void 316 | { 317 | $scheduled = $this->parseOptionsToDateTime($options); 318 | 319 | $update = 'UPDATE ' . $this->options->getTableName() . ' ' . 320 | 'SET status = ?, finished = ? , scheduled = ?, data = ? ' . 321 | 'WHERE id = ? AND status = ?'; 322 | 323 | $time = microtime(true); 324 | $micro = sprintf("%06d", ($time - floor($time)) * 1000000); 325 | 326 | $rows = $this->connection->executeUpdate( 327 | $update, 328 | [ 329 | static::STATUS_PENDING, 330 | $this->now->format('Y-m-d H:i:s.u'), 331 | $scheduled->format('Y-m-d H:i:s.u'), 332 | $this->serializeJob($job), 333 | $job->getId(), 334 | static::STATUS_RUNNING, 335 | ], 336 | [Types::SMALLINT, Types::STRING, Types::STRING, Types::STRING, Types::INTEGER, Types::SMALLINT] 337 | ); 338 | 339 | if ($rows !== 1) { 340 | throw new LogicException("Race-condition detected while updating item in queue."); 341 | } 342 | } 343 | 344 | /** 345 | * Parses options to a datetime object 346 | * 347 | * valid options keys: 348 | * 349 | * scheduled: the time when the job will be scheduled to run next 350 | * - numeric string or integer - interpreted as a timestamp 351 | * - string parserable by the DateTime object 352 | * - DateTime instance 353 | * delay: the delay before a job become available to be popped (defaults to 0 - no delay -) 354 | * - numeric string or integer - interpreted as seconds 355 | * - string parserable (ISO 8601 duration) by DateTimeInterval::__construct 356 | * - string parserable (relative parts) by DateTimeInterval::createFromDateString 357 | * - DateTimeInterval instance 358 | * 359 | * @see http://en.wikipedia.org/wiki/Iso8601#Durations 360 | * @see http://www.php.net/manual/en/datetime.formats.relative.php 361 | */ 362 | protected function parseOptionsToDateTime(array $options): DateTime 363 | { 364 | $time = microtime(true); 365 | $micro = sprintf("%06d", ($time - floor($time)) * 1000000); 366 | $this->now = new DateTime( 367 | date('Y-m-d H:i:s.' . $micro, (int)$time), 368 | new DateTimeZone(date_default_timezone_get()) 369 | ); 370 | $scheduled = clone ($this->now); 371 | 372 | if (isset($options['scheduled'])) { 373 | switch (true) { 374 | case is_numeric($options['scheduled']): 375 | $scheduled = new DateTime( 376 | sprintf("@%d", (int) $options['scheduled']), 377 | new DateTimeZone(date_default_timezone_get()) 378 | ); 379 | break; 380 | case is_string($options['scheduled']): 381 | $scheduled = new DateTime($options['scheduled'], new DateTimeZone(date_default_timezone_get())); 382 | break; 383 | case $options['scheduled'] instanceof DateTime: 384 | $scheduled = $options['scheduled']; 385 | break; 386 | } 387 | } 388 | 389 | if (isset($options['delay'])) { 390 | switch (true) { 391 | case is_numeric($options['delay']): 392 | $delay = new DateInterval(sprintf("PT%dS", abs((int) $options['delay']))); 393 | $delay->invert = ($options['delay'] < 0) ? 1 : 0; 394 | break; 395 | case is_string($options['delay']): 396 | try { 397 | // first try ISO 8601 duration specification 398 | $delay = new DateInterval($options['delay']); 399 | } catch (\Exception $e) { 400 | // then try normal date parser 401 | $delay = DateInterval::createFromDateString($options['delay']); 402 | } 403 | break; 404 | case $options['delay'] instanceof DateInterval: 405 | $delay = $options['delay']; 406 | break; 407 | default: 408 | $delay = null; 409 | } 410 | 411 | if ($delay instanceof DateInterval) { 412 | $scheduled->add($delay); 413 | } 414 | } 415 | 416 | return $scheduled; 417 | } 418 | 419 | /** 420 | * Cleans old jobs in the table according to the configured lifetime of successful and failed jobs. 421 | */ 422 | protected function purge(): void 423 | { 424 | if ($this->options->getBuriedLifetime() > static::LIFETIME_UNLIMITED) { 425 | $options = ['delay' => - ($this->options->getBuriedLifetime() * 60)]; 426 | $buriedLifetime = $this->parseOptionsToDateTime($options); 427 | 428 | $delete = 'DELETE FROM ' . $this->options->getTableName() . ' ' . 429 | 'WHERE finished < ? AND status = ? AND queue = ? AND finished IS NOT NULL'; 430 | 431 | $this->connection->executeUpdate( 432 | $delete, 433 | [$buriedLifetime->format('Y-m-d H:i:s.u'), static::STATUS_BURIED, $this->getName()], 434 | [Types::STRING, Types::INTEGER, Types::STRING] 435 | ); 436 | } 437 | 438 | if ($this->options->getDeletedLifetime() > static::LIFETIME_UNLIMITED) { 439 | $options = ['delay' => - ($this->options->getDeletedLifetime() * 60)]; 440 | $deletedLifetime = $this->parseOptionsToDateTime($options); 441 | 442 | $delete = 'DELETE FROM ' . $this->options->getTableName() . ' ' . 443 | 'WHERE finished < ? AND status = ? AND queue = ? AND finished IS NOT NULL'; 444 | 445 | $this->connection->executeUpdate( 446 | $delete, 447 | [$deletedLifetime->format('Y-m-d H:i:s.u'), static::STATUS_DELETED, $this->getName()], 448 | [Types::STRING, Types::INTEGER, Types::STRING] 449 | ); 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/Queue/DoctrineQueueInterface.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach( 20 | WorkerEventInterface::EVENT_PROCESS_JOB, 21 | [$this, 'onClear'], 22 | 1000 23 | ); 24 | } 25 | 26 | public function onClear(ProcessJobEvent $event): void 27 | { 28 | /** @var ObjectManagerAwareInterface $job */ 29 | $job = $event->getJob(); 30 | 31 | 32 | if (! ($job instanceof ObjectManagerAwareInterface || $job instanceof DMObjectManagerAwareInterface)) { 33 | return; 34 | } 35 | 36 | if (!$manager = $job->getObjectManager()) { 37 | return; 38 | } 39 | 40 | $manager->clear(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Strategy/IdleNapStrategy.php: -------------------------------------------------------------------------------- 1 | napDuration = $napDuration; 24 | } 25 | 26 | public function getNapDuration(): int 27 | { 28 | return $this->napDuration; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function attach(EventManagerInterface $events, $priority = 1): void 35 | { 36 | $this->listeners[] = $events->attach( 37 | WorkerEventInterface::EVENT_PROCESS_IDLE, 38 | [$this, 'onIdle'], 39 | 1 40 | ); 41 | } 42 | 43 | public function onIdle(ProcessIdleEvent $event): void 44 | { 45 | $queue = $event->getQueue(); 46 | 47 | if ($queue instanceof DoctrineQueueInterface) { 48 | sleep($this->napDuration); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Worker/DoctrineWorker.php: -------------------------------------------------------------------------------- 1 | execute(); 30 | $queue->delete($job); 31 | 32 | return ProcessJobEvent::JOB_STATUS_SUCCESS; 33 | } catch (ReleasableException $exception) { 34 | $queue->release($job, $exception->getOptions()); 35 | 36 | return ProcessJobEvent::JOB_STATUS_FAILURE_RECOVERABLE; 37 | } catch (BuryableException $exception) { 38 | $queue->bury($job, $exception->getOptions()); 39 | 40 | return ProcessJobEvent::JOB_STATUS_FAILURE; 41 | } catch (Throwable $exception) { 42 | $queue->bury($job, [ 43 | 'message' => $exception->getMessage(), 44 | 'trace' => $exception->getTraceAsString() 45 | ]); 46 | 47 | return ProcessJobEvent::JOB_STATUS_FAILURE; 48 | } 49 | } 50 | } 51 | --------------------------------------------------------------------------------