Skip to content

[BUG] RESTORING gets baselined as a database's expected state, alerting forever once it goes ONLINE #2189

Closed

Description

@erikdarlingdata

Component

Both

Describe the Bug

Found by dogfooding on the 52-server production monitoring box. In the last 24 hours it fired 636 Database State alerts from only 5 distinct (server, database) pairs — roughly 127 identical repeats each:

636 fires / 5 distinct (server,detail) pairs

multi-22       Database: db-A  Expected: RESTORING  Current: ONLINE  -> x134
psi            Database: db-B  Expected: RESTORING  Current: ONLINE  -> x130
multi-53       Database: db-C  Expected: RESTORING  Current: ONLINE  -> x128
multi-44       Database: db-D  Expected: RESTORING  Current: ONLINE  -> x125
multi-49       Database: db-E  Expected: RESTORING  Current: ONLINE  -> x119

(Database names are relabelled db-A..db-E — this repo is public and the originals are client-identifying. Server names, counts and every number are unchanged.)

Every one is a database that was observed mid-restore during a consolidation, had RESTORING learned as its accepted baseline, and has been "deviating" ever since by being healthy.

SeedDatabaseStateExpectedSql refuses to baseline a critical first observation, but its exclusion list is only SUSPECT, RECOVERY_PENDING, EMERGENCY:

AND (CASE WHEN ds.is_in_standby THEN 'STANDBY' ELSE ds.state_desc END)
    NOT IN ('SUSPECT', 'RECOVERY_PENDING', 'EMERGENCY')

RESTORING is every bit as transient as RECOVERY_PENDING — it is a database in the middle of an operation, not a steady state anyone would choose as "expected." Baselining it inverts the alert permanently: the database is now paged about for being ONLINE, forever, and the only escape is an operator noticing and re-baselining by hand.

The guard's own doc says onboarding a server mid-outage "must not learn the bad state as expected." A restore in progress is that same situation; the list just doesn't include it.

Steps to Reproduce

  1. Add a server, or let one be swept, while a database is RESTORING (a restore, a log-shipping cutover, a consolidation).
  2. Let the seed baseline it.
  3. Restore completes; database goes ONLINE.
  4. It now deviates from RESTORING forever and alerts every cooldown.

Expected Behavior

Don't baseline transient operational states. At minimum add RESTORING (and I'd argue RECOVERING) to the seed exclusion so those databases stay pending until they settle. Worth also considering a re-baseline when a pending database first reaches ONLINE, so the steady state gets learned rather than the transition.

Actual Behavior

RESTORING is learned as the expected state, and every healthy observation afterwards is a deviation. 636 alerts in 24 hours from 5 databases on one box.

Additional Context

#2166 does NOT cover this, which is why this is filed separately. I checked before assuming: DatabaseStateTokens.RepeatsAreNoise keys off the CURRENT state, and the current state in this flood is ONLINE, which returns false. So the edge-trigger in #2166 leaves alreadyAnnounced false here and these keep repeating every cooldown even after that ships. #2166 silences a database parked in a quiet state; this is a database parked against a bogus baseline.

That distinction is worth carrying into #2166's design discussion: an edge trigger keyed on "is the current state one of the quiet ones" covers the reported case but not the more general one, which is "this exact deviation is unchanged since the last time we announced it." The general form would have caught both. Not proposing to widen #2166 mid-review — noting it so the narrower choice is deliberate rather than accidental.

Also note the pairing: fix the seed and these 5 databases still need their existing bad baselines corrected, so a fix probably wants to handle already-poisoned rows rather than only new ones.

Activity

  1. added 4 commits that reference this issue on Aug 11, 2026
  2. erikdarlingdata commented on Aug 11, 2026

    @erikdarlingdata
    OwnerAuthor

    Shipped in #2202. Two implementations of this fix collided today - #2196 from the fleet-side session, #2202 from a repo-side lane - and #2196 was closed in #2202's favor on four grounds now on both records: the exclusion list lives in ONE shared constant (DatabaseStateTokens.NeverBaselinedSqlList) that all five seed/heal surfaces interpolate, pinned by test, instead of three hand-copied SQL strings (the reviewer had already caught a third-copy divergence in #2196 - the instance was fixed, the mechanism remained); the heal UPDATEs the poisoned row to the observed steady state in place rather than deleting and re-seeding, so there is no window where the baseline is absent and no ordering dependency (idempotency and no-intermediate-fire both asserted live against real Postgres); the reset-to-current path - which records whatever it sees with no state filter and would have re-poisoned mid-restore forever - is closed by the same constant; and the heal deliberately spares what it must not touch, with the broad ONLINE-is-never-alertable rule REJECTED after review caught two real regressions it causes (healing a STANDBY secondary that recovered out of standby announces the repair instead of the break; healing an auto-baselined OFFLINE database parked after maintenance re-fires forever in Lite) - both pinned watched-red. A database mid-RESTORE/RECOVERING now stays pending and silent until it settles; the five poisoned production rows self-correct on the next sweep with no migration and no operator step. By-product: #2203 tracks #2166's untracked Lite parity gap (no persisted alerted-state memory), which gained teeth here.

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions