diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 87385b6338..3334081fd4 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -268,8 +268,6 @@ is **not** available on the Postgres backend yet — it degrades to no-context. These are Cloudflare-platform features; they degrade cleanly and the core reviewer is unaffected: - **Visual PR capture** (Browser Rendering binding) — off; reviews run text-only. -- **The `/mcp` server** (Durable-Object-backed Agents SDK) — returns `501`. The deterministic API + review - path is unaffected; a native MCP-on-Node port is a follow-up. - **Distributed rate limiting** (RateLimiter Durable Object) — off by default; set `REDIS_URL` for a Redis-backed fixed-window limiter (see §7). Otherwise put a reverse proxy / WAF in front. - **Vectorize-backed RAG** and **R2 audit storage** — inert unless you wire equivalent backends. diff --git a/src/orb/relay.ts b/src/orb/relay.ts index 4f6e88eefd..ef9fe1f2c9 100644 --- a/src/orb/relay.ts +++ b/src/orb/relay.ts @@ -102,13 +102,20 @@ export async function storeRelayFailure( /** Re-attempt pending relay failures. Called by the `retry-orb-relay` cron job every sweep cycle (≈2 min). * Each row gets up to RELAY_RETRY_MAX_ATTEMPTS (5) retries within a 1-hour TTL; on success or expiry the row - * is removed. Never throws — a bad DB row or a persistently-down container is silently dropped after exhaustion. */ + * is removed. Never throws — a bad DB row or a persistently-down container is dropped (with an alertable log, + * below) after exhaustion. */ export async function retryFailedRelays(env: Env, opts?: { fetchImpl?: typeof fetch }): Promise { // Prune rows whose TTL has elapsed or whose attempt budget is exhausted. - await env.DB + const pruned = await env.DB .prepare("DELETE FROM orb_relay_failures WHERE expires_at < datetime('now') OR attempts >= ?") .bind(RELAY_RETRY_MAX_ATTEMPTS) .run(); + // Make the drop VISIBLE (#5): a pruned row is a relay event we gave up delivering (1-hour TTL elapsed or 5 + // 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 })); + } const { results } = await env.DB .prepare( "SELECT delivery_id, event_name, installation_id, raw_body FROM orb_relay_failures WHERE expires_at >= datetime('now') AND attempts < ? ORDER BY created_at, delivery_id LIMIT ?", diff --git a/test/integration/orb-relay.test.ts b/test/integration/orb-relay.test.ts index 2ab477a5bd..997c4ab040 100644 --- a/test/integration/orb-relay.test.ts +++ b/test/integration/orb-relay.test.ts @@ -281,13 +281,17 @@ describe("retryFailedRelays", () => { expect(untouched?.n).toBe(5); }); - it("PRUNES rows that have exhausted their attempt budget (attempts >= 5)", async () => { + it("PRUNES rows that have exhausted their attempt budget (attempts >= 5) and logs the drop (#5)", async () => { const e = brokeredEnv(); + const warn = vi.spyOn(console, "warn").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(); }); it("PRUNES expired rows (expires_at in the past) without attempting to forward", async () => {