Skip to content

world-postgres: create run row, slot marker, and run_created event atomically #3656

Description

@tbharthur

Package: @workflow/world-postgres@5.0.0-beta.34 (@workflow/world@5.0.0-beta.27), Node 22, PostgreSQL 17 (Cloud SQL).

@workflow/world-postgres@5.0.0-beta.34 can create a current slot-numbered run with a legacy ULID event because run creation is split across three autocommits.

allocateEventId() treats absence of a workflow_event_slots row as the signal that a run predates slot numbering and returns wevt_<ulid>. Both run creation paths currently make the run visible before inserting that marker:

INSERT workflow_runs                  -- commits
INSERT workflow_event_slots           -- commits later
INSERT workflow_events (run_created)  -- commits later

The affected paths are the resilient run_started creation path and the ordinary run_created path in packages/world-postgres/src/storage.ts.

A deterministic reproduction is:

  1. delay INSERT on workflow.workflow_event_slots with a test trigger;
  2. call world.events.create(runId, { eventType: "run_created", ... });
  3. as soon as the workflow_runs row is visible, concurrently call world.events.create(runId, { eventType: "run_started" });
  4. inspect workflow.workflow_events.

On beta.34 the persisted log contains slot 1 run_created plus a wevt_<ulid> run_started. The runtime later rejects that log because the event ID is not slot-numbered.

Please wrap the run row, slot marker, and first run_created event in one drizzle.transaction() at both creation sites. Preserve onConflictDoNothing() and the resilient path's re-read when a concurrent ordinary run_created wins. The ordinary path must also insert its run_created event inside the transaction and skip the shared tail insertion, otherwise the marker can commit before slot 1 and a concurrent writer can take the first slot.

The invariant should be: for a slot-era run, no transaction can observe the run row without also observing its slot marker and slot-1 run_created event.

We are carrying this as a local pnpm patch (both creation sites wrapped in one drizzle.transaction()) with a real-Postgres concurrency regression; happy to open a PR if that shape is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions