Skip to content

TimescaleDB availability is probed only at startup - a transient store blip disables the store health checks for the life of the process #3815

Closed

Description

@erikdarlingdata

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:

  1. Re-probe then converge. On a slow cadence (hourly is ample), if the flag is currently false, re-run TryEnableAsync; if it now returns true, run the setup sequence the startup block runs (convert/compression/CAGGs/retention) and flip the flag. This heals fully.
  2. 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

  1. erikdarlingdata commented on Sep 20, 2026

    @erikdarlingdata
    OwnerAuthor

    #3812 establishes the hourly re-evaluation tick in DarlingWorker (PR #3829); this item is a second tenant of that tick — lane after #3812 lands.

  2. erikdarlingdata commented on Sep 21, 2026

    @erikdarlingdata
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions