201 lines
6.2 KiB
Go
201 lines
6.2 KiB
Go
package console
|
|
|
|
// What discovery heard from each runtime (2026-10-05): the console said a module ran nowhere when the
|
|
// largest runtime's answer arrived after the window closed, and nothing showed that it had. Every
|
|
// round of discovery is remembered per runtime, so a runtime heard before and not now is named, and
|
|
// `mesh_runtimes` says who answered, how fast and how much.
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/announce"
|
|
)
|
|
|
|
// RuntimeForgotten is how long a runtime heard once is expected again: within it, its silence is said
|
|
// wherever an address it might answer is asked for; after it, it is listed only by mesh_runtimes.
|
|
var RuntimeForgotten = 15 * time.Minute
|
|
|
|
// runtimeSeen is one runtime instance as discovery last heard it.
|
|
type runtimeSeen struct {
|
|
Name string
|
|
ID string
|
|
Machine string
|
|
LastHeard time.Time
|
|
Took time.Duration
|
|
Bytes int
|
|
Modules int
|
|
Tools int
|
|
SeatVerbs int
|
|
Shortened string
|
|
Answered bool // in the latest round
|
|
Why string // why not, when it did not
|
|
}
|
|
|
|
func runtimeKey(name, id string) string { return name + "/" + id }
|
|
|
|
// seenOf is what one answer announced, counted.
|
|
func seenOf(h announce.Heard, at time.Time) *runtimeSeen {
|
|
modules := map[string]bool{}
|
|
tools := map[string]bool{}
|
|
verbs := map[string]bool{}
|
|
for _, e := range announce.Endpoints(h.Info) {
|
|
switch e.Kind {
|
|
case announce.KindTool:
|
|
modules[e.Module] = true
|
|
tools[e.Module+"."+e.Tool] = true
|
|
case announce.KindSeat:
|
|
verbs[e.Seat+"."+e.Tool] = true
|
|
}
|
|
}
|
|
return &runtimeSeen{Name: h.Info.Name, ID: h.Info.ID, Machine: h.Machine(), LastHeard: at, Took: h.Took,
|
|
Bytes: h.Bytes, Modules: len(modules), Tools: len(tools), SeatVerbs: len(verbs), Shortened: h.Shortened, Answered: true}
|
|
}
|
|
|
|
// remember records a round of discovery and adds to the index every runtime heard lately that did not
|
|
// answer this time. Called with s.mu held.
|
|
func (s *Surface) remember(x *index) {
|
|
if s.runtimes == nil {
|
|
s.runtimes = map[string]*runtimeSeen{}
|
|
}
|
|
at := x.Discovery.At
|
|
if at.IsZero() {
|
|
at = time.Now().UTC()
|
|
}
|
|
heard := map[string]bool{}
|
|
for _, h := range x.Discovery.Heard {
|
|
k := runtimeKey(h.Info.Name, h.Info.ID)
|
|
seen := seenOf(h, at)
|
|
if heard[k] {
|
|
// Two instances under one name and id: both counted, the slower answer's time kept.
|
|
prev := s.runtimes[k]
|
|
seen.Modules += prev.Modules
|
|
seen.Tools += prev.Tools
|
|
seen.SeatVerbs += prev.SeatVerbs
|
|
seen.Bytes += prev.Bytes
|
|
if prev.Took > seen.Took {
|
|
seen.Took = prev.Took
|
|
}
|
|
}
|
|
heard[k] = true
|
|
s.runtimes[k] = seen
|
|
}
|
|
// A runtime restarted answers under a new instance id: the old one is gone, not silent, and saying
|
|
// it was missed would hide nothing but cry wolf after every restart.
|
|
now := map[string]bool{}
|
|
for k := range heard {
|
|
now[s.runtimes[k].Name+"@"+s.runtimes[k].Machine] = true
|
|
}
|
|
for k, r := range s.runtimes {
|
|
if !heard[k] && r.Machine != "" && now[r.Name+"@"+r.Machine] {
|
|
delete(s.runtimes, k)
|
|
}
|
|
}
|
|
silent := map[string]string{}
|
|
for _, u := range x.Discovery.Silent {
|
|
silent[runtimeKey(u.Name, u.ID)] = fmt.Sprintf("it answered PING; what it serves did not arrive within %s", announce.Patience)
|
|
}
|
|
for k, r := range s.runtimes {
|
|
if heard[k] {
|
|
continue
|
|
}
|
|
r.Answered = false
|
|
if why, ok := silent[k]; ok {
|
|
r.Why = why
|
|
continue // already in the index's unheard
|
|
}
|
|
r.Why = "it did not answer this round of discovery"
|
|
if time.Since(r.LastHeard) <= RuntimeForgotten {
|
|
x.Unheard = append(x.Unheard, unheard{Runtime: r.Name, Machine: r.Machine,
|
|
Why: fmt.Sprintf("it answered discovery %s ago, not now", time.Since(r.LastHeard).Round(time.Second))})
|
|
}
|
|
}
|
|
for _, u := range x.Discovery.Silent {
|
|
k := runtimeKey(u.Name, u.ID)
|
|
if _, known := s.runtimes[k]; !known {
|
|
s.runtimes[k] = &runtimeSeen{Name: u.Name, ID: u.ID, Machine: u.Machine, Why: silent[k]}
|
|
}
|
|
}
|
|
}
|
|
|
|
// runtimesAnswer is mesh_runtimes: discovery asked now, and every runtime as it was heard.
|
|
func (s *Surface) runtimesAnswer() map[string]any {
|
|
x, err := s.indexAsked(true)
|
|
if err != nil {
|
|
return failure("the mesh's discovery failed: " + err.Error())
|
|
}
|
|
s.mu.Lock()
|
|
var all []runtimeSeen
|
|
for _, r := range s.runtimes {
|
|
all = append(all, *r)
|
|
}
|
|
s.mu.Unlock()
|
|
sort.Slice(all, func(i, j int) bool {
|
|
if all[i].Machine != all[j].Machine {
|
|
return all[i].Machine < all[j].Machine
|
|
}
|
|
return runtimeKey(all[i].Name, all[i].ID) < runtimeKey(all[j].Name, all[j].ID)
|
|
})
|
|
now := time.Now()
|
|
var answered, notHeard []map[string]any
|
|
onMachine := map[string]bool{}
|
|
for _, r := range all {
|
|
entry := map[string]any{"runtime": r.Name, "instance": r.ID}
|
|
if r.Machine != "" {
|
|
entry["machine"] = r.Machine
|
|
}
|
|
if !r.LastHeard.IsZero() {
|
|
entry["last heard"] = r.LastHeard.Format(time.RFC3339)
|
|
entry["last heard, ago"] = now.Sub(r.LastHeard).Round(time.Second).String()
|
|
entry["took"] = r.Took.Round(time.Millisecond).String()
|
|
entry["bytes"] = r.Bytes
|
|
entry["modules"] = r.Modules
|
|
entry["tools"] = r.Tools
|
|
entry["seat verbs"] = r.SeatVerbs
|
|
entry["shortened"] = r.Shortened != ""
|
|
if r.Shortened != "" {
|
|
entry["shortened, how"] = r.Shortened
|
|
}
|
|
}
|
|
if r.Answered {
|
|
onMachine[r.Machine] = true
|
|
answered = append(answered, entry)
|
|
} else {
|
|
entry["why"] = r.Why
|
|
if r.LastHeard.IsZero() {
|
|
entry["last heard"] = "never in full, since this console started"
|
|
}
|
|
notHeard = append(notHeard, entry)
|
|
}
|
|
}
|
|
for _, m := range x.Machines {
|
|
if !onMachine[m] && !machineListed(notHeard, m) {
|
|
notHeard = append(notHeard, map[string]any{"machine": m,
|
|
"why": "the mesh knows this machine and no runtime on it has answered discovery since this console started"})
|
|
}
|
|
}
|
|
if answered == nil {
|
|
answered = []map[string]any{}
|
|
}
|
|
if notHeard == nil {
|
|
notHeard = []map[string]any{}
|
|
}
|
|
return answerText(map[string]any{
|
|
"asked": x.Discovery.At.Format(time.RFC3339),
|
|
"waited": x.Discovery.Waited.Round(time.Millisecond).String(),
|
|
"window": fmt.Sprintf("at least %s; up to %s for a runtime that answered PING", announce.Window, announce.Patience),
|
|
"answered": answered,
|
|
"not heard": notHeard,
|
|
})
|
|
}
|
|
|
|
func machineListed(entries []map[string]any, machine string) bool {
|
|
for _, e := range entries {
|
|
if e["machine"] == machine {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|