Ask each machine's engine for the root-free word again: the engines were deaf, not absent (issue 774)
The sweep took the node seat node-engine's root-free for one nothing answers; the engines had been refused their subscriptions by the bus since 12:10 (fixed in mesh-host), so the address was right. This puts it back and keeps what the previous commit added: every failed ask said with its error, a fresh word not thrown away, and the failure as the held ask's reason.
This commit is contained in:
@@ -242,8 +242,9 @@ func (h *Holder) carriers(level asks.Level) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// RootVerdict is the word on one machine (novox/hq ADR 0259 §8): free only when it was measured, recently, that no
|
||||
// agent can become root there without a person.
|
||||
// RootVerdict is a machine's engine's word on that machine (the node seat node-engine's root-free, novox/hq ADR 0259
|
||||
// §8 as ADR 0563 §6 changes it): free only when it measured, recently, that no agent can become root there without a
|
||||
// person.
|
||||
type RootVerdict struct {
|
||||
Machine string `json:"machine"`
|
||||
Free bool `json:"free"`
|
||||
@@ -251,61 +252,63 @@ type RootVerdict struct {
|
||||
Judged string `json:"judged,omitempty"`
|
||||
}
|
||||
|
||||
// RootFreeVerb is the verb the router asks, as this module, which machines are root-free: the controller's, which
|
||||
// judges each machine from its node-engine's own verdict (ADR 0259 §8). ADR 0563 §6 moves the word to the node seat
|
||||
// node-engine and ADR 0571 §11 to the self-check's probe; neither answers on the mesh yet (the engine's acts are left
|
||||
// out of its declared state, mesh-host #178, and no seat mesh-self-check is held), so asking them held every ask
|
||||
// that offers an approval (issue 774, the sweep of 2026-10-30). The controller's verb is the one that answers.
|
||||
const RootFreeVerb = "seat:" + ControllerSeat + ".root-free"
|
||||
// EngineSeat is the node seat each machine's engine holds, and RootFreeVerb its verb the router asks of a machine, as
|
||||
// this module: `seat:node-engine.root-free@<machine>`, which that machine's engine alone answers (ADR 0563 §6).
|
||||
const (
|
||||
EngineSeat = "node-engine"
|
||||
RootFreeVerb = "seat:" + EngineSeat + ".root-free"
|
||||
)
|
||||
|
||||
// RootFreeWithin is how long the router waits for the word; RootFreeKept how long a word is used to render a
|
||||
// RootFreeWithin is how long the router waits for the machines' words; RootFreeKept how long a word is used to render a
|
||||
// message's buttons. An answer is always judged on a word asked then.
|
||||
const (
|
||||
RootFreeWithin = 10 * time.Second
|
||||
RootFreeKept = 30 * time.Second
|
||||
)
|
||||
|
||||
// askRootFree asks the word on machines at once, failing on no answer in time. Only a verdict on a machine asked is
|
||||
// taken.
|
||||
// askRootFree asks each machine's own engine about that machine, at once, failing on no answer in time. A machine's
|
||||
// word is the verdict its engine gives on it; a verdict an engine gives on another machine is not taken.
|
||||
func (h *Holder) askRootFree(machines []string) (map[string]RootVerdict, error) {
|
||||
if h.RootFree != nil {
|
||||
return h.RootFree(machines)
|
||||
}
|
||||
if h.Ask == nil {
|
||||
return nil, errors.New("this router cannot ask the controller")
|
||||
return nil, errors.New("this router cannot ask a machine's engine")
|
||||
}
|
||||
type answer struct {
|
||||
raw json.RawMessage
|
||||
err error
|
||||
machine string
|
||||
raw json.RawMessage
|
||||
err error
|
||||
}
|
||||
done := make(chan answer, 1)
|
||||
go func() {
|
||||
raw, err := h.Ask(RootFreeVerb, map[string]any{"machines": machines})
|
||||
done <- answer{raw, err}
|
||||
}()
|
||||
var a answer
|
||||
select {
|
||||
case a = <-done:
|
||||
case <-time.After(RootFreeWithin):
|
||||
return nil, fmt.Errorf("%s did not answer within %s", RootFreeVerb, RootFreeWithin)
|
||||
}
|
||||
if a.err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", RootFreeVerb, a.err)
|
||||
}
|
||||
var body struct {
|
||||
Machines []RootVerdict `json:"machines"`
|
||||
}
|
||||
if err := json.Unmarshal(a.raw, &body); err != nil {
|
||||
return nil, fmt.Errorf("%s's answer could not be read: %v", RootFreeVerb, err)
|
||||
}
|
||||
asked := map[string]bool{}
|
||||
done := make(chan answer, len(machines))
|
||||
for _, m := range machines {
|
||||
asked[m] = true
|
||||
go func(m string) {
|
||||
raw, err := h.Ask(RootFreeVerb+"@"+m, map[string]any{})
|
||||
done <- answer{m, raw, err}
|
||||
}(m)
|
||||
}
|
||||
out := map[string]RootVerdict{}
|
||||
for _, v := range body.Machines {
|
||||
if asked[v.Machine] {
|
||||
out[v.Machine] = v
|
||||
deadline := time.After(RootFreeWithin)
|
||||
for range machines {
|
||||
var a answer
|
||||
select {
|
||||
case a = <-done:
|
||||
case <-deadline:
|
||||
return nil, fmt.Errorf("a machine's engine did not answer within %s", RootFreeWithin)
|
||||
}
|
||||
if a.err != nil {
|
||||
return nil, fmt.Errorf("%s's engine: %w", a.machine, a.err)
|
||||
}
|
||||
var body struct {
|
||||
Machines []RootVerdict `json:"machines"`
|
||||
}
|
||||
if err := json.Unmarshal(a.raw, &body); err != nil {
|
||||
return nil, fmt.Errorf("%s's engine's answer could not be read: %v", a.machine, err)
|
||||
}
|
||||
for _, v := range body.Machines {
|
||||
if v.Machine == a.machine {
|
||||
out[v.Machine] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
@@ -382,7 +385,7 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
|
||||
v, said := verdicts[m]
|
||||
switch {
|
||||
case !said:
|
||||
return "the controller said nothing of whether an agent can become root on " + m
|
||||
return "its engine said nothing of whether an agent can become root on " + m
|
||||
case !v.Free:
|
||||
why := v.Why
|
||||
if why == "" {
|
||||
|
||||
@@ -256,14 +256,8 @@ func (r *busRuntime) runtime(id json.RawMessage, method string, params json.RawM
|
||||
case "mesh/seat.publish":
|
||||
return map[string]any{}, r.nc.Publish(p.Subject, p.Body)
|
||||
case "mesh/ask":
|
||||
if p.Key == RootFreeVerb {
|
||||
var args struct{ Machines []string }
|
||||
_ = json.Unmarshal(p.Body, &args)
|
||||
var out []RootVerdict
|
||||
for _, m := range args.Machines {
|
||||
out = append(out, RootVerdict{Machine: m, Free: true})
|
||||
}
|
||||
return map[string]any{"machines": out}, nil
|
||||
if m, ok := strings.CutPrefix(p.Key, RootFreeVerb+"@"); ok {
|
||||
return map[string]any{"machines": []RootVerdict{{Machine: m, Free: true}}}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("the test runtime answers nothing for %s", p.Key)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ type Holder struct {
|
||||
Emit func(event string, body any) error
|
||||
// Ask calls a seat verb through the mesh, as this module: the answers the operator chooses.
|
||||
Ask Asker
|
||||
// RootFree asks the root-free word on machines (the controller's verb, RootFreeVerb); nil asks it through Ask
|
||||
// RootFree asks each machine's engine's root-free (the node seat node-engine's, ADR 0563 §6); nil asks it through Ask
|
||||
// (askRootFree).
|
||||
RootFree func(machines []string) (map[string]RootVerdict, error)
|
||||
// Zone is the operator's time zone, which every time in a message is said in; nil is this machine's.
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
|
||||
// It performs nothing: the verbs it calls read the open conditions, whether root is free, and which issues
|
||||
// track which conditions.
|
||||
if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{"seat:" + ControllerSeat + ".conditions",
|
||||
"seat:mesh-conditions.conditions", RootFreeVerb, TrackingVerb}) {
|
||||
"seat:mesh-conditions.conditions", "seat:" + EngineSeat + ".root-free", TrackingVerb}) {
|
||||
t.Fatalf("emits %v invokes %v", m.Emits, m.Invokes)
|
||||
}
|
||||
var state []string
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -73,32 +74,34 @@ func TestApprovalNeedsTheControllersWordThatRootIsFree(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// H3: the word on the router's machine and the channel's is asked of the verb that answers it on the mesh, the
|
||||
// controller's root-free, once with both machines, as this module; a verdict on a machine not asked is not taken.
|
||||
// The sweep of 2026-10-30 (issue 774): the node seat node-engine's root-free answered nowhere, so every ask that
|
||||
// offers an approval was held, silently.
|
||||
func TestTheRootWordIsAskedOfTheVerbThatAnswers(t *testing.T) {
|
||||
// H3, as ADR 0563 §6 changes it: the word on each machine is asked of that machine's own engine, the node seat
|
||||
// node-engine's root-free there, as this module, and read from its answer; a machine's word is that machine's alone.
|
||||
func TestTheRootWordIsAskedOfEachMachinesEngine(t *testing.T) {
|
||||
h, _, _, _, _, _ := newRouter(t)
|
||||
h.RootFree = nil
|
||||
var asked []string
|
||||
h.Ask = func(key string, body any) (json.RawMessage, error) {
|
||||
ms, _ := body.(map[string]any)["machines"].([]string)
|
||||
asked = append(asked, key+" "+strings.Join(ms, ","))
|
||||
return json.RawMessage(`{"machines":[{"machine":"anchor","free":true,"why":"judged","judged":"2026-10-09T10:00:00Z"}]}`), nil
|
||||
asked = append(asked, key)
|
||||
machine := strings.TrimPrefix(key, "seat:node-engine.root-free@")
|
||||
if machine == "elsewhere" {
|
||||
machine = "anchor" // an engine that answers for another machine is not believed
|
||||
}
|
||||
return json.RawMessage(`{"machines":[{"machine":"` + machine + `","free":true,"why":"judged","judged":"2026-10-09T10:00:00Z"}]}`), nil
|
||||
}
|
||||
info, _ := h.Bench.Kind("telegram")
|
||||
if why := h.rootFree(info, false); why != "" {
|
||||
t.Fatalf("free not read: %s", why)
|
||||
}
|
||||
if len(asked) != 1 || asked[0] != "seat:mesh-controller.root-free anchor" {
|
||||
if len(asked) != 1 || asked[0] != "seat:node-engine.root-free@anchor" {
|
||||
t.Errorf("asked %v", asked)
|
||||
}
|
||||
asked = nil
|
||||
h.Node = "elsewhere"
|
||||
if why := h.rootFree(info, false); !strings.Contains(why, "said nothing of whether an agent can become root on elsewhere") {
|
||||
t.Errorf("a machine left out of the answer was a pass: %q", why)
|
||||
t.Errorf("one machine's word was taken for another's: %q", why)
|
||||
}
|
||||
if len(asked) != 1 || asked[0] != "seat:mesh-controller.root-free elsewhere,anchor" {
|
||||
sort.Strings(asked)
|
||||
if len(asked) != 2 || asked[0] != "seat:node-engine.root-free@anchor" || asked[1] != "seat:node-engine.root-free@elsewhere" {
|
||||
t.Errorf("the router's machine and the channel's were asked as %v", asked)
|
||||
}
|
||||
h.Node = "anchor"
|
||||
@@ -113,8 +116,8 @@ func TestTheRootWordIsAskedOfTheVerbThatAnswers(t *testing.T) {
|
||||
}
|
||||
|
||||
// A failed ask of the root word is said with its error, keeps the word still fresh for rendering, never passes an
|
||||
// answer, and is the reason a held ask carries (the sweep of 2026-10-30: the word was deleted silently and the held
|
||||
// reason said only "not recent").
|
||||
// answer, and is the reason a held ask carries (the sweep of 2026-10-30, issue 774: the engines did not answer, the
|
||||
// word was deleted silently and the held reason said only "not recent").
|
||||
func TestAFailedRootAskIsSaidAndKeepsAFreshWord(t *testing.T) {
|
||||
h, _, _, c, store, _ := newRouter(t)
|
||||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||||
@@ -125,13 +128,13 @@ func TestAFailedRootAskIsSaidAndKeepsAFreshWord(t *testing.T) {
|
||||
t.Fatalf("free not read: %s", why)
|
||||
}
|
||||
h.RootFree = func([]string) (map[string]RootVerdict, error) {
|
||||
return nil, errors.New("nothing in the mesh is called node-engine")
|
||||
return nil, errors.New("anchor's engine: no responders")
|
||||
}
|
||||
c.pass(RootFreeKept / 3)
|
||||
if why := h.rootFree(info, false); !strings.Contains(why, "could not be asked: nothing in the mesh is called node-engine") {
|
||||
if why := h.rootFree(info, false); !strings.Contains(why, "could not be asked: anchor's engine: no responders") {
|
||||
t.Errorf("a failed ask judged an answer: %q", why)
|
||||
}
|
||||
if len(logs) != 1 || !strings.Contains(logs[0], "nothing in the mesh is called node-engine") || !strings.Contains(logs[0], "kept for rendering") {
|
||||
if len(logs) != 1 || !strings.Contains(logs[0], "no responders") || !strings.Contains(logs[0], "kept for rendering") {
|
||||
t.Errorf("the failure was not said with its error: %v", logs)
|
||||
}
|
||||
if why := h.rootFree(info, true); why != "" {
|
||||
@@ -139,10 +142,10 @@ func TestAFailedRootAskIsSaidAndKeepsAFreshWord(t *testing.T) {
|
||||
}
|
||||
c.pass(RootFreeKept)
|
||||
stand(h, c)
|
||||
if why := h.rootFree(info, true); !strings.Contains(why, "failed: nothing in the mesh is called node-engine") {
|
||||
if why := h.rootFree(info, true); !strings.Contains(why, "failed: anchor's engine: no responders") {
|
||||
t.Errorf("a stale word passed, or its reason hid the failure: %q", why)
|
||||
}
|
||||
if why := h.holdWhy(heldAsk(c, "d-9")); !strings.Contains(why, "nothing in the mesh is called node-engine") {
|
||||
if why := h.holdWhy(heldAsk(c, "d-9")); !strings.Contains(why, "no responders") {
|
||||
t.Errorf("a held ask does not say why: %q", why)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"invokes": [
|
||||
"seat:mesh-controller.conditions",
|
||||
"seat:mesh-conditions.conditions",
|
||||
"seat:mesh-controller.root-free",
|
||||
"seat:node-engine.root-free",
|
||||
"seat:issue-tracker.tracking"
|
||||
],
|
||||
"reads": [
|
||||
|
||||
Reference in New Issue
Block a user