diff --git a/cmd/mesh-controller/gate.go b/cmd/mesh-controller/gate.go index 8d1e9f79..b4dd98e2 100644 --- a/cmd/mesh-controller/gate.go +++ b/cmd/mesh-controller/gate.go @@ -644,7 +644,14 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs p, s := gateWants(m, shelf[m]) wants, settles = max(wants, p), settles || s } + if len(modules) == 0 { + // A send judged on no machine of its modules (its first machine not running them) has no class to + // be judged by: it is judged as the core, never passed at once without a reading. + wants, settles = gatePasses, true + } g.Wants = wants + // A module's own pass is its own class's: a leaf beside a core module that fails keeps its build after + // its one reading, and only what did not pass is put back (issue 318's rule, by class since issue 524). passedAlone := func(m string) bool { p, s := gateWants(m, shelf[m]) return (settled || !s) && reading[m] <= healthPerson && g.Healthy[m] >= p diff --git a/cmd/mesh-controller/gate_test.go b/cmd/mesh-controller/gate_test.go index d25fe4b9..ced2ccce 100644 --- a/cmd/mesh-controller/gate_test.go +++ b/cmd/mesh-controller/gate_test.go @@ -300,6 +300,7 @@ func TestAModuleIsJudgedByItsClass(t *testing.T) { {"mesh-host", nil, 3, true}, {"node-tools", nil, 3, true}, {"nats", nil, 3, true}, + {catalogue.DeliverySeat, nil, 3, true}, {"dnsmasq", []catalogue.Claim{{Name: resolverSeat}}, 3, true}, } { p, s := gateWants(c.module, catalogue.Manifest{Module: c.module, Claims: c.claims}) diff --git a/cmd/mesh-controller/tier_send_test.go b/cmd/mesh-controller/tier_send_test.go index ba99dd49..c91a8fc0 100644 --- a/cmd/mesh-controller/tier_send_test.go +++ b/cmd/mesh-controller/tier_send_test.go @@ -439,3 +439,38 @@ func TestATierPutsABrokenModuleBackAtOnce(t *testing.T) { }) } } + +// **A send carrying a core module waits for the core's readings** (issue 524 review): the leaf beside the node +// tools has its one reading at once, but the send is judged as the core: not before the settle time, and three +// readings. +func TestASendCarryingTheCoreIsJudgedAsTheCore(t *testing.T) { + tm := aTierMesh(t, broker.RuntimeModule, "app1") + ctx := t.Context() + // One reading counted per advance, as every test with a gate left open does: a gate judged with no time + // between readings would count a pass at every look and never let an advance rest. + gateEvery, gateSettle = time.Hour, time.Hour + for i := 0; i < 6; i++ { + advancePlans(ctx, tm.open) + } + p := tm.plan(t) + lead := p.Modules[broker.RuntimeModule].Gate + if lead == nil { + lead = p.Modules["app1"].Gate + } + if !reflect.DeepEqual(tm.sent, [][]string{{"anchor"}}) || !p.Open() || lead == nil || lead.Verdict != "" || lead.Wants != gatePasses { + t.Fatalf("passed before the core's settle time: sent %v, plan %s (%s), gate %+v", tm.sent, p.State, p.Note, lead) + } + gateEvery, gateSettle = 0, 0 + for i := 0; i < 6; i++ { + advancePlans(ctx, tm.open) + } + p = tm.plan(t) + if p.State != inventory.PlanDone || !reflect.DeepEqual(tm.sent, [][]string{{"anchor"}, {"laptop"}}) { + t.Fatalf("the plan is %s (%s), sent %v", p.State, p.Note, tm.sent) + } + for _, m := range []string{broker.RuntimeModule, "app1"} { + if g := p.Modules[m].Gate; g != nil && (g.Verdict != inventory.GatePassed || g.Passes < gatePasses) { + t.Fatalf("%s's gate: %+v; want passed on %d readings", m, g, gatePasses) + } + } +}