Repository navigation
Fleet overview: bound the last-collection scan and fix its null-fallback (false Offline) - #2699
Conversation
…ack (false Offline) Two bugs, both real, both confirmed: 1. `FleetLastCollectionSql` was a bare `GROUP BY server_id` over the whole `collection_log` table, with no time bound at all — a full scan of every row ever collected (2.2M rows / 89MB on the use1 store and growing forever) just to find each server's most recent timestamp. Same "materialize a bound, don't scan the whole history" mistake fixed elsewhere today (pg_statement_stats #2691, pg_wait_stats #2695), and inconsistent with this file's own stated design ("a fixed ~9 round-trips regardless of fleet size" via bounded/windowed aggregates) and with its sibling `FleetCollectionHealthSql`, which already carries a `WHERE collection_time >= $1` bound. Added the same 48-hour bound here so TimescaleDB can chunk-exclude old data — wide enough that a server genuinely offline for hours still reports its true last-seen time correctly. 2. Confirmed root cause of a live false-positive: `ReadLastCollectionAsync` returns `Dictionary<int, DateTime>` (non-nullable value type). At the call site, `lastCollection.TryGetValue(server.ServerId, out var lastColl)` left `lastColl` as `default(DateTime)` (0001-01-01) on ANY dictionary miss — and default(DateTime) is not null, so `ClassifyFreshness` never reached its `NeverCollected` branch. It fell through to the age-vs-OfflineThreshold check with an age of ~2000 years, which always bands Offline. This is exactly what was observed live on prod-pos-use1-monitor-01 today: a rotating subset of actively-collecting servers (multi-20, multi-03, gooddayfarm-01, apex-01, multi-31, multi-53 across two checks) flagged "Offline - no recent collection" while their real collection_log rows were 22-100 seconds old. Fixed the call site to propagate a genuine miss as `null`, which correctly reaches ClassifyFreshness's NeverCollected branch -> AwaitingFirstCollection=true, IsOnline=null, band=Warning (a bootstrap state), never Offline. No confirmed smoking-gun for WHY a server_id could go missing from the unbounded query's result under normal operation (an ad-hoc timing sample ran in 840ms, not obviously pathological, and no aggressive command timeout is configured for this reader) — but bug #2 means ANY future cause of a miss (a query hiccup, a brand-new server, a row landing just outside a bound) no longer misclassifies as a fake-ancient Offline server. Bug #1 is the architectural fix that should make misses far rarer in the first place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| /// <summary>Newest collection time per server — drives each card's freshness status. $1 window start. | ||
| /// Bounded (not a bare GROUP BY over the whole table) so TimescaleDB can chunk-exclude: this table only | ||
| /// grows, and every collector run adds a row, so an unbounded MAX(collection_time) over ALL history was | ||
| /// re-scanning the server's ENTIRE collection archive (millions of rows) on every fleet-overview call just | ||
| /// to find a timestamp from the last few minutes — the exact "materialize a bound, don't scan the whole | ||
| /// history" mistake fixed elsewhere today (pg_statement_stats #2691, pg_wait_stats #2695). The window is | ||
| /// 48 hours, not the 15-minute OfflineThreshold this feeds: a server genuinely offline for HOURS must still | ||
| /// report its true last-seen time (age computed correctly, still bands Offline) rather than falling out of | ||
| /// the result entirely and being treated as having no history at all.</summary> | ||
| public const string FleetLastCollectionSql = @" | ||
| SELECT server_id, MAX(collection_time) AS last_collection_time | ||
| FROM v_collection_log | ||
| WHERE collection_time >= $1 | ||
| GROUP BY server_id"; |
There was a problem hiding this comment.
Correctness: the 48-hour bound reintroduces a false negative for real outages, in the opposite direction from the bug this PR fixes.
Before this PR, MAX(collection_time) scanned the whole table, so a server that had been dead for a week still had some row in the result — its true (very old) last_collection_time — and correctly banded Offline (ClassifyFreshness → age > OfflineThreshold → FleetHealthBand.Offline, rank 4000).
After this PR, a server whose newest row is older than 48 hours falls completely out of FleetLastCollectionSql's result set (line 213: WHERE collection_time >= $1, bound to now.AddHours(-48) at ReadLastCollectionAsync, line 760). lastCollection.TryGetValue then misses, lastColl becomes null (by this PR's own fix), ClassifyFreshness returns NeverCollected, and ClassifyBand maps that to Warning (rank 2000) with the "Awaiting first collection" label — not Offline.
So a server down for >48 hours now silently downgrades from the worst-first band (Offline, red, rank 4000) to a mid-severity, benign-sounding bootstrap state (Warning/"Awaiting first collection", amber, rank 2000), and drops in the worst-first ranking below genuinely-Critical servers. That's a real outage being masked as "not yet reached," which is a worse failure mode for a monitoring tool than the false-Offline this PR is fixing — a server dead for a week reads as if the service just hasn't gotten to it yet.
The comment above (lines 206-209) reasons about "a server genuinely offline for HOURS" but the 48h window doesn't cover multi-day outages, and neither this PR's tests nor its description address that case — the new live test (GetFleetOverview_ServerWithNoCollectionHistory_...) only covers "zero rows ever," not "had rows, then went silent for days."
Worth considering: keep an unbounded (or much wider, e.g. no upper cutoff) fallback specifically for the "genuinely never seen in the window" case — e.g., a second cheap query only for server_ids missing from the bounded read, or just don't let a real Offline server disappear from the aggregate at all (index-only MAX per server_id can stay cheap without a time bound, since it's not the full-scan GROUP BY this PR is rightly killing).
| ## [Unreleased] | ||
|
|
||
| ### Added | ||
| - **Fleet overview stops false-flagging active servers as Offline** ([#2699]) - `get_fleet_overview`'s last-collection query was a bare `GROUP BY server_id` with no time bound, scanning the ENTIRE `collection_log` history (2.2M rows and growing) on every call, and a dictionary miss from that query fell through to `default(DateTime)` rather than null - a value old enough to always band Offline instead of reaching the NeverCollected branch. Live impact: a rotating set of actively-collecting servers (seen: multi-20, multi-03, gooddayfarm-01, apex-01, multi-31, multi-53) read “Offline - no recent collection” while their real collection_time rows were 22-100 seconds old. Bounded the query to a 48-hour window (matching the sibling FleetCollectionHealthSql's WHERE bound) and fixed the miss-handling to propagate null, so a genuine gap reads AwaitingFirstCollection/Warning, never a fake-ancient Offline. |
There was a problem hiding this comment.
Minor: "matching the sibling FleetCollectionHealthSql's WHERE bound" isn't accurate — FleetCollectionHealthSql is bound to 7 days (AddDays(-7), DarlingFleetReader.cs line 780), not 48 hours. The two queries now use different windows for what both feed into a server's collection/health status, which is worth calling out explicitly rather than implying they match — see the inline comment on FleetLastCollectionSql for why the shorter 48h window on this specific query is the more consequential issue.
|
Reviewed. Both stated bugs (unbounded full-history scan, One correctness concern worth resolving before merge (left inline on Also flagged a small factual mismatch in the CHANGELOG entry: it claims the 48h bound matches sibling Lite/Darling parity: not applicable here — Lite has no server-side fleet-overview aggregate equivalent to No security or T-SQL style issues — this is Darling/C#-only, parameterized queries throughout, no secrets or injection surface touched. |
…ird-party name from two comments erikdarlingdata#2490's guard matches only the full hostname SHAPE, so three references that are not hostname-shaped survived erikdarlingdata#2900's pass. CHANGELOG.md's erikdarlingdata#2699 server list carried two bare short names whose slugs are not on the allowlist. Both now read allowlisted slugs that named no other server anywhere in the tree, so neither swap merges two distinct identities: epsilon-01 (epsilon occurred exactly once before this change, in the allowlist declaration itself) and dummy-01 (dummy occurred only as the adjective - "dummy SQL", "dummy 0-columns", "dummy test data"). DarlingWorker's plan_correction note and DetachedCollectorGate's erikdarlingdata#2701 paragraph used a third-party product name as a workload descriptor. Both now read "workload-class", which keeps each signature named by its own mechanism (distinct-plan-population, decimal-parameter-instability) rather than by a vendor. Every measurement in both comments is unchanged. apex is untouched everywhere else: it is this codebase's term for a blocking chain's head blocker (~150 uses), and the fleet's "apex box"/"apex replica" superlative. Only the apex-01 in that one server list was an identifier. No schema change, no migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What
Two confirmed bugs behind a live false-positive on the use1 monitoring host today:
get_fleet_overviewrepeatedly flagged a rotating set of actively-collecting servers (multi-20, multi-03, epsilon-01, dummy-01, multi-31, multi-53 across two separate checks) as "Offline - no recent collection" while their realcollection_logrows were 22-100 seconds old.Bug 1 — unbounded full-history scan
DarlingFleetReader.FleetLastCollectionSqlwas a bareGROUP BY server_idover the wholecollection_logtable — noWHEREclause at all. That's a full scan of every row ever collected (2.2M rows / 89 MB on the use1 store, growing forever) just to find each server's most recent timestamp, on every singleget_fleet_overviewcall. Same "materialize a bound, don't scan the whole history" mistake fixed elsewhere today (pg_statement_stats#2691,pg_wait_stats#2695), and inconsistent with this file's own documented design ("a fixed ~9 round-trips regardless of fleet size" via bounded/windowed aggregates) and with its siblingFleetCollectionHealthSql, which already carriesWHERE collection_time >= $1.Fix: added the same 48-hour bound so TimescaleDB can chunk-exclude old data. Wide enough that a server genuinely offline for hours still reports its true last-seen time correctly (not zero rows).
Bug 2 — the actual misclassification mechanism (confirmed)
ReadLastCollectionAsyncreturnsDictionary<int, DateTime>— a non-nullable value type. At the call site,lastCollection.TryGetValue(server.ServerId, out var lastColl)leftlastCollasdefault(DateTime)(0001-01-01) on any dictionary miss.default(DateTime)is not null, soClassifyFreshnessnever reached itsNeverCollectedbranch — it fell straight to the age-vs-OfflineThresholdcheck with an age of ~2000 years, which always bandsOffline.Fix: the call site now propagates a genuine miss as
null, correctly reachingClassifyFreshness'sNeverCollectedbranch →AwaitingFirstCollection=true,IsOnline=null, band=Warning(a bootstrap state) — never a fake-ancientOffline.Honesty note
I could not catch bug 1's query in the act being slow — an ad-hoc timing sample ran in 840 ms against the live store, not obviously pathological, and no aggressive command timeout is configured for this reader. So I can't prove bug 1 was THE mechanism that dropped rows from the dictionary on the day in question. What I can prove: bug 2 is real and would produce exactly the observed symptom regardless of why a row goes missing, and bug 1 is a genuine, independently-justified architectural fix that makes misses far less likely. Shipping both together closes the loop either way.
Tests
FleetLastCollectionSql_IsWindowed_NotAFullHistoryScan— SQL-shape pin (mirrors the existing pin style forFleetCollectionHealthSql).GetFleetOverview_ServerWithNoCollectionHistory_ReadsAwaitingFirstCollection_NotOffline— live-Postgres regression test: a registered server with zerocollection_logrows must bandWarning/AwaitingFirstCollection, neverOffline.🤖 Generated with Claude Code