pingql/apps/api/src/cache/monitor-list.ts

63 lines
2.3 KiB
TypeScript

// 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<string, any>;
type Entry = { rows: MonitorRow[]; expiresAt: number };
const cache = new Map<string, Entry>();
const inflight = new Map<string, Promise<MonitorRow[]>>();
async function fetchForRegion(region: string): Promise<MonitorRow[]> {
return sql<MonitorRow[]>`
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<MonitorRow[]> {
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();
}