mesh/merge-gate pass: builds desk-channel, messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that wa…
mesh/delivery-group group fix/373-one-message-per-condition delivered: every member is delivered
Seen at 14:40: machine.shanks.units was silenced for 7 days with a reason, and its critical popup stood on the desk as "Still open" while the router logged nothing. A silence now edits the message where it is shown to "Silenced until <day>: <headline>" with the reason, no answers and no urgency, so the desk's popup times out; nothing more is said while it lasts. When the silence ends and the condition is still open, the message is edited back to its open words. Resolved while silenced, it is said only where an edit notifies nobody. The silence is taken from the controller's event and from the 10-min reading of the open set alike.
180 lines
5.3 KiB
Go
180 lines
5.3 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, wasSilenced := false, 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()
|
|
wasSilenced = old.silenced(now)
|
|
if !old.Cleared.IsZero() {
|
|
// Ended here, open there: back to open, quietly — the reading is not an event.
|
|
old.Cleared = time.Time{}
|
|
old.ClearUnsaid, old.ClearSilent, old.ClearHeld = false, false, ""
|
|
old.Count++
|
|
}
|
|
old.Summary, old.Subject, old.Kind = rec.Summary, rec.Subject, rec.Kind
|
|
old.SilencedTill, old.SilencedWhy = rec.SilencedTill, rec.SilencedWhy
|
|
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()
|
|
if known {
|
|
h.silenceWords(r, wasSilenced)
|
|
}
|
|
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
|
|
}
|