fix: improve sql queries

This commit is contained in:
2026-04-09 04:48:50 +04:00
parent 89f0856a04
commit 91ca996e74
10 changed files with 495 additions and 219 deletions
+88 -18
View File
@@ -1,16 +1,91 @@
import { Elysia } from "elysia";
import sql from "../db";
import { safeTokenCompare } from "../../../shared/auth";
import { getMonitorsForRegion } from "../cache/monitor-list";
// Chunked retention prune. A single big DELETE on the pings table holds locks
// for the duration of the scan, blocks autovacuum from reclaiming dead tuples,
// and can stall replication. We loop in 10k-row batches so each commit
// releases its locks and lets autovacuum keep up. Total wall-clock is similar
// to a one-shot DELETE but the per-statement impact on the rest of the system
// is dramatically lower.
const PRUNE_BATCH_SIZE = 10_000;
export async function pruneOldPings(retentionDays = 90) {
const result = await sql`DELETE FROM pings WHERE checked_at < now() - ${retentionDays + ' days'}::interval`;
return result.count;
const interval = `${retentionDays} days`;
let total = 0;
while (true) {
const result = await sql`
WITH victims AS (
SELECT id FROM pings
WHERE checked_at < now() - ${interval}::interval
LIMIT ${PRUNE_BATCH_SIZE}
)
DELETE FROM pings WHERE id IN (SELECT id FROM victims)
`;
const batch = result.count ?? 0;
total += batch;
if (batch < PRUNE_BATCH_SIZE) break;
}
return total;
}
setInterval(() => {
const days = Number(process.env.PING_RETENTION_DAYS ?? 90);
pruneOldPings(days).catch((err) => console.error("Retention cleanup failed:", err));
}, 60 * 60 * 1000);
// Periodic prune is gated behind a Postgres session-level advisory lock so
// horizontally scaled api replicas don't race each other. Without the lock,
// two replicas running the same chunked DELETE would interleave their
// LIMIT 10000 batches and compete for row locks. The lock is acquired with
// pg_try_advisory_lock so a busy replica skips its tick instead of blocking.
//
// Session-level locks live on a specific Postgres backend connection, so we
// reserve a connection from the pool with sql.reserve() and run the lock,
// the chunked prune, and the unlock all on it. Releasing the reservation
// returns the connection to the pool. If the api process crashes mid-prune,
// the backend dies with it and Postgres releases the lock automatically.
//
// 134678338 is just an arbitrary lock id unique within this app. If we add
// more global jobs later, give each its own constant in this file.
const PRUNE_LOCK_ID = 134678338;
let pruneJobStarted = false;
async function runPruneTickWithLock(retentionDays: number): Promise<void> {
const reserved = await (sql as any).reserve();
try {
const [{ locked }] = await reserved`
SELECT pg_try_advisory_lock(${PRUNE_LOCK_ID}) AS locked
`;
if (!locked) return; // another replica is pruning right now
try {
const interval = `${retentionDays} days`;
let total = 0;
while (true) {
const result = await reserved`
WITH victims AS (
SELECT id FROM pings
WHERE checked_at < now() - ${interval}::interval
LIMIT ${PRUNE_BATCH_SIZE}
)
DELETE FROM pings WHERE id IN (SELECT id FROM victims)
`;
const batch = result.count ?? 0;
total += batch;
if (batch < PRUNE_BATCH_SIZE) break;
}
if (total > 0) console.log(`[prune] retention pruned ${total} pings (>${retentionDays}d)`);
} finally {
await reserved`SELECT pg_advisory_unlock(${PRUNE_LOCK_ID})`;
}
} finally {
reserved.release();
}
}
export function startPruneJob() {
if (pruneJobStarted) return;
pruneJobStarted = true;
setInterval(() => {
const days = Number(process.env.PING_RETENTION_DAYS ?? 90);
runPruneTickWithLock(days).catch((err) => console.error("Retention cleanup failed:", err));
}, 60 * 60 * 1000);
}
export const internal = new Elysia({ prefix: "/internal", detail: { hide: true } })
.derive(({ headers, set }) => {
@@ -37,18 +112,13 @@ export const internal = new Elysia({ prefix: "/internal", detail: { hide: true }
// creation is a tick. We pull all enabled monitors that match this
// region, compute the next tick in JS, and return the ones whose next
// tick falls within the lookahead window.
const monitors = await 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
`;
//
// The monitor list itself is memoized in apps/api/src/cache/monitor-list.ts
// with a 5s TTL — runners poll this endpoint roughly once a second per
// region, but the underlying list almost never changes between polls. The
// cache is busted from monitor create/patch/delete/toggle so edits show up
// immediately.
const monitors = await getMonitorsForRegion(region);
const nowMs = Date.now();
const lookaheadEnd = nowMs + lookaheadMs;
+5
View File
@@ -3,6 +3,7 @@ import { requireAuth } from "./auth";
import sql from "../db";
import { validateMonitorUrl } from "../utils/ssrf";
import { getPlanLimits } from "../../../shared/plans";
import { invalidateMonitorList } from "../cache/monitor-list";
const MonitorBody = t.Object({
name: t.String({ maxLength: 200, description: "Human-readable name" }),
@@ -111,6 +112,7 @@ export const monitors = new Elysia({ prefix: "/monitors" })
`;
if (body.channel_ids) await replaceMonitorChannels(monitor.id, accountId, body.channel_ids);
if (body.tags) await replaceMonitorTags(monitor.id, body.tags);
invalidateMonitorList();
return monitor;
}, { body: MonitorBody, detail: { summary: "Create monitor", tags: ["monitors"] } })
@@ -177,6 +179,7 @@ export const monitors = new Elysia({ prefix: "/monitors" })
if (!monitor) { set.status = 404; return { error: "Not found" }; }
if (body.channel_ids) await replaceMonitorChannels(monitor.id, accountId, body.channel_ids);
if (body.tags) await replaceMonitorTags(monitor.id, body.tags);
invalidateMonitorList();
return monitor;
}, { body: t.Partial(MonitorBody), detail: { summary: "Update monitor", tags: ["monitors"] } })
@@ -185,6 +188,7 @@ export const monitors = new Elysia({ prefix: "/monitors" })
DELETE FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id
`;
if (!deleted) { set.status = 404; return { error: "Not found" }; }
invalidateMonitorList();
return { deleted: true };
}, { detail: { summary: "Delete monitor", tags: ["monitors"] } })
@@ -195,6 +199,7 @@ export const monitors = new Elysia({ prefix: "/monitors" })
RETURNING id, enabled
`;
if (!monitor) { set.status = 404; return { error: "Not found" }; }
invalidateMonitorList();
return monitor;
}, { detail: { summary: "Toggle monitor on/off", tags: ["monitors"] } })
+22 -14
View File
@@ -56,10 +56,28 @@ export const ingest = new Elysia()
const token = headers["x-monitor-token"];
if (!safeTokenCompare(token, process.env.MONITOR_TOKEN)) { set.status = 401; return { error: "Unauthorized" }; }
const [monitor_check] = await sql`
SELECT id, account_id, name, url, resend_interval, cert_alert_days
FROM monitors WHERE id = ${body.monitor_id}
`;
// Per-region transition state. Region is always populated by current runners;
// legacy null values from older pings collapse to "default" so state and
// notifications never carry an empty label.
const region = body.region && body.region.length > 0 ? body.region : 'default';
// The monitor lookup and the per-region state lookup are independent —
// the state row's primary key doesn't depend on anything from the monitor
// row. Fire them in parallel to halve the wall-clock cost on the hottest
// path in the system. (Combining them into a JOIN is a wash on a warm
// pool: both sides are PK lookups, and a JOIN just adds nested-loop
// planner overhead. Promise.all keeps each query's plan trivial.)
const [[monitor_check], [stateRow]] = await Promise.all([
sql`
SELECT id, account_id, name, url, resend_interval, cert_alert_days
FROM monitors WHERE id = ${body.monitor_id}
`,
sql`
SELECT last_state, consecutive_down, cert_alert_sent
FROM monitor_region_state
WHERE monitor_id = ${body.monitor_id} AND region = ${region}
`,
]);
if (!monitor_check) { set.status = 404; return { error: "Monitor not found" }; }
const meta = body.meta ? { ...body.meta } : {};
@@ -72,16 +90,6 @@ export const ingest = new Elysia()
const scheduledAt = body.scheduled_at ? new Date(body.scheduled_at) : null;
const jitterMs = body.jitter_ms ?? null;
// Per-region transition state. Region is always populated by current runners;
// legacy null values from older pings collapse to "default" so state and
// notifications never carry an empty label.
const region = body.region && body.region.length > 0 ? body.region : 'default';
const [stateRow] = await sql`
SELECT last_state, consecutive_down, cert_alert_sent
FROM monitor_region_state
WHERE monitor_id = ${body.monitor_id} AND region = ${region}
`;
const newState = body.up ? 'up' : 'down';
const prevState: string | null = stateRow?.last_state ?? null;
let consecutiveDown: number = stateRow?.consecutive_down ?? 0;
+14 -9
View File
@@ -62,17 +62,22 @@ async function replaceGroupsAndMonitors(
if (groups !== undefined) {
await sql`DELETE FROM status_page_groups WHERE status_page_id = ${pageId}`;
}
// Single bulk INSERT instead of one round-trip per group. The RETURNING set
// comes back in INSERT order, which equals the array order — that lets us
// map index → id without a follow-up SELECT. Mirrors the bulk insert pattern
// used by the monitors block right below.
const groupIds: string[] = [];
if (groups && groups.length > 0) {
for (let i = 0; i < groups.length; i++) {
const g = groups[i]!;
const [row] = await sql<{ id: string }[]>`
INSERT INTO status_page_groups (status_page_id, name, position)
VALUES (${pageId}, ${g.name}, ${g.position ?? i})
RETURNING id
`;
groupIds.push(row!.id);
}
const rows = groups.map((g, i) => ({
status_page_id: pageId,
name: g.name,
position: g.position ?? i,
}));
const inserted = await sql<{ id: string }[]>`
INSERT INTO status_page_groups ${sql(rows, "status_page_id", "name", "position")}
RETURNING id
`;
for (const r of inserted) groupIds.push(r.id);
}
if (monitorsList !== undefined) {