Initial Commit
This commit is contained in:
145
packages/api-stack/api/api-2pc/src/phases/rollback.test.ts
Normal file
145
packages/api-stack/api/api-2pc/src/phases/rollback.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user