Make a seat pass no repair, read every machine before passing, keep the gate's pass in memory (issue 1317, review round 1)

This commit is contained in:
2026-10-19 21:38:41 +00:00
parent 12db1d3748
commit 3448f98000
7 changed files with 138 additions and 14 deletions
+4
View File
@@ -72,6 +72,10 @@ var handActVerbs = []handActVerb{
// An act the operator chose on a warrant (novox/hq ADR 0259): asked by the controller, answered on a
// channel that proved who answered, performed by the controller as itself.
{Verb: handActWarrant, Decision: "the operator chose it, answering what the controller asked (ADR 0259)"},
// A seat passed by the controller to its only claimant when its holder on record left (novox/hq ADR 0449
// §4): the mesh working as decided. A person's own `seat --to` is not recorded under this cause.
{Verb: "seat", Decision: "the controller passes a seat held once to its only claimant when its recorded " +
"holder is assigned nowhere (ADR 0449 §4)", DecidedFor: []string{"seat-passed"}},
{Verb: "broker consumer-reset"},
// Silencing the same condition twice says the condition, or what it watches, wants mending — unless
// it is the operator's answer on a notification: a decision to live with it (novox/hq ADR 0258).
+8 -2
View File
@@ -289,12 +289,18 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
// seat held once whose holder on record is assigned on no machine needs the whole mesh to be known, and it
// passes to its only claimant here, in the composition that finds it.
everyAssigned := map[string][]string{}
var assignedErr error
for _, n := range nodes {
if theirs, err := inv.Assigned(ctx, n.Name); err == nil && len(theirs) > 0 {
theirs, err := inv.Assigned(ctx, n.Name)
if err != nil {
assignedErr = errors.Join(assignedErr, fmt.Errorf("%s: %w", n.Name, err))
continue
}
if len(theirs) > 0 {
everyAssigned[n.Name] = theirs
}
}
holdings = theMeshPassesSeatsOn(ctx, inv, shelf, everyAssigned, holdings)
holdings = theMeshPassesSeatsOn(ctx, inv, shelf, everyAssigned, assignedErr, holdings)
for _, n := range nodes {
if n.Name == exclude {
continue
+8
View File
@@ -93,6 +93,13 @@ func TestAFailedPlanThatCannotBeRetriedHasNoActionAndSaysWhy(t *testing.T) {
})
superseded := aFailedPlan("plan-3", func(p *inventory.Plan) { p.State, p.Note = inventory.PlanSuperseded, "superseded by plan-9" })
done := aFailedPlan("plan-4", func(p *inventory.Plan) { p.State = inventory.PlanDone })
first := time.Date(2026, 10, 19, 18, 20, 0, 0, time.UTC)
gate := aFailedPlan("plan-5", func(p *inventory.Plan) {
p.Note = "openrazer stopped at its first machine"
p.Modules = map[string]*inventory.PlanModule{"openrazer": {State: "built", FirstAt: &first, First: []string{"g14"},
Why: "the build did not become healthy on g14", Gate: &inventory.PlanGate{Machines: []string{"g14"},
Verdict: inventory.GateFailed, Why: "not healthy", From: "aaaaaaa", To: "bbbbbbb"}}}
})
for name, c := range map[string]struct {
p inventory.Plan
others []inventory.Plan
@@ -101,6 +108,7 @@ func TestAFailedPlanThatCannotBeRetriedHasNoActionAndSaysWhy(t *testing.T) {
"a newer plan": {older, []inventory.Plan{newer}, "supersedes it"},
"superseded": {superseded, nil, "superseded"},
"done": {done, nil, "nothing to retry"},
"gate failed": {gate, nil, "failed its gate"},
} {
f := calm(now)
f.failed = []failedPlanFacts{failedFactsFor(c.p, c.others...)}
+37 -12
View File
@@ -73,23 +73,31 @@ func seatPassHandAct(p catalogue.SeatPass) link.HandAct {
}
}
// theMeshPassesSeatsOn is passSeatsOn as the composition runs it: against the store's record, with the hand act
// on the bus. Said to where the composition says its other lines (sayLeftOut).
func theMeshPassesSeatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
assigned map[string][]string, holdings []catalogue.Held) []catalogue.Held {
var assignments []catalogue.Assignment
// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. **A module assigned and
// missing from the shelf is still assigned**: it is an assignment with no claims, so its holder is not taken for gone.
func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment {
var out []catalogue.Assignment
for node, modules := range assigned {
for _, module := range modules {
if m, ok := shelf[module]; ok {
assignments = append(assignments, catalogue.Assignment{Node: node, Manifest: m})
m, ok := shelf[module]
if !ok {
m = catalogue.Manifest{Module: module}
}
out = append(out, catalogue.Assignment{Node: node, Manifest: m})
}
}
var w io.Writer = os.Stderr
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
w = lw
return out
}
// seatPassingOn is how a composition on this inventory passes a seat: against the store's record with the hand act
// on the bus, or, on a throwaway mesh (the merge gate's), in memory only, writing nothing anywhere.
func seatPassingOn(inv *inventory.Inventory, w io.Writer) seatPassing {
if inv.Throwaway() {
return seatPassing{say: w,
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return true, nil },
act: func(context.Context, catalogue.SeatPass) error { return nil }}
}
return passSeatsOn(ctx, seatPassing{
return seatPassing{
say: w,
pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) {
return inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To)
@@ -100,5 +108,22 @@ func theMeshPassesSeatsOn(ctx context.Context, inv *inventory.Inventory, shelf m
return err
})
},
}, holdings, assignments)
}
}
// theMeshPassesSeatsOn is passSeatsOn as the composition runs it. **Only when every machine's assignments were
// read** (assignedErr nil): a machine whose read failed would look as if its modules were assigned nowhere, and
// its holder would lose the seat. Said to where the composition says its other lines (sayLeftOut).
func theMeshPassesSeatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
assigned map[string][]string, assignedErr error, holdings []catalogue.Held) []catalogue.Held {
var w io.Writer = os.Stderr
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
w = lw
}
if assignedErr != nil {
fmt.Fprintf(w, "no seat is passed on in this composition: a machine's assignments could not be read, so a "+
"holder on record might look assigned nowhere: %v\n", assignedErr)
return holdings
}
return passSeatsOn(ctx, seatPassingOn(inv, w), holdings, assignmentsOf(shelf, assigned))
}
+71
View File
@@ -3,10 +3,13 @@ package main
import (
"bytes"
"context"
"errors"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
@@ -120,3 +123,71 @@ func TestALostPassWritesNoActAndAFailedOneLeavesTheRecord(t *testing.T) {
t.Fatalf("got %+v said %q", got, said.String())
}
}
// A pass is the mesh working as ADR 0449 §4 decided, never a repair: two of them raise no healer-wanted and
// count as no repair.
func TestTwoSeatPassesAreNoRepairAndRaiseNoHealerWanted(t *testing.T) {
withBusSeat(t)
now := time.Date(2026, 10, 19, 20, 0, 0, 0, time.UTC)
mk := func(at time.Time) link.HandAct {
a := seatPassHandAct(catalogue.SeatPass{Seat: catalogue.Seat{Name: "mesh-broker"},
From: catalogue.Held{Node: "novox", Module: "old"}, To: catalogue.Held{Node: "novox", Module: "gate"}})
a.At = at
return a
}
acts := []link.HandAct{mk(now.Add(-26 * time.Hour)), mk(now.Add(-time.Hour))}
if r := repairs(acts); len(r) != 0 {
t.Fatalf("a seat pass counts as a repair: %+v", r)
}
f := calm(now)
f.handActs = acts
if got := watchHandActs(f); len(got) != 0 {
t.Fatalf("two passes raised %+v", got)
}
}
// A failed read of one machine's assignments passes no seat: its holder would look assigned nowhere.
func TestNoSeatIsPassedWhenAMachinesAssignmentsCouldNotBeRead(t *testing.T) {
withBusSeat(t)
var said bytes.Buffer
ctx := leftOutSaidBy(t.Context(), &said)
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "ace", Module: "old-broker"}}
shelf := map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")}
got := theMeshPassesSeatsOn(ctx, nil, shelf, map[string][]string{"novox": {"gate"}},
errors.New("ace: the bus did not answer"), holdings)
if len(got) != 1 || got[0].Module != "old-broker" {
t.Fatalf("the record moved: %+v", got)
}
if !strings.Contains(said.String(), "no seat is passed") || !strings.Contains(said.String(), "ace") {
t.Errorf("not said: %q", said.String())
}
}
// A holder that is assigned but missing from the shelf is still assigned.
func TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone(t *testing.T) {
withBusSeat(t)
as := assignmentsOf(map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")},
map[string][]string{"novox": {"gate", "old-broker"}})
passes, _ := catalogue.SeatsToPassOn(
[]catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}, as)
if len(passes) != 0 {
t.Fatalf("a holder missing from the shelf lost its seat: %+v", passes)
}
}
// The merge gate's throwaway mesh passes the seat in memory and writes nothing: no record, no hand act.
func TestAThrowawayMeshPassesTheSeatInMemoryAndWritesNothing(t *testing.T) {
withBusSeat(t)
inv := &inventory.Inventory{}
inv.MarkThrowaway()
var said bytes.Buffer
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
got := passSeatsOn(t.Context(), seatPassingOn(inv, &said), holdings,
[]catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}})
if len(got) != 1 || got[0].Module != "gate" {
t.Fatalf("the gate's view does not hold the pass: %+v", got)
}
if said.Len() != 0 {
t.Errorf("the throwaway composition said %q (a write was tried)", said.String())
}
}
+7
View File
@@ -85,6 +85,7 @@ func (i *Inventory) UseChain(c *chain.Chain) {
// beside the real one, as the merge gate raises the facts snapshot's mesh in a throwaway store (issue 1121). Nothing of it
// reaches the bus.
func (i *Inventory) UseThrowawayChain() {
i.MarkThrowaway()
i.UseChain(chain.InMemory(nil))
i.markLaidOut()
}
@@ -129,3 +130,9 @@ func (i *Inventory) theChain(ctx context.Context) (*chain.Chain, error) {
}
return c, nil
}
// MarkThrowaway says this inventory is a mesh raised for a moment: a composition on it writes nothing outside it.
func (i *Inventory) MarkThrowaway() { i.throwaway = true }
// Throwaway says whether this inventory is a mesh raised for a moment (UseThrowawayChain).
func (i *Inventory) Throwaway() bool { return i != nil && i.throwaway }
+3
View File
@@ -39,6 +39,9 @@ type Inventory struct {
state *chainState
// recorded, when set, takes the durations a save of the chain measures in place of the store: a test with no store.
recorded func(context.Context, Duration) error
// throwaway is set on an inventory raised for a moment beside the real mesh (the merge gate's): nothing it
// composes is to be written to the store or the bus.
throwaway bool
}
// Open connects to the inventory store.