Raise the bus module's watch when it falls silent or says it cannot see (hq issue 270, review 274)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head d24132d54f01 merged into trunk 04730b54a8f2 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head d24132d54f01 merged into trunk 04730b54a8f2 — pass: its merge-check.sh passed

A dead watch of slow consumers reads as none. The watch now beats after every
look; S9 reads the newest heartbeat and raises bus-watch-dead when it is older
than two minutes or says the watch is not healthy, on the bus's machine. A
stream with no heartbeat at all is a bus module older than its watch.
This commit is contained in:
jochen
2026-10-14 09:19:57 +02:00
parent 04730b54a8
commit d24132d54f
6 changed files with 136 additions and 0 deletions
@@ -90,3 +90,34 @@ func TestABusModuleReadThatFailsKeepsTheControllersOwnSignals(t *testing.T) {
}
}
}
// **A dead watch is heard** (review 274): the bus module's watch silent for longer than three looks, or saying
// it cannot see, is a condition on the bus's machine — never read as no slow consumers. A stream with no
// heartbeat at all is a bus module older than its watch, and says nothing.
func TestTheBusWatchSilentOrBlindIsAConditionAndNoHeartbeatAtAllIsNone(t *testing.T) {
now := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
look := func(b link.BusWatchBeat, heard bool) []conditions.Observation {
var out []conditions.Observation
for _, o := range watchAdvisories(&signalFacts{now: now, busWatch: b, busWatchHeard: heard}) {
if o.Kind == kindBusWatchDead {
out = append(out, o)
}
}
return out
}
if got := look(link.BusWatchBeat{}, false); len(got) != 0 {
t.Fatalf("no heartbeat ever raised %+v", got)
}
if got := look(link.BusWatchBeat{At: now.Add(-30 * time.Second), Healthy: true, Machine: "anchor"}, true); len(got) != 0 {
t.Fatalf("a healthy watch heard 30s ago raised %+v", got)
}
silent := look(link.BusWatchBeat{At: now.Add(-5 * time.Minute), Healthy: true, Machine: "anchor"}, true)
if len(silent) != 1 || silent[0].Machine != "anchor" || !strings.Contains(silent[0].Summary, "said nothing") {
t.Fatalf("a watch silent for 5 minutes raised %+v", silent)
}
blind := look(link.BusWatchBeat{At: now.Add(-10 * time.Second), Healthy: false, Machine: "anchor",
Why: "the watch of slow consumers last read the bus's closed connections 95s ago: connection refused"}, true)
if len(blind) != 1 || !strings.Contains(blind[0].Said, "connection refused") {
t.Fatalf("a watch that says it cannot see raised %+v", blind)
}
}
+6
View File
@@ -620,6 +620,12 @@ var plainWordings = map[string]func(conditions.Observation) words{
Explanation: "The bus reports a listener too slow to keep up, so messages to it are late.",
Resolved: "The listener keeps up again"}
}),
kindBusWatchDead: worded(func(o conditions.Observation) words {
return words{Headline: "The bus's watch of slow clients is down",
Explanation: "The part of the bus that notices clients too slow to keep up has stopped, or cannot see, so " +
"such a client would go unnoticed. It is part of the bus's tools on " + machineOr(o, "the bus's machine") + ".",
Resolved: "Resolved: the bus's watch of slow clients works again"}
}),
kindBusFaultsUnread: worded(func(o conditions.Observation) words {
return words{Headline: "Slow clients of the bus cannot be checked",
Explanation: "The controller could not read what the bus said about its clients' connections, so a " +
+34
View File
@@ -589,9 +589,43 @@ func watchAdvisories(f *signalFacts) []conditions.Observation {
if f.busFaultsErr != nil {
out = append(out, busFaultsUnread(f.busFaultsErr))
}
if o, ok := busWatchDead(f); ok {
out = append(out, o)
}
return append(out, watchDeadLetters(f)...)
}
// kindBusWatchDead is the bus module's watch not heard, or saying it cannot see (novox/hq ADR 0357, review
// 274): while it lasts, a slow consumer would read as none.
const kindBusWatchDead = "bus-watch-dead"
// busWatchStale is how long the watch may be silent: three of its looks, and one more for the tick.
const busWatchStale = 2 * time.Minute
// busWatchDead says the watch silent past busWatchStale, or saying it is not healthy. Nothing while the stream
// holds no heartbeat at all: a bus module older than its watch says nothing either way.
func busWatchDead(f *signalFacts) (conditions.Observation, bool) {
if !f.busWatchHeard {
return conditions.Observation{}, false
}
b := f.busWatch
o := conditions.Observation{Scope: conditions.ScopeBus, ID: "bus-module", Kind: kindBusWatchDead,
Machine: b.Machine, Severity: conditions.Warning}
switch {
case f.now.Sub(b.At) > busWatchStale:
o.Summary = fmt.Sprintf("the bus module's watch of slow consumers has said nothing for %s, so a client "+
"the bus cut off would not be said", ago(f.now.Sub(b.At)))
o.Said = "last heartbeat " + b.At.UTC().Format(time.RFC3339)
return o, true
case !b.Healthy:
o.Summary = "the bus module's watch of slow consumers says it cannot see, so a client the bus cut off " +
"would not be said"
o.Said = firstLine(b.Why)
return o, true
}
return conditions.Observation{}, false
}
// kindBusFaultsUnread is the bus module's events not read back (novox/hq ADR 0357): every other user's
// slow consumer is unheard while it lasts, and the controller's own signals are still said.
const kindBusFaultsUnread = "bus-faults-unread"
+6
View File
@@ -94,6 +94,9 @@ type signalFacts struct {
advisoriesErr error
// busFaultsErr is why the bus module's events could not be read this tick (novox/hq ADR 0357).
busFaultsErr error
// busWatch is the bus module's watch's newest heartbeat, when the stream holds one (review 274).
busWatch link.BusWatchBeat
busWatchHeard bool
selfCheck selfCheckFacts
@@ -368,6 +371,9 @@ func (w *watchdogs) gather(ctx context.Context) *signalFacts {
if faults, f.busFaultsErr = w.server.BusFaults(ctx, now.Add(-advisoryQuiet)); f.busFaultsErr == nil {
f.advisories = link.MergeAdvisories(f.advisories, faults)
}
if f.busFaultsErr == nil {
f.busWatch, f.busWatchHeard, f.busFaultsErr = w.server.BusWatchLast(ctx)
}
}
if f.advisoriesErr == nil && w.js != nil {
f.deadLetters, f.advisoriesErr = link.HeldDeadLetters(w.js.Context())