world-postgres: enforce per-(run, correlation) uniqueness for entity-creating events#1878
Conversation
🦋 Changeset detectedLatest commit: 774f4fe The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
🧪 E2E Test Results✅ All tests passed Summary
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
|
There was a problem hiding this comment.
Pull request overview
Adds a Postgres-level deduplication guard for entity-creating events so concurrent (or repeated) creates with the same (runId, correlationId, type) surface as EntityConflictError, aligning world-postgres behavior with the runtime’s existing dedup contract.
Changes:
- Add a partial unique index on
workflow_events(run_id, correlation_id, type)forstep_created/hook_created/wait_created. - Translate Postgres unique-violation (
23505) duringevents.create()intoEntityConflictErrorfor entity-creating event types. - Add regression tests for duplicate
step_createdandwait_createdscenarios.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/world-postgres/src/storage.ts | Catch and translate unique-violation errors into EntityConflictError for entity-creating events. |
| packages/world-postgres/src/drizzle/schema.ts | Add Drizzle schema definition for the partial unique index. |
| packages/world-postgres/src/drizzle/migrations/0010_add_events_entity_creation_unique_index.sql | Create the partial unique index in SQL migration. |
| packages/world-postgres/src/drizzle/migrations/meta/_journal.json | Register the new migration in the Drizzle journal. |
| packages/world-postgres/test/storage.test.ts | Add tests asserting duplicate entity-creation attempts surface as EntityConflictError and don’t duplicate event rows. |
| .changeset/fix-world-postgres-events-uniqueness.md | Publish a patch changeset describing the new uniqueness + error translation behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
72af83e to
5ce19f0
Compare
Summary
Adds a unique partial index on
workflow_events(run_id, correlation_id, type)filtered tostep_created/hook_created/wait_created, and translates the resulting unique-violation (pg code23505, surfaced viaDrizzleQueryError.cause) intoEntityConflictError.Background
Concurrent invocations producing identical correlationIds previously both succeeded at the events-table level, leaving duplicate rows in the log. The
stepstable already deduped viaonConflictDoNothing, but the corresponding event row still inserted, so the storage was internally inconsistent (one step, twostep_createdevents). The runtime's existing dedup catch path (if (EntityConflictError.is(err)) continueinruntime/snapshot-entrypoint.ts) was the intended consumer of this signal but never received it from world-postgres.This is the postgres counterpart of PR #1877 (world-local).
Fix
workflow_events_entity_creation_uniqueon(run_id, correlation_id, type)filtered to entity-creating events (migration0010_add_events_entity_creation_unique_index.sql).events.create()wraps the INSERT in try/catch, detects pg23505(read offDrizzleQueryError.cause.code), and re-throws asEntityConflictErrorfor the relevant event types.step_createdandwait_createdduplicate scenarios.Verification
Extracted from PR #1300 (snapshot-runtime). The snapshot runtime produces deterministic correlationIds across concurrent VM invocations of the same resumption by design — that path made the dedup gap reliably reproducible — but the fix is also valuable on its own for replay-runtime concurrent scenarios.