├── .styleci.yml ├── src ├── Contracts │ └── QueueButtlerBatchWorkerInterface.php ├── Exceptions │ └── StopBatch.php ├── Laravel │ ├── README.md │ ├── Looping.php │ ├── WorkerOptions.php │ ├── WorkCommand.php │ └── Worker.php ├── QueueButlerServiceProvider.php ├── BatchOptions.php ├── BatchCommand.php └── BatchWorker.php ├── CHANGELOG.md ├── .editorconfig ├── phpunit.xml ├── LICENSE.md ├── CONTRIBUTING.md ├── composer.json └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /src/Contracts/QueueButtlerBatchWorkerInterface.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 40 | $this->connectionName = $connectionName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Justin Fossey 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 | -------------------------------------------------------------------------------- /src/QueueButlerServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(QueueButtlerBatchWorkerInterface::class, function ($app) { 33 | $isDownForMaintenance = function () { 34 | return $this->app->isDownForMaintenance(); 35 | }; 36 | 37 | return new BatchWorker( 38 | $app['queue'], 39 | $app['events'], 40 | $app[ExceptionHandler::class], 41 | $isDownForMaintenance 42 | ); 43 | }); 44 | 45 | $this->commands($this->commands); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/JFossey/QueueButler). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-chefs/queue-butler", 3 | "type": "library", 4 | "description": "Laravel Artisan commands that make it easy to run job queues using the Scheduler without the need for installing for running the Queue Daemon or installing Supervisor, allowing for effectively running Job Queues from a cron via the Scheduler.", 5 | "keywords": [ 6 | "QueueButler" 7 | ], 8 | "homepage": "https://github.com/web-chefs/QueueButler", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Justin Fossey" 13 | } 14 | ], 15 | "require": { 16 | "php": "~7.0", 17 | "laravel/framework": ">=5.5" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~5.7", 21 | "squizlabs/php_codesniffer": "^2.3", 22 | "doctrine/dbal": "^2.5", 23 | "web-chefs/laravel-app-spawn": ">=1.5" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "WebChefs\\QueueButler\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "WebChefs\\QueueButler\\Tests\\": "tests" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "phpunit", 37 | "check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests", 38 | "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests" 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "WebChefs\\QueueButler\\QueueButlerServiceProvider" 44 | ] 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /src/BatchOptions.php: -------------------------------------------------------------------------------- 1 | timeLimit = (int)$timeLimit; 59 | $this->jobLimit = (int)$jobLimit; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Laravel/WorkerOptions.php: -------------------------------------------------------------------------------- 1 | delay = $delay; 81 | $this->sleep = $sleep; 82 | $this->force = $force; 83 | $this->memory = $memory; 84 | $this->timeout = $timeout; 85 | $this->maxTries = $maxTries; 86 | $this->stopWhenEmpty = $stopWhenEmpty; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/BatchCommand.php: -------------------------------------------------------------------------------- 1 | downForMaintenance() && $this->option('once')) { 47 | return $this->worker->sleep($this->option('sleep')); 48 | } 49 | 50 | // We'll listen to the processed and failed events so we can write information 51 | // to the console as jobs are processed, which will let the developer watch 52 | // which jobs are coming through a queue and be informed on its progress. 53 | $this->listenForEvents(); 54 | 55 | $connection = $this->argument('connection') 56 | ?: $this->laravel['config']['queue.default']; 57 | 58 | // We need to get the right queue for the connection which is set in the queue 59 | // configuration file for the application. We will pull it based on the set 60 | // connection being run for the queue operation currently being executed. 61 | $queue = $this->getQueue($connection); 62 | 63 | return $this->runWorker( 64 | $connection, $queue 65 | ); 66 | } 67 | 68 | /** 69 | * Run the worker instance. 70 | * 71 | * @param string $connection 72 | * @param string $queue 73 | * 74 | * @return integer 75 | */ 76 | protected function runWorker($connection, $queue) 77 | { 78 | $this->worker->setCache($this->laravel['cache']->driver()); 79 | 80 | return $this->worker->batch( $connection, $queue, $this->gatherWorkerOptions() ); 81 | } 82 | 83 | /** 84 | * Gather all of the queue worker options as a single object. 85 | * 86 | * @return BatchOptions 87 | */ 88 | protected function gatherWorkerOptions(): WorkerOptions 89 | { 90 | return new BatchOptions( 91 | $this->option('delay'), 92 | $this->option('memory'), 93 | $this->option('timeout'), 94 | $this->option('sleep'), 95 | $this->option('tries'), 96 | $this->option('force'), 97 | $this->option('stop-when-empty'), 98 | $this->option('time-limit'), 99 | $this->option('job-limit') 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/BatchWorker.php: -------------------------------------------------------------------------------- 1 | options = $options; 44 | $this->startTime = microtime(true); 45 | $this->jobCount = 0; 46 | $this->exitCode = null; 47 | 48 | return $this->batchDaemon($connectionName, $queue, $options); 49 | } 50 | 51 | /** 52 | * Listen to the given queue in a loop. 53 | * 54 | * @param string $connectionName 55 | * @param string $queue 56 | * @param \Illuminate\Queue\WorkerOptions $options 57 | * 58 | * @return void 59 | */ 60 | public function batchDaemon($connectionName, $queue, WorkerOptions $options) 61 | { 62 | if ($this->supportsAsyncSignals()) { 63 | $this->listenForSignals(); 64 | } 65 | 66 | $lastRestart = $this->getTimestampOfLastQueueRestart(); 67 | 68 | while (true) { 69 | // Before reserving any jobs, we will make sure this queue is not paused and 70 | // if it is we will just pause this worker for a given amount of time and 71 | // make sure we do not need to kill this worker process off completely. 72 | if (! $this->daemonShouldRun($options, $connectionName, $queue)) { 73 | $this->pauseWorker($options, $lastRestart); 74 | 75 | continue; 76 | } 77 | 78 | // First, we will attempt to get the next job off of the queue. We will also 79 | // register the timeout handler and reset the alarm for this job so it is 80 | // not stuck in a frozen state forever. Then, we can fire off this job. 81 | $job = $this->getNextJob( 82 | $this->manager->connection($connectionName), $queue 83 | ); 84 | 85 | if ($this->supportsAsyncSignals()) { 86 | $this->registerTimeoutHandler($job, $options); 87 | } 88 | 89 | // If the daemon should run (not in maintenance mode, etc.), then we can run 90 | // fire off this job for processing. Otherwise, we will need to sleep the 91 | // worker so no more jobs are processed until they should be processed. 92 | if ($job) { 93 | $this->runJob($job, $connectionName, $options); 94 | } else { 95 | $this->sleep($options->sleep); 96 | } 97 | 98 | if ($this->supportsAsyncSignals()) { 99 | $this->resetTimeoutHandler(); 100 | } 101 | 102 | // Finally, we will check to see if we have exceeded our memory limits or if 103 | // the queue should restart based on other indications. If so, we'll stop 104 | // this worker and let whatever is "monitoring" it restart the process. 105 | $this->stopIfNecessary($options, $lastRestart, $job); 106 | 107 | // gracefully exit loop and batch if we have a exit code 108 | if ($this->exitCode !== null) { 109 | // return exit code 110 | return $this->exitCode; 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * Raise the after queue job event. 117 | * 118 | * @param string $connectionName 119 | * @param \Illuminate\Contracts\Queue\Job $job 120 | * 121 | * @return void 122 | */ 123 | protected function raiseAfterJobEvent($connectionName, $job) 124 | { 125 | $this->jobCount++; 126 | parent::raiseAfterJobEvent($connectionName, $job); 127 | $this->checkLimits(); 128 | } 129 | 130 | /** 131 | * Stop the process if necessary. 132 | * 133 | * @param WorkerOptions $options 134 | * @param int $lastRestart 135 | * 136 | * @return void 137 | */ 138 | protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $job = null) 139 | { 140 | parent::stopIfNecessary($options, $lastRestart, $job); 141 | $this->checkLimits(); 142 | } 143 | 144 | /** 145 | * Stop listening and bail out of the script. 146 | * 147 | * @param int $status 148 | * 149 | * @return void 150 | */ 151 | public function stop($status = 0) 152 | { 153 | $this->events->dispatch(new WorkerStopping($status)); 154 | 155 | // Cleanly handle stopping a batch without resorting to killing the process 156 | // This is required for end to end testing 157 | $this->exitCode = $status; 158 | } 159 | 160 | /** 161 | * Sleep the script for a given number of seconds. 162 | * 163 | * @param int $seconds 164 | * 165 | * @return void 166 | */ 167 | public function sleep($seconds) 168 | { 169 | $this->checkLimits(); 170 | parent::sleep($seconds); 171 | } 172 | 173 | /** 174 | * Check our batch limits and stop the command if we reach a limit. 175 | * 176 | * @param WorkerOptions $options 177 | * 178 | * @return void 179 | */ 180 | protected function checkLimits() 181 | { 182 | if ($this->isTimeLimit() || $this->isJobLimit()) { 183 | $this->stop(); 184 | } 185 | } 186 | 187 | /** 188 | * Check if the batch timelimit has been reached. 189 | * 190 | * @return boolean 191 | */ 192 | protected function isTimeLimit(): bool 193 | { 194 | return (microtime(true) - $this->startTime) > $this->options->timeLimit; 195 | } 196 | 197 | /** 198 | * Check if the batch job limit has been reached. 199 | * 200 | * @return boolean 201 | */ 202 | protected function isJobLimit(): bool 203 | { 204 | return $this->jobCount >= $this->options->jobLimit; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Laravel/WorkCommand.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 84 | $this->worker = $worker; 85 | } 86 | 87 | /** 88 | * Execute the console command. 89 | * 90 | * @return void 91 | */ 92 | public function handle() 93 | { 94 | if ($this->downForMaintenance() && $this->option('once')) { 95 | return $this->worker->sleep($this->option('sleep')); 96 | } 97 | 98 | // We'll listen to the processed and failed events so we can write information 99 | // to the console as jobs are processed, which will let the developer watch 100 | // which jobs are coming through a queue and be informed on its progress. 101 | $this->listenForEvents(); 102 | 103 | $connection = $this->argument('connection') 104 | ?: $this->laravel['config']['queue.default']; 105 | 106 | // We need to get the right queue for the connection which is set in the queue 107 | // configuration file for the application. We will pull it based on the set 108 | // connection being run for the queue operation currently being executed. 109 | $queue = $this->getQueue($connection); 110 | 111 | $this->runWorker( 112 | $connection, $queue 113 | ); 114 | } 115 | 116 | /** 117 | * Run the worker instance. 118 | * 119 | * @param string $connection 120 | * @param string $queue 121 | * @return array 122 | */ 123 | protected function runWorker($connection, $queue) 124 | { 125 | $this->worker->setCache($this->cache); 126 | 127 | return $this->worker->{$this->option('once') ? 'runNextJob' : 'daemon'}( 128 | $connection, $queue, $this->gatherWorkerOptions() 129 | ); 130 | } 131 | 132 | /** 133 | * Gather all of the queue worker options as a single object. 134 | * 135 | * @return \Illuminate\Queue\WorkerOptions 136 | */ 137 | protected function gatherWorkerOptions() 138 | { 139 | return new WorkerOptions( 140 | $this->option('delay'), 141 | $this->option('memory'), 142 | $this->option('timeout'), 143 | $this->option('sleep'), 144 | $this->option('tries'), 145 | $this->option('force'), 146 | $this->option('stop-when-empty') 147 | ); 148 | } 149 | 150 | /** 151 | * Listen for the queue events in order to update the console output. 152 | * 153 | * @return void 154 | */ 155 | protected function listenForEvents() 156 | { 157 | $this->laravel['events']->listen(JobProcessing::class, function ($event) { 158 | $this->writeOutput($event->job, 'starting'); 159 | }); 160 | 161 | $this->laravel['events']->listen(JobProcessed::class, function ($event) { 162 | $this->writeOutput($event->job, 'success'); 163 | }); 164 | 165 | $this->laravel['events']->listen(JobFailed::class, function ($event) { 166 | $this->writeOutput($event->job, 'failed'); 167 | 168 | $this->logFailedJob($event); 169 | }); 170 | } 171 | 172 | /** 173 | * Write the status output for the queue worker. 174 | * 175 | * @param \Illuminate\Contracts\Queue\Job $job 176 | * @param string $status 177 | * @return void 178 | */ 179 | protected function writeOutput(Job $job, $status) 180 | { 181 | switch ($status) { 182 | case 'starting': 183 | return $this->writeStatus($job, 'Processing', 'comment'); 184 | case 'success': 185 | return $this->writeStatus($job, 'Processed', 'info'); 186 | case 'failed': 187 | return $this->writeStatus($job, 'Failed', 'error'); 188 | } 189 | } 190 | 191 | /** 192 | * Format the status output for the queue worker. 193 | * 194 | * @param \Illuminate\Contracts\Queue\Job $job 195 | * @param string $status 196 | * @param string $type 197 | * @return void 198 | */ 199 | protected function writeStatus(Job $job, $status, $type) 200 | { 201 | $this->output->writeln(sprintf( 202 | "<{$type}>[%s][%s] %s %s", 203 | Carbon::now()->format('Y-m-d H:i:s'), 204 | $job->getJobId(), 205 | str_pad("{$status}:", 11), $job->resolveName() 206 | )); 207 | } 208 | 209 | /** 210 | * Store a failed job event. 211 | * 212 | * @param \Illuminate\Queue\Events\JobFailed $event 213 | * @return void 214 | */ 215 | protected function logFailedJob(JobFailed $event) 216 | { 217 | $this->laravel['queue.failer']->log( 218 | $event->connectionName, $event->job->getQueue(), 219 | $event->job->getRawBody(), $event->exception 220 | ); 221 | } 222 | 223 | /** 224 | * Get the queue name for the worker. 225 | * 226 | * @param string $connection 227 | * @return string 228 | */ 229 | protected function getQueue($connection) 230 | { 231 | return $this->option('queue') ?: $this->laravel['config']->get( 232 | "queue.connections.{$connection}.queue", 'default' 233 | ); 234 | } 235 | 236 | /** 237 | * Determine if the worker should run in maintenance mode. 238 | * 239 | * @return bool 240 | */ 241 | protected function downForMaintenance() 242 | { 243 | return $this->option('force') ? false : $this->laravel->isDownForMaintenance(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QueueButler 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Total Downloads][ico-downloads]][link-downloads] 6 | [![Build Status][ico-travis]][link-travis] 7 | 8 | Laravel Artisan commands that make it easy to run job queues using the Scheduler without the need for installing for running the Queue Daemon or installing Supervisor. 9 | 10 | This is ideal for shared hosting or situations where you are not fully in control of the services or management of your hosting infrastructure and all you have access to is a Cron. 11 | 12 | ## Versions Support Matrix 13 | 14 | | QueueButler | PHP | Laravel | 15 | | :---------: | :-------: | :-------: | 16 | | 1.4 | 5.6 - 7.3 | 5.3 - 5.6 | 17 | | 2.0 | 7.0 - 7.4 | 5.5 - 7.x | 18 | 19 | **Note:** The PHP version support corresponds with the Laravel PHP support. 20 | 21 | ## Laravel 8+ 22 | 23 | As of Laravel 8, the QueueButler package is no longer required for running batch job queue processing via the cron scheduler. 24 | 25 | Laravel 8 has introduced the `max-jobs` and `max-time` options, making it possible to use Laravel's built-in `queue:work` command. 26 | 27 | ``` php 28 | $schedule->command('queue:work --queue=default,something,somethingelse --max-time=50 --max-jobs=100 --sleep=10 --tries=3 --backoff=60') 29 | ->everyMinute() 30 | ->runInBackground() 31 | ->withoutOverlapping(1); 32 | ``` 33 | 34 | To run multiple queue workers in Laravel 8 for the same queues it is recommended to use the `--name=` option. 35 | 36 | ``` php 37 | $schedule->command('queue:work --name=job-worker-1 --queue=default,priority --max-time=280 --max-jobs=1000 --sleep=5 --tries=3 --backoff=180') 38 | ->everyMinute() 39 | ->runInBackground() 40 | ->withoutOverlapping(5); 41 | 42 | $schedule->command('queue:work --name=job-worker-2 --queue=default,priority --max-time=280 --max-jobs=1000 --sleep=5 --tries=3 --backoff=180') 43 | ->everyMinute() 44 | ->runInBackground() 45 | ->withoutOverlapping(5); 46 | ``` 47 | 48 | ## Install 49 | 50 | __Via Composer__ 51 | 52 | ``` bash 53 | $ composer require web-chefs/queue-butler 54 | ``` 55 | 56 | __Add Service Provider to `config/app.php`__ 57 | 58 | ```php 59 | 'providers' => [ 60 | // Other Service Providers 61 | WebChefs\QueueButler\QueueButlerServiceProvider::class, 62 | ]; 63 | ``` 64 | 65 | ## Benefits 66 | 67 | * Works on any standard hosting system that supports a cron. 68 | * Resilient to failures and will automatically be restarted by the scheduler. 69 | * Newly deployed code will automatically be updated and run the next time the scheduler runs. 70 | * Can create multiple queue batch commands based on volume and types of jobs. 71 | 72 | ## Usage 73 | 74 | ### Artisan command 75 | 76 | Run `queue:batch` artisan command, supports many of the same options as `queue:work`. Two additional options `time-limit` in seconds (defaults to 60 seconds) and 'job-limit' (defaults to 100) need to be set based on your Scheduling setup. 77 | 78 | **Example:** 79 | 80 | Run batch for 4 min 30 seconds or 1000 jobs, then stop. 81 | 82 | `artisan queue:batch --time-limit=270 --job-limit=1000` 83 | 84 | ### Scheduler 85 | 86 | In your `App\Console\Kernel.php` in the `schedule()` method add your `queue:batch` commands. 87 | 88 | Because job queue processing is a long-running process setting `runInBackground()` is highly recommended, else each `queue:batch` command will hold up all scheduled preceding items set up to run after it. 89 | 90 | The Scheduler requires a __Cron__ to be setup. See [Laravel documentation](https://laravel.com/docs/master/scheduling) for details on how the Scheduler works. 91 | 92 | **Queue Batch Life-cycle** 93 | 94 | When scheduling batch processing of a queue, a command will be scheduled to run at a set interval, for a set amount of time or number of jobs and then exits. 95 | 96 | It is not recommended to schedule the timeout and the scheduler interval to be the same amount of time as you are more likely going to overlap where the scheduler is called to start the next batch but the last has not finished. 97 | 98 | It is best to consider how long your average job takes to complete and then decide on how close you want the batch end time to be to the next start time. 99 | 100 | This period between batch commands where no jobs are being processed is your life-cycle overhead. 101 | 102 | **Impact Of Code Deployments** 103 | 104 | When a queue batch process is started no new code changes will take effect and you will need to wait for a life-cycle to complete and then on the next run newly deployed code will be run and changes reflect. 105 | 106 | Errors are rare but can occur when new code is deployed. This happens when a class that has already been parsed and loaded into memory includes a class that was changed after a deploy. In this situation, the first class is running on the old version and the second class is running on the new version. 107 | 108 | This is a very rare situation but if you want to avoid this you can look to time your deploys to take place during the life-cycle overhead window. 109 | 110 | **`withoutOverlapping()` and Mutex cache expiry** 111 | 112 | It is recommended to not overlap the same queue batch commands by using the `withoutOverlapping` scheduler function. 113 | 114 | When using `withoutOverlapping()` a cache Mutex is used to keep track of running jobs. The default cache expiry is 1440 minutes (24 hours). 115 | 116 | If your batch process is interrupted the scheduler will ignore the task for the time of the expiry and you will have no jobs processing for 24 hours. The only way to resolve this is to clear the cache or manually remove the batch processes cache entry. 117 | 118 | To prevent long-running cache expiries it is advised to match your cache expiry time with your task frequency so when a batch command is interrupted it will be restarted by the scheduler within one scheduled life-cycle. 119 | 120 | _Note:_ See Laravel 5.3 example that is different from later versions. 121 | 122 | **Basic Example:** 123 | 124 | Running a batch every minute for 50 seconds or 100 jobs in the background using `runInBackground()`, then stopping. 125 | 126 | To prevent overlapping batches running simultaneously with `withoutOverlapping()` matching the life-cycle time in minutes. 127 | 128 | If no jobs are found in the queue sleep for 10 seconds before polling the queue for new jobs. 129 | 130 | ``` php 131 | $schedule->command('queue:batch --queue=default,something,somethingelse --time-limit=50 --job-limit=100 --sleep=10') 132 | ->everyMinute() 133 | ->runInBackground() 134 | ->withoutOverlapping(1); 135 | ``` 136 | 137 | **Recommended Example:** 138 | 139 | For most use cases a 5-minute life-cycle will work well by creating a batch command that runs for 4 min 40 seconds or 1000 jobs, with a maxim of 20 seconds life-cycle overhead. 140 | 141 | ``` php 142 | $schedule->command('queue:batch --time-limit=280 --job-limit=1000 --sleep=10') 143 | ->everyFiveMinutes() 144 | ->runInBackground() 145 | ->withoutOverlapping(5); 146 | ``` 147 | 148 | **Multiple Queues Example:** 149 | 150 | If your application is processing a larger number of jobs using multiple queues, it is recommended setting up different batch scheduler commands per queue. 151 | 152 | ``` php 153 | // Low volume queues 154 | $schedule->command('queue:batch --queue=default,something,somethingelse --time-limit=50 --job-limit=100 --sleep=10') 155 | ->everyMinute() 156 | ->runInBackground() 157 | ->withoutOverlapping(1); 158 | 159 | // High volume dedicated "notifications" queue 160 | $schedule->command('queue:batch --queue=notifications, --time-limit=175 --job-limit=500 --sleep=2') 161 | ->everyThreeMinutes() 162 | ->runInBackground() 163 | ->withoutOverlapping(1); 164 | ``` 165 | 166 | It is not recommended to create multiple batches to the same queue and rather limit one process per queue. Also, see database driver and deadlocks. 167 | 168 | **Laravel 5.3 example** 169 | 170 | In Laravel 5.3 we needed to set the `expiresAt` cache expiry mutex directly. 171 | 172 | ```php 173 | // Create Batch Job Queue Processor Task 174 | $scheduledEvent = $schedule->command('queue:batch --time-limit=280 --job-limit=1000 --sleep=10'); 175 | 176 | // Match cache expiry with frequency 177 | // Set cache mutex expiry to One min (default is 1440) 178 | $scheduledEvent->expiresAt = 5; 179 | $scheduledEvent->everyFiveMinutes() 180 | ->withoutOverlapping() 181 | ->runInBackground(); 182 | ``` 183 | 184 | ### Database Queue Driver 185 | 186 | The Laravel queue driver is a common option when used with Queue Butler. The only downside is that there is a limit to the number of simultaneous queue processing commands a MySQL database can support as each process will be trying to lock the tip of the queue when processing new jobs. 187 | 188 | > Error: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction. 189 | 190 | These errors are not serious as jobs will continue to be processed, and the biggest impact will be the speed of job processing as processes wait. 191 | 192 | We are not aware of similar issues related to using PostgreSQL. 193 | 194 | ## Standards 195 | 196 | * psr-1 197 | * psr-2 198 | * psr-4 199 | 200 | ## Change log 201 | 202 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 203 | 204 | ## Contributing 205 | 206 | All code submissions will only be evaluated and accepted as pull-requests. If you have any questions or find any bugs please feel free to open an issue. 207 | 208 | ## Credits 209 | 210 | - [All Contributors][link-contributors] 211 | 212 | ## License 213 | 214 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 215 | 216 | [ico-version]: https://img.shields.io/packagist/v/web-chefs/queue-butler.svg?style=flat-square 217 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 218 | [ico-downloads]: https://img.shields.io/packagist/dt/web-chefs/queue-butler.svg?style=flat-square 219 | [ico-travis]: https://img.shields.io/travis/web-chefs/QueueButler/master.svg?style=flat-square 220 | 221 | [link-packagist]: https://packagist.org/packages/web-chefs/queue-butler 222 | [link-travis]: https://travis-ci.org/web-chefs/QueueButler 223 | [link-downloads]: https://packagist.org/packages/web-chefs/queue-butler 224 | [link-author]: https://github.com/JFossey 225 | [link-contributors]: ../../contributors 226 | -------------------------------------------------------------------------------- /src/Laravel/Worker.php: -------------------------------------------------------------------------------- 1 | events = $events; 108 | $this->manager = $manager; 109 | $this->exceptions = $exceptions; 110 | $this->isDownForMaintenance = $isDownForMaintenance; 111 | } 112 | 113 | /** 114 | * Listen to the given queue in a loop. 115 | * 116 | * @param string $connectionName 117 | * @param string $queue 118 | * @param \Illuminate\Queue\WorkerOptions $options 119 | * 120 | * @return void 121 | */ 122 | public function daemon($connectionName, $queue, WorkerOptions $options) 123 | { 124 | if ($this->supportsAsyncSignals()) { 125 | $this->listenForSignals(); 126 | } 127 | 128 | $lastRestart = $this->getTimestampOfLastQueueRestart(); 129 | 130 | while (true) { 131 | // Before reserving any jobs, we will make sure this queue is not paused and 132 | // if it is we will just pause this worker for a given amount of time and 133 | // make sure we do not need to kill this worker process off completely. 134 | if (! $this->daemonShouldRun($options, $connectionName, $queue)) { 135 | $this->pauseWorker($options, $lastRestart); 136 | 137 | continue; 138 | } 139 | 140 | // First, we will attempt to get the next job off of the queue. We will also 141 | // register the timeout handler and reset the alarm for this job so it is 142 | // not stuck in a frozen state forever. Then, we can fire off this job. 143 | $job = $this->getNextJob( 144 | $this->manager->connection($connectionName), $queue 145 | ); 146 | 147 | if ($this->supportsAsyncSignals()) { 148 | $this->registerTimeoutHandler($job, $options); 149 | } 150 | 151 | // If the daemon should run (not in maintenance mode, etc.), then we can run 152 | // fire off this job for processing. Otherwise, we will need to sleep the 153 | // worker so no more jobs are processed until they should be processed. 154 | if ($job) { 155 | $this->runJob($job, $connectionName, $options); 156 | } else { 157 | $this->sleep($options->sleep); 158 | } 159 | 160 | if ($this->supportsAsyncSignals()) { 161 | $this->resetTimeoutHandler(); 162 | } 163 | 164 | // Finally, we will check to see if we have exceeded our memory limits or if 165 | // the queue should restart based on other indications. If so, we'll stop 166 | // this worker and let whatever is "monitoring" it restart the process. 167 | $this->stopIfNecessary($options, $lastRestart, $job); 168 | } 169 | } 170 | 171 | /** 172 | * Register the worker timeout handler. 173 | * 174 | * @param \Illuminate\Contracts\Queue\Job|null $job 175 | * @param \Illuminate\Queue\WorkerOptions $options 176 | * 177 | * @return void 178 | */ 179 | protected function registerTimeoutHandler($job, WorkerOptions $options) 180 | { 181 | // We will register a signal handler for the alarm signal so that we can kill this 182 | // process if it is running too long because it has frozen. This uses the async 183 | // signals supported in recent versions of PHP to accomplish it conveniently. 184 | pcntl_signal(SIGALRM, function () use ($job, $options) { 185 | if ($job) { 186 | $this->markJobAsFailedIfWillExceedMaxAttempts( 187 | $job->getConnectionName(), $job, (int) $options->maxTries, $this->maxAttemptsExceededException($job) 188 | ); 189 | } 190 | 191 | $this->kill(1); 192 | }); 193 | 194 | pcntl_alarm( 195 | max($this->timeoutForJob($job, $options), 0) 196 | ); 197 | } 198 | 199 | /** 200 | * Reset the worker timeout handler. 201 | * 202 | * @return void 203 | */ 204 | protected function resetTimeoutHandler() 205 | { 206 | pcntl_alarm(0); 207 | } 208 | 209 | /** 210 | * Get the appropriate timeout for the given job. 211 | * 212 | * @param \Illuminate\Contracts\Queue\Job|null $job 213 | * @param \Illuminate\Queue\WorkerOptions $options 214 | * 215 | * @return int 216 | */ 217 | protected function timeoutForJob($job, WorkerOptions $options) 218 | { 219 | return $job && ! is_null($job->timeout()) ? $job->timeout() : $options->timeout; 220 | } 221 | 222 | /** 223 | * Determine if the daemon should process on this iteration. 224 | * 225 | * @param \Illuminate\Queue\WorkerOptions $options 226 | * @param string $connectionName 227 | * @param string $queue 228 | * 229 | * @return bool 230 | */ 231 | protected function daemonShouldRun(WorkerOptions $options, $connectionName, $queue) 232 | { 233 | return ! ((($this->isDownForMaintenance)() && ! $options->force) || 234 | $this->paused || 235 | $this->events->until(new Looping($connectionName, $queue)) === false); 236 | } 237 | 238 | /** 239 | * Pause the worker for the current loop. 240 | * 241 | * @param \Illuminate\Queue\WorkerOptions $options 242 | * @param int $lastRestart 243 | * 244 | * @return void 245 | */ 246 | protected function pauseWorker(WorkerOptions $options, $lastRestart) 247 | { 248 | $this->sleep($options->sleep > 0 ? $options->sleep : 1); 249 | 250 | $this->stopIfNecessary($options, $lastRestart); 251 | } 252 | 253 | /** 254 | * Stop the process if necessary. 255 | * 256 | * @param \Illuminate\Queue\WorkerOptions $options 257 | * @param int $lastRestart 258 | * @param mixed $job 259 | * 260 | * @return void 261 | */ 262 | protected function stopIfNecessary(WorkerOptions $options, $lastRestart, $job = null) 263 | { 264 | if ($this->shouldQuit) { 265 | $this->stop(); 266 | } elseif ($this->memoryExceeded($options->memory)) { 267 | $this->stop(12); 268 | } elseif ($this->queueShouldRestart($lastRestart)) { 269 | $this->stop(); 270 | } elseif ($options->stopWhenEmpty && is_null($job)) { 271 | $this->stop(); 272 | } 273 | } 274 | 275 | /** 276 | * Process the next job on the queue. 277 | * 278 | * @param string $connectionName 279 | * @param string $queue 280 | * @param \Illuminate\Queue\WorkerOptions $options 281 | * 282 | * @return void 283 | */ 284 | public function runNextJob($connectionName, $queue, WorkerOptions $options) 285 | { 286 | $job = $this->getNextJob( 287 | $this->manager->connection($connectionName), $queue 288 | ); 289 | 290 | // If we're able to pull a job off of the stack, we will process it and then return 291 | // from this method. If there is no job on the queue, we will "sleep" the worker 292 | // for the specified number of seconds, then keep processing jobs after sleep. 293 | if ($job) { 294 | return $this->runJob($job, $connectionName, $options); 295 | } 296 | 297 | $this->sleep($options->sleep); 298 | } 299 | 300 | /** 301 | * Get the next job from the queue connection. 302 | * 303 | * @param \Illuminate\Contracts\Queue\Queue $connection 304 | * @param string $queue 305 | * 306 | * @return \Illuminate\Contracts\Queue\Job|null 307 | */ 308 | protected function getNextJob($connection, $queue) 309 | { 310 | try { 311 | foreach (explode(',', $queue) as $queue) { 312 | if (! is_null($job = $connection->pop($queue))) { 313 | return $job; 314 | } 315 | } 316 | } 317 | catch (Throwable $e) { 318 | $this->exceptions->report($e); 319 | 320 | $this->stopWorkerIfLostConnection($e); 321 | 322 | $this->sleep(1); 323 | } 324 | } 325 | 326 | /** 327 | * Process the given job. 328 | * 329 | * @param \Illuminate\Contracts\Queue\Job $job 330 | * @param string $connectionName 331 | * @param \Illuminate\Queue\WorkerOptions $options 332 | * 333 | * @return void 334 | */ 335 | protected function runJob($job, $connectionName, WorkerOptions $options) 336 | { 337 | try { 338 | return $this->process($connectionName, $job, $options); 339 | } 340 | catch (\Error $e) { 341 | dd($e); 342 | throw $e; 343 | } 344 | catch (Throwable $e) { 345 | dd($e); 346 | $this->exceptions->report($e); 347 | 348 | $this->stopWorkerIfLostConnection($e); 349 | } 350 | } 351 | 352 | /** 353 | * Stop the worker if we have lost connection to a database. 354 | * 355 | * @param \Throwable $e 356 | * 357 | * @return void 358 | */ 359 | protected function stopWorkerIfLostConnection($e) 360 | { 361 | if ($this->causedByLostConnection($e)) { 362 | $this->shouldQuit = true; 363 | } 364 | } 365 | 366 | /** 367 | * Process the given job from the queue. 368 | * 369 | * @param string $connectionName 370 | * @param \Illuminate\Contracts\Queue\Job $job 371 | * @param \Illuminate\Queue\WorkerOptions $options 372 | * 373 | * @return void 374 | * 375 | * @throws \Throwable 376 | */ 377 | public function process($connectionName, $job, WorkerOptions $options) 378 | { 379 | try { 380 | // First we will raise the before job event and determine if the job has already ran 381 | // over its maximum attempt limits, which could primarily happen when this job is 382 | // continually timing out and not actually throwing any exceptions from itself. 383 | $this->raiseBeforeJobEvent($connectionName, $job); 384 | 385 | // dd($job); 386 | 387 | $this->markJobAsFailedIfAlreadyExceedsMaxAttempts( 388 | $connectionName, $job, (int) $options->maxTries 389 | ); 390 | 391 | if ($job->isDeleted()) { 392 | return $this->raiseAfterJobEvent($connectionName, $job); 393 | } 394 | 395 | // Here we will fire off the job and let it process. We will catch any exceptions so 396 | // they can be reported to the developers logs, etc. Once the job is finished the 397 | // proper events will be fired to let any listeners know this job has finished. 398 | $job->fire(); 399 | 400 | $this->raiseAfterJobEvent($connectionName, $job); 401 | } catch (Throwable $e) { 402 | $this->handleJobException($connectionName, $job, $options, $e); 403 | } 404 | } 405 | 406 | /** 407 | * Handle an exception that occurred while the job was running. 408 | * 409 | * @param string $connectionName 410 | * @param \Illuminate\Contracts\Queue\Job $job 411 | * @param \Illuminate\Queue\WorkerOptions $options 412 | * @param \Throwable $e 413 | * 414 | * @return void 415 | * 416 | * @throws \Throwable 417 | */ 418 | protected function handleJobException($connectionName, $job, WorkerOptions $options, Throwable $e) 419 | { 420 | try { 421 | // First, we will go ahead and mark the job as failed if it will exceed the maximum 422 | // attempts it is allowed to run the next time we process it. If so we will just 423 | // go ahead and mark it as failed now so we do not have to release this again. 424 | if (! $job->hasFailed()) { 425 | $this->markJobAsFailedIfWillExceedMaxAttempts( 426 | $connectionName, $job, (int) $options->maxTries, $e 427 | ); 428 | 429 | if (method_exists($job, 'uuid') && method_exists($job, 'maxExceptions')) { 430 | $this->markJobAsFailedIfWillExceedMaxExceptions( 431 | $connectionName, $job, $e 432 | ); 433 | } 434 | } 435 | 436 | $this->raiseExceptionOccurredJobEvent( 437 | $connectionName, $job, $e 438 | ); 439 | } finally { 440 | // If we catch an exception, we will attempt to release the job back onto the queue 441 | // so it is not lost entirely. This'll let the job be retried at a later time by 442 | // another listener (or this same one). We will re-throw this exception after. 443 | if (! $job->isDeleted() && ! $job->isReleased() && ! $job->hasFailed()) { 444 | $job->release( 445 | method_exists($job, 'delaySeconds') && ! is_null($job->delaySeconds()) 446 | ? $job->delaySeconds() 447 | : $options->delay 448 | ); 449 | } 450 | } 451 | 452 | throw $e; 453 | } 454 | 455 | /** 456 | * Mark the given job as failed if it has exceeded the maximum allowed attempts. 457 | * 458 | * This will likely be because the job previously exceeded a timeout. 459 | * 460 | * @param string $connectionName 461 | * @param \Illuminate\Contracts\Queue\Job $job 462 | * @param int $maxTries 463 | * 464 | * @return void 465 | * 466 | * @throws \Throwable 467 | */ 468 | protected function markJobAsFailedIfAlreadyExceedsMaxAttempts($connectionName, $job, $maxTries) 469 | { 470 | $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; 471 | 472 | $timeoutAt = $job->timeoutAt(); 473 | 474 | if ($timeoutAt && Carbon::now()->getTimestamp() <= $timeoutAt) { 475 | return; 476 | } 477 | 478 | if (! $timeoutAt && ($maxTries === 0 || $job->attempts() <= $maxTries)) { 479 | return; 480 | } 481 | 482 | $this->failJob($job, $e = $this->maxAttemptsExceededException($job)); 483 | 484 | throw $e; 485 | } 486 | 487 | /** 488 | * Mark the given job as failed if it has exceeded the maximum allowed attempts. 489 | * 490 | * @param string $connectionName 491 | * @param \Illuminate\Contracts\Queue\Job $job 492 | * @param int $maxTries 493 | * @param \Throwable $e 494 | * 495 | * @return void 496 | */ 497 | protected function markJobAsFailedIfWillExceedMaxAttempts($connectionName, $job, $maxTries, Throwable $e) 498 | { 499 | $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; 500 | 501 | if ($job->timeoutAt() && $job->timeoutAt() <= Carbon::now()->getTimestamp()) { 502 | $this->failJob($job, $e); 503 | } 504 | 505 | if ($maxTries > 0 && $job->attempts() >= $maxTries) { 506 | $this->failJob($job, $e); 507 | } 508 | } 509 | 510 | /** 511 | * Mark the given job as failed if it has exceeded the maximum allowed attempts. 512 | * 513 | * @param string $connectionName 514 | * @param \Illuminate\Contracts\Queue\Job $job 515 | * @param \Throwable $e 516 | * 517 | * @return void 518 | */ 519 | protected function markJobAsFailedIfWillExceedMaxExceptions($connectionName, $job, Throwable $e) 520 | { 521 | if (! $this->cache || is_null($uuid = $job->uuid()) || 522 | is_null($maxExceptions = $job->maxExceptions())) { 523 | return; 524 | } 525 | 526 | if (! $this->cache->get('job-exceptions:'.$uuid)) { 527 | $this->cache->put('job-exceptions:'.$uuid, 0, Carbon::now()->addDay()); 528 | } 529 | 530 | if ($maxExceptions <= $this->cache->increment('job-exceptions:'.$uuid)) { 531 | $this->cache->forget('job-exceptions:'.$uuid); 532 | 533 | $this->failJob($job, $e); 534 | } 535 | } 536 | 537 | /** 538 | * Mark the given job as failed and raise the relevant event. 539 | * 540 | * @param \Illuminate\Contracts\Queue\Job $job 541 | * @param \Throwable $e 542 | * 543 | * @return void 544 | */ 545 | protected function failJob($job, Throwable $e) 546 | { 547 | return $job->fail($e); 548 | } 549 | 550 | /** 551 | * Raise the before queue job event. 552 | * 553 | * @param string $connectionName 554 | * @param \Illuminate\Contracts\Queue\Job $job 555 | * 556 | * @return void 557 | */ 558 | protected function raiseBeforeJobEvent($connectionName, $job) 559 | { 560 | $this->events->dispatch(new JobProcessing( 561 | $connectionName, $job 562 | )); 563 | } 564 | 565 | /** 566 | * Raise the after queue job event. 567 | * 568 | * @param string $connectionName 569 | * @param \Illuminate\Contracts\Queue\Job $job 570 | * 571 | * @return void 572 | */ 573 | protected function raiseAfterJobEvent($connectionName, $job) 574 | { 575 | $this->events->dispatch(new JobProcessed( 576 | $connectionName, $job 577 | )); 578 | } 579 | 580 | /** 581 | * Raise the exception occurred queue job event. 582 | * 583 | * @param string $connectionName 584 | * @param \Illuminate\Contracts\Queue\Job $job 585 | * @param \Throwable $e 586 | * 587 | * @return void 588 | */ 589 | protected function raiseExceptionOccurredJobEvent($connectionName, $job, Throwable $e) 590 | { 591 | $this->events->dispatch(new JobExceptionOccurred( 592 | $connectionName, $job, $e 593 | )); 594 | } 595 | 596 | /** 597 | * Determine if the queue worker should restart. 598 | * 599 | * @param int|null $lastRestart 600 | * @return bool 601 | */ 602 | protected function queueShouldRestart($lastRestart) 603 | { 604 | return $this->getTimestampOfLastQueueRestart() != $lastRestart; 605 | } 606 | 607 | /** 608 | * Get the last queue restart timestamp, or null. 609 | * 610 | * @return int|null 611 | */ 612 | protected function getTimestampOfLastQueueRestart() 613 | { 614 | if ($this->cache) { 615 | return $this->cache->get('illuminate:queue:restart'); 616 | } 617 | } 618 | 619 | /** 620 | * Enable async signals for the process. 621 | * 622 | * @return void 623 | */ 624 | protected function listenForSignals() 625 | { 626 | pcntl_async_signals(true); 627 | 628 | pcntl_signal(SIGTERM, function () { 629 | $this->shouldQuit = true; 630 | }); 631 | 632 | pcntl_signal(SIGUSR2, function () { 633 | $this->paused = true; 634 | }); 635 | 636 | pcntl_signal(SIGCONT, function () { 637 | $this->paused = false; 638 | }); 639 | } 640 | 641 | /** 642 | * Determine if "async" signals are supported. 643 | * 644 | * @return bool 645 | */ 646 | protected function supportsAsyncSignals() 647 | { 648 | return extension_loaded('pcntl') && function_exists('pcntl_async_signals'); 649 | } 650 | 651 | /** 652 | * Determine if the memory limit has been exceeded. 653 | * 654 | * @param int $memoryLimit 655 | * 656 | * @return bool 657 | */ 658 | public function memoryExceeded($memoryLimit) 659 | { 660 | return (memory_get_usage(true) / 1024 / 1024) >= $memoryLimit; 661 | } 662 | 663 | /** 664 | * Stop listening and bail out of the script. 665 | * 666 | * @param int $status 667 | * 668 | * @return void 669 | */ 670 | public function stop($status = 0) 671 | { 672 | $this->events->dispatch(new WorkerStopping($status)); 673 | 674 | exit($status); 675 | } 676 | 677 | /** 678 | * Kill the process. 679 | * 680 | * @param int $status 681 | * @return void 682 | */ 683 | public function kill($status = 0) 684 | { 685 | $this->events->dispatch(new WorkerStopping($status)); 686 | 687 | if (extension_loaded('posix')) { 688 | posix_kill(getmypid(), SIGKILL); 689 | } 690 | 691 | exit($status); 692 | } 693 | 694 | /** 695 | * Create an instance of MaxAttemptsExceededException. 696 | * 697 | * @param \Illuminate\Contracts\Queue\Job $job 698 | * 699 | * @return \Illuminate\Queue\MaxAttemptsExceededException 700 | */ 701 | protected function maxAttemptsExceededException($job) 702 | { 703 | return new MaxAttemptsExceededException( 704 | $job->resolveName().' has been attempted too many times or run too long. The job may have previously timed out.' 705 | ); 706 | } 707 | 708 | /** 709 | * Sleep the script for a given number of seconds. 710 | * 711 | * @param int|float $seconds 712 | * 713 | * @return void 714 | */ 715 | public function sleep($seconds) 716 | { 717 | if ($seconds < 1) { 718 | usleep($seconds * 1000000); 719 | } else { 720 | sleep($seconds); 721 | } 722 | } 723 | 724 | /** 725 | * Set the cache repository implementation. 726 | * 727 | * @param \Illuminate\Contracts\Cache\Repository $cache 728 | * 729 | * @return void 730 | */ 731 | public function setCache(CacheContract $cache) 732 | { 733 | $this->cache = $cache; 734 | } 735 | 736 | /** 737 | * Get the queue manager instance. 738 | * 739 | * @return \Illuminate\Queue\QueueManager 740 | */ 741 | public function getManager() 742 | { 743 | return $this->manager; 744 | } 745 | 746 | /** 747 | * Set the queue manager instance. 748 | * 749 | * @param \Illuminate\Contracts\Queue\Factory $manager 750 | * @return void 751 | */ 752 | public function setManager(QueueManager $manager) 753 | { 754 | $this->manager = $manager; 755 | } 756 | } 757 | --------------------------------------------------------------------------------