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
|
// 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.
|
// 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)"},
|
{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"},
|
{Verb: "broker consumer-reset"},
|
||||||
// Silencing the same condition twice says the condition, or what it watches, wants mending — unless
|
// 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).
|
// 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
|
// 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.
|
// passes to its only claimant here, in the composition that finds it.
|
||||||
everyAssigned := map[string][]string{}
|
everyAssigned := map[string][]string{}
|
||||||
|
var assignedErr error
|
||||||
for _, n := range nodes {
|
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
|
everyAssigned[n.Name] = theirs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holdings = theMeshPassesSeatsOn(ctx, inv, shelf, everyAssigned, holdings)
|
holdings = theMeshPassesSeatsOn(ctx, inv, shelf, everyAssigned, assignedErr, holdings)
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.Name == exclude {
|
if n.Name == exclude {
|
||||||
continue
|
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" })
|
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 })
|
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 {
|
for name, c := range map[string]struct {
|
||||||
p inventory.Plan
|
p inventory.Plan
|
||||||
others []inventory.Plan
|
others []inventory.Plan
|
||||||
@@ -101,6 +108,7 @@ func TestAFailedPlanThatCannotBeRetriedHasNoActionAndSaysWhy(t *testing.T) {
|
|||||||
"a newer plan": {older, []inventory.Plan{newer}, "supersedes it"},
|
"a newer plan": {older, []inventory.Plan{newer}, "supersedes it"},
|
||||||
"superseded": {superseded, nil, "superseded"},
|
"superseded": {superseded, nil, "superseded"},
|
||||||
"done": {done, nil, "nothing to retry"},
|
"done": {done, nil, "nothing to retry"},
|
||||||
|
"gate failed": {gate, nil, "failed its gate"},
|
||||||
} {
|
} {
|
||||||
f := calm(now)
|
f := calm(now)
|
||||||
f.failed = []failedPlanFacts{failedFactsFor(c.p, c.others...)}
|
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
|
// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. **A module assigned and
|
||||||
// on the bus. Said to where the composition says its other lines (sayLeftOut).
|
// missing from the shelf is still assigned**: it is an assignment with no claims, so its holder is not taken for gone.
|
||||||
func theMeshPassesSeatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
|
func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment {
|
||||||
assigned map[string][]string, holdings []catalogue.Held) []catalogue.Held {
|
var out []catalogue.Assignment
|
||||||
var assignments []catalogue.Assignment
|
|
||||||
for node, modules := range assigned {
|
for node, modules := range assigned {
|
||||||
for _, module := range modules {
|
for _, module := range modules {
|
||||||
if m, ok := shelf[module]; ok {
|
m, ok := shelf[module]
|
||||||
assignments = append(assignments, catalogue.Assignment{Node: node, Manifest: m})
|
if !ok {
|
||||||
|
m = catalogue.Manifest{Module: module}
|
||||||
}
|
}
|
||||||
|
out = append(out, catalogue.Assignment{Node: node, Manifest: m})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var w io.Writer = os.Stderr
|
return out
|
||||||
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
|
}
|
||||||
w = lw
|
|
||||||
|
// 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,
|
say: w,
|
||||||
pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) {
|
pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) {
|
||||||
return inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To)
|
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
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"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())
|
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
|
// 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.
|
// reaches the bus.
|
||||||
func (i *Inventory) UseThrowawayChain() {
|
func (i *Inventory) UseThrowawayChain() {
|
||||||
|
i.MarkThrowaway()
|
||||||
i.UseChain(chain.InMemory(nil))
|
i.UseChain(chain.InMemory(nil))
|
||||||
i.markLaidOut()
|
i.markLaidOut()
|
||||||
}
|
}
|
||||||
@@ -129,3 +130,9 @@ func (i *Inventory) theChain(ctx context.Context) (*chain.Chain, error) {
|
|||||||
}
|
}
|
||||||
return c, nil
|
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
|
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, 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
|
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.
|
// Open connects to the inventory store.
|
||||||
|
|||||||
Reference in New Issue
Block a user