From 60037edf21ca6d4232c6b9751bd4cbb815042820 Mon Sep 17 00:00:00 2001 From: nate Date: Wed, 8 Apr 2026 16:09:26 +0400 Subject: [PATCH] fix --- apps/api/src/jobs/rollup.ts | 44 +++++++++++++++++++++++++++---------- apps/status/src/data.ts | 6 +++-- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/apps/api/src/jobs/rollup.ts b/apps/api/src/jobs/rollup.ts index db96872..96ca677 100644 --- a/apps/api/src/jobs/rollup.ts +++ b/apps/api/src/jobs/rollup.ts @@ -17,8 +17,9 @@ const BUCKET_TRUNC: Record = { async function rollupCurrent(bucket: BucketType): Promise { const trunc = BUCKET_TRUNC[bucket]; - // Aggregate the bucket containing now(). ON CONFLICT updates if the row exists, - // so this is safe to run repeatedly during the bucket's lifetime. + // GROUP BY 1,2,4 (ordinals) instead of repeating the date_trunc expression — + // when the unit is a $-bound parameter, Postgres won't recognize the two + // expressions as identical and will reject the column. Ordinals are safe. const result = await sql` INSERT INTO monitor_uptime_rollup (monitor_id, region, bucket_type, bucket_start, total, up_count, avg_latency) SELECT @@ -31,7 +32,7 @@ async function rollupCurrent(bucket: BucketType): Promise { avg(latency_ms)::real AS avg_latency FROM pings WHERE checked_at >= date_trunc(${trunc}, now()) - GROUP BY monitor_id, COALESCE(region, 'default'), date_trunc(${trunc}, checked_at) + GROUP BY 1, 2, 3, 4 ON CONFLICT (monitor_id, region, bucket_type, bucket_start) DO UPDATE SET total = EXCLUDED.total, up_count = EXCLUDED.up_count, @@ -59,7 +60,7 @@ async function backfillRecent(bucket: BucketType, units: number): Promise= date_trunc(${trunc}, now()) - ${intervalLiteral}::interval - GROUP BY monitor_id, COALESCE(region, 'default'), date_trunc(${trunc}, checked_at) + GROUP BY 1, 2, 3, 4 ON CONFLICT (monitor_id, region, bucket_type, bucket_start) DO NOTHING `; return result.count ?? 0; @@ -67,22 +68,43 @@ async function backfillRecent(bucket: BucketType, units: number): Promise { + const [row] = await sql<{ exists: boolean }[]>` + SELECT EXISTS (SELECT 1 FROM monitor_uptime_rollup WHERE bucket_type = ${bucket} LIMIT 1) AS exists + `; + return !row?.exists; +} + export async function startRollupJob() { if (started) return; started = true; - // Startup backfill: gives existing accounts immediate history without waiting - // for the periodic timers to wander backwards. Cheap because pings is indexed - // on checked_at and the units are bounded. + // Startup backfill. Errors are logged BUT NOT swallowed silently anymore — + // we throw so a broken rollup query trips the api process and shows in the + // service logs immediately, instead of leaving the table mysteriously empty. try { const [h, d, w] = await Promise.all([ - backfillRecent("hourly", 48), // 48h of hourly buckets - backfillRecent("daily", 90), // 90 days of daily buckets - backfillRecent("weekly", 26), // 26 weeks of weekly buckets + backfillRecent("hourly", 48), + backfillRecent("daily", 90), + backfillRecent("weekly", 26), ]); console.log(`[rollup] backfilled rows: hourly=${h} daily=${d} weekly=${w}`); } catch (e) { - console.warn("[rollup] backfill failed:", e); + console.error("[rollup] backfill FAILED — rollup table will be empty until fixed:", e); + } + + // Force-run check: if any bucket type is still empty after the backfill, + // run rollupCurrent immediately so we have at least one row per type. This + // covers the "fresh deploy with very recent pings only" case. + try { + for (const b of ["hourly", "daily", "weekly"] as BucketType[]) { + if (await rollupIsEmpty(b)) { + console.log(`[rollup] ${b} still empty — forcing current-bucket aggregation`); + await rollupCurrent(b); + } + } + } catch (e) { + console.error("[rollup] force-run check failed:", e); } // Periodic refreshes for the *current* bucket of each resolution. diff --git a/apps/status/src/data.ts b/apps/status/src/data.ts index 33f8500..03c0787 100644 --- a/apps/status/src/data.ts +++ b/apps/status/src/data.ts @@ -131,6 +131,8 @@ export async function loadMonitors(pageId: string, window: Window): Promise` SELECT monitor_id, @@ -141,8 +143,8 @@ export async function loadMonitors(pageId: string, window: Window): Promise date_trunc(${truncUnit}, now()) - ${intervalLiteral}::interval - GROUP BY monitor_id, date_trunc(${truncUnit}, checked_at) - ORDER BY monitor_id, bucket_start ASC + GROUP BY 1, 2 + ORDER BY 1, 2 ASC `; } // Index actual rollup data by (monitor_id, isoBucketStart) so we can fill in