Skip to content

feat(jobs): durable job store, leased runner, and WP-Cron dispatch - #176

Merged
TheAggressive merged 2 commits into
masterfrom
feat/durable-jobs
Oct 11, 2026
Merged

TheAggressive merged 2 commits into
masterfrom
feat/durable-jobs

Conversation

@TheAggressive

Copy link
Copy Markdown
Owner

Closes #168. Part of #66 (M5).

What this adds

The foundation for M5's background work: jobs survive process crashes and late cron, and a job whose handler isn't safe to re-run is never re-run.

  • Schema 7: {prefix}campaignbridge_jobs. Each job records:
    • type and target;
    • a small scalar payload that refuses credential-like keys;
    • state (queued, claimed, succeeded, failed, dead);
    • attempts, run time, lease, and a stable last-error code.
  • Job_Repository (behind the Job_Source port).
    • Every claim and later transition is one conditional UPDATE, so two workers can't hold one job, and a worker whose lease expired can no longer finish it.
    • A dedupe key keeps one active job per type and target, and is released when the job finishes.
    • Expired leases are taken over first. A lease that expired on the job's last attempt is claimed as exhausted, so it can't sit "claimed" forever.
  • Job_Runner, Job_Handler, Job_Queue (workflow).
    • Each handler declares whether it's retry_safe.
    • A job taken over from a stopped worker runs again only if its handler is retry-safe. The same applies when a handler that isn't retry-safe throws partway. Otherwise the handler's abandoned() hook runs and the job ends dead.
    • Retries back off 1, 2, 4 … minutes, capped at an hour, and end dead with the last error once attempts are used up.
    • Exception messages are never stored, only error codes.
  • Cron/Job_Dispatcher. A one-minute WP-Cron tick runs a batch of 10 and purges succeeded jobs older than a week. It's unscheduled on deactivation.
  • Status health. Reports waiting, stopped, and overdue jobs, and warns when work has stalled or the tick isn't scheduled.

No job types are registered yet. Background reconciliation (#171) is the first.

Evidence (#168)

  • Migration from a populated schema 6 (a scheduled campaign is kept), including idempotent re-runs, plus an index check.

  • Job_Queue_Test:

    • dedupe, and re-queueing finished work;
    • credential-like payload keys are refused;
    • a true claim race: both workers selected the row, but only one conditional update lands;
    • lease takeover locks out the stopped worker, and heartbeats keep a lease;
    • backoff timings, then dead with the last error;
    • exception messages are not stored;
    • permanent failure, and a job with no handler;
    • crash recovery: an unsafe job isn't re-run (abandoned() instead), a retry-safe job runs again, an unsafe throw is abandoned rather than retried, and a crash on the last attempt ends the job.

    Removing the takeover guard makes the crash test fail.

  • Job_Dispatcher_Test: the plugin registers the tick, schedules it once, runs it through the production runner, and clears it on deactivation.

  • Job_Dispatch_Performance_Test: an idle tick takes 5 or fewer queries. A tick with three batches queued stays within 5 + 3 per job in the batch, so its cost is bounded by the batch size, not by how much is queued.

  • SQLite (Studio): a probe ran dedupe, the claim race, takeover (normal and exhausted), lockout, retry, finish, counts, and stalled detection. Studio was then re-stamped to schema 6, so master keeps working there until this merges.

Verification

  • pnpm qa:fast passes.
  • Unit (1126), integration (450), security (117), accessibility (25), and performance (9) suites: all pass.

Notes

  • The one-minute interval is deliberate (it carries a justified WPCS ignore): an idle tick is one indexed query, and due work shouldn't wait 15 minutes.
  • Rollback across schema 7 is documented in the runbook: lower the stamped version to 6. The table is ignored by older code, and queued jobs survive.

🤖 Generated with Claude Code

Background work now has a durable home that survives process crashes and
late cron, the foundation for M5 reconciliation, retries, and webhooks.

- Schema 7 adds {prefix}campaignbridge_jobs. A job records its type,
  target, a small scalar payload that refuses credential-like keys, state
  (queued, claimed, succeeded, failed, dead), attempts, run time, lease, and
  a stable last-error code.
- Job_Repository claims and transitions jobs with conditional updates, so
  two workers cannot hold one job and a worker whose lease expired can no
  longer finish it. A dedupe key keeps one active job per type and target.
- Job_Runner runs a bounded batch through each type's Job_Handler. A job
  taken over from a stopped worker, or whose handler is not retry-safe and
  threw part way, runs again only if its handler is retry-safe; otherwise
  the handler's abandoned() hook runs and the job ends dead, so an
  irreversible effect is never repeated. Retries back off exponentially and
  end dead once attempts are used up.
- Job_Dispatcher runs a batch on a one-minute WP-Cron tick, purges week-old
  successes, and is unscheduled on deactivation.
- Status health reports waiting, stopped, and overdue jobs and warns when
  work has stalled or the tick is not scheduled.

No job types are registered yet; background reconciliation (#171) is the
first. Verified on MySQL and on the Studio SQLite site.

Closes #168

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added area:php PHP or Composer code area:tests Automated tests or test configuration risk:high Security or release sensitive; never auto-merged type:feature New capability needs-attention Automation stopped; maintainer action required labels Oct 10, 2026
@TheAggressive
TheAggressive enabled auto-merge (squash) October 11, 2026 10:22
@TheAggressive
TheAggressive merged commit 1cbe105 into master Oct 11, 2026
21 checks passed
@TheAggressive
TheAggressive deleted the feat/durable-jobs branch October 11, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:php PHP or Composer code area:tests Automated tests or test configuration needs-attention Automation stopped; maintainer action required risk:high Security or release sensitive; never auto-merged type:feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(jobs): durable job store, leased runner, and WP-Cron dispatch

1 participant