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 pass: its merge-check.sh passed
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-controller@4291fee68e95 (merged as a5a132ac into main, walk plan-17913907…
mesh/delivery-group group feat/health-the-field delivered: every member is delivered
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 covered: a later merge that contains it was delivered: novox/mesh-controller@4291fee68e95 (merged as a5a132ac into main, walk plan-17913907…
mesh/delivery-group group feat/health-the-field delivered: every member is delivered
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:
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
@@ -286,3 +287,57 @@ func TestReplayCrashLoopFailsItsGateOnTheFirstMachine(t *testing.T) {
|
||||
t.Fatalf("the module is registered at %s, not put back to c1", current["app"].Commit)
|
||||
}
|
||||
}
|
||||
|
||||
// **R145 — a web application that accepts TCP and answers nothing is raised within two looks** (novox/hq
|
||||
// ADR 0240 rule 4 and Phase B, issue 145). For eleven hours a web application's port was open and its
|
||||
// program ran while every request hung, and the mesh said its machine was healthy; a person found it.
|
||||
// Liveness cannot see it and a TCP check cannot either: the port is open. The module's declared HTTP check
|
||||
// can. The engine's half (mesh-host internal/liveness TestReplaySilentWebAppIsSaidUnhealthy) states what it
|
||||
// found looking at such a program; here the controller hears that statement on two looks in a row and
|
||||
// raises the module's condition — the second, never the first. `null` is an engine older than the
|
||||
// readiness check: it states the program alive, and nothing is raised.
|
||||
func TestReplaySilentWebAppIsRaisedWithinTwoLooks(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
silent := []byte(`{"contract":2,"at":"2026-10-07T00:00:00Z","resources":[{"module":"app","resource":"app.server",` +
|
||||
`"kind":"container","target":"app-server","state":"unhealthy","reason":"http / on web: no answer within 5s",` +
|
||||
`"since":"2026-10-07T00:00:00Z","streak":3,"check":"http"}]}`)
|
||||
if path := os.Getenv("MESH_REPLAY_STATEMENT"); path != "" {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("the engine's statement of the silent web application: %v", err)
|
||||
}
|
||||
silent = raw
|
||||
}
|
||||
var h link.Health
|
||||
if err := json.Unmarshal(silent, &h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Contract == 0 {
|
||||
t.Fatal("the engine states nothing of the silent web application: it is older than the judging")
|
||||
}
|
||||
open1 := func() []conditions.Condition {
|
||||
t.Helper()
|
||||
list, err := conditionsFrom.Open(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return list
|
||||
}
|
||||
for look := 1; look <= 2; look++ {
|
||||
at := time.Now().Add(time.Duration(look) * time.Second)
|
||||
h.At = at
|
||||
if err := stateHealth(ctx, open.inventory, conditionsFrom, "anchor", h, at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raised := open1()
|
||||
switch {
|
||||
case look == 1 && len(raised) != 0:
|
||||
t.Fatalf("one look raised %v", raised[0].Key)
|
||||
case look == 2 && (len(raised) != 1 || raised[0].Key != "module.app.anchor.unhealthy"):
|
||||
t.Fatalf("two looks in a row did not raise the module's condition: %v", raised)
|
||||
case look == 2:
|
||||
t.Logf("raised on the second look: %s", raised[0].Summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user