Changelog
v1.2.0
Released 2026-11-01. Run unfinished backfills after a deploy, and test code that starts them.
backfill:pendinglists, runs or queues every backfill that has not finished, the waymigratehandles migrations. Addbackfill:pending --queue --forceto a deploy script and new backfills run themselves. Paused runs resume from their cursor, finished ones are skipped. See running after a deploy.$manualkeeps a backfill out ofbackfill:pending. Operator-panel backfills and ones with a required parameter are left out automatically.Backfills::fake()records runs without touching the data, for testing code that starts backfills. Assert withassertRan,assertRanTimes,assertNotRanandassertNothingRan. See testing.- Assertion helpers on
InteractsWithBackfills:assertBackfillCompleted,assertBackfillProcessedandassertBackfillHasNoFailures, with failure messages that name the stop reason or the first failed row.
v1.1.0
Released 2026-11-01. Fixes for queue mode under real workers, a tighter row-ceiling guard, and static analysis in CI.
Queue mode
- Queued slices are time-boxed. A slice now ends after
batches_per_jobbatches ormax_seconds_per_jobseconds, whichever comes first, so a slowprocess()can no longer outlive the worker timeout. The newtime_limitstop code chains likemax_batches. - Jobs carry their own timeout.
RunBackfillJobsets$timeoutfrombackfill.queue.timeoutand turns onfailOnTimeout. - A killed job is reported, not lost. Its
failed()hook marks the runinterrupted, releases the lock, and firesBackfillFailed, so notifications go out. Before, the run sat atrunninguntil its heartbeat went stale and nobody was told. - The worker's own shutdown handling is preserved. The runner used to replace
queue:work'sSIGTERMhandler and then reset it to the default. The worker never learned it should quit, and after the job the nextSIGTERMkilled it mid-job. The previous handler is now called alongside the runner's and restored afterwards.
Guards and retries
--no-countno longer bypasses the row ceiling. Whilemax_rows_without_confirmationis set, skipping the count is refused without--force.backfill:retry-failedis tenant-aware. It takes the same per-tenant lock as the runner, switches into the run's tenant, and re-applies the run's parameters.
Dashboard
- Throughput leaves out paused time. Runs record active seconds per session, so a run paused overnight is no longer shown at a fraction of its real speed.
Housekeeping
- Laravel 11 is no longer supported. It stopped receiving security fixes in March 2026, and Composer now refuses to install its releases by default because of the open advisories. Laravel 12 and 13 are supported.
- Requires
laravel/frameworkrather than threeilluminate/*components. The package always used parts only the full framework ships. - PHPStan (Larastan, level 5) and Pint run in CI.
v1.0.0
The first tagged release. It bundles four milestones, listed below in the order they were built.
v0.4
The operator panel, ledger mode, per-tenant cursors, and the Pulse card.
- Operator panel — mark a backfill
$operatorRunnable, declare its inputs, and support staff can run it from a browser. Own route, own gate, validated inputs, progress in plain words. - Parameters —
ids,text,textarea,number,booleanandselect, withrequired,min,max,default,helpandplaceholder. Recorded on the run and re-applied on resume; resuming with different parameters is refused. - Ledger mode — claim before, confirm after, for work the database cannot roll back. Unconfirmed claims are surfaced, never retried automatically. Declaring side effects without a ledger logs a loud warning.
- Multi-tenancy — a cursor, run row and lock per tenant, so one tenant crashing never rewinds another and tenants can run side by side.
- Pulse card — in-flight and troubled runs, failed first, deliberately outside Pulse's period filter.
--paramand--tenantonbackfill:run.
v0.3
The dashboard, queue mode, events and notifications.
- Livewire dashboard — live progress, throughput, cursor, batch-duration sparkline, failed rows with retry. Actions are queued, not run in the request. Closed by default outside local.
- Queue mode —
--queuedispatches a job that runs a slice and chains the next. Short jobs mean a deploy costs one batch. The chain stops on a pause nobody asked for. - Eight lifecycle events with a machine-readable
StopReasonon every pause. - Notifications on completion, failure and automatic pause. An operator pausing on purpose is never notified.
- Finished runs prunable via
model:prune.
v0.2
Dry run, throttling, retries, the circuit breaker and production guards.
- Dry run — scope, index check, duration estimate, and real before/after diffs from rows processed inside a rolled-back transaction. Mail, notifications, jobs and HTTP intercepted; events recorded but not suppressed.
- Adaptive throttling on replication lag, with a rolling-median batch-duration fallback.
- Transient-failure retries with exponential backoff; everything else fails immediately.
- Circuit breaker that auto-pauses on a sustained failure rate, counted per session so a fixed run can finish.
- Statement and lock timeouts per engine.
- Production guards — row ceiling, deploy freeze windows.
backfill:retry-failed, and an optional per-batch audit trail.
v0.1
The MVP.
Backfillclass,make:backfill, and discovery.- Keyset runner with cursor persistence — never
OFFSET. - Per-row error isolation with savepoints.
run,status,list,pause,resume,cancel.- The run lock, graceful
SIGTERM/SIGINTshutdown, and the migration guard. - The chaos test: a real
SIGKILLmid-batch, asserting the resume matches an uninterrupted run.