Say every failed unit, the module's and the machine's (hq issue 315)
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/315-a-failed-unit-is-a-condition delivered: every member is delivered
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/315-a-failed-unit-is-a-condition delivered: every member is delivered
Liveness judged only what a module runs long-lived, so a module whose daemon is a package's unit started by D-Bus activation failed at every start while its machine read healthy, and a degraded service manager was said by nothing but the profile. The engine now reads the failed units of the machine's manager and of every account manager the declaration names, on the two-look rule, and says whose each is: a declared service or process, a unit file the mesh writes, or a package the mesh installs makes it that module's, said as an unhealthy resource of kind unit; anything else is the machine's own, said in the statement's new units field. It reads; it never acts.
This commit is contained in:
+72
-3
@@ -43,6 +43,7 @@ import (
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
"github.com/novox/mesh-host/internal/tunnel"
|
||||
"github.com/novox/mesh-host/internal/units"
|
||||
"github.com/novox/mesh-host/internal/upgrade"
|
||||
)
|
||||
|
||||
@@ -1090,6 +1091,8 @@ func runLink(ctx context.Context, opts options) error {
|
||||
return held
|
||||
}
|
||||
judging = j
|
||||
// And which units its service managers say failed, and whose each is (novox/hq issue 315).
|
||||
unitJudge = units.New(units.Exec{Run: apply.ExecRunner, System: builtFor})
|
||||
}
|
||||
|
||||
// **Standing aside for a successor happens between reconciles and nowhere else** (novox/hq ADR
|
||||
@@ -1384,6 +1387,10 @@ const ReconcileEvery = 5 * time.Minute
|
||||
// reports no health, and in a test.
|
||||
var judging *liveness.Judge
|
||||
|
||||
// unitJudge reads which units the machine's service managers say failed, and whose each is (novox/hq
|
||||
// issue 315); nil where judging is.
|
||||
var unitJudge *units.Judge
|
||||
|
||||
// netJudge is the serving host's judge of its machine's networking (novox/hq ADR 0241); empty in a
|
||||
// one-shot command and in a test, which say nothing of the network.
|
||||
var netJudge networkJudge
|
||||
@@ -1478,6 +1485,17 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
|
||||
}
|
||||
st, changed := j.Look(ctx)
|
||||
owed = owed || changed
|
||||
var unitSt *units.Statement
|
||||
if u := unitJudge; u != nil {
|
||||
us, unitsChanged := u.Look(ctx)
|
||||
unitSt = &us
|
||||
owed = owed || unitsChanged
|
||||
if unitsChanged {
|
||||
for _, f := range us.Failed {
|
||||
say(failedUnitWords(f))
|
||||
}
|
||||
}
|
||||
}
|
||||
var netSt *network.Statement
|
||||
if n := netJudge.get(); n != nil {
|
||||
ns, netChanged := n.Look(ctx)
|
||||
@@ -1500,7 +1518,8 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
|
||||
spaced = sp
|
||||
}
|
||||
since := time.Since(lastSaid)
|
||||
healthy := st.Healthy() && (netSt == nil || netSt.State != network.Unhealthy)
|
||||
healthy := st.Healthy() && (netSt == nil || netSt.State != network.Unhealthy) &&
|
||||
(unitSt == nil || len(unitSt.Failed) == 0)
|
||||
if !owed && !(!healthy && since >= sayUnhealthyAgain) && since < sayAnyway {
|
||||
continue
|
||||
}
|
||||
@@ -1512,7 +1531,7 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Queue, sa
|
||||
}
|
||||
}
|
||||
}
|
||||
if queue.SayHealth(ctx, *healthAsReported(st, netSt)) {
|
||||
if queue.SayHealth(ctx, *withUnits(healthAsReported(st, netSt), unitSt)) {
|
||||
lastSaid, owed = time.Now(), false
|
||||
}
|
||||
}
|
||||
@@ -1538,6 +1557,50 @@ func healthAsReported(st liveness.Statement, ns *network.Statement) *link.Health
|
||||
return h
|
||||
}
|
||||
|
||||
// withUnits adds to a statement the machine's failed units (novox/hq issue 315): each a module places,
|
||||
// among the resources as that module's unhealthy unit; the rest, the machine's own, beside them. Nil — an
|
||||
// engine not reading them — says nothing of them, which the controller reads as not known.
|
||||
func withUnits(h *link.Health, us *units.Statement) *link.Health {
|
||||
if us == nil || us.State == "" {
|
||||
return h
|
||||
}
|
||||
h.Units = &link.UnitsHealth{State: us.State, Failed: []link.FailedUnit{}, Unread: us.Unread}
|
||||
for _, f := range us.Failed {
|
||||
if f.Module == "" {
|
||||
h.Units.Failed = append(h.Units.Failed, link.FailedUnit{Unit: f.Unit, Scope: f.Scope, Load: f.Load,
|
||||
Result: f.Result, Resource: f.Resource, Since: f.Since.UTC()})
|
||||
continue
|
||||
}
|
||||
reason := "failed"
|
||||
if f.Scope == units.ScopeUser {
|
||||
reason += " in the account's own service manager"
|
||||
}
|
||||
if f.Result != "" {
|
||||
reason += " (" + f.Result + ")"
|
||||
}
|
||||
h.Resources = append(h.Resources, link.ResourceHealth{Module: f.Module, Resource: f.Resource,
|
||||
Kind: link.KindUnit, Target: f.Unit, State: link.StateUnhealthy, Reason: reason, Since: f.Since.UTC(),
|
||||
Streak: f.Streak})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// failedUnitWords is a failed unit as the console says it.
|
||||
func failedUnitWords(f units.Failed) string {
|
||||
whose := "no module places it"
|
||||
if f.Module != "" {
|
||||
whose = fmt.Sprintf("%s's, by its %s %s", f.Module, f.Via, f.Resource)
|
||||
}
|
||||
return fmt.Sprintf("the unit %s (%s) failed%s: %s", f.Unit, f.Scope, orNothing(" ("+f.Result+")", f.Result), whose)
|
||||
}
|
||||
|
||||
func orNothing(s, unless string) string {
|
||||
if unless == "" {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// holdTheMachine asks for a reconcile every ReconcileEvery. **It asks; it does not apply** (novox/hq
|
||||
// to-be 45 §6): the queue's worker does, when its turn comes, and a reconcile due while a delivery is
|
||||
// waiting is that delivery's apply.
|
||||
@@ -1775,7 +1838,13 @@ func applyAndKeepHeld(ctx context.Context, opts options, raw []byte, signed *sto
|
||||
ns := n.Last()
|
||||
netSt = &ns
|
||||
}
|
||||
report.Health = healthAsReported(st, netSt)
|
||||
var unitSt *units.Statement
|
||||
if u := unitJudge; u != nil {
|
||||
u.Set(units.OwnedBy(declared, held))
|
||||
us := u.Last()
|
||||
unitSt = &us
|
||||
}
|
||||
report.Health = withUnits(healthAsReported(st, netSt), unitSt)
|
||||
}
|
||||
// Which of this machine's links face outside, for the filter the mesh writes around them
|
||||
// (novox/hq ADR 0140). Reported whatever the node's mode: a converged node's filter needs it,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/liveness"
|
||||
"github.com/novox/mesh-host/internal/units"
|
||||
)
|
||||
|
||||
// The machine's failed units in the engine's statement (novox/hq issue 315): a module's is among the
|
||||
// resources, unhealthy, as that module's unit, naming it; the machine's own beside them; and an engine
|
||||
// not reading them says nothing of them.
|
||||
func TestTheStatementCarriesTheFailedUnits(t *testing.T) {
|
||||
at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
||||
us := &units.Statement{At: at, State: units.Degraded, Failed: []units.Failed{
|
||||
{Unit: "openrazer-daemon.service", Scope: units.ScopeUser, Load: "loaded", Result: "exit-code",
|
||||
Module: "openrazer", Resource: "openrazer.daemon", Via: units.ViaPackage, Since: at, Streak: 2},
|
||||
{Unit: "storage-media.mount", Scope: units.ScopeSystem, Load: "loaded", Result: "timeout", Since: at, Streak: 2},
|
||||
}}
|
||||
h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), us)
|
||||
if len(h.Resources) != 1 {
|
||||
t.Fatalf("one module's unit among the resources: %+v", h.Resources)
|
||||
}
|
||||
r := h.Resources[0]
|
||||
if r.Module != "openrazer" || r.Resource != "openrazer.daemon" || r.Kind != link.KindUnit ||
|
||||
r.Target != "openrazer-daemon.service" || r.State != link.StateUnhealthy ||
|
||||
!strings.Contains(r.Reason, "account's own service manager") || !strings.Contains(r.Reason, "exit-code") {
|
||||
t.Fatalf("the module's failed unit: %+v", r)
|
||||
}
|
||||
if h.Units == nil || h.Units.State != units.Degraded || len(h.Units.Failed) != 1 ||
|
||||
h.Units.Failed[0].Unit != "storage-media.mount" || h.Units.Failed[0].Result != "timeout" {
|
||||
t.Fatalf("the machine's own: %+v", h.Units)
|
||||
}
|
||||
if h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), nil); h.Units != nil {
|
||||
t.Fatal("an engine not reading units said them")
|
||||
}
|
||||
if h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), &units.Statement{}); h.Units != nil {
|
||||
t.Fatal("a judge not yet given a declaration said units")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user