146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { TWOPC_CODES, TwoPcError } from "../error-codes/error-codes.js";
|
|
import { createTwoPcInstruments } from "../instruments/instruments.js";
|
|
import type { RegisteredTwoPcParticipant } from "../types/types.types.js";
|
|
import type { ParticipantPrepareOutcome } from "./prepare.js";
|
|
import { isUnknownPreparedTransaction, rollbackPhaseTwo } from "./rollback.js";
|
|
|
|
const INSTRUMENTS = createTwoPcInstruments({
|
|
outstandingPreparedSnapshot: () => 0,
|
|
});
|
|
|
|
const OUTCOME: ParticipantPrepareOutcome = {
|
|
participantId: "billing",
|
|
vote: "commit",
|
|
prepared: true,
|
|
gid: "2pc-h-t-billing",
|
|
};
|
|
|
|
function makeParticipant(opts: { fail?: unknown } = {}) {
|
|
const calls: string[] = [];
|
|
const p: RegisteredTwoPcParticipant = {
|
|
id: "billing",
|
|
voterMode: "writer",
|
|
connection: {
|
|
engine: "postgres",
|
|
run: async (sql: string) => {
|
|
calls.push(sql);
|
|
if (opts.fail !== undefined) throw opts.fail;
|
|
return undefined;
|
|
},
|
|
},
|
|
prepare: async () => ({ vote: "commit", writeIntent: true }),
|
|
};
|
|
return { participant: p, calls };
|
|
}
|
|
|
|
describe("rollbackPhaseTwo", () => {
|
|
it("issues ROLLBACK PREPARED against a prepared writer", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { participant, calls } = makeParticipant();
|
|
await rollbackPhaseTwo({
|
|
txId: "t",
|
|
participant,
|
|
outcome: OUTCOME,
|
|
instruments: INSTRUMENTS,
|
|
});
|
|
expect(calls[0]).toBe("ROLLBACK PREPARED '2pc-h-t-billing'");
|
|
});
|
|
|
|
it("is a no-op when outcome.prepared === false", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { participant, calls } = makeParticipant();
|
|
await rollbackPhaseTwo({
|
|
txId: "t",
|
|
participant,
|
|
outcome: { ...OUTCOME, prepared: false, gid: "" },
|
|
instruments: INSTRUMENTS,
|
|
});
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it("treats 42704 (undefined_object) as a no-op (idempotency)", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const cause = Object.assign(new Error("does not exist"), {
|
|
code: "42704",
|
|
});
|
|
const { participant } = makeParticipant({ fail: cause });
|
|
await expect(
|
|
rollbackPhaseTwo({
|
|
txId: "t",
|
|
participant,
|
|
outcome: OUTCOME,
|
|
instruments: INSTRUMENTS,
|
|
})
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("treats 'does not exist' message-only errors as a no-op", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const cause = new Error(
|
|
"prepared transaction with identifier 'x' does not exist"
|
|
);
|
|
const { participant } = makeParticipant({ fail: cause });
|
|
await expect(
|
|
rollbackPhaseTwo({
|
|
txId: "t",
|
|
participant,
|
|
outcome: OUTCOME,
|
|
instruments: INSTRUMENTS,
|
|
})
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("wraps a non-idempotency SQL throw into TWOPC.ROLLBACK_FAILED", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const cause = new Error("connection broken");
|
|
const { participant } = makeParticipant({ fail: cause });
|
|
try {
|
|
await rollbackPhaseTwo({
|
|
txId: "t",
|
|
participant,
|
|
outcome: OUTCOME,
|
|
instruments: INSTRUMENTS,
|
|
});
|
|
throw new Error("should throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(TwoPcError);
|
|
expect((err as TwoPcError).twoPcCode).toBe(
|
|
TWOPC_CODES.ROLLBACK_FAILED
|
|
);
|
|
expect((err as TwoPcError).cause).toBe(cause);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("isUnknownPreparedTransaction", () => {
|
|
it("returns true for SQLSTATE 42704", { timeout: 5000 }, () => {
|
|
expect(isUnknownPreparedTransaction({ code: "42704" })).toBe(true);
|
|
});
|
|
|
|
it("returns true for messages containing 'does not exist'", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
isUnknownPreparedTransaction({ message: "tx X does not exist" })
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false for null / undefined / primitives", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(isUnknownPreparedTransaction(null)).toBe(false);
|
|
expect(isUnknownPreparedTransaction(undefined)).toBe(false);
|
|
expect(isUnknownPreparedTransaction(42)).toBe(false);
|
|
});
|
|
|
|
it("returns false for unrelated errors", { timeout: 5000 }, () => {
|
|
expect(isUnknownPreparedTransaction(new Error("other"))).toBe(false);
|
|
});
|
|
});
|