From 38d850f99fdabeecb3141c28a0a75994961a23a3 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:40:06 +0200 Subject: [PATCH] version the snapshot keys, and stop the worker writing one by hand (#2886) The reader serves an Upstash blob until it is ten minutes old, and a blob written under an older shape decodes with the new fields simply absent. When the product dashboard's timeframe control shipped, that bought six minutes where it rendered the 30d figures under all three labels. The fallback did its job and the page still showed one number three ways, which is a quieter wrong answer than an empty one. hl-hip3 already had the convention and the other two had not followed it. hl-frontends and hl-builder go to v2, so an old blob becomes unreachable rather than stale-but-readable. The per-builder key was also written by hand in two places in the reader and a third in the worker. A bump that missed the worker would have left the two writing and reading different keys, so the snapshot would simply never hit and every render would go to Prom. It is one exported helper now. Co-authored-by: Claude Opus 5 (1M context) --- src/lib/hl-builder-stats.ts | 25 +++++++++++++++++++++---- worker/index.ts | 3 ++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/lib/hl-builder-stats.ts b/src/lib/hl-builder-stats.ts index c4f3f9464..aea5c1813 100644 --- a/src/lib/hl-builder-stats.ts +++ b/src/lib/hl-builder-stats.ts @@ -297,16 +297,16 @@ async function fetchHlBuilderStatsRaw( slug: string, ): Promise { const snapshot = await readCohortSnapshot( - `hl-builder:${slug}`, + hlBuilderKey(slug), ); if (snapshot) return snapshot.data; const fresh = await fetchHlBuilderStatsFresh(slug); if (fresh) { try { - await writeCohortSnapshot(`hl-builder:${slug}`, fresh); + await writeCohortSnapshot(hlBuilderKey(slug), fresh); } catch (err) { console.warn( - `hl-builder:${slug} writeback failed: ${ + `${hlBuilderKey(slug)} writeback failed: ${ err instanceof Error ? err.message : String(err) }`, ); @@ -387,10 +387,27 @@ export type HlHip3Summary = { * /api/cron/snapshot-hl-cohort. Bump the suffix here if either summary * shape changes so a stale-shape blob can never deserialize into a * misaligned payload. The cohort-snapshot module appends its own `:v1`. */ -export const HL_FRONTENDS_KEY = "hl-frontends"; +/** + * Upstash snapshot keys. **Bump the suffix whenever the stored shape + * changes**, the way hl-hip3 went to v2. + * + * The reader serves a blob until it is ten minutes old, and a blob written + * under an older shape decodes with the new fields simply absent. On + * 2026-10-08 that meant six minutes where the product dashboard's new + * timeframe control rendered the 30d figures under all three labels: the + * fallback did its job and the page still showed the same number three ways, + * which is a quieter wrong answer than an empty one. A key bump makes the + * old blob unreachable instead of stale-but-readable. + */ +export const HL_FRONTENDS_KEY = "hl-frontends-v2"; export const HL_HIP3_KEY = "hl-hip3-v2"; export const HL_HISTORY_KEY = "hl-history"; +/** Per-builder stats snapshot. v2 added `feed` and `byWindow`. */ +export function hlBuilderKey(slug: string): string { + return `hl-builder-v2:${slug}`; +} + /** One evenly-spaced point on a rolling-window history series. `v = null` * means the underlying gauge had no sample at that timestamp (harness * gap, pre-mainnet epoch, ...) — kept as a sentinel so a range's shape diff --git a/worker/index.ts b/worker/index.ts index 635e5c713..45dc06857 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -53,6 +53,7 @@ import { fetchHlHip3CohortFresh, fetchHlHistoryFresh, HL_FRONTENDS_KEY, + hlBuilderKey, HL_HIP3_KEY, HL_HISTORY_KEY, } from "@/lib/hl-builder-stats"; @@ -409,7 +410,7 @@ async function sweep(iteration: number): Promise { // global semaphore keeps the concurrency bounded and the whole // batch typically completes in a few seconds. ...hlBuilders.map((slug) => ({ - key: `hl-builder:${slug}`, + key: hlBuilderKey(slug), build: () => fetchHlBuilderStatsFresh(slug), })), ];