a sev4 alarm stops when the service is back, or after two hours (V-535)

It repeated every five minutes for over two hours. WasAcked was the only
stop condition and nothing reachable from telegram can mark a nudge acked
— the only ack is a voice 'готово' on a box that runs no voice loop.

Two endings now. The condition cleared, which the rule answers through the
new Rule.StillTrue — deliberately not Predicate, which is edge-triggered
and reads false one tick after the alarm is raised, so building the stop on
it would cancel every alarm immediately. Or the alarm got old, which is the
bound that needs no cooperation from the rule. A rule with no StillTrue is
never read as resolved and stops only on age.

Covered by tests including the flap case, since none of it can be
reproduced by hand without waiting hours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 01:42:44 +04:00
parent 8e7aa0d451
commit 8a13d189bb
3 changed files with 285 additions and 0 deletions
+20
View File
@@ -35,6 +35,22 @@ type Rule struct {
// the prefix here instead. Prefixes never make a rule inert: an empty
// family is the predicate's own "no data" case.
WantPrefixes []string
// StillTrue — is the CONDITION still true, ignoring whether it is worth
// saying again? Distinct from Predicate on purpose, and the distinction is
// the whole reason this field exists (Vikunja #535).
//
// Predicate answers "should this fire now", which folds in edge-triggering:
// ServiceDownRule ends in !s.NudgedSince(...), so it reads false the instant
// a nudge goes out even though the service is still down. A repeat loop that
// consulted Predicate would cancel every alarm one tick after raising it,
// which is exactly backwards.
//
// Only a rule whose alarm repeats needs this. nil means "I cannot tell you",
// and the caller must then fall back to a bound it can enforce without the
// rule's help. nil must never be read as "the condition cleared": a rule
// that says nothing about its condition is not a rule that resolved.
StillTrue func(State) bool
}
// Cooldown — tunable bounded by the envelope so a weird week (auto-tuned) can't
@@ -158,6 +174,10 @@ func ServiceDownRule() Rule {
}
return !s.NudgedSince("service_down", newest)
},
// The condition without the edge trigger. DownServices is the same
// helper the predicate and the phraser read, so the repeat stops on
// exactly the monitors he was told about.
StillTrue: func(s State) bool { return len(DownServices(s)) > 0 },
}
}