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:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user