ADR 0590 supersedes the lease write: mesh-grants says no lease event any more, so the watchman's lease-altered and removal-failed findings and their row had nothing to hear. The ended controller's consumers are removed as hand acts, so their deletion is no consumer lost; only the one on DEAD_LETTER_NOTICES is still read, since its removal hands the notices to the watchman. The builder hears artifacts-gone from the artifact sweep seat alone, and mesh-enrolment's tokens no longer report an issuing gate that nothing can turn off. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
92 lines
3.7 KiB
Go
92 lines
3.7 KiB
Go
package watch
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The dead letters' findings (novox/hq ADR 0564 §5, issue 330): a consumer DEAD_LETTERS holds a message for is said for as
|
|
// long as it holds any, `bus.<stream>.<consumer>.max-deliveries`, and the notices waiting behind the ended controller's
|
|
// consumer are said as `bus.DEAD_LETTER_NOTICES.controller.notices-waiting`.
|
|
|
|
// DeadRecountEvery is how often DEAD_LETTERS is counted again while a consumer's finding stands: a letter delivered again
|
|
// or dropped by the controller's verb, which the watchman does not hear, ends its finding within it. The expiry of the
|
|
// watchman's own pulse, never a loop.
|
|
const DeadRecountEvery = 5 * time.Minute
|
|
|
|
// KindNoticesWaiting is the finding of notices waiting while the ended controller's consumer stands.
|
|
const KindNoticesWaiting = "notices-waiting"
|
|
|
|
// DeadLettersHeld is DEAD_LETTERS' count per consumer, `<stream>.<consumer>`, read whole: each consumer held writes its
|
|
// finding, each one no longer held has it deleted. who words a consumer.
|
|
func (w *Watchman) DeadLettersHeld(held map[string]int, who func(stream, consumer string) string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
want := map[string]bool{}
|
|
keys := make([]string, 0, len(held))
|
|
for k := range held {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
n := held[key]
|
|
stream, consumer, ok := strings.Cut(key, ".")
|
|
if !ok || n <= 0 {
|
|
continue
|
|
}
|
|
messages, them := "a message", "it is"
|
|
if n > 1 {
|
|
messages, them = fmt.Sprintf("%d messages", n), "they are"
|
|
}
|
|
f := Finding{Scope: ScopeBus, ID: key, Kind: KindDeadLetters, Severity: Warning, Source: "S9",
|
|
Machine: consumerMachine(stream, consumer),
|
|
Summary: fmt.Sprintf("%s gave up on %s; %s kept in DEAD_LETTERS until delivered again or dropped, with why",
|
|
who(stream, consumer), messages, them),
|
|
Said: fmt.Sprintf("%d held for %s (counted again every %s while any is held, so one delivered again or dropped "+
|
|
"ends this within it)", n, key, DeadRecountEvery),
|
|
Needs: "deliver them again or drop them, from the mesh MCP server."}
|
|
want[f.Key()] = true
|
|
w.find(f)
|
|
}
|
|
for key := range w.findings {
|
|
if strings.HasPrefix(key, ScopeBus+".") && strings.HasSuffix(key, "."+KindDeadLetters) && !want[key] {
|
|
w.unfind(key)
|
|
}
|
|
}
|
|
if len(want) == 0 {
|
|
w.unsignal("dead", "recount")
|
|
return
|
|
}
|
|
w.signal("dead", "recount", DeadRecountEvery, signalDue{Do: dueRecount,
|
|
Finding: Finding{Scope: ScopeBus, ID: "DEAD_LETTERS", Kind: KindDeadLetters, Severity: Warning, Summary: "counted again", Source: "S9"}})
|
|
}
|
|
|
|
// consumerMachine is the machine a module's consumer on EVENTS is on; empty for the others.
|
|
func consumerMachine(stream, consumer string) string {
|
|
if stream == "EVENTS" {
|
|
if node, _, ok := strings.Cut(consumer, "_"); ok {
|
|
return node
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// NoticesWaiting says whether the ended controller's consumer on DEAD_LETTER_NOTICES still stands: every notice then waits
|
|
// in the queue, kept a week, until that consumer is removed (novox/hq ADR 0590 clause 6).
|
|
func (w *Watchman) NoticesWaiting(waiting bool) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
f := Finding{Scope: ScopeBus, ID: "DEAD_LETTER_NOTICES.controller", Kind: KindNoticesWaiting, Severity: Warning, Source: "dead-letters",
|
|
Summary: "the controller is unassigned but its consumer on DEAD_LETTER_NOTICES still stands, so every notice of a " +
|
|
"message given up on waits in the queue, kept a week, and nothing keeps it in DEAD_LETTERS",
|
|
Said: "the consumer controller on DEAD_LETTER_NOTICES stands after the controller's end",
|
|
Needs: "remove the consumer controller on DEAD_LETTER_NOTICES with the bus module's tool, recorded as a hand act (ADR 0590)."}
|
|
if waiting {
|
|
w.find(f)
|
|
return
|
|
}
|
|
w.unfind(f.Key())
|
|
}
|