Files
skills/docs/api-stack/api/api-2pc/src/phases/commit.md
2026-05-29 08:20:56 +01:00

3.0 KiB

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

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:

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.