Skip to content

[BUG] create_mute_rule returns "created" but the mute does not take effect - config_mute_rules has no bump trigger, so the evaluator's cached rules are not reloaded #3315

Closed

Description

@erikdarlingdata

Summary

create_mute_rule returns {"status":"created", ...} and the row really is in config.config_mute_rules — but the running service keeps delivering matching alerts, for an unbounded period, because nothing invalidates the evaluator's in-memory mute cache.

The chain:

  1. DarlingMcpAlertTools.CreateMuteRule constructs a fresh store and writes directly: await new PgMuteRuleStore(postgres).InsertAsync(rule);. It does not touch the MuteRuleService instance the alert engine holds.
  2. PgMuteRuleStore.InsertAsync is a bare INSERT INTO config_mute_rules. No version bump.
  3. There is no bump trigger on config_mute_rules. The schema has five: trg_bump_monitored_servers, trg_bump_alert_settings, trg_bump_notification, trg_bump_collector_schedules, trg_service_self_bump.
  4. MuteRuleService caches rules in memory. LoadAsync() has exactly two call sites: DarlingWorker.cs:1462 (startup) and DarlingWorker.cs:2869 (inside the control-plane reload, gated on a config_service.config_version change).

So a mute created through the MCP takes effect only when the service restarts, or when an unrelated write to one of the five bump-triggered tables happens to fire a reload. The comment at DarlingWorker.cs:1453 — "the mute service is hoisted too so a reload can re-LoadAsync() it (closes F16 …)" — is accurate about the reload path; the gap is that a mute write is not one of the things that causes a reload.

Observed

On a live 42-server store:

time (UTC) event
20:42:39 create_mute_rule with no scoping → status: created, summary: "(matches all alerts)"
20:43:58 a matching Deadlocks Detected fires → notification_type: webhook, muted: false — delivered
20:46 update_alert_settings {"cooldown_minutes": 120} → writes config_alert_settings, trg_bump_alert_settings fires
after the next sweep matching alerts begin recording notification_type: muted, muted: true

An all-metrics, all-servers mute rule did not suppress an alert that fired 79 seconds later. Activation required an unrelated setting change to force the version bump.

On a second store, mute rules created the same way hours earlier are active (notification_type: "muted"). That is the failure mode at its most misleading: it works eventually, so the tool looks correct, and the window in which it silently does not work has no upper bound and no signal.

Why this matters more than the delay

An operator mutes a noisy alert precisely when it is firing repeatedly. The success response tells them the mute is in place. It may not be, and the only way to find out is to notice that the pages keep coming and then doubt a tool that said created. get_mute_rules will confirm the rule exists, reinforcing the wrong conclusion — it reads the table, not the cache.

Suggested

Add a bump trigger on config.config_mute_rules matching the other four, so any write — MCP, Viewer, or hand-written SQL — causes the next sweep to reload. That is the smallest fix and it covers every writer rather than just this tool.

Alternatives, both narrower: have CreateMuteRule/DeleteMuteRule bump config_version explicitly after a successful write, or route the MCP through the live MuteRuleService so its persist-then-cache path updates the cache directly (the behavior MuteRuleServiceTests already pins for the Viewer path).

The same gap applies to delete_mute_rule, in the more dangerous direction: an operator un-mutes and believes alerting is restored while the stale cache keeps suppressing.

Test

A live-store test can assert the trigger directly: insert into config_mute_rules, then read config_service.config_version and assert it advanced. That pins the mechanism rather than the delivery outcome, and it fails if the trigger is ever dropped from the migration ladder.

Activity

  1. erikdarlingdata commented on Sep 11, 2026

    @erikdarlingdata
    OwnerAuthor

    Claude posting for Erik Darling

    Correcting the blast radius I claimed, because the lane measured it and I had guessed.

    I wrote that a bump trigger is the right fix because it "covers every writer — the MCP tool, the Viewer's Manage Mute Rules surface, and hand-written SQL — rather than just this tool." The Viewer half of that is wrong: the Viewer path was never broken. Every writer in ViewerDataService.MuteRules calls SignalConfigReloadAsync explicitly — five call sites, at lines 169, 193, 204, 211 and 245. The MCP tools are the ones that do not.

    That also explains the symptom I could not account for in the filing. I noted that mutes created hours earlier on another store WERE active while one created seconds ago was not, and called it an unbounded activation window with no signal. The mechanism is narrower than that: any Viewer action bumps the beacon, so an MCP-created mute activates on the next unrelated Viewer write. It reads as intermittent because it is downstream of whether a human happened to touch the desktop app.

    The conclusion survives, for a better reason than the one I gave. PgMuteRuleStore has five mutating statements, not the two the filing implies — INSERT (:90), UPDATE ... SET (:118), UPDATE ... SET enabled (:139), and DELETE twice (:148, :163). A per-call-site bump would have to find all five and stay found, while one AFTER INSERT OR UPDATE OR DELETE statement-level trigger covers them by construction and cannot be missed by a later writer. So: trigger, but because the write surface is five-wide, not because the Viewer needed rescuing.

    One design decision in the fix worth surfacing, since it is not obvious from the diff

    The V117 probe sentinel reads pg_trigger, not information_schema.triggers. That view only lists triggers on relations the current role owns or holds a non-SELECT privilege on, and the read-only viewer role holds neither on this table — so the sentinel would report the rung absent on a fully-migrated store, and the viewer's connect gate would refuse a store that is actually current. A guard that fails toward "refuse a healthy store" is the expensive direction.

    Privileges were measured rather than reasoned: a role with SELECT on config_service plus full CRUD on config_mute_rules still gets 42501 from inside config_bump_version(), and exactly GRANT UPDATE (config_version, updated_at) ON config.config_service clears it. That is what mcp already holds, so no grant moves. The trap worth recording: a missing SELECT on config_service raises the identical error, so only isolating the grants separates the two causes.

    And a note on delete_mute_rule

    The filing's warning stands and is now covered: the trigger is AFTER INSERT OR UPDATE OR DELETE, so un-muting bumps the beacon too. That was the more dangerous direction — an operator un-mutes, believes alerting is restored, and a stale cache keeps suppressing.

  2. added a commit that references this issue on Sep 11, 2026
  3. erikdarlingdata commented on Sep 11, 2026

    @erikdarlingdata
    OwnerAuthor

    Claude posting for Erik Darling

    Fixed by #3326, merged to dev as f87e3c3c. StorageVersion.SchemaVersion is now 117 and trg_bump_mute_rules is live on config.config_mute_rules. Closing explicitly — closing keywords no-op on dev.

    See the correction above: the Viewer path was never broken, so the trigger is the right fix because PgMuteRuleStore has five mutating statements rather than because a second writer needed rescuing.

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