Skip to content

A fresh store's fleet overview answers instead of failing with 'Out of range of DateTime' before collection_health_hourly materializes (#3973) - #3981

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/3973-fresh-store-watermark
Sep 23, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
fix/3973-fresh-store-watermark

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Closes #3973.

Why

On every fresh TimescaleDB store, get_fleet_overview answered Out of range of DateTime (year must be between 1 and 9999) and /api/fleet returned a bare 500. The web dashboard's Fleet Overview and sidebar were broken until collect.collection_health_hourly had materialized something.

DarlingFleetReader.CollectionHealthWatermarkSql (#3893, #3911) guarded the aggregate's watermark with isfinite(), assuming "nothing materialized" reads -infinity. It doesn't: TimescaleDB reports the minimum finite timestamp. Npgsql cannot convert that to a DateTime, so the value throws InvalidCastException. The guard's catch (PostgresException) did not cover that, and the whole overview failed.

Measured on the rig (TimescaleDB 2.28.1, a throwaway hypertable plus an aggregate created WITH NO DATA):

State cagg_watermark as timestamp isfinite
never refreshed -210866803200000000 4714-11-24 00:00:00 BC true
after a refresh over an EMPTY log unchanged 4714-11-24 00:00:00 BC true
after rows land and a refresh covers them 2026-09-23 01:00:00 true

The middle row answers the issue's unmeasured scope: a refresh over an empty log does not move the watermark. So a new install stayed broken until collection had rows AND a refresh (end offset one hour, hourly) had covered them, not just until the first refresh.

What changes

  • CollectionHealthWatermarkSql: CASE WHEN isfinite(w) AND w >= '2000-01-01'::timestamp THEN w END. A new MaterializedWatermarkFloor (2000-01-01) names the floor, and a test holds the SQL literal to the constant. No store holds a collection from before 2000, so a watermark below that is the extension's "nothing materialized" sentinel, whichever one it uses. It now reads NULL, which the guard already treats as all real-time from raw. That is exact: the aggregate is materialized_only = false, so with nothing materialized every bucket is computed from raw.
  • CollectionHealthRollupUsableAsync: it catches InvalidCastException as well as PostgresException. Its own contract already said a failed guard read falls back to the exact raw scan, and a value the client cannot convert is a failed guard read. It used to fail the whole overview instead.
  • Doc correction in TimescaleSupport.BaselineBackfillProbeSql: it said a WITH NO DATA aggregate's watermark is -infinity. That remark carried the same assumption, though no code acted on it.
  • Other readers: I searched the tree for cagg_watermark and isfinite. This is the only CAGG watermark read in product code, in either app. Lite has no TimescaleDB, so there is nothing to mirror.

Test plan

  • Red first. On the rig, before the fix, both new live tests failed with System.InvalidCastException: Out of range of DateTime (year must be between 1 and 9999). That includes the empty-log case, which confirms the finding above.
  • New FreshStoreWatermarkLiveTests. Each builds its own scratch store the product's way: migrations, TimescaleDB, the collection_log hypertable, EnsureContinuousAggregatesAsync creating the aggregate WITH NO DATA, and the policy switched off.
    • Before the first refresh: the raw watermark is below the floor (measured, not assumed). The product's watermark SQL returns NULL, and CollectionHealthRollupUsableAsync returns true. GetFleetOverviewAsync answers, and both carded servers carry their three collectors through the unmaterialized aggregate.
    • After one policy run: the watermark is a real instant after the floor, and a fresh data source's overview still answers with the same counts.
    • A refresh over an empty log: the guard hands the reader NULL or a real instant, and the overview answers.
  • FreshStoreWatermarkTests (pure): the SQL carries the floor, and the guard's catch covers InvalidCastException.
  • Red-watch, test project rebuilt each time:
    • with the SQL guard reverted and the catch kept, 3 of the 4 fail;
    • with the catch reverted and the SQL guard kept, the pin fails.
  • DARLING01 (TimescaleDB 2.30.1, read-only): the real watermark passes the new guard unchanged (2026-09-23 01:00:00 before and after). All 26 aggregates there are materialized.
  • Full Darling.Tests with DARLING_TEST_PG on the rig: 12,730 total, 1 failed, 24 skipped. The failure is ForcePlanFailuresAccessPathTests.TheShippedRead_PlansAsAnIndexOnlyScanOnTheCoveringIndex_AgainstDevPostgres: a plan-shape assertion that got a Merge Join on the shared store's statistics mid-run, in a read this PR does not touch. Re-run alone, the class passes 4/4. Every skip is environment-gated.
  • Build: 0 Warning(s).

🤖 Generated with Claude Code

https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif

erikdarlingdata and others added 2 commits September 22, 2026 22:33
…f range of DateTime' before collection_health_hourly materializes (#3973)

CollectionHealthWatermarkSql guarded the aggregate's watermark with isfinite(),
expecting "nothing materialized" to read -infinity. TimescaleDB (2.28.1, 2.30.1)
reports the minimum finite timestamp, 4714-11-24 BC, both before the first
refresh and after a refresh over an empty log. Npgsql cannot convert that to a
DateTime, and the InvalidCastException escaped the guard's
catch (PostgresException). So get_fleet_overview and /api/fleet failed on every
fresh store until rows had landed and a refresh had covered them.

- A watermark before MaterializedWatermarkFloor (2000-01-01) now reads as nothing
  materialized: NULL, the all-real-time path, which is exact.
- The guard also treats a value the client cannot convert as a failed guard read
  (the exact raw scan), as its contract already said.
- A TimescaleSupport doc that repeated the -infinity assumption is corrected.

Live tests build a scratch store the product's way with a never-refreshed
aggregate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif
…#3973)

The empty-log refresh was measured on 2.28.1 only; the never-refreshed
sentinel on 2.28.1 and 2.30.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 02:35
@erikdarlingdata
erikdarlingdata merged commit 2a1011b into dev Sep 23, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3973-fresh-store-watermark branch September 23, 2026 02:42
erikdarlingdata added a commit that referenced this pull request Sep 23, 2026
…3920, #3927, #3931, #3932, #3940, #3942, #3946, #3947, #3950, #3952, #3955, #3956, #3957, #3964, #3965, #3966, #3968, #3972, #3975, #3979, #3980, #3981, #3983, #3984, #3985) (#3989)

The wave's fix PRs deliberately carried no CHANGELOG edits (parallel-agent hot-spot protocol); each agent reported its entry and this commit lands them together, byte-verified against origin/dev.


Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant