Repository navigation
TimescaleDB availability is probed only at startup - a transient store blip disables the store health checks for the life of the process #3815
Description
Summary
_timescaleAvailable is probed exactly once, at startup, and never re-evaluated. A transient store condition during service start silently downgrades the service to plain-PostgreSQL mode for the entire lifetime of the process — including switching off the very self-health checks that would report the degradation.
Companion to #3812 (same class of defect: a decision that can only be revisited by restarting a service designed to run for months).
The latch
TimescaleSupport.TryEnableAsync has one call site: Darling/PerformanceMonitor.Darling.Service/DarlingWorker.cs:1364. The field is also force-cleared in the block's catch at DarlingWorker.cs:1415:
_timescaleAvailable = false;
_logger.LogWarning("TimescaleDB setup failed — continuing in plain-PostgreSQL mode: {Message}", ex.Message);
That catch is deliberately broad, and correctly so — the comment above it is right that "a partially-converted store is fine" and that falling back is always safe. The defect is not the fallback. It is that the fallback is permanent, and the store conditions that trigger it are usually transient: the store restarting, a failover, a lock held by something else, a momentary connection failure. The store-migrate step above it retries with a budget (StartupFailureTriage); this one does not, and never reconsiders.
What the flag turns off for the rest of the run
_timescaleAvailable is read at six decision points after startup:
| Site | Consequence when falsely false |
|---|---|
DarlingWorker.cs:2159 |
The whole hourly store-job health check is skipped — no compression-job self-heal (#1581), no Store Job Over Cadence alert (#2136), and no Retention Held alert (#2813). |
DarlingWorker.cs:2018 |
The daily purge falls back from drop_chunks to DELETE-based retention. |
DarlingWorker.cs:5328 |
StoreSelfMetrics.SweepAsync drops the per-hypertable size/compression rows (#2068), so capacity forecasting loses its series. |
DarlingWorker.cs:1745, :5952 |
Two provider delegates resolve to the plain-PG shape. |
The first row is the bad one. A store that is genuinely on TimescaleDB, running with a spuriously-false flag, has its compression policy jobs un-self-healed AND cannot report that fact, because the alert that reports it lives behind the same gate. That is the #1581 field incident (uncompressed data grows without bound until the disk fills and collection stops for the whole fleet) with its backstop disabled.
It also means a store in this state cannot raise the Retention Held alert — so #3812's symptom would be invisible too.
Suggested fix
Re-probe on a cadence. TryEnableAsync is already idempotent (it is a CREATE EXTENSION IF NOT EXISTS + detect), tolerant, and cheap.
The natural home is the fleet-level maintenance block that already exists in the same loop — but note the ordering problem: the compression check at :2159 is gated on the flag, so the re-probe has to run before it and outside its gate, or a false flag keeps its own correction from running.
Two shapes, either is fine:
- Re-probe then converge. On a slow cadence (hourly is ample), if the flag is currently
false, re-runTryEnableAsync; if it now returnstrue, run the setup sequence the startup block runs (convert/compression/CAGGs/retention) and flip the flag. This heals fully. - Re-probe only. Flip the flag on a successful detect and let the startup sequence wait for the next restart. Cheaper, and it at least restores the health checks and the alerting, which is where the real damage is.
(1) is the better answer and folds naturally into #3812's periodic convergence pass — if that lands, this becomes "run the convergence pass on a cadence, and let it re-probe first."
Also worth logging
Today a service running in falsely-degraded mode says so exactly once, in a warning at startup, and then looks completely normal forever. Whatever cadence lands, a store that is detected as Timescale-capable while the flag is false should log that it is recovering, at Information.
Activity
erikdarlingdata commentedon Sep 20, 2026
#3812 establishes the hourly re-evaluation tick in DarlingWorker (PR #3829); this item is a second tenant of that tick — lane after #3812 lands.
TimescaleDB availability was decided once, by a probe whose failures …
erikdarlingdata commentedon Sep 21, 2026
Claude posting for Erik Darling
Fixed in #3842 (merged 7f57ed0). The hourly store-maintenance tick #3812 established now re-probes TimescaleDB availability FIRST and outside the gate it corrects — inside it, a false flag blocks its own correction — under a ten-second budget on its own connection, and logs at Information when the latch flips back without a restart.
Shape chosen: re-probe only, not re-probe-then-converge. Every consumer reads the flag at call time, so the flip alone restores the job health check, the drop_chunks purge branch, the self-metrics per-hypertable rows and both provider delegates on their next pass. Running the setup sequence here would have built #3817 inside this lane and collided with it; #3817 already names this re-probe as its precondition. The recovery line says the setup sequence is still start-path-only, so nobody is told more was restored than was.
The ordering is pinned structurally rather than by matching a literal: the test reconstructs the if-conditions whose braces enclose the re-probe and asserts none tests the flag positively, with a positive control (the compression read, which IS behind the flag, must come back as such). Nine mutations were each proved red on the intended pin and green on restore, including two that crippled the instrument itself.
One cost stated rather than left to be found: on a store with no TimescaleDB library, the hourly failed CREATE EXTENSION writes an ERROR to that store's own PostgreSQL log, ~24/day, visible in get_store_log's census — which is unbanded and feeds no alert. DetectAsync alone would avoid it but would not heal the 'library present, extension never created' case this issue prescribes.
Note for anyone working #3816 or #3817: every line number in this issue is stale. On dev at 158418a the TryEnableAsync call site is :1440 (not :1364), the force-clear :1538 (not :1415), the hourly health check :2300 (not :2159), the purge :2146, SweepAsync :6055, the delegates :1873 and :6668. The relative structure is exactly as described. Also, the table says six post-startup reads; six reads exist but one is the start-path gate itself, so five are post-startup.