Files
suaveplan/openspec/archive/2026-05-26-api-2pc/proposal.md
2026-05-27 23:29:53 +01:00

5.5 KiB

@suaveplan/api-2pc — two-phase commit coordinator for atomic cross-plugin writes

Package: @suaveplan/api-2pc Status: PROPOSED Type: new-package Stage: 6 (Distributed transactions + Better-Auth) — see umbrella openspec/changes/api-plugin-foundation/

Why

Some workflows genuinely require atomic cross-DB writes — no partial visibility is acceptable (classic finance/inventory). Per design doc §9.2 the foundation ships a 2PC coordinator on top of engines supporting PREPARE TRANSACTION. Postgres is the only mainstream engine that supports it; SQLite and SurrealDB cannot participate as 2PC writers and the coordinator must refuse them at install time. Without this package, plugins needing strict atomicity have no first-class option and would have to roll bespoke coordinator code that nobody can audit.

This package is also the second half of Stage-6 acceptance in the umbrella: "api-2pc commits + aborts an integration scenario against real Postgres."

What changes

Net-new package depending on @suaveplan/api-types, @suaveplan/api-host (consumes the metadata DB), @suaveplan/api-manifest, @suaveplan/db (for the db.preparable capability check + PREPARE TRANSACTION exec), @suaveplan/error, @suaveplan/logger, @suaveplan/opentelemetry, and zod.

  • ADDED: createTwoPcCoordinator(deps) — factory binding the coordinator to a host's metadata DB, capability registry, OTel meter, and clock.
  • ADDED: run2pc(opts, ctx) — primary entry point. opts: { participants: Array<{ plugin, action, input }>, timeoutMs?: number }. Default timeoutMs is 5000ms, hard ceiling 30000ms. Exceeding the ceiling is refused at call time.
  • ADDED: TwoPcParticipantRegistry — registers participating plugins; refuses registration if the plugin's bound DB engine does not support db.preparable (Surreal / SQLite plugins MAY still register as read-only voters and contribute to the vote but MUST NOT participate in write phases).
  • ADDED: TwoPcCoordinatorState — "voting" | "committing" | "aborting" | "committed" | "aborted" | "in_doubt".
  • ADDED: TwoPcVote — "commit" | "abort".
  • ADDED: Boot-time orphan resolution — on coordinator start, query every participant's pg_prepared_xacts for prepared transactions tagged with the host's instance ID; commit those whose ledger row says "all-voted-commit", roll back the rest.
  • ADDED: outstandingPreparedCap — default cap of 256 outstanding prepared transactions per coordinator (design doc §14 left this as an implementation choice; this is the spec'd default and is operator-tunable via host config). Exceeding the cap rejects new run2pc calls with TWOPC_OUTSTANDING_LIMIT_EXCEEDED.
  • ADDED: run2pcParticipantConformance(participantFactory) — re-export of the conformance suite from api-types/twopc.
  • ADDED: TWOPC_* error codes: TWOPC_TIMEOUT, TWOPC_VOTE_ABORT, TWOPC_PREPARE_FAILED, TWOPC_COMMIT_FAILED, TWOPC_ROLLBACK_FAILED, TWOPC_IN_DOUBT, TWOPC_PARTICIPANT_NOT_PREPARABLE, TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE, TWOPC_OUTSTANDING_LIMIT_EXCEEDED, TWOPC_TIMEOUT_CEILING_EXCEEDED.
  • ADDED: OTel instruments: 2pc.transaction span (per transaction lifetime), 2pc.prepare / 2pc.commit / 2pc.rollback spans per participant phase, 2pc.transactions_total counter { outcome }, 2pc.prepare_duration_ms histogram, 2pc.commit_duration_ms histogram, 2pc.outstanding_prepared observable gauge.

Out of scope

  • The suaveplan_2pc_transactions table schema — ships with api-host's migrations. This package only reads/writes rows.
  • Engines other than Postgres — design doc §9.2 explicitly scopes the writer surface to Postgres. SQLite and SurrealDB participate only as read-only voters; this package SHALL refuse them as writers at install time and the spec pins this.
  • Cross-region / multi-host coordinator coordination — umbrella's out-of-scope list excludes multi-region.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
run2pc API + phase-1 prepare / phase-2 commit §9.2 (paragraph 2 + code block)
Default timeoutMs 5000 / hard ceiling 30000 §9.2 (operational warning paragraph)
Coordinator state in suaveplan_2pc_transactions survives restart §9.2 (paragraph 3)
Orphan resolution via pg_prepared_xacts on boot §9.2 (paragraph 3)
Postgres-only writer; Surreal/SQLite as read-only voters only §9.2 (paragraph 1)
db.preparable capability requirement §9.2 (paragraph 4)
2pc.prepare_duration_ms + 2pc.commit_duration_ms instruments §9.2 (operational warning paragraph)

Verification approach

  • src/coordinator/coordinator.test.ts — happy-path commit, vote-abort path, timeout-then-rollback path, in-doubt path.
  • src/coordinator/coordinator.spec-conformance.test.ts — runs run2pcParticipantConformance against a reference Postgres participant.
  • src/registry/participant-registry.test.ts — refusal of non-preparable engines as writers; acceptance as read-only voters.
  • src/recovery/recovery.test.ts (integration, 30000ms) — kills coordinator mid-prepare, restarts, asserts orphan resolution commits or rolls back based on ledger state.
  • src/limits/outstanding-cap.test.ts — exceeds the 256-prepared cap, asserts TWOPC_OUTSTANDING_LIMIT_EXCEEDED.
  • tests/integration/postgres-2pc.test.ts (60000ms) — real @suaveplan/testing-integration-postgres: full commit + abort + restart-recovery scenarios end-to-end.

Migration impact

None — additive only.