Uh oh!
There was an error while loading. Please reload this page.
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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
72af83e to
5ce19f0Compare
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.