From c18c320927e0c00f5de885be36ab0494fff64609 Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Tue, 27 Oct 2026 14:00:08 +0100 Subject: [PATCH] =?UTF-8?q?coordinator:status:=20the=20capture=20windows?= =?UTF-8?q?=20are=20priorities=200=E2=80=932,=20the=20probe=20backlog=20fr?= =?UTF-8?q?om=20the=20planner's=20estimates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sampling ranges in other years are capture too, at history priorities: they made a band each. The database size walked the HDD partitions for minutes and the probe counts took 10 s on 11 M rows: dropped, and the partial indexes' reltuples instead. Co-Authored-By: Claude Fable 5.1 --- app/services/status.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/services/status.ts b/app/services/status.ts index db887d4..0a4c165 100644 --- a/app/services/status.ts +++ b/app/services/status.ts @@ -42,7 +42,6 @@ export interface Status { gb: number capGb: number full: boolean - databaseGb: number freeGb: number } bands: RangeBand[] @@ -58,6 +57,7 @@ export interface Status { held: HeldGroup[] snapshots: { count: number; gb: number; oldestHours: number | null; largest: SnapshotFile[] } releases: { estimated: number; provisional: number } + /** `ready` and `waiting` are the planner's estimates (the last analyze), the others counts. */ probes: { ready: number; waiting: number; pendingWaiting: number; pendingNamed: number } judge: { dirty: number } nodes: { name: string; lastSeenSeconds: number | null }[] @@ -76,20 +76,22 @@ class StatusService { async collect(): Promise { const tail = coordinatorConfig.tail.priority const cap = coordinatorConfig.catalogMaxNvmeGb + // The catalog's tablespace only (CatalogSpace measures it every minute anyway): pg_database_size walks the + // chunk partitions and the probe cache on the HDD, minutes on homelab. const { rows: space } = await db.rawQuery( `SELECT coalesce(nullif(current_setting('default_tablespace'), ''), 'pg_default') AS tablespace, - pg_tablespace_size(coalesce(nullif(current_setting('default_tablespace'), ''), 'pg_default')) AS bytes, - pg_database_size(current_database()) AS database_bytes` + pg_tablespace_size(coalesce(nullif(current_setting('default_tablespace'), ''), 'pg_default')) AS bytes` ) const tablespaceGb = Number(space[0].bytes) / 1e9 const fs = await statfs('/').catch(() => null) const freeGb = fs ? (fs.bavail * fs.bsize) / 1e9 : 0 - // The ranges by band: the tail, the capture windows by priority, the history by its thousand. + // The ranges by band: the tail, the capture windows (priorities 0–2; the sampling ranges in other years are + // capture too, at their history priorities), the history by its thousand. const { rows: bands } = await db.rawQuery( `SELECT CASE WHEN priority <= ? THEN 'tail' WHEN priority < 0 THEN 'raised' - WHEN capture THEN 'window ' || priority + WHEN capture AND priority <= 2 THEN 'window ' || priority WHEN priority < 1000 THEN 'history 0-999' ELSE 'history ' || (priority / 1000) * 1000 || '+' END AS band, count(*) FILTER (WHERE status = 'planned') AS planned, @@ -149,11 +151,13 @@ class StatusService { } files.sort((a, b) => b.mb - a.mb) + // The probe backlog from the planner's estimates of the partial indexes (counting 11 M rows took 10 s on + // homelab); the rest are small partial indexes. const { rows: releases } = await db.rawQuery( `SELECT (SELECT reltuples::bigint FROM pg_class WHERE oid = 'releases'::regclass) AS estimated, (SELECT count(*) FROM releases WHERE settled_at IS NULL) AS provisional, - (SELECT count(*) FROM probe_candidates WHERE status = 'waiting' AND ready) AS ready, - (SELECT count(*) FROM probe_candidates WHERE status = 'waiting') AS waiting, + (SELECT greatest(0, reltuples)::bigint FROM pg_class WHERE oid = 'probe_candidates_ready_idx'::regclass) AS ready, + (SELECT greatest(0, reltuples)::bigint FROM pg_class WHERE oid = 'probe_candidates_waiting_idx'::regclass) AS waiting, (SELECT count(*) FROM pending_releases WHERE status = 0) AS pending_waiting, (SELECT count(*) FROM pending_releases WHERE status = 4) AS pending_named, (SELECT count(*) FROM judge_dirty) AS dirty` @@ -168,7 +172,6 @@ class StatusService { gb: tablespaceGb, capGb: cap, full: cap > 0 && tablespaceGb > cap, - databaseGb: Number(space[0].database_bytes) / 1e9, freeGb, }, bands: bands.map((b: any) => ({ @@ -255,7 +258,7 @@ class StatusService { lines.push(`Status at ${s.at}`) lines.push('') lines.push( - `Catalog: ${s.catalog.gb.toFixed(1)} GB in tablespace ${s.catalog.tablespace}${s.catalog.capGb ? ` of ${s.catalog.capGb} GB${s.catalog.full ? ' (FULL: history paused)' : ''}` : ' (no cap)'}; database ${s.catalog.databaseGb.toFixed(1)} GB; ${s.catalog.freeGb.toFixed(1)} GB free on /` + `Catalog: ${s.catalog.gb.toFixed(1)} GB in tablespace ${s.catalog.tablespace}${s.catalog.capGb ? ` of ${s.catalog.capGb} GB${s.catalog.full ? ' (FULL: history paused)' : ''}` : ' (no cap)'}; ${s.catalog.freeGb.toFixed(1)} GB free on /` ) lines.push( `Last hour: ${b(s.lastHour.indexedArticles)} articles indexed in ${n(s.lastHour.doneRanges)} ranges (${n(s.lastHour.indexedArticles / 3600)}/s); corpus fill ${b(s.lastHour.storedArticles)} in ${n(s.lastHour.storedRanges)} ranges, ${n(s.corpus.pendingRanges)} capture ranges (${b(s.corpus.pendingArticles)}) still to store` @@ -321,7 +324,7 @@ class StatusService { `Releases: ~${n(s.releases.estimated)}, ${n(s.releases.provisional)} provisional. Judge: ${n(s.judge.dirty)} dirty` ) lines.push( - `Probes: ${n(s.probes.ready)} ready of ${n(s.probes.waiting)} waiting; pending ladder ${n(s.probes.pendingWaiting)} waiting, ${n(s.probes.pendingNamed)} named` + `Probes: ~${n(s.probes.ready)} ready of ~${n(s.probes.waiting)} waiting; pending ladder ${n(s.probes.pendingWaiting)} waiting, ${n(s.probes.pendingNamed)} named` ) lines.push( `Nodes: ${s.nodes.map((x) => `${x.name} (${age(x.lastSeenSeconds)})`).join(', ') || 'none'}`