first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# `phases/prepare`
## Purpose
Implements phase 1 of the 2PC protocol for a single participant. The
coordinator iterates participants and calls this module for each;
unanimous commit votes trigger phase 2, any abort or prepare failure
short-circuits to rollback. This module is the only place the
`PREPARE TRANSACTION '<gid>'` SQL is emitted, so the gid format
(`phases/gid`) and the SQL emission point stay co-located.
Phase 1 is the latency-sensitive half of 2PC: every participant holds
row-level locks for the duration. The module records
`2pc.prepare_duration_ms` so operators can spot creeping lock holds
before they cause throughput regressions.
## Features
- Runs the participant's `prepare` callback, then the
`PREPARE TRANSACTION` SQL when the vote is `commit` AND the
participant is a writer.
- Skips the SQL for read-only voters — they participate in the vote
only.
- Refuses a read-only voter that returned `writeIntent: true` with
`TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`.
- Honours an `AbortSignal` for the phase-1 deadline; aborted SQL is
remapped to `TWOPC_TIMEOUT`.
- Emits a `2pc.prepare` span around the whole call and records a
`2pc.prepare_duration_ms` observation tagged with the outcome.
- Failure cases preserve the underlying cause on the thrown
`TwoPcError`.
## Usage: Basic
```ts
import { preparePhaseOne } from "@suaveplan/api-2pc/phases";
const outcome = await preparePhaseOne({
hostInstanceId: "host-01",
txId,
participant,
payload: { invoiceId: "inv-1" },
instruments,
signal: ac.signal,
});
if (outcome.vote === "abort") {
// run rollback for every participant that voted commit
}
```
## Usage: Advanced
The coordinator runs this in a loop. Read-only voters interleave
freely with writers — the outcome's `prepared` flag tells phase 2
whether to send commit/rollback SQL:
```ts
import { preparePhaseOne } from "@suaveplan/api-2pc/phases";
const outcomes: ParticipantPrepareOutcome[] = [];
for (const p of registry.list()) {
const o = await preparePhaseOne({
hostInstanceId,
txId,
participant: p,
payload: payloadFor(p),
instruments,
signal,
});
outcomes.push(o);
if (o.vote === "abort") break; // short-circuit
}
// phase 2: commit only those with prepared === true
const writers = outcomes.filter((o) => o.prepared);
```
## API Reference
- `interface ParticipantPrepareOutcome` — `{ participantId, vote,
prepared, gid }`.
- `interface PreparePhaseInput` — `{ hostInstanceId, txId,
participant, payload, instruments, signal }`.
- `function preparePhaseOne(input): Promise<ParticipantPrepareOutcome>`
- throws `TWOPC_PREPARE_FAILED` when the participant callback or
SQL raises (signal not aborted),
- throws `TWOPC_TIMEOUT` when the signal aborted around the SQL,
- throws `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE` when a read-only
voter returned `writeIntent: true`.
## Implementation Notes
The SQL escape strategy is conservative: gids are built from the host
instance id, transaction id, and participant id — all three are
already validated at host config / id-generator / registry time — but
the function still calls `.replace(/'/g, "''")` before interpolating
into the `PREPARE TRANSACTION` statement. The runtime cost is zero on
the happy path and the defence-in-depth is cheap. The coordinator
never accepts user-supplied SQL fragments.
The span is started before the prepare callback so the participant's
own work shows up in the trace as a child of the `2pc.prepare` span if
the participant opens its own spans against the same tracer. The
`finally`-block `span.end()` guarantees the span is closed regardless
of which branch fires.