Skip to content

[world-postgres] Skip parked runs in startup recovery and key recovery jobs by run - #5407

Open
div-cowboy wants to merge 1 commit into
vercel:mainfrom
div-cowboy:fix/postgres-recovery-parked-runs
Open

div-cowboy wants to merge 1 commit into
vercel:mainfrom
div-cowboy:fix/postgres-recovery-parked-runs

Conversation

@div-cowboy

Copy link
Copy Markdown

Description

Fixes #3119.

Startup recovery lists every pending/running run and enqueues each one with a fresh Graphile job key. A run parked on a hook or a wait is replayed on every boot, and repeated boots stack duplicate jobs for the same run.

This change:

  • Leaves a running run out of the recovery listing when all of these hold: its only live state is an open hook or a wait that isn't due yet, it has no unfinished step, and no wake is owed. Everything else still recovers, including legacy runs.
  • Adds workflow_runs.wake_pending (migration 0031) to record an owed wake. The hook row alone can't distinguish a parked run from a stored hook_received whose job was never queued, which is the problem raised on fix(world-postgres): skip parked runs and dedupe recovery jobs on startup #3162.
    • The flag is set in the same transaction as run_started, hook_received, hook_disposed, wait_completed, step_completed, and step_failed, and cleared when an orchestrator delivery completes.
    • It is written before the transaction locks the hook, wait, or step row, so the lock order matches the other writers. Writing it after the event insert deadlocks against a concurrent hook_received.
    • Clearing is best-effort, so a missing table or column never fails a delivery.
  • Starts rows that exist before the migration with the flag set, so the first boot after upgrading recovers them as it does today. After that, parked runs are skipped.
  • Keys each recovery job startup-recovery:<runId> as a Graphile job key only. It is not a message idempotencyKey, so the delivery keeps per-run serialization. A boot replaces the job an earlier boot left queued instead of adding another one.

Not covered:

How did you test your changes?

  • test/startup-recovery.test.ts (Postgres via testcontainers):
    • Parked hook and parked wait runs are skipped.
    • Runs with an unfinished step, a due wait, no suspension state, or still pending are recovered.
    • A stored hook_received without its job is recovered, and so is a first delivery that died after creating its hook.
    • Five boots leave exactly one keyed job per recovered run.
    • Concurrent step_completed and hook_received on one run don't deadlock.
  • Unit tests check that:
    • Recovery jobs carry the run key and no idempotency key.
    • The flag is cleared on orchestrator completion but not on step deliveries.
    • A failed clear doesn't fail the delivery.
  • Ran the world-postgres unit suite and the queue integration suites (queue-http, queue-lease, queue-lease-default).

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

Startup recovery listed every pending or running run and enqueued each
with a fresh Graphile job key, so a run parked on a hook or wait was
replayed on every boot and repeated boots stacked duplicate jobs.

The recovery listing now leaves out a running run whose only live state
is an open hook or a wait that is not yet due, with no unfinished step
and no wake owed. A new workflow_runs.wake_pending flag records an owed
wake: it is set in the transaction of run_started, hook_received,
hook_disposed, wait_completed, step_completed and step_failed (taking
the run row lock before the hook, wait or step row, the order other
writers use), and cleared when an orchestrator delivery completes. A
wake whose job was never queued therefore still recovers. Rows that
predate the migration start with the flag set, so the first boot after
upgrading recovers them as before.

Each recovery job is keyed startup-recovery:<runId>, as a Graphile job
key only (not a message idempotency key, which would skip per-run
serialization), so a boot replaces the job an earlier boot left queued.

Fixes vercel#3119

Signed-off-by: div-cowboy <6291624+div-cowboy@users.noreply.github.com>
@div-cowboy
div-cowboy requested a review from a team as a code owner November 13, 2026 02:44
@changeset-bot

changeset-bot Bot commented Nov 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 202e88c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@workflow/world-postgres Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@div-cowboy is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

headers: opts?.headers,
delaySeconds: opts?.delaySeconds,
jobKey: opts?.idempotencyKey ?? messageId,
jobKey: opts?.jobKey ?? opts?.idempotencyKey ?? messageId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a startup-recovery delivery returns { timeoutSeconds }, its replacement loses the per-run job key, so subsequent boots enqueue additional recovery jobs for the same active run.

Fix on Vercel

Comment on lines +1442 to +1443
const completedRunId = orchestratorRunIdOf(body);
if (completedRunId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A successful delivery to another registered queue prefix clears wake_pending for a matching runId in its payload, allowing an unrelated consumer to suppress workflow startup recovery.

Fix on Vercel

@div-cowboy

Copy link
Copy Markdown
Author

@vercel/workflow this is ready for review. The changeset is in and the DCO sign-off passes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

world-postgres: startup recovery re-enqueues parked runs and accumulates duplicate jobs

1 participant