3.6 KiB
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
preparecallback, then thePREPARE TRANSACTIONSQL when the vote iscommitAND 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: truewithTWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE. - Honours an
AbortSignalfor the phase-1 deadline; aborted SQL is remapped toTWOPC_TIMEOUT. - Emits a
2pc.preparespan around the whole call and records a2pc.prepare_duration_msobservation tagged with the outcome. - Failure cases preserve the underlying cause on the thrown
TwoPcError.
Usage: Basic
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:
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_FAILEDwhen the participant callback or SQL raises (signal not aborted), - throws
TWOPC_TIMEOUTwhen the signal aborted around the SQL, - throws
TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITEwhen a read-only voter returnedwriteIntent: true.
- throws
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.