Skip to content

Background-job self-heal covers compression jobs only - a dead CAGG refresh or retention job is never detected or re-armed #3816

Closed

Description

@erikdarlingdata

Summary

The #1581 background-job self-heal covers compression and columnstore jobs only. A policy_refresh_continuous_aggregate or policy_retention job that dies the same way is not detected, not alerted, and not re-armed — and a dead CAGG refresh job is the upstream cause of the retention holds in #3812.

The scope gap

TimescaleSupport.StuckCompressionJobsSql (Darling/PerformanceMonitor.Darling.Storage/TimescaleSupport.cs:5715) ends:

WHERE j.proc_name LIKE '%compression%'
   OR j.proc_name LIKE '%columnstore%'

Everything downstream inherits that filter: the -infinity dead-job detection (TimescaleSupport.cs:5672, "next_start is -infinity — the scheduler will never run it again"), the stuck-Running bound, the EvaluateCompressionJobsAsync self-alert, and the TryRearmJobAsync self-heal wired in at DarlingWorker.cs:5243-5246.

The failure mode #1581 was written for is not compression-specific. next_start = -infinity is how the TimescaleDB scheduler retires any job it has given up on. A CAGG refresh job and a retention job can both reach it.

Why the other two hourly checks do not cover it

Both ride the same connection and cadence, and neither sees a dead job:

js.total_failures is recorded into collect.store_metrics by StoreSelfMetrics (StoreSelfMetrics.cs:151) and exposed through get_store_metrics, but nothing alerts on it. It is a series someone has to go looking at.

Why this one matters more than compression

A dead policy_refresh_continuous_aggregate job stops a rollup from materializing. Silently. The store keeps collecting, the raw tier keeps filling, and the first externally visible consequence is the coverage gate refusing to arm that tier's retention policy — at which point the operator is told to run --backfill-rollups, which will fix the symptom and leave the dead job in place to cause it again.

That is the loop worth closing: #3812 makes the release automatic, this makes the cause detectable.

Suggested fix

Widen the existing pass rather than adding a second one. The per-proc reasoning differs, so this is not literally a one-line LIKE change:

  • Detection: generalize StuckCompressionJobsSql to read every collect-scoped policy job, carrying proc_name through so the evaluator can band per family. The -infinity NULLIF handling and the stuck-Running bound apply unchanged.
  • Self-heal: TryRearmJobAsync is proc-agnostic (alter_job(job_id, next_start => now())), so re-arming a dead refresh or retention job needs no new mechanism. One caveat that must be honored: re-arming a retention job is not equivalent to arming one. The Retention policies run their first check immediately — no external window to pause them, caused permanent data loss #1680/Retention gate is arm-only, so a policy whose coverage list GROWS is never re-held — a store upgrading into a new consumer keeps purging its source #1877 coverage gate deliberately leaves held policies scheduled = false, and a self-heal that "revives dead jobs" must not treat a held policy as a dead one — job_stats.next_start = -infinity and a deliberately paused job are different states, and conflating them would drop history the gate exists to protect. Gate the retention arm on MeasureRetentionCoverageAsync returning Covered, exactly as EnsureRetentionPoliciesAsync does, or restrict the self-heal to refresh jobs only and alert-without-healing on retention.
  • Alert: the existing EvaluateCompressionJobsAsync shape extends naturally; per-family text so "the archival tier is stalled" and "a rollup has stopped materializing" do not read the same.

Evidence this is the live cause on at least one store

DARLING01 currently holds four retention policies, all in the query_store_* family, with the daily tiers ~25 hours behind their hourly sources and query_store_stats_daygrain_daily completely empty. See #3812 for the measurements. Whether that is a dead refresh job or simply a never-backfilled cold start is not yet established on that box — but nothing in the product would tell an operator which, and that is the point.

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

    Closed by the watcher: delivered in PR #3846, merged to dev.

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