Read how a module says each resource is ready, and send it to engines that read it (hq ADR 0240, to-be 48 Phase B)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: FAIL github.com/novox/mesh-controller/cmd/mesh-controller 1800.059s
mesh/delivery-group group feat/health-the-field rejected: its order contradicts itself: novox/mesh-controller@468d509462fe, novox/mesh-host@96bf9415aae4
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: FAIL github.com/novox/mesh-controller/cmd/mesh-controller 1800.059s
mesh/delivery-group group feat/health-the-field rejected: its order contradicts itself: novox/mesh-controller@468d509462fe, novox/mesh-host@96bf9415aae4
mesh/delivery superseded: a newer head of the same pull request
A module could say nothing about what ready means for what it runs, so a web application with its port open and its requests hanging passed everything for eleven hours (issue 145). A long-running resource now carries `health` — the image's own check adopted by name, http, tcp, exec, unit or a module's own tool, with its timing — refused near its author when it names a port or an address, an endpoint the module does not declare, a tool it does not serve, a tool check alone, or a timing outside the record's bounds. It is composed with the endpoint as the port this machine published it on, and sent only to a node-engine whose statement says it reads it: an older one would refuse the whole declaration. The engine is granted its own machine's instance of each health tool. `module check` warns of every long-running resource without `health`, counts them for the catalogue, and refuses them from 2026-11-18. A check's findings stay out of a condition's summary. The node-engine's validator is vendored at its Phase B commit, so what is composed is judged by the words the engine takes.
This commit is contained in:
+41
-3
@@ -590,6 +590,23 @@ type Process struct {
|
||||
// For a process that stays up; a step or a scheduled run is not running a moment later by
|
||||
// design, so there is nothing to hand over to.
|
||||
Replaces []string `json:"replaces,omitempty"`
|
||||
|
||||
// Witness is how the node-engine judges a new build of this process, and restores the build
|
||||
// before it when the new one is not healthy in bound (novox/hq to-be 45 §8): "lease" — the
|
||||
// controller this machine started holds the controller's lease; "ping" — this machine's runtime
|
||||
// answers the services protocol's PING; "none". Absent is the default for the process's name:
|
||||
// the mesh's two core processes are judged, nothing else is. For a process that stays up.
|
||||
Witness string `json:"witness,omitempty"`
|
||||
|
||||
// NotReversible says why this build may not be rolled back, when it may not: the build before it
|
||||
// would run against what this one changes — a migration it runs that the older build cannot read
|
||||
// (to-be 45 §8, rule 8). A build so declared that is not healthy in bound is left running and said
|
||||
// as urgent; the build before it is never started against the newer data.
|
||||
NotReversible string `json:"not-reversible,omitempty"`
|
||||
|
||||
// Health is how this resource is ready (novox/hq ADR 0240 rule 2, Phase B): one kind and its
|
||||
// timing, judged by the node-engine beside liveness. Absent: judged alive or not, and nothing more.
|
||||
Health *Health `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Process) Identity() string { return d.ID }
|
||||
@@ -621,7 +638,7 @@ func ProcessNameProblem(name string) string {
|
||||
}
|
||||
|
||||
func (d *Process) validate(where string, _ bool) []string {
|
||||
var problems []string
|
||||
problems := d.Health.problems(where, false, !d.RunOnce && d.Schedule == "")
|
||||
if problem := ProcessNameProblem(d.Name); problem != "" {
|
||||
problems = append(problems, where+": "+problem)
|
||||
}
|
||||
@@ -637,6 +654,19 @@ func (d *Process) validate(where string, _ bool) []string {
|
||||
if len(d.Run) == 0 {
|
||||
problems = append(problems, where+": a process needs to say what to run")
|
||||
}
|
||||
switch d.Witness {
|
||||
case "", "lease", "ping", "none":
|
||||
default:
|
||||
problems = append(problems, fmt.Sprintf("%s: witness %q is not one this host keeps: lease, ping or none",
|
||||
where, d.Witness))
|
||||
}
|
||||
if d.Witness != "" && d.Witness != "none" && (d.RunOnce || d.Schedule != "") {
|
||||
problems = append(problems, where+": a witness judges a process that stays up; a step or a "+
|
||||
"scheduled run is not running between its runs")
|
||||
}
|
||||
if strings.ContainsAny(d.NotReversible, "\n\r") {
|
||||
problems = append(problems, where+": not-reversible is one line")
|
||||
}
|
||||
for _, part := range d.Run {
|
||||
if part == "" {
|
||||
problems = append(problems, where+": a process command has an empty element")
|
||||
@@ -756,6 +786,10 @@ type Service struct {
|
||||
// said by the controller, which knows the found tunnel's key is this node's own: without that,
|
||||
// starting this unit on the found one's port would drop every peer's packets.
|
||||
TakesOver *TakeOver `json:"takes-over,omitempty"`
|
||||
|
||||
// Health is how this resource is ready (novox/hq ADR 0240 rule 2, Phase B): one kind and its
|
||||
// timing, judged by the node-engine beside liveness. Absent: judged alive or not, and nothing more.
|
||||
Health *Health `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
// TakeOver is a found tunnel a service replaces: its interface, the unit that raised it, and its
|
||||
@@ -784,7 +818,7 @@ const (
|
||||
func (s *Service) UserScoped() bool { return s.Scope == ScopeUser }
|
||||
|
||||
func (s *Service) validate(where string, _ bool) []string {
|
||||
var problems []string
|
||||
problems := s.Health.problems(where, false, s.State == "running")
|
||||
if s.Unit == "" {
|
||||
problems = append(problems, where+": a service needs a unit")
|
||||
}
|
||||
@@ -1096,6 +1130,10 @@ type Container struct {
|
||||
// offline job says *before*, not *instead of*; a recurring window is the case order cannot
|
||||
// express, and the only one this serves.
|
||||
WhileStopped []string `json:"while-stopped,omitempty"`
|
||||
|
||||
// Health is how this resource is ready (novox/hq ADR 0240 rule 2, Phase B): one kind and its
|
||||
// timing, judged by the node-engine beside liveness. Absent: judged alive or not, and nothing more.
|
||||
Health *Health `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Container) Identity() string { return c.ID }
|
||||
@@ -1103,7 +1141,7 @@ func (c *Container) Kind() Type { return TypeContainer }
|
||||
func (c *Container) Target() string { return c.Name }
|
||||
|
||||
func (c *Container) validate(where string, _ bool) []string {
|
||||
var problems []string
|
||||
problems := c.Health.problems(where, true, !c.RunOnce && c.Schedule == "")
|
||||
if c.Name == "" {
|
||||
problems = append(problems, where+": a container needs a name")
|
||||
}
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Health is how a long-running resource is ready, as the controller composed it from the module's
|
||||
// `health` (novox/hq ADR 0240 rule 2, to-be 48 §2–§3, Phase B): one kind and its timing, the endpoint
|
||||
// already the port this machine published it on.
|
||||
//
|
||||
// **The node-engine runs every kind and owns every verdict.** http and tcp it makes itself, from the
|
||||
// machine to the port; unit it reads from the service manager it already reads; exec and runtime it hands
|
||||
// to the container runtime as the container's own check, with this timing, and reads the state; tool it
|
||||
// asks of its own node tools. Nothing else on the machine sets a container's check.
|
||||
//
|
||||
// Refused here as the controller refuses it near the author, in the same bounds: an engine that took a
|
||||
// check it could not judge would say a module ready that nothing looked at.
|
||||
type Health struct {
|
||||
Kind string `json:"kind"`
|
||||
// Endpoint is the module's name for what Port is: for the words a verdict is said in.
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Tool string `json:"tool,omitempty"`
|
||||
Interval string `json:"interval"`
|
||||
Timeout string `json:"timeout"`
|
||||
Looks int `json:"looks"`
|
||||
Grace string `json:"grace"`
|
||||
// Needs is the provision the check exercises (to-be 48 §6): said with every verdict, so the
|
||||
// controller can hold what it finds under the provider's own condition.
|
||||
Needs string `json:"needs,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads a health strictly, as everything a declaration carries is read: a field this host
|
||||
// does not know is a part of the check the controller believes it asked for, and nothing would look at it.
|
||||
func (h *Health) UnmarshalJSON(raw []byte) error {
|
||||
type plain Health
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("health: %w", err)
|
||||
}
|
||||
*h = Health(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// The kinds.
|
||||
const (
|
||||
HealthRuntime = "runtime"
|
||||
HealthHTTP = "http"
|
||||
HealthTCP = "tcp"
|
||||
HealthExec = "exec"
|
||||
HealthUnit = "unit"
|
||||
HealthTool = "tool"
|
||||
)
|
||||
|
||||
// The bounds (ADR 0240 rule 2) — the controller's, held again here.
|
||||
const (
|
||||
HealthIntervalFloor = 10 * time.Second
|
||||
HealthLooksFloor = 2
|
||||
HealthWithin = 5 * time.Minute
|
||||
)
|
||||
|
||||
// Every, Within and GraceOf are the timing, read. Validated on arrival, so a parse error here is
|
||||
// impossible on a declaration that was accepted; it reads as zero.
|
||||
func (h *Health) Every() time.Duration { d, _ := time.ParseDuration(h.Interval); return d }
|
||||
func (h *Health) Within() time.Duration { d, _ := time.ParseDuration(h.Timeout); return d }
|
||||
func (h *Health) GraceOf() time.Duration { d, _ := time.ParseDuration(h.Grace); return d }
|
||||
|
||||
// RunByRuntime says the container runtime runs this check as the container's own: exec and runtime.
|
||||
func (h *Health) RunByRuntime() bool {
|
||||
return h != nil && (h.Kind == HealthExec || h.Kind == HealthRuntime)
|
||||
}
|
||||
|
||||
// Words is the check in a few words, as a verdict is said: "http /healthz on web".
|
||||
func (h *Health) Words() string {
|
||||
switch h.Kind {
|
||||
case HealthHTTP:
|
||||
return "http " + h.Path + " on " + orPort(h.Endpoint, h.Port)
|
||||
case HealthTCP:
|
||||
return "tcp on " + orPort(h.Endpoint, h.Port)
|
||||
case HealthTool:
|
||||
return "its tool " + h.Tool
|
||||
case HealthRuntime:
|
||||
return "its image's own check"
|
||||
case HealthExec:
|
||||
return "its command"
|
||||
case HealthUnit:
|
||||
return "its unit"
|
||||
}
|
||||
return h.Kind
|
||||
}
|
||||
|
||||
func orPort(endpoint string, port int) string {
|
||||
if endpoint != "" {
|
||||
return endpoint
|
||||
}
|
||||
return fmt.Sprint(port)
|
||||
}
|
||||
|
||||
// problems holds a resource's health to its kind and bounds. container says whether the resource is a
|
||||
// container; longRunning whether it stays up.
|
||||
func (h *Health) problems(where string, container, longRunning bool) []string {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, where+": "+fmt.Sprintf(format, args...))
|
||||
}
|
||||
if !longRunning {
|
||||
say("health is judged on what stays up; a step or a scheduled run is judged by its own outcome")
|
||||
}
|
||||
switch h.Kind {
|
||||
case HealthHTTP, HealthTCP:
|
||||
if h.Port < 1 || h.Port > 65535 {
|
||||
say("a %s check needs the port it looks at", h.Kind)
|
||||
}
|
||||
case HealthExec:
|
||||
if !container {
|
||||
say("an exec check runs inside a container")
|
||||
}
|
||||
if strings.TrimSpace(h.Command) == "" {
|
||||
say("an exec check needs a command")
|
||||
}
|
||||
case HealthRuntime:
|
||||
if !container {
|
||||
say("a runtime check is a container image's own")
|
||||
}
|
||||
case HealthUnit:
|
||||
if container {
|
||||
say("a unit check is a service's or a process's own")
|
||||
}
|
||||
case HealthTool:
|
||||
if strings.TrimSpace(h.Tool) == "" {
|
||||
say("a tool check names the tool")
|
||||
}
|
||||
default:
|
||||
say("health of kind %q; it is runtime, http, tcp, exec, unit or tool", h.Kind)
|
||||
}
|
||||
if h.Kind == HealthHTTP {
|
||||
if !strings.HasPrefix(h.Path, "/") {
|
||||
say("an http check asks a path starting with /")
|
||||
}
|
||||
if h.Status != 0 && (h.Status < 100 || h.Status > 599) {
|
||||
say("an http check expects status %d, which is not one", h.Status)
|
||||
}
|
||||
if h.Scheme != "" && h.Scheme != "http" && h.Scheme != "https" {
|
||||
say("an http check is over http or https, not %q", h.Scheme)
|
||||
}
|
||||
}
|
||||
if strings.ContainsAny(h.Command, "\n\r") {
|
||||
say("an exec check's command is one line")
|
||||
}
|
||||
every, everyErr := time.ParseDuration(h.Interval)
|
||||
within, withinErr := time.ParseDuration(h.Timeout)
|
||||
grace, graceErr := time.ParseDuration(h.Grace)
|
||||
switch {
|
||||
case everyErr != nil || withinErr != nil || graceErr != nil:
|
||||
say("health's interval, timeout and grace are durations")
|
||||
default:
|
||||
if every < HealthIntervalFloor {
|
||||
say("a check looks no more often than every %s, not every %s", HealthIntervalFloor, every)
|
||||
}
|
||||
if within <= 0 || within >= every {
|
||||
say("a look takes more than nothing and less than its interval")
|
||||
}
|
||||
if grace < 0 {
|
||||
say("a grace is not negative")
|
||||
}
|
||||
if h.Looks >= HealthLooksFloor && grace+time.Duration(h.Looks)*every > HealthWithin {
|
||||
say("a grace and the failing looks take at most %s", HealthWithin)
|
||||
}
|
||||
}
|
||||
if h.Looks < HealthLooksFloor {
|
||||
say("a check is unhealthy after at least %d failing looks, not %d", HealthLooksFloor, h.Looks)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
Reference in New Issue
Block a user