Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/orb/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ async function pruneRelayPending(env: Env): Promise<number> {
.prepare("DELETE FROM orb_relay_pending WHERE created_at < datetime('now', '-' || ? || ' hours')")
.bind(RELAY_PENDING_TTL_HOURS)
.run();
// Make pull-mode loss VISIBLE too (parity with the push-path drop): a pruned row is a webhook a long-down tailnet
// container never drained — emit an alertable error-level log (distinct event name) so it leaves a Sentry trace.
if (pruned.meta.changes > 0) {
console.error(JSON.stringify({ level: "error", event: "orb_relay_pending_dropped", count: pruned.meta.changes }));
}
return pruned.meta.changes;
}

Expand Down Expand Up @@ -244,7 +249,7 @@ export async function retryFailedRelays(env: Env, opts?: { fetchImpl?: typeof fe
// retries exhausted) — e.g. a container down for over an hour. Emit an alertable structured log so the loss
// leaves a trace instead of vanishing silently.
if (pruned.meta.changes > 0) {
console.warn(JSON.stringify({ level: "warn", event: "orb_relay_events_dropped", count: pruned.meta.changes }));
console.error(JSON.stringify({ level: "error", event: "orb_relay_events_dropped", count: pruned.meta.changes }));
}
const { results } = await env.DB
.prepare(
Expand Down
13 changes: 8 additions & 5 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,12 +743,15 @@ async function main(): Promise<void> {
PUBLIC_API_ORIGIN: process.env.PUBLIC_API_ORIGIN,
})
.then((r) => {
if (r !== "skipped")
console.log(
JSON.stringify({ event: "selfhost_orb_relay_register", result: r }),
);
if (r === "registered") {
console.log(JSON.stringify({ event: "selfhost_orb_relay_register", result: r }));
} else if (r === "failed") {
// A failed registration means the central Orb won't forward this install's webhooks here — the container
// looks alive but reviews NOTHING. Surface at error level so the operator sees a deaf container.
console.error(JSON.stringify({ level: "error", event: "selfhost_orb_relay_register_failed" }));
}
})
.catch(() => {});
.catch((error) => captureError(error, { kind: "orb_relay_register" }));

// Graceful shutdown: stop accepting HTTP, let the queue finish, close the backend.
let shuttingDown = false;
Expand Down
16 changes: 10 additions & 6 deletions test/integration/orb-relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,17 +344,17 @@ describe("retryFailedRelays", () => {
expect(untouched?.n).toBe(5);
});

it("PRUNES rows that have exhausted their attempt budget (attempts >= 5) and logs the drop (#5)", async () => {
it("PRUNES rows that have exhausted their attempt budget (attempts >= 5) and logs the drop at error level (#5)", async () => {
const e = brokeredEnv();
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const errLog = vi.spyOn(console, "error").mockImplementation(() => undefined);
// Manually insert a row at the attempt ceiling.
await db(e).prepare("INSERT INTO orb_relay_failures (delivery_id, event_name, installation_id, raw_body, attempts) VALUES (?, ?, ?, ?, ?)").bind("exhausted-1", "pull_request", 9300, "{}", 5).run();
await retryFailedRelays(e);
const row = await db(e).prepare("SELECT delivery_id FROM orb_relay_failures WHERE delivery_id='exhausted-1'").first();
expect(row ?? null).toBeNull(); // pruned on the DELETE pass before the SELECT
// The drop is no longer silent — an alertable structured log records the lost event count.
expect(warn.mock.calls.some(([line]) => String(line).includes("orb_relay_events_dropped"))).toBe(true);
warn.mockRestore();
// The drop is no longer silent OR warn-only — an alertable level:error log reaches the Sentry forwarder.
expect(errLog.mock.calls.some(([line]) => String(line).includes("orb_relay_events_dropped") && String(line).includes('"level":"error"'))).toBe(true);
errLog.mockRestore();
});

it("PRUNES expired rows (expires_at in the past) without attempting to forward", async () => {
Expand Down Expand Up @@ -468,14 +468,18 @@ describe("pullRelayPending", () => {
expect((await pullRelayPending(e, 9705, { limit: 5 })).length).toBe(5); // a smaller requested limit is honoured
});

it("PRUNES rows older than the TTL before returning the batch", async () => {
it("PRUNES rows older than the TTL before returning the batch, and logs the drop at error level", async () => {
const e = brokeredEnv();
const errLog = vi.spyOn(console, "error").mockImplementation(() => undefined);
await db(e).prepare("INSERT INTO orb_relay_pending (delivery_id, installation_id, event_name, raw_body, created_at) VALUES (?, ?, ?, ?, datetime('now', '-25 hours'))").bind("stale-1", 9706, "pull_request", "{}").run();
await enqueueRelayPending(e, { deliveryId: "fresh-1", installationId: 9706, eventName: "pull_request", rawBody: "{}" });
const events = await pullRelayPending(e, 9706);
expect(events.map((ev) => ev.deliveryId)).toEqual(["fresh-1"]); // the 25h-old row was pruned (TTL 24h)
const stale = await db(e).prepare("SELECT delivery_id FROM orb_relay_pending WHERE delivery_id='stale-1'").first();
expect(stale ?? null).toBeNull();
// Pull-mode loss is now traced for the operator at error level (parity with the push-path drop).
expect(errLog.mock.calls.some(([line]) => String(line).includes("orb_relay_pending_dropped") && String(line).includes('"level":"error"'))).toBe(true);
errLog.mockRestore();
});
});

Expand Down
Loading