Files
mesh-catalog/modules/messenger/cmd/messenger/sync.go
T
jochen d83711f11e
mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
Take only acknowledgements from a desk click, and make each answer a one-time ticket
Review found that any process of the operator's account can choose a dunst action,
so a click could release or stop a delivery in the operator's name; that tokens
lived for ever; that a warning's answers died in ten seconds; and that a restarted
notifier left dead buttons (hq ADR 0258).
2026-10-08 14:38:24 +02:00

174 lines
5.1 KiB
Go

package main
// Reading the state (novox/hq issue 271): what is open now, from the controller's `conditions`
// verb, is what this holder believes — not the events it happened to be handed. Read on start, every
// ResyncEvery, and at once when an old event arrives (a replay, a recreated consumer, the holder back
// from away). Everything before the reading is in it, so no event older than it is acted on.
import (
"sort"
"time"
)
// SyncResult is what a reading of the state changed, for the log and the status.
type SyncResult struct {
Open int `json:"open"`
Said []string `json:"said,omitempty"` // urgent and never said: said now, in one message
News []string `json:"news,omitempty"` // a warning raised within FreshFor and not heard: said like any other
State []string `json:"as_state,omitempty"` // open, older, never said: kept, not said
Gone []string `json:"gone,omitempty"` // open here, not in the controller's state: ended, quietly
Skipped []string `json:"newer_here,omitempty"`
}
// Sync takes the controller's open conditions, read by a request made at `asked`.
func (h *Holder) Sync(open []Condition, asked time.Time) SyncResult {
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
h.init()
now := h.Now()
h.mu.Unlock()
res := SyncResult{Open: len(open)}
in := map[string]bool{}
for _, c := range open {
in[c.Key] = true
}
// What is open here and no longer there ended while this holder was not told: forgotten without
// a message — said only where an edit notifies nobody. What this holder heard of after the
// reading was asked for is newer than it, and stands.
h.mu.Lock()
var gone []*Record
for _, r := range h.open {
if r.Origin == "condition" && r.Cleared.IsZero() && !in[r.Key] && !r.Seen.After(asked) {
gone = append(gone, r)
}
}
h.mu.Unlock()
sort.Slice(gone, func(i, j int) bool { return gone[i].Key < gone[j].Key })
for _, r := range gone {
res.Gone = append(res.Gone, r.Key)
h.cleared(r.Key, true)
}
var urgent []*Record
var fresh []*Record
for _, c := range open {
rec := recordOf(c)
h.mu.Lock()
old := h.open[c.Key]
var r *Record
known := old != nil
wasQuiet := false
switch {
case old != nil && old.Seen.After(asked):
h.mu.Unlock()
res.Skipped = append(res.Skipped, c.Key)
continue
case old != nil:
wasQuiet = needsNothing(old) && !old.told()
if !old.Cleared.IsZero() {
// Ended here, open there: back to open, quietly — the reading is not an event.
old.Cleared = time.Time{}
old.Count++
}
old.Summary, old.Subject, old.Kind, old.SilencedTill = rec.Summary, rec.Subject, rec.Kind, rec.SilencedTill
old.takeWords(rec)
if rec.Severity != "" {
old.Severity = rec.Severity
}
if !rec.Raised.IsZero() {
old.Raised = rec.Raised
}
r = old
default:
n := rec
n.Count = 1
if n.Raised.IsZero() {
n.Raised = now
}
n.Sent = map[string]string{}
n.History = true
h.open[n.Key] = &n
r = &n
}
r.Seen = now
told := r.told()
silenced := r.silenced(now)
h.mu.Unlock()
switch {
case told || silenced || h.queued(r.Key):
case r.Severity == Urgent:
urgent = append(urgent, r)
case !known && now.Sub(r.Raised) <= FreshFor:
fresh = append(fresh, r)
case wasQuiet && !needsNothing(r):
// Kept quiet as needing nothing; its words now need the operator (ADR 0258).
fresh = append(fresh, r)
default:
res.State = append(res.State, r.Key)
}
h.persist(r)
}
// Every urgent condition open and never said: one message, now, whatever the grace.
if len(urgent) > 0 {
var ready []Channel
for _, ch := range h.channels() {
if ch.Ready() == nil {
ready = append(ready, ch)
}
}
for _, r := range urgent {
res.Said = append(res.Said, r.Key)
if len(ready) == 0 {
h.mu.Lock()
r.Pending = "raised"
h.mu.Unlock()
continue
}
for _, ch := range ready {
h.enqueue(ch.Name(), item{Key: r.Key, What: "raised", Severity: Urgent, At: now})
}
}
for _, ch := range ready {
h.flushChannel(ch, now, true)
}
}
for _, r := range fresh {
res.News = append(res.News, r.Key)
h.mu.Lock()
r.History = false
h.mu.Unlock()
h.deliver(r, "raised")
}
h.mu.Lock()
h.syncedAt = asked
h.syncErr = ""
h.wantSync = false
h.mu.Unlock()
if len(res.Said)+len(res.News)+len(res.Gone) > 0 || len(res.State) > 0 {
h.Logf("[messenger] read the open conditions: %d open; said %d urgent never said, %d new, kept %d as state, %d ended meanwhile",
res.Open, len(res.Said), len(res.News), len(res.State), len(res.Gone))
}
return res
}
// SyncFailed notes that the state could not be read; it is asked for again.
func (h *Holder) SyncFailed(err error) {
h.mu.Lock()
defer h.mu.Unlock()
h.init()
first := h.syncErr == ""
h.syncErr = err.Error()
if first {
h.Logf("[messenger] cannot read the open conditions from the controller (%v): old events are kept as state, never said", err)
}
}
// WantsSync says whether the state should be read again: never read, read too long ago, or an old
// event arrived since.
func (h *Holder) WantsSync() bool {
h.mu.Lock()
defer h.mu.Unlock()
h.init()
return h.wantSync || h.syncedAt.IsZero() || h.Now().Sub(h.syncedAt) >= ResyncEvery
}