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 pass: its merge-check.sh passed
mesh/delivery-group group feat/health-the-field delivering: 1 of 3 delivered
mesh/delivery held for a person: merged, and the controller opened no walk for it within 10m0s — nothing it holds follows that branch, or the merge was…
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.
189 lines
6.3 KiB
Go
189 lines
6.3 KiB
Go
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
|
|
}
|