Tell the operator at once when sends wait for the bus's planned step, and say it before the merge (hq issue 336)
For 28 minutes on 2026-10-08 every send to the control-node was refused for a new bus build that only a person's bus upgrade moves, and no condition said so: the refusal lived only in each walk's note, and S3 would have called it lateness after half an hour, in words that named neither the bus nor the verb. - Row S17, bus.<module>.step-waiting: raised on the first watchdog tick after a walk's send is refused for the bus, for the operator, naming the machines, what waits, the bus build from and to, since when and mesh-controller.bus upgrade. It clears once the bus's machine runs the build the mesh holds. - S3 leaves out a walk held only by the bus's step. - A change that builds the bus says in its delivery plan and summary (which mesh/merge-gate carries) that merging it needs a person's bus upgrade, and that nothing else reaches its machine until then. - TestReplay336 fails on the commit before and passes on this one.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// TestReplay336 replays novox/hq issue 336 (2026-10-08): a catalogue merge built a new bus build for the
|
||||
// control-node; from then on every send to that machine was refused for the bus's planned step, which only a
|
||||
// person starts, and for 28 minutes nothing but the walks' notes said so. The outcome asserted: the first
|
||||
// watchdog tick after the first refusal opens a condition for the operator that names what waits and the verb
|
||||
// that ends it, and it clears once the bus's machine runs the new build. Written with only what the controller
|
||||
// had before its fix — the stores, register, assign, a plan saved and advanced, the watchdogs' gathering and
|
||||
// seeing — so it is laid over the commit before.
|
||||
func TestReplay336(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
bus := catalogue.Manifest{Module: "nats", Version: "2", Provides: []catalogue.Offer{{Name: "mesh-bus"}}}
|
||||
if err := inv.RegisterModule(ctx, bus, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git",
|
||||
Path: "modules/nats", BuiltFrom: "32307bd1", Head: "32307bd1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, catalogue.Manifest{Module: "engine", Version: "1"})
|
||||
for _, m := range []string{"nats", "engine"} {
|
||||
if _, err := inv.Assign(ctx, "anchor", m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// The control-node runs the bus build it was last sent; the mesh holds a newer one, which waits for its step.
|
||||
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"nats": "88135ad0"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A walk of the engine, built, whose tier sends to the control-node.
|
||||
now := time.Now().UTC()
|
||||
plan := inventory.Plan{ID: "plan-engine", Repository: "novox/mesh-host", Commit: "e1e1e1e1", Created: now,
|
||||
State: inventory.PlanBuilding, Tiers: [][]string{{"engine"}},
|
||||
Modules: map[string]*inventory.PlanModule{"engine": {State: "built", BuiltAt: &now, Commit: "e1e1e1e1", Build: "b"}}}
|
||||
if err := inv.SavePlan(ctx, &plan); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
advancePlans(ctx, open)
|
||||
p, err := inv.PlanByID(ctx, "plan-engine")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(p.Note, errBusWaits.Error()) {
|
||||
t.Fatalf("the walk's send to the bus's machine was not refused for the bus: %s %q", p.State, p.Note)
|
||||
}
|
||||
|
||||
store := conditions.NewInMemory()
|
||||
k := conditions.NewKeeper(ctx, conditions.Options{Store: store, History: store, Teller: &conditions.Told{}})
|
||||
defer k.Close(context.Background())
|
||||
w := &watchdogs{open: open, keeper: k, started: now.Add(-time.Hour)}
|
||||
waiting := func() []conditions.Condition {
|
||||
t.Helper()
|
||||
w.see(ctx, w.gather(ctx))
|
||||
all, err := k.Open(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out []conditions.Condition
|
||||
for _, c := range all {
|
||||
if strings.HasPrefix(c.Key, "bus.") && c.Resolver == conditions.ResolverOperator {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
got := waiting()
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("the first tick after the refusal told the operator nothing about the bus's step: %d condition(s)", len(got))
|
||||
}
|
||||
for _, want := range []string{"anchor", "engine", "bus upgrade", "32307bd1"} {
|
||||
if !strings.Contains(got[0].Summary, want) {
|
||||
t.Errorf("the condition does not say %q: %s", want, got[0].Summary)
|
||||
}
|
||||
}
|
||||
// The step taken: the control-node is sent the new bus build, and the wait is over.
|
||||
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor-2", map[string]string{"nats": "32307bd1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := waiting(); len(got) != 0 {
|
||||
t.Fatalf("the bus's machine runs the new build, and the operator is still asked: %+v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user