refactor(gateway): separate ciao classification from logging

This commit is contained in:
Peter Steinberger 2026-03-22 15:01:55 -07:00
parent 31ee442d3f
commit ee077804b0
No known key found for this signature in database
4 changed files with 91 additions and 56 deletions

View File

@ -1,70 +1,51 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
const logDebugMock = vi.hoisted(() => vi.fn());
const logWarnMock = vi.hoisted(() => vi.fn());
vi.mock("../logger.js", () => ({
logDebug: (...args: unknown[]) => logDebugMock(...args),
logWarn: (...args: unknown[]) => logWarnMock(...args),
}));
const { ignoreCiaoUnhandledRejection } = await import("./bonjour-ciao.js");
const { classifyCiaoUnhandledRejection, ignoreCiaoUnhandledRejection } =
await import("./bonjour-ciao.js");
describe("bonjour-ciao", () => {
it("ignores and logs ciao announcement cancellation rejections", () => {
it("classifies ciao cancellation rejections separately from side effects", () => {
expect(classifyCiaoUnhandledRejection(new Error("CIAO PROBING CANCELLED"))).toEqual({
kind: "cancellation",
formatted: "CIAO PROBING CANCELLED",
});
});
it("classifies ciao interface assertions separately from side effects", () => {
expect(
classifyCiaoUnhandledRejection(
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
),
).toEqual({
kind: "interface-assertion",
formatted: "Reached illegal state! IPV4 address change from defined to undefined!",
});
});
it("suppresses ciao announcement cancellation rejections", () => {
expect(ignoreCiaoUnhandledRejection(new Error("Ciao announcement cancelled by shutdown"))).toBe(
true,
);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
expect(logWarnMock).not.toHaveBeenCalled();
});
it("ignores and logs ciao probing cancellation rejections", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
it("suppresses ciao probing cancellation rejections", () => {
expect(ignoreCiaoUnhandledRejection(new Error("CIAO PROBING CANCELLED"))).toBe(true);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
expect(logWarnMock).not.toHaveBeenCalled();
});
it("ignores lower-case string cancellation reasons too", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
it("suppresses lower-case string cancellation reasons too", () => {
expect(ignoreCiaoUnhandledRejection("ciao announcement cancelled during cleanup")).toBe(true);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
expect(logWarnMock).not.toHaveBeenCalled();
});
it("suppresses ciao interface assertion rejections as non-fatal", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
const error = Object.assign(
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
{ name: "AssertionError" },
);
expect(ignoreCiaoUnhandledRejection(error)).toBe(true);
expect(logWarnMock).toHaveBeenCalledWith(
expect.stringContaining("suppressing ciao interface assertion"),
);
expect(logDebugMock).not.toHaveBeenCalled();
});
it("keeps unrelated rejections visible", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
expect(ignoreCiaoUnhandledRejection(new Error("boom"))).toBe(false);
expect(logDebugMock).not.toHaveBeenCalled();
expect(logWarnMock).not.toHaveBeenCalled();
});
});

View File

@ -1,21 +1,27 @@
import { logDebug, logWarn } from "../logger.js";
import { formatBonjourError } from "./bonjour-errors.js";
const CIAO_CANCELLATION_MESSAGE_RE = /^CIAO (?:ANNOUNCEMENT|PROBING) CANCELLED\b/u;
const CIAO_INTERFACE_ASSERTION_MESSAGE_RE =
/REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM DEFINED TO UNDEFINED!?/u;
export function ignoreCiaoUnhandledRejection(reason: unknown): boolean {
export type CiaoUnhandledRejectionClassification =
| { kind: "cancellation"; formatted: string }
| { kind: "interface-assertion"; formatted: string };
export function classifyCiaoUnhandledRejection(
reason: unknown,
): CiaoUnhandledRejectionClassification | null {
const formatted = formatBonjourError(reason);
const message = formatted.toUpperCase();
if (!CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
if (!CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return false;
}
logWarn(`bonjour: suppressing ciao interface assertion: ${formatted}`);
return true;
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
return { kind: "cancellation", formatted };
}
logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatted}`);
return true;
if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "interface-assertion", formatted };
}
return null;
}
export function ignoreCiaoUnhandledRejection(reason: unknown): boolean {
return classifyCiaoUnhandledRejection(reason) !== null;
}

View File

@ -241,6 +241,39 @@ describe("gateway bonjour advertiser", () => {
expect(order).toEqual(["shutdown", "cleanup"]);
});
it("logs ciao handler classifications at the bonjour caller", async () => {
enableAdvertiserUnitMode();
const destroy = vi.fn().mockResolvedValue(undefined);
const advertise = vi.fn().mockResolvedValue(undefined);
mockCiaoService({ advertise, destroy });
const started = await startGatewayBonjourAdvertiser({
gatewayPort: 18789,
sshPort: 2222,
});
const handler = registerUnhandledRejectionHandler.mock.calls[0]?.[0] as
| ((reason: unknown) => boolean)
| undefined;
expect(handler).toBeTypeOf("function");
expect(handler?.(new Error("CIAO PROBING CANCELLED"))).toBe(true);
expect(logDebug).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
logDebug.mockClear();
expect(
handler?.(new Error("Reached illegal state! IPV4 address change from defined to undefined!")),
).toBe(true);
expect(logWarn).toHaveBeenCalledWith(
expect.stringContaining("suppressing ciao interface assertion"),
);
await started.stop();
});
it("logs advertise failures and retries via watchdog", async () => {
enableAdvertiserUnitMode();
vi.useFakeTimers();

View File

@ -1,6 +1,6 @@
import { logDebug, logWarn } from "../logger.js";
import { getLogger } from "../logging.js";
import { ignoreCiaoUnhandledRejection } from "./bonjour-ciao.js";
import { classifyCiaoUnhandledRejection } from "./bonjour-ciao.js";
import { formatBonjourError } from "./bonjour-errors.js";
import { isTruthyEnvValue } from "./env.js";
import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js";
@ -94,6 +94,21 @@ function isAnnouncedState(state: BonjourServiceState | "unknown") {
return String(state) === "announced";
}
function handleCiaoUnhandledRejection(reason: unknown): boolean {
const classification = classifyCiaoUnhandledRejection(reason);
if (!classification) {
return false;
}
if (classification.kind === "interface-assertion") {
logWarn(`bonjour: suppressing ciao interface assertion: ${classification.formatted}`);
return true;
}
logDebug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`);
return true;
}
export async function startGatewayBonjourAdvertiser(
opts: GatewayBonjourAdvertiseOpts,
): Promise<GatewayBonjourAdvertiser> {
@ -175,7 +190,7 @@ export async function startGatewayBonjourAdvertiser(
const cleanupUnhandledRejection =
services.length > 0
? registerUnhandledRejectionHandler(ignoreCiaoUnhandledRejection)
? registerUnhandledRejectionHandler(handleCiaoUnhandledRejection)
: undefined;
return { responder, services, cleanupUnhandledRejection };