Files
mesh-tools/node-tools/internal/console/runtimes.go
T
jochen 379d19d907 Wait for every runtime discovery hears from, and name the ones it misses
The console gathered $SRV.INFO answers for a fixed 750 ms. The laptop's
runtime (48 modules, 341 endpoints, 164 kB) answers last every time: its
answer crosses to the broker on another machine and back, a median of
365 ms on a quiet link and 813 ms in one of 25 rounds measured. When it
missed the window the console said its modules ran nowhere ("nothing in
the mesh is called slack") or only on another machine.

Discovery now asks $SRV.PING alongside $SRV.INFO and waits, past the
window and up to 5 s, for every instance that answered PING. A runtime
that never sends what it serves, or answered before and not now, is
named in every answer that might concern it instead of the module being
called missing. An answer still too large after first-line descriptions
drops them, and says so in its metadata.

mesh_runtimes reports, per runtime, its machine, how long its answer
took, its size, its modules and tools, whether it was shortened and when
it was last heard, and the runtimes and machines not heard.
2026-10-05 15:36:10 +02:00

190 lines
5.8 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
}
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
}