Description
Expanding on #679 — this isn't limited to sleep(). All workflow execution is non-recoverable after a process crash in Postgres World, including mid-step execution.
Root cause
As @jcourson-bg identified in #679, the postgres world delegates to localWorld.queue() which fires execution in an unawaited IIFE. The graphile-worker job completes immediately, so after a crash there's nothing left to retry — the event log is in postgres but nothing re-triggers the workflow.
Reproduction
Tested with workflow@4.2.0-beta.67, @workflow/world-postgres@4.1.0-beta.42, PostgreSQL 16, Next.js dev server.
Using the postgres example with WORKFLOW_TARGET_WORLD="@workflow/world-postgres".
Scenario 1: sleep() (confirms #679)
Workflow:
export async function handleUserSignup(email: string) {
"use workflow";
const user = await createUser(email);
await sendWelcomeEmail(user);
await sleep("2m");
await sendOnboardingEmail(user);
return { userId: user.id, status: "onboarded" };
}
- Trigger workflow —
createUser and sendWelcomeEmail complete, enters sleep("2m")
- Verify:
workflow_waits shows status = 'waiting', resume_at set 2 minutes out
- Kill the server (
kill -9)
- Wait past
resume_at
- Restart the server
Result: Run stays running, wait stays waiting (past resume_at), 0 graphile-worker jobs. sendOnboardingEmail never executes.
Scenario 2: Long-running step (no sleep())
Workflow:
export async function handleUserSignup(email: string) {
"use workflow";
const user = await createUser(email);
await sendWelcomeEmail(user);
await processUserData(user);
await sendOnboardingEmail(user);
return { userId: user.id, status: "onboarded" };
}
async function processUserData(user: { id: string; email: string }) {
"use step";
console.log(`Processing user data for: ${user.id}`);
await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
return { processed: true };
}
- Trigger workflow —
createUser and sendWelcomeEmail complete, processUserData starts
- Verify: events show
step_started for processUserData
- Kill the server (
kill -9) mid-step
- Restart the server
Result: Run stays running, step has step_started but no step_completed, 0 graphile-worker jobs. No recovery activity in server logs. The step is never retried.
DB state after restart (both scenarios)
workflow.workflow_runs: status = 'running'
graphile_worker._private_jobs: (0 rows)
Expected behavior
Both scenarios should recover after server restart. The postgres world's value proposition is durability - workflows should survive process crashes regardless of whether they're in a sleep() or mid-step execution.
Description
Expanding on #679 — this isn't limited to
sleep(). All workflow execution is non-recoverable after a process crash in Postgres World, including mid-step execution.Root cause
As @jcourson-bg identified in #679, the postgres world delegates to
localWorld.queue()which fires execution in an unawaited IIFE. The graphile-worker job completes immediately, so after a crash there's nothing left to retry — the event log is in postgres but nothing re-triggers the workflow.Reproduction
Tested with
workflow@4.2.0-beta.67,@workflow/world-postgres@4.1.0-beta.42, PostgreSQL 16, Next.js dev server.Using the postgres example with
WORKFLOW_TARGET_WORLD="@workflow/world-postgres".Scenario 1:
sleep()(confirms #679)Workflow:
createUserandsendWelcomeEmailcomplete, enterssleep("2m")workflow_waitsshowsstatus = 'waiting',resume_atset 2 minutes outkill -9)resume_atResult: Run stays
running, wait stayswaiting(pastresume_at), 0 graphile-worker jobs.sendOnboardingEmailnever executes.Scenario 2: Long-running step (no
sleep())Workflow:
createUserandsendWelcomeEmailcomplete,processUserDatastartsstep_startedforprocessUserDatakill -9) mid-stepResult: Run stays
running, step hasstep_startedbut nostep_completed, 0 graphile-worker jobs. No recovery activity in server logs. The step is never retried.DB state after restart (both scenarios)
Expected behavior
Both scenarios should recover after server restart. The postgres world's value proposition is durability - workflows should survive process crashes regardless of whether they're in a
sleep()or mid-step execution.