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,94 @@
# `phases/commit`
## Purpose
Phase-2 commit for a single participant. Once every participant has
returned `commit` from phase 1 and the coordinator has persisted the
`all-voted-commit` state to the ledger, this module emits
`COMMIT PREPARED '<gid>'` against each writer participant and records
`2pc.commit_duration_ms`.
Phase 2 is intentionally unbounded by timeout — the protocol cannot
tolerate giving up on a commit-prepared mid-flight, because doing so
would leave the system in `in_doubt` state and require the recovery
runner to resolve it on the next boot.
## Features
- Skips participants that did not actually prepare (`outcome.prepared
=== false`) — read-only voters and abort-voters never see commit
SQL.
- Emits a `2pc.commit` span with `tx.id`, `participant.id`, and `gid`
attributes.
- Records `2pc.commit_duration_ms` tagged with `outcome: "committed"`
on success.
- Failure preserves the underlying cause on a thrown
`TwoPcError(TWOPC_COMMIT_FAILED)`; the coordinator escalates to
`TWOPC_IN_DOUBT` once it has tried every participant.
## Usage: Basic
```ts
import { commitPhaseTwo } from "@suaveplan/api-2pc/phases";
await commitPhaseTwo({
txId,
participant,
outcome: preparedOutcome,
instruments,
});
```
## Usage: Advanced
The coordinator collects per-participant failures rather than
short-circuiting — every participant gets attempted exactly once so
operators can see the full picture in the ledger:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
import { commitPhaseTwo } from "@suaveplan/api-2pc/phases";
const failures: TwoPcError[] = [];
for (const outcome of preparedOutcomes) {
if (!outcome.prepared) continue;
try {
await commitPhaseTwo({
txId,
participant: registry.get(outcome.participantId)!,
outcome,
instruments,
});
} catch (err) {
if (err instanceof TwoPcError) failures.push(err);
else throw err;
}
}
if (failures.length > 0) {
// ledger.markInDoubt(txId); recovery resolves on next boot
}
```
## API Reference
- `interface CommitPhaseInput`
- `txId: string`
- `participant: RegisteredTwoPcParticipant`
- `outcome: ParticipantPrepareOutcome` — the phase-1 result.
- `instruments: TwoPcInstruments`
- `function commitPhaseTwo(input): Promise<void>`
- no-op for `outcome.prepared === false`
- throws `TwoPcError(TWOPC_COMMIT_FAILED)` on SQL failure.
## Implementation Notes
The gid stored on the prepare outcome is the single source of truth —
this module never rebuilds it. Reusing the prepare-time value
defends against the unlikely case where a host-config change between
phases would have produced a different gid: the SQL the coordinator
needs to send is exactly the one it prepared with.
The SQL escape (`replace(/'/g, "''")`) matches the prepare-phase
treatment so a gid containing the unlikely quote character would
still produce valid SQL. The escape is cheap; the defence-in-depth
costs nothing on the happy path.