// In-memory cache of the enabled monitor list, keyed by region. The /internal/due // endpoint is polled by every runner roughly once a second per region; without // this cache each poll re-runs a 500-row scan against `monitors` with an array // predicate, which dominates the api's Postgres traffic at any real fleet size. // // The list almost never changes between polls — monitor create/edit/delete is at // most a few times per hour. So we memoize per-region with a short TTL and bust // the cache from the monitor mutation handlers so edits are visible instantly. import sql from "../db"; const TTL_MS = 5000; type MonitorRow = Record; type Entry = { rows: MonitorRow[]; expiresAt: number }; const cache = new Map(); const inflight = new Map>(); async function fetchForRegion(region: string): Promise { return sql` SELECT id, url, method, request_headers, request_body, timeout_ms, interval_s, query, regions, max_retries, retry_interval_s, created_at FROM monitors WHERE enabled = true AND ( array_length(regions, 1) IS NULL OR regions = '{}' OR ${region} = ANY(regions) ) LIMIT 500 `; } export async function getMonitorsForRegion(region: string): Promise { const now = Date.now(); const hit = cache.get(region); if (hit && hit.expiresAt > now) return hit.rows; // Coalesce concurrent refreshes for the same region so a thundering herd of // runner polls doesn't fan out into N parallel SELECTs against an expired // entry. let pending = inflight.get(region); if (!pending) { pending = fetchForRegion(region) .then((rows) => { cache.set(region, { rows, expiresAt: Date.now() + TTL_MS }); return rows; }) .finally(() => { inflight.delete(region); }); inflight.set(region, pending); } return pending; } // Called by monitor create/patch/delete/toggle handlers. Wipes the entire // region map — fine because (a) entries are tiny, (b) refresh is cheap, and // (c) we don't know which regions a freshly-edited monitor belongs to without // reading it back. Simpler than per-region invalidation, identical net effect. export function invalidateMonitorList(): void { cache.clear(); }