Files
2026-05-27 23:29:53 +01:00

61 lines
5.5 KiB
Markdown

# `@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.