├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── long-running-tasks.php ├── database ├── factories │ └── LongRunningTaskLogItemFactory.php └── migrations │ └── create_long_running_task_log_items_table.php.stub ├── resources └── views │ └── .gitkeep └── src ├── Commands └── RestartPendingTasksCommand.php ├── Enums ├── LogItemStatus.php └── TaskResult.php ├── Events ├── DispatchingNewRunEvent.php ├── TaskCompletedEvent.php ├── TaskDidNotCompleteEvent.php ├── TaskRunEndedEvent.php └── TaskRunStartingEvent.php ├── Exceptions ├── InvalidJob.php ├── InvalidModel.php ├── InvalidStrategyClass.php └── InvalidTask.php ├── Jobs └── RunLongRunningTaskJob.php ├── LongRunningTask.php ├── LongRunningTasksServiceProvider.php ├── Models └── LongRunningTaskLogItem.php ├── Strategies ├── CheckStrategy.php ├── DefaultCheckStrategy.php ├── ExponentialBackoffCheckStrategy.php ├── LinearBackoffCheckStrategy.php └── StandardBackoffCheckStrategy.php └── Support └── Config.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-long-running-tasks` will be documented in this file. 4 | 5 | ## 1.0.0 - 2025-03-27 6 | 7 | ### What's Changed 8 | 9 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/8 10 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/9 11 | * Laravel 12 compatibility by @azimidev in https://github.com/spatie/laravel-long-running-tasks/pull/11 12 | 13 | **Full Changelog**: https://github.com/spatie/laravel-long-running-tasks/compare/0.0.4...1.0.0 14 | 15 | ## 0.0.4 - 2024-09-15 16 | 17 | ### What's Changed 18 | 19 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/6 20 | * Add task check strategies by @patinthehat in https://github.com/spatie/laravel-long-running-tasks/pull/7 21 | 22 | ### New Contributors 23 | 24 | * @patinthehat made their first contribution in https://github.com/spatie/laravel-long-running-tasks/pull/7 25 | 26 | **Full Changelog**: https://github.com/spatie/laravel-long-running-tasks/compare/0.0.3...0.0.4 27 | 28 | ## 0.0.3 - 2024-05-14 29 | 30 | ### What's Changed 31 | 32 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/2 33 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/3 34 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/4 35 | * Update RunLongRunningTaskJob.php by @azimidev in https://github.com/spatie/laravel-long-running-tasks/pull/5 36 | 37 | ### New Contributors 38 | 39 | * @azimidev made their first contribution in https://github.com/spatie/laravel-long-running-tasks/pull/5 40 | 41 | **Full Changelog**: https://github.com/spatie/laravel-long-running-tasks/compare/0.0.2...0.0.3 42 | 43 | ## 0.0.2 - 2024-03-15 44 | 45 | ### What's Changed 46 | 47 | * Update nunomaduro/collision requirement from ^7.8 to ^8.1 by @dependabot in https://github.com/spatie/laravel-long-running-tasks/pull/1 48 | 49 | ### New Contributors 50 | 51 | * @dependabot made their first contribution in https://github.com/spatie/laravel-long-running-tasks/pull/1 52 | 53 | **Full Changelog**: https://github.com/spatie/laravel-long-running-tasks/compare/0.0.1...0.0.2 54 | 55 | ## 0.0.1 - 2024-03-15 56 | 57 | **Full Changelog**: https://github.com/spatie/laravel-long-running-tasks/commits/0.0.1 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Handle long running tasks in a Laravel app 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-long-running-tasks.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-long-running-tasks) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-long-running-tasks/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/laravel-long-running-tasks/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-long-running-tasks/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/spatie/laravel-long-running-tasks/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-long-running-tasks.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-long-running-tasks) 7 | 8 | Some services, like AWS Rekognition, allow you to start a task on their side. Instead of sending a webhook when the task is finished, the services expects you to regularly poll to know when it is finished (or get an updated status). 9 | 10 | This package can help you monitor such long-running tasks that are executed externally. 11 | 12 | You do so by creating a task like this. 13 | 14 | ```php 15 | use Spatie\LongRunningTasks\LongRunningTask; 16 | use Spatie\LongRunningTasks\Enums\TaskResult; 17 | use Spatie\LongRunningTasks\LongRunningTask; 18 | 19 | class MyTask extends LongRunningTask 20 | { 21 | public function check(LongRunningTaskLogItem $logItem): TaskResult 22 | { 23 | // get some information about this task 24 | $meta = $logItem->meta 25 | 26 | // do some work here 27 | $allWorkIsDone = /* ... */ 28 | 29 | // return wheter we should continue the task in a new run 30 | 31 | return $allWorkIsDone 32 | ? TaskResult::StopChecking 33 | : TaskResult::ContinueChecking 34 | } 35 | } 36 | ``` 37 | 38 | When `TaskResult::ContinueChecking` is returned, this `check` function will be called again in 10 seconds (as defined in the `default_check_frequency_in_seconds` of the config file). 39 | 40 | After you have created your task, you can start it like this. 41 | 42 | ```php 43 | MyTask::make()->meta($anArray)->start(); 44 | ``` 45 | 46 | The `check` method of `MyTask` will be called every 10 seconds until it returns `TaskResult::StopChecking` 47 | 48 | ## Installation 49 | 50 | You can install the package via composer: 51 | 52 | ```bash 53 | composer require spatie/laravel-long-running-tasks 54 | ``` 55 | 56 | You can publish and run the migrations with: 57 | 58 | ```bash 59 | php artisan vendor:publish --tag="long-running-tasks-migrations" 60 | php artisan migrate 61 | ``` 62 | 63 | You can publish the config file with: 64 | 65 | ```bash 66 | php artisan vendor:publish --tag="long-running-tasks-config" 67 | ``` 68 | 69 | This is the contents of the published config file: 70 | 71 | ```php 72 | return [ 73 | /* 74 | * Behind the scenes, this packages use a queue to call tasks. 75 | * Here you can choose the queue that should be used by default. 76 | */ 77 | 'queue' => 'default', 78 | 79 | /* 80 | * If a task determines that it should be continued, it will 81 | * be called again after this amount of time 82 | */ 83 | 'default_check_frequency_in_seconds' => 10, 84 | 85 | /* 86 | * The default class that implements a strategy for determining the check frequency in seconds. 87 | * - `DefaultCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds. 88 | * - `StandardBackoffCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds, 89 | * but will increase the frequency with the multipliers 1, 6, 12, 30, 60, with the maximum being 60 times the original frequency. 90 | * With the default check frequency, this translates to 10, 60, 120, 300, and 600 seconds between checks. 91 | * - `LinearBackoffCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds, 92 | * but will increase the frequency linearly with each attempt, up to a maximum multiple of 6 times the original frequency. 93 | * - `ExponentialBackoffCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds, 94 | * but will increase the frequency exponentially with each attempt, up to a maximum of 6 times the original frequency. 95 | */ 96 | 'default_check_strategy_class' => Spatie\LongRunningTasks\Strategies\DefaultCheckStrategy::class, 97 | 98 | /* 99 | * When a task is not completed in this amount of time, 100 | * it will not run again, and marked as `didNotComplete`. 101 | */ 102 | 'keep_checking_for_in_seconds' => 60 * 5, 103 | 104 | /* 105 | * The model that will be used by default to track 106 | * the status of all tasks. 107 | */ 108 | 'log_model' => Spatie\LongRunningTasks\Models\LongRunningTaskLogItem::class, 109 | 110 | /* 111 | * The job responsible for calling tasks. 112 | */ 113 | 'task_job' => Spatie\LongRunningTasks\Jobs\RunLongRunningTaskJob::class, 114 | ]; 115 | ``` 116 | 117 | This package makes use of queues to call tasks again after a certain amount of time. Make sure you've set up [queues](https://laravel.com/docs/10.x/queues) in your Laravel app. 118 | 119 | ## Usage 120 | 121 | To monitor a long-running task on an external service, you should define a task class. It should extend the `Spatie\LongRunningTasks\LongRunningTask` provided by the package. 122 | 123 | It's `check` function should perform the work you need it to do and return a `TaskResult`. When returning `TaskResult::StopChecking` the task will not be called again. When returning `TaskResult::ContinueChecking` it will be called again in 10 seconds by default. 124 | 125 | ```php 126 | use Spatie\LongRunningTasks\LongRunningTask; 127 | use Spatie\LongRunningTasks\Enums\TaskResult; 128 | 129 | class MyTask extends LongRunningTask 130 | { 131 | public function check(LongRunningTaskLogItem $logItem): TaskResult 132 | { 133 | // get some information about this task 134 | $meta = $logItem->meta // returns an array 135 | 136 | // do some work here 137 | $allWorkIsDone = /* ... */ 138 | 139 | // return wheter we should continue the task in a new run 140 | 141 | return $allWorkIsDone 142 | ? TaskResult::StopChecking 143 | : TaskResult::ContinueChecking 144 | } 145 | } 146 | ``` 147 | 148 | To start the task above, you can call the `start` method. 149 | 150 | ```php 151 | MyTask::make()->start(); 152 | ``` 153 | 154 | This will create a record in the `long_running_task_log_items` table that tracks the progress of this task. The `check` method of `MyTask` will be called every 10 seconds until it returns `TaskResult::StopChecking`. 155 | 156 | ### Adding meta data 157 | 158 | In most cases, you'll want to give a task some specific data it can act upon. This can be done by passing an array to the `meta` method. 159 | 160 | ```php 161 | MyTask::make()->meta($arrayWithMetaData)->start(); 162 | ``` 163 | 164 | Alternatively, you could also pass it to the `start` method. 165 | 166 | ```php 167 | MyTask::make()->start($arrayWithMetaData); 168 | ``` 169 | 170 | The given array will be available on the `LongRunningTaskLogItem` instance that is passed to the `check` method of your task. 171 | 172 | ```php 173 | class MyTask extends LongRunningTask 174 | { 175 | public function check(LongRunningTaskLogItem $logItem): TaskResult 176 | { 177 | // get some information about this task 178 | $meta = $logItem->meta // returns an array 179 | 180 | // rest of method 181 | } 182 | } 183 | ``` 184 | 185 | ### Customizing the check interval 186 | 187 | By default, when the `check` method of your task returns `TaskResult::ContinueChecking`, it will be called again in 10 seconds. You can customize that timespan by changing the value of the `default_check_frequency_in_seconds` key in the `long-running-tasks` config file. 188 | 189 | You can also specify a check interval on your task itself. 190 | 191 | ```php 192 | class MyTask extends LongRunningTask 193 | { 194 | public int $checkFrequencyInSeconds = 20; 195 | } 196 | ``` 197 | 198 | To specify a checking interval on a specific instance of a task, you can use the `checkInterval` method. 199 | 200 | ```php 201 | MyTask::make() 202 | ->checkFrequencyInSeconds(30) 203 | ->start(); 204 | ``` 205 | 206 | ### Customizing the check strategy 207 | 208 | By default, the package uses the `DefaultCheckStrategy` to determine the check frequency in seconds, which always returns the value of the `check_frequency_in_seconds` attribute. 209 | You can customize the strategy that should be used by changing the value of the `default_check_strategy_class` key in the `long-running-tasks` config file. 210 | 211 | You can also specify a strategy on your task itself. 212 | 213 | ```php 214 | use Spatie\LongRunningTasks\Strategies\StandardBackoffCheckStrategy; 215 | 216 | class MyTask extends LongRunningTask 217 | { 218 | public string $checkStrategy = StandardBackoffCheckStrategy::class; 219 | } 220 | ``` 221 | 222 | To specify a check strategy on a specific instance of a task, you can use the `checkStrategy` method. 223 | 224 | ```php 225 | MyTask::make() 226 | ->checkStrategy(StandardBackoffCheckStrategy::class) 227 | ->start(); 228 | ``` 229 | 230 | ### Using a different queue 231 | 232 | This package uses queues to call tasks again after a certain amount of time. By default, it will use the `default` queue. You can customize the queue that should be used by changing the value of the `queue` key in the `long-running-tasks` config file. 233 | 234 | You can also specify a queue on your task itself. 235 | 236 | ```php 237 | class MyTask extends LongRunningTask 238 | { 239 | public string $queue = 'my-custom-queue'; 240 | } 241 | ``` 242 | 243 | To specify a queue on a specific instance of a task, you can use the `onQueue` method. 244 | 245 | ```php 246 | MyTask::make() 247 | ->queue('my-custom-queue') 248 | ->start(); 249 | ``` 250 | 251 | ### Tracking the status of tasks 252 | 253 | For each task that is started, a record will be created in the `long_running_task_log_items` table. This record will track the status of the task. 254 | 255 | The `LongRunningTaskLogItem` model has a `status` attribute that can have the following values: 256 | 257 | - `pending`: The task has not been started yet. 258 | - `running`: The task is currently running. 259 | - `completed`: The task has completed. 260 | - `failed`: The task has failed. Probably an unhanded exception was thrown. 261 | - `didNotComplete`: The task did not complete in the given amount of time. 262 | 263 | The table also contains these properties: 264 | 265 | - `task`: The fully qualified class name of the task. 266 | - `queue`: The queue the task is running on. 267 | - `check_frequency_in_seconds`: The amount of time in seconds that should pass before the task is checked again. 268 | - `meta`: An array of meta data that was passed to the task. 269 | - `last_check_started_at`: The date and time the task was started. 270 | - `last_check_ended_at`: The date and time the task was ended. 271 | - `stop_checking_at`: The date and time the task should stop being checked. 272 | - `lastest_exception`: An array with keys `message` and `trace` that contains the latest exception that was thrown. 273 | - `run_count`: The amount of times the task has been run. 274 | - `attempt`: The amount of times the task has been attempted after a failure occurred. 275 | - `created_at`: The date and time the record was created. 276 | 277 | ### Preventing never-ending tasks 278 | 279 | The package has a way of preventing task to run indefinitely. 280 | 281 | When a task is not completed in the amount of time specified in the `keep_checking_for_in_seconds` key of the `long-running-tasks` config file, it will not run again, and marked as `didNotComplete`. 282 | 283 | You can customize that timespan on a specific task. 284 | 285 | ```php 286 | class MyTask extends LongRunningTask 287 | { 288 | public int $keepCheckingForInSeconds = 60 * 10; 289 | } 290 | ``` 291 | 292 | You can also specify the timespan on a specific instance of a task. 293 | 294 | ```php 295 | MyTask::make() 296 | ->keepCheckingForInSeconds(60 * 10) 297 | ->start(); 298 | ``` 299 | 300 | ### Handling exceptions 301 | 302 | When an exception is thrown in the `check` method of your task, it will be caught and stored in the `latest_exception` attribute of the `LongRunningTaskLogItem` model. 303 | 304 | Optionally, you can define an `onFailure` method on your task. This method will be called when an exception is thrown in the `check` method. 305 | 306 | ```php 307 | use Spatie\LongRunningTasks\LongRunningTask; 308 | use Spatie\LongRunningTasks\Enums\TaskResult; 309 | 310 | class MyTask extends LongRunningTask 311 | { 312 | public function check(LongRunningTaskLogItem $logItem): TaskResult 313 | { 314 | throw new Exception('Something went wrong'); 315 | } 316 | 317 | public function onFail(LongRunningTaskLogItem $logItem, Exception $exception): ?TaskResult 318 | { 319 | // handle the exception 320 | } 321 | } 322 | ``` 323 | 324 | You can let the `onFail` method return a `TaskResult`. When it returns `TaskResult::ContinueChecking`, the task will be called again. If it doesn't return anything, the task will not be called again. 325 | 326 | ### Events 327 | 328 | ### Using your own model 329 | 330 | If you need extra fields or functionality on the `LongRunningTaskLogItem` model, you can create your own model that extends the `LongRunningTaskLogItem` model provided by this package. 331 | 332 | ```php 333 | namespace App\Models; 334 | 335 | use Spatie\LongRunningTasks\Models\LongRunningTaskLogItem as BaseLongRunningTaskLogItem; 336 | 337 | class LongRunningTaskLogItem extends BaseLongRunningTaskLogItem 338 | { 339 | // your custom functionality 340 | } 341 | ``` 342 | 343 | You should then update the `log_model` key in the `long-running-tasks` config file to point to your custom model. 344 | 345 | ```php 346 | // in config/long-running-tasks.php 347 | 348 | return [ 349 | // ... 350 | 351 | 'log_model' => App\Models\LongRunningTaskLogItem::class, 352 | ]; 353 | ``` 354 | 355 | To fill the extra custom fields of your model, you could use the `creating` and `updating` events. You could use the `meta` property to pass data to the model. 356 | 357 | ```php 358 | namespace App\Models; 359 | 360 | use Spatie\LongRunningTasks\Models\LongRunningTaskLogItem as BaseLongRunningTaskLogItem; 361 | 362 | class LongRunningTaskLogItem extends BaseLongRunningTaskLogItem 363 | { 364 | protected static function booted() 365 | { 366 | static::creating(function ($logItem) { 367 | $customValue = $logItem->meta['some_key']; 368 | 369 | // optionally, you could unset the custom value from the meta array 370 | unset($logItem->meta['some_key']); 371 | 372 | $logItem->custom_field = $customValue; 373 | }); 374 | } 375 | } 376 | ``` 377 | 378 | ### Using your own job 379 | 380 | By default, the package uses the `RunLongRunningTaskJob` job to call tasks. If you want to use your own job, you can create a job that extends the `RunLongRunningTaskJob` job provided by this package. 381 | 382 | ```php 383 | namespace App\Jobs; 384 | 385 | use Spatie\LongRunningTasks\Jobs\RunLongRunningTaskJob as BaseRunLongRunningTaskJob; 386 | 387 | class RunLongRunningTaskJob extends BaseRunLongRunningTaskJob 388 | { 389 | // your custom functionality 390 | } 391 | ``` 392 | 393 | You should then update the `task_job` key in the `long-running-tasks` config file to point to your custom job. 394 | 395 | ```php 396 | // in config/long-running-tasks.php 397 | 398 | return [ 399 | // ... 400 | 401 | 'task_job' => App\Jobs\RunLongRunningTaskJob::class, 402 | ]; 403 | ``` 404 | 405 | ### Events 406 | 407 | The package fires events that you can listen to in your application to perform additional actions when certain events occur. 408 | 409 | All of these events have a property `$longRunningTaskLogItem` that contains a `LongRunningTaskLogItem` model. 410 | 411 | #### `Spatie\LongRunningTasks\Events\TaskRunStarting` 412 | 413 | This event will be fired when a task is about to be run. 414 | 415 | #### `Spatie\LongRunningTasks\Events\TaskRunEnded` 416 | 417 | This event will be fired when a task has ended. 418 | 419 | #### `Spatie\LongRunningTasks\Events\TaskCompleted` 420 | 421 | This event will be fired when a task has completed. 422 | 423 | #### `Spatie\LongRunningTasks\Events\TaskRunFailed` 424 | 425 | This event will be fired when a task has failed. 426 | 427 | #### `Spatie\LongRunningTasks\Events\TaskRunDidNotComplete` 428 | 429 | This event will be fired when a task did not complete in the given amount of time. 430 | 431 | #### `Spatie\LongRunningTasks\Events\DispatchingNewRunEvent` 432 | 433 | This event will be fired when a new run of a task is about to be dispatched. 434 | 435 | ## Testing 436 | 437 | ```bash 438 | composer test 439 | ``` 440 | 441 | ## Changelog 442 | 443 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 444 | 445 | ## Contributing 446 | 447 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 448 | 449 | ## Security Vulnerabilities 450 | 451 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 452 | 453 | ## Credits 454 | 455 | - [Freek Van der Herten](https://github.com/freekmurze) 456 | - [All Contributors](../../contributors) 457 | 458 | ## License 459 | 460 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 461 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-long-running-tasks", 3 | "description": "Handle long running tasks in a Laravel app", 4 | "keywords": [ 5 | "laravel", 6 | "laravel-long-running-tasks" 7 | ], 8 | "homepage": "https://github.com/spatie/laravel-long-running-tasks", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Freek Van der Herten", 13 | "email": "freek@spatie.be", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.2", 19 | "illuminate/contracts": "^10.0|^11.0|^12.0", 20 | "spatie/laravel-package-tools": "^1.20.1", 21 | "spatie/test-time": "^1.3.3" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.0", 25 | "nunomaduro/collision": "^7.0|^8.1", 26 | "orchestra/testbench": "^8.8|^9.0|^10.0", 27 | "pestphp/pest": "^2.20|^3.0", 28 | "pestphp/pest-plugin-arch": "^2.5|^3.0", 29 | "pestphp/pest-plugin-laravel": "^2.0|^3.0", 30 | "spatie/laravel-ray": "^1.26" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Spatie\\LongRunningTasks\\": "src/", 35 | "Spatie\\LongRunningTasks\\Database\\Factories\\": "database/factories/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Spatie\\LongRunningTasks\\Tests\\": "tests/", 41 | "Workbench\\App\\": "workbench/app/" 42 | } 43 | }, 44 | "scripts": { 45 | "post-autoload-dump": "@composer run prepare", 46 | "clear": "@php vendor/bin/testbench package:purge-laravel-long-running-tasks --ansi", 47 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 48 | "build": [ 49 | "@composer run prepare", 50 | "@php vendor/bin/testbench workbench:build --ansi" 51 | ], 52 | "start": [ 53 | "Composer\\Config::disableProcessTimeout", 54 | "@composer run build", 55 | "@php vendor/bin/testbench serve" 56 | ], 57 | "analyse": "vendor/bin/phpstan analyse", 58 | "test": "vendor/bin/pest", 59 | "test-coverage": "vendor/bin/pest --coverage", 60 | "format": "vendor/bin/pint" 61 | }, 62 | "config": { 63 | "sort-packages": true, 64 | "allow-plugins": { 65 | "pestphp/pest-plugin": true, 66 | "phpstan/extension-installer": true 67 | } 68 | }, 69 | "extra": { 70 | "laravel": { 71 | "providers": [ 72 | "Spatie\\LongRunningTasks\\LongRunningTasksServiceProvider" 73 | ] 74 | } 75 | }, 76 | "minimum-stability": "dev", 77 | "prefer-stable": true 78 | } 79 | -------------------------------------------------------------------------------- /config/long-running-tasks.php: -------------------------------------------------------------------------------- 1 | 'default', 9 | 10 | /* 11 | * If a task determines that it should be continued, it will 12 | * be called again after this amount of time 13 | */ 14 | 'default_check_frequency_in_seconds' => 10, 15 | 16 | /* 17 | * The default class that implements a strategy for determining the check frequency in seconds. 18 | * - `DefaultCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds. 19 | * - `StandardBackoffCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds, 20 | * but will increase the frequency with the multipliers 1, 6, 12, 30, 60, with the maximum being 60 times the original frequency. 21 | * With the default check frequency, this translates to 10, 60, 120, 300, and 600 seconds between checks. 22 | * - `LinearBackoffCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds, 23 | * but will increase the frequency linearly with each attempt, up to a maximum multiple of 6 times the original frequency. 24 | * - `ExponentialBackoffCheckStrategy` will check the task every `LongRunningTaskLogItem::check_frequency_in_seconds` seconds, 25 | * but will increase the frequency exponentially with each attempt, up to a maximum of 6 times the original frequency. 26 | */ 27 | 'default_check_strategy_class' => Spatie\LongRunningTasks\Strategies\DefaultCheckStrategy::class, 28 | 29 | /* 30 | * When a task is not completed in this amount of time, 31 | * it will not run again, and marked as `didNotComplete`. 32 | */ 33 | 'keep_checking_for_in_seconds' => 60 * 5, 34 | 35 | /* 36 | * The model that will be used by default to track 37 | * the status of all tasks. 38 | */ 39 | 'log_model' => Spatie\LongRunningTasks\Models\LongRunningTaskLogItem::class, 40 | 41 | /* 42 | * The job responsible for calling tasks. 43 | */ 44 | 'task_job' => Spatie\LongRunningTasks\Jobs\RunLongRunningTaskJob::class, 45 | ]; 46 | -------------------------------------------------------------------------------- /database/factories/LongRunningTaskLogItemFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 17 | 'queue' => 'default', 18 | 'status' => LogItemStatus::Pending, 19 | 'check_frequency_in_seconds' => 10, 20 | 'meta' => [], 21 | 'stop_checking_at' => now()->addSeconds(config('long-running-tasks.keep_checking_for_in_seconds')), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/create_long_running_task_log_items_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('type'); 15 | $table->string('status'); 16 | $table->string('queue'); 17 | $table->integer('check_frequency_in_seconds'); 18 | $table->json('meta'); 19 | $table->timestamp('last_check_started_at')->nullable(); 20 | $table->timestamp('last_check_ended_at')->nullable(); 21 | $table->timestamp('stop_checking_at')->nullable(); 22 | $table->text('latest_exception')->nullable(); 23 | $table->integer('run_count')->default(0); 24 | $table->integer('attempt')->default(1); 25 | 26 | $table->timestamps(); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/laravel-long-running-tasks/4fdbbfb26bd048ca0badd368e1629428d84618b1/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Commands/RestartPendingTasksCommand.php: -------------------------------------------------------------------------------- 1 | info('Starting long running tasks...'); 17 | 18 | $logItems = Config::getLongRunningTaskLogItemModelClass(); 19 | 20 | $logItems::query() 21 | ->where('status', LogItemStatus::Pending) 22 | ->each(function (LongRunningTaskLogItem $logItem) { 23 | $this->comment("Dispatching job for log item {$logItem->id}..."); 24 | 25 | $logItem->dispatchJob(); 26 | }); 27 | 28 | $this->info('All done!'); 29 | 30 | return self::SUCCESS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Enums/LogItemStatus.php: -------------------------------------------------------------------------------- 1 | longRunningTaskLogItem->task(); 31 | 32 | $this->longRunningTaskLogItem->markAsRunning(); 33 | 34 | try { 35 | event(new TaskRunStartingEvent($this->longRunningTaskLogItem)); 36 | 37 | $checkResult = $task->check($this->longRunningTaskLogItem); 38 | 39 | event(new TaskRunEndedEvent($this->longRunningTaskLogItem)); 40 | } catch (Exception $exception) { 41 | $this->handleException($task, $exception); 42 | 43 | return; 44 | } 45 | 46 | $this->handleTaskResult($checkResult); 47 | } 48 | 49 | protected function handleTaskResult(TaskResult $checkResult): void 50 | { 51 | if ($checkResult === TaskResult::StopChecking) { 52 | $this->longRunningTaskLogItem->markAsCheckedEnded(LogItemStatus::Completed); 53 | 54 | event(new TaskCompletedEvent($this->longRunningTaskLogItem)); 55 | 56 | return; 57 | } 58 | 59 | if (! $this->longRunningTaskLogItem->shouldKeepChecking()) { 60 | $this->longRunningTaskLogItem->markAsCheckedEnded(LogItemStatus::DidNotComplete); 61 | 62 | event(new TaskDidNotCompleteEvent($this->longRunningTaskLogItem)); 63 | 64 | return; 65 | } 66 | 67 | $this->dispatchAgain(); 68 | } 69 | 70 | protected function handleException(LongRunningTask $task, Exception $exception): void 71 | { 72 | $checkResult = $task->onFail($this->longRunningTaskLogItem, $exception); 73 | 74 | $checkResult ??= TaskResult::StopChecking; 75 | 76 | $this->longRunningTaskLogItem->update([ 77 | 'latest_exception' => [ 78 | 'message' => $exception->getMessage(), 79 | 'trace' => $exception->getTraceAsString(), 80 | ], 81 | ]); 82 | 83 | $this->longRunningTaskLogItem->markAsCheckedEnded(LogItemStatus::Failed); 84 | 85 | if ($checkResult == TaskResult::ContinueChecking) { 86 | $this->dispatchAgain(); 87 | } 88 | } 89 | 90 | protected function dispatchAgain(): void 91 | { 92 | $this->longRunningTaskLogItem->markAsPending(); 93 | 94 | event(new DispatchingNewRunEvent($this->longRunningTaskLogItem)); 95 | 96 | $job = new static($this->longRunningTaskLogItem); 97 | 98 | $task = $this->longRunningTaskLogItem->task(); 99 | $delay = $task->getCheckStrategy()->checkFrequencyInSeconds($this->longRunningTaskLogItem); 100 | 101 | $queue = $this->longRunningTaskLogItem->queue; 102 | 103 | dispatch($job) 104 | ->onQueue($queue) 105 | ->delay($delay); 106 | } 107 | 108 | public function uniqueId(): string|int 109 | { 110 | return $this->longRunningTaskLogItem->id; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/LongRunningTask.php: -------------------------------------------------------------------------------- 1 | meta = $meta; 34 | 35 | return $this; 36 | } 37 | 38 | public function queue(string $queue): self 39 | { 40 | $this->queue = $queue; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @param class-string $checkStrategy 47 | * @return $this 48 | */ 49 | public function checkStrategy(string $checkStrategy): self 50 | { 51 | $this->checkStrategy = $checkStrategy; 52 | 53 | return $this; 54 | } 55 | 56 | public function checkFrequencyInSeconds(int $seconds): self 57 | { 58 | $this->checkFrequencyInSeconds = $seconds; 59 | 60 | return $this; 61 | } 62 | 63 | public function keepCheckingForInSeconds(int $seconds) 64 | { 65 | $this->keepCheckingForInSeconds = $seconds; 66 | 67 | return $this; 68 | } 69 | 70 | public function start(?array $meta = null): LongRunningTaskLogItem 71 | { 72 | if ($meta) { 73 | $this->meta($meta); 74 | } 75 | 76 | $logModel = Config::getLongRunningTaskLogItemModelClass(); 77 | 78 | /** @var LongRunningTaskLogItem $logItem */ 79 | $logItem = $logModel::create([ 80 | 'status' => LogItemStatus::Pending, 81 | 'queue' => $this->getQueue(), 82 | 'meta' => $this->meta, 83 | 'type' => $this->type(), 84 | 'check_frequency_in_seconds' => $this->getCheckFrequencyInSeconds(), 85 | 'attempt' => 1, 86 | 'stop_checking_at' => $this->stopCheckingAt(), 87 | ]); 88 | 89 | $logItem->dispatchJob(); 90 | 91 | return $logItem; 92 | } 93 | 94 | protected function type(): string 95 | { 96 | return static::class; 97 | } 98 | 99 | protected function getCheckFrequencyInSeconds(): int 100 | { 101 | if (isset($this->checkFrequencyInSeconds)) { 102 | return $this->checkFrequencyInSeconds; 103 | } 104 | 105 | return config('long-running-tasks.default_check_frequency_in_seconds'); 106 | } 107 | 108 | public function getQueue(): string 109 | { 110 | if (isset($this->queue)) { 111 | return $this->queue; 112 | } 113 | 114 | return config('long-running-tasks.queue'); 115 | } 116 | 117 | public function getCheckStrategy(): CheckStrategy 118 | { 119 | $strategyClass = config('long-running-tasks.default_check_strategy_class', DefaultCheckStrategy::class); 120 | 121 | if (isset($this->checkStrategy)) { 122 | $strategyClass = $this->checkStrategy; 123 | } 124 | 125 | if (! class_exists($strategyClass)) { 126 | throw InvalidStrategyClass::classDoesNotExist($strategyClass); 127 | } 128 | 129 | if (! class_implements($strategyClass, CheckStrategy::class)) { 130 | throw InvalidStrategyClass::classIsNotAStrategy($strategyClass); 131 | } 132 | 133 | return app()->make($strategyClass); 134 | } 135 | 136 | public function stopCheckingAt(): Carbon 137 | { 138 | $timespan = config('long-running-tasks.keep_checking_for_in_seconds'); 139 | 140 | if (isset($this->keepCheckingForInSeconds)) { 141 | $timespan = $this->keepCheckingForInSeconds; 142 | } 143 | 144 | return now()->addSeconds($timespan); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/LongRunningTasksServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-long-running-tasks') 15 | ->hasConfigFile() 16 | ->hasMigration('create_long_running_task_log_items_table') 17 | ->hasCommand(RestartPendingTasksCommand::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/LongRunningTaskLogItem.php: -------------------------------------------------------------------------------- 1 | LogItemStatus::class, 20 | 'meta' => 'array', 21 | 'last_check_started_at' => 'datetime', 22 | 'last_check_ended_at' => 'datetime', 23 | 'stop_checking_at' => 'datetime', 24 | 'latest_exception' => 'array', 25 | 'run_count' => 'integer', 26 | ]; 27 | 28 | public function task(): LongRunningTask 29 | { 30 | $taskClass = $this->type; 31 | 32 | if (! class_exists($taskClass)) { 33 | throw InvalidTask::classDoesNotExist($taskClass); 34 | } 35 | 36 | if (! is_a($taskClass, LongRunningTask::class, true)) { 37 | throw InvalidTask::classIsNotATask($taskClass); 38 | } 39 | 40 | /** @var LongRunningTask $task */ 41 | return new $taskClass; 42 | } 43 | 44 | public function markAsPending(): self 45 | { 46 | $this->update([ 47 | 'status' => LogItemStatus::Pending, 48 | ]); 49 | 50 | return $this; 51 | } 52 | 53 | public function markAsRunning(): self 54 | { 55 | $this->update([ 56 | 'last_check_started_at' => now(), 57 | 'status' => LogItemStatus::Running, 58 | 'run_count' => $this->run_count + 1, 59 | 'latest_exception' => null, 60 | ]); 61 | 62 | return $this; 63 | } 64 | 65 | public function markAsCheckedEnded(LogItemStatus $logItemStatus): self 66 | { 67 | $this->update([ 68 | 'last_check_ended_at' => now(), 69 | 'status' => $logItemStatus, 70 | ]); 71 | 72 | return $this; 73 | } 74 | 75 | public function shouldKeepChecking(): bool 76 | { 77 | if (is_null($this->stop_checking_at)) { 78 | return true; 79 | } 80 | 81 | return $this->stop_checking_at > now(); 82 | } 83 | 84 | public function dispatchJob(): void 85 | { 86 | $jobClass = Config::getTaskJobClass(); 87 | 88 | dispatch(new $jobClass($this)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Strategies/CheckStrategy.php: -------------------------------------------------------------------------------- 1 | check_frequency_in_seconds; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Strategies/ExponentialBackoffCheckStrategy.php: -------------------------------------------------------------------------------- 1 | attempt === 1) { 12 | return $logItem->check_frequency_in_seconds; 13 | } 14 | 15 | return ($logItem->check_frequency_in_seconds * 0.5) ** min($logItem->attempt, 4); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Strategies/LinearBackoffCheckStrategy.php: -------------------------------------------------------------------------------- 1 | check_frequency_in_seconds * min([$logItem->attempt, 6]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Strategies/StandardBackoffCheckStrategy.php: -------------------------------------------------------------------------------- 1 | check_frequency_in_seconds; 12 | 13 | return [ 14 | $frequency, 15 | $frequency * 6, 16 | $frequency * 12, 17 | $frequency * 30, 18 | $frequency * 60, 19 | ]; 20 | 21 | } 22 | 23 | public function checkFrequencyInSeconds(LongRunningTaskLogItem $logItem): int 24 | { 25 | $frequencies = $this->frequencies($logItem); 26 | 27 | return $logItem->attempt >= count($frequencies) 28 | ? $frequencies[count($frequencies) - 1] 29 | : $frequencies[($logItem->attempt % count($frequencies)) - 1]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Support/Config.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public static function getTaskJobClass(): string 16 | { 17 | $jobClass = config('long-running-tasks.task_job'); 18 | 19 | if (! is_a($jobClass, RunLongRunningTaskJob::class, true)) { 20 | throw InvalidJob::make($jobClass); 21 | } 22 | 23 | return $jobClass; 24 | } 25 | 26 | /** 27 | * @return class-string 28 | */ 29 | public static function getLongRunningTaskLogItemModelClass(): string 30 | { 31 | $modelClass = config('long-running-tasks.log_model'); 32 | 33 | if (! is_a($modelClass, LongRunningTaskLogItem::class, true)) { 34 | throw InvalidModel::make($modelClass); 35 | } 36 | 37 | return $modelClass; 38 | } 39 | } 40 | --------------------------------------------------------------------------------