Files
mesh-tools/node-tools/internal/alive/alive.go
T
jochen 0307df0850 Say the node tools are there, every minute (hq to-be 45 Phase 1, S11)
A machine whose node-engine is heard and whose runtime is gone is a
machine nobody can ask anything, and nothing said so. The runtime now
says on mesh.control.<node>.tools-alive, every minute, that it is there,
with its interval and build; the controller raises tools-silent after
three missed. Core NATS, like the host's heartbeat: a lost one is the
next one. A heartbeat the bus refuses is logged when that starts and
when it stops.
2026-10-06 10:06:29 +02:00

81 lines
2.4 KiB
Go

// Package alive is the node tools saying they are there (novox/hq to-be 45 §3, S11).
//
// **A machine heard and its runtime gone is a machine nobody can ask anything.** The node-engine says
// every minute that the machine is there; nothing said whether the runtime serving every module's
// tools and every held seat's verbs was. Now it says so too, on its own subject, every interval, with
// the interval — the controller's watchdog raises `tools-silent` after three missed — and its build.
// Core NATS, kept by nobody, as the host's heartbeat: a lost one is the next one.
package alive
import (
"encoding/json"
"runtime/debug"
"sync"
"time"
)
// Every is how often the runtime says it is there.
const Every = 60 * time.Second
// Subject is where one machine's node tools say it.
func Subject(node string) string { return "mesh.control." + node + ".tools-alive" }
// Beat is what is said: the machine, the interval, and the build.
type Beat struct {
Node string `json:"node"`
IntervalSeconds int `json:"interval_seconds"`
Version string `json:"version,omitempty"`
}
// Sayer publishes one message, kept by nobody.
type Sayer interface {
Say(subject string, body []byte) error
}
// Version is this build's commit, as the toolchain stamped it; empty when it did not.
func Version() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
for _, s := range info.Settings {
if s.Key == "vcs.revision" {
return s.Value
}
}
return ""
}
// Keep says the runtime is there now and every interval until the returned function is called. A
// heartbeat that cannot be said is logged when that starts and when it stops — not every minute.
func Keep(bus Sayer, node string, every time.Duration, logf func(string, ...any)) func() {
body, _ := json.Marshal(Beat{Node: node, IntervalSeconds: int(every / time.Second), Version: Version()})
done := make(chan struct{})
var once sync.Once
go func() {
failing := ""
tick := time.NewTicker(every)
defer tick.Stop()
for {
why := ""
if err := bus.Say(Subject(node), body); err != nil {
why = err.Error()
}
if why != failing {
if why != "" {
logf("node-tools: could not say this runtime is there — the mesh will call it silent: %s", why)
} else {
logf("node-tools: says again that this runtime is there")
}
failing = why
}
select {
case <-done:
return
case <-tick.C:
}
}
}()
return func() { once.Do(func() { close(done) }) }
}