muyki-labs/laravel-resumable-jobs
Composer 安装命令:
composer require muyki-labs/laravel-resumable-jobs
包简介
Resumable, checkpointed Laravel queue jobs that survive failures and resume from the last checkpoint, with built-in progress tracking.
README 文档
README
Long-running queue jobs — large imports/exports, video/file processing, multi-step pipelines — start over from scratch when they fail. Laravel Resumable Jobs adds a thin, practical checkpoint layer on top of the queue: record progress with $this->checkpoint('step', ...), resume from the last successful checkpoint after a failure, and bind the live progress to a UI.
class ImportLargeCsv implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, Resumable; public function __construct(public int $importId) {} public function checkpointKey(): string { return "csv-import:{$this->importId}"; } public function handle(): void { $rows = $this->checkpoint('parse', fn () => $this->parseFile()); $this->withTotalSteps(count($rows)); foreach (array_chunk($rows, 500) as $i => $chunk) { $this->checkpoint("chunk.$i", fn () => $this->upsert($chunk)); $this->progress(($i + 1) * 500); } $this->checkpoint('finalize', fn () => $this->cleanup()); } }
If this job fails at chunk.42, the retry skips parse and chunks 0–41 and resumes at chunk.42.
Installation
composer require muyki-labs/laravel-resumable-jobs
Publish and run the migration (only needed for the default database store):
php artisan vendor:publish --tag=resumable-jobs-migrations php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag=resumable-jobs-config
Requires PHP 8.3+ and Laravel 12 or 13.
Usage
Add the Resumable trait to any queued job. The trait registers a job middleware that loads checkpoint state before handle() and clears/marks it on success.
use MuykiLabs\ResumableJobs\Concerns\Resumable; class ProcessVideo implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, Resumable; public function handle(): void { $this->checkpoint('transcode', fn () => $this->transcode()); $this->checkpoint('thumbnails', fn () => $this->generateThumbnails()); $this->checkpoint('publish', fn () => $this->publish()); } }
Two checkpoint APIs
Closure form (recommended). If the step already completed, the closure does not run and its previously stored return value is returned:
$rows = $this->checkpoint('parse', fn () => $this->parseFile());
Imperative form. Records a step as complete and stores a value, without running anything:
$this->checkpoint('header', ['columns' => $columns]); if ($this->completed('header')) { $columns = $this->checkpointData('header')['columns']; }
Progress
$this->withTotalSteps(1000); // declare the total $this->progress(250); // 25%
Read it anywhere (polling UIs, Livewire, dashboards):
use MuykiLabs\ResumableJobs\Facades\ResumableJobs; $progress = ResumableJobs::progressFor("csv-import:{$importId}"); $progress?->percent(); // 25.0
⚠️ Idempotency: read this before shipping
A checkpoint only decides whether to skip a step, it does not make the step itself atomic. If a step is interrupted halfway (e.g. 600 of 1000 rows written, then the process is killed), the checkpoint for that step was never recorded, so on retry the entire step runs again.
That is fine — as long as each step is idempotent. Make steps safe to re-run:
- Use
upsert()/updateOrCreate()/firstOrCreate()instead of blindinsert(). - Keep checkpoints small and granular (per chunk), so a re-run repeats at most one chunk.
- Avoid side effects that can't be repeated (e.g. "send email") inside a large step; put them behind their own checkpoint.
// Good: re-running this chunk is harmless. $this->checkpoint("chunk.$i", fn () => User::upsert($chunk, ['email'], ['name']));
Choosing the checkpoint key
The checkpoint key must be stable across retries. Resolution order:
checkpointKey()override (recommended). Tie it to a business entity, e.g."video:{$this->videoId}". This is the most robust option and makes the work idempotent even across separate dispatches of the same logical job.- Queued job UUID. The default. Stable across automatic retries (
$tries,backoff,release). - Generated fallback. Used only for synchronous/standalone execution where no queue UUID exists.
Events
Bind to any of these (e.g. to drive a broadcasted progress bar or notifications):
| Event | Fired when |
|---|---|
JobStarted |
A job runs for the first time (no prior state). |
JobResumed |
A job runs again with existing checkpoint state. |
CheckpointReached |
A step completes and is persisted. |
CheckpointSkipped |
A step is skipped on resume. |
ProgressUpdated |
progress() / withTotalSteps() is called. |
JobCompleted |
handle() finishes successfully. |
JobFailedPermanently |
The job fails after exhausting its retries. |
Stores
Configure the store in config/resumable-jobs.php via RESUMABLE_JOBS_STORE:
database(default) — durable and queryable; ideal for progress dashboards.cache— fast, TTL-based (Redis/Memcached); pruning is handled by TTL.null— disables resumability (every run starts fresh).
Register a custom driver:
use MuykiLabs\ResumableJobs\CheckpointManager; app(CheckpointManager::class)->extend('dynamodb', fn ($app) => new DynamoCheckpointStore(...));
Concurrency
To prevent two workers from processing the same key simultaneously, enable locking (requires a cache store with atomic locks):
'lock' => ['enabled' => true, 'store' => 'redis', 'ttl' => 600, 'wait' => 0],
You can also combine this with Laravel's WithoutOverlapping middleware keyed by your checkpointKey().
Commands
php artisan resumable:prune --hours=72 # remove old completed checkpoints php artisan resumable:status {key} # inspect a checkpoint php artisan resumable:clear {key} # force a fresh restart
Enable scheduled pruning in the config under prune.schedule.
Retention
By default completed checkpoints are kept for retention.completed_hours (then pruned), and failed checkpoints are kept indefinitely so the job can be resumed. Set retention.forget_on_completion to true to delete a checkpoint the moment its job succeeds.
Testing
composer test
composer analyse
composer format
License
The MIT License (MIT). See LICENSE.md.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 3
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-15