Merge pull request 'Send a merge's builds to every machine at once, with no first-machine gate (hq ADR 0360)' (#332) from feat/lean-rollout into main
This commit was merged in pull request #332.
This commit is contained in:
@@ -125,6 +125,7 @@ func TestAWalksBuildSaysWhetherItReusedOrBuilt(t *testing.T) {
|
|||||||
// build as it judges one the walk built. A scratch reference is refused. Needs the store (MESH_TEST_POSTGRES), and
|
// build as it judges one the walk built. A scratch reference is refused. Needs the store (MESH_TEST_POSTGRES), and
|
||||||
// is named in testdata/store-tests.
|
// is named in testdata/store-tests.
|
||||||
func TestAReusedCheckBuildIsRegisteredByItsDigestAndGatedAsABuiltOne(t *testing.T) {
|
func TestAReusedCheckBuildIsRegisteredByItsDigestAndGatedAsABuiltOne(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate's own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
g := aGateMesh(t)
|
g := aGateMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := g.open.inventory
|
inv := g.open.inventory
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func foundDirectory(module string, since time.Time) inventory.ResourceHealth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestADirectoryFoundBeforeTheSendIsAWaitForAPerson(t *testing.T) {
|
func TestADirectoryFoundBeforeTheSendIsAWaitForAPerson(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
sent := now.Add(-time.Minute)
|
sent := now.Add(-time.Minute)
|
||||||
f := gateFacts{now: now, health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: now,
|
f := gateFacts{now: now, health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: now,
|
||||||
@@ -48,6 +49,7 @@ func TestADirectoryFoundBeforeTheSendIsAWaitForAPerson(t *testing.T) {
|
|||||||
// The whole walk: a module whose directory was used as found long before still gets its fix to every machine,
|
// The whole walk: a module whose directory was used as found long before still gets its fix to every machine,
|
||||||
// its pass kept and the wait said; a directory this very send found holds it and puts it back.
|
// its pass kept and the wait said; a directory this very send found holds it and puts it back.
|
||||||
func TestAFixGoesThroughPastADirectoryFoundBefore(t *testing.T) {
|
func TestAFixGoesThroughPastADirectoryFoundBefore(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
name string
|
name string
|
||||||
found time.Duration // when the directory was found, against now
|
found time.Duration // when the directory was found, against now
|
||||||
@@ -116,6 +118,7 @@ func TestAFixGoesThroughPastADirectoryFoundBefore(t *testing.T) {
|
|||||||
|
|
||||||
// The condition says the wait in its own kind: the operator's, never urgent, and cleared once handed over.
|
// The condition says the wait in its own kind: the operator's, never urgent, and cleared once handed over.
|
||||||
func TestADirectoryUsedAsFoundIsItsOwnCondition(t *testing.T) {
|
func TestADirectoryUsedAsFoundIsItsOwnCondition(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
k, _ := withConditionsInMemory(t)
|
k, _ := withConditionsInMemory(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
rs := map[string][]inventory.ResourceHealth{"notes": {foundDirectory("notes", time.Now().Add(-time.Hour))}}
|
rs := map[string][]inventory.ResourceHealth{"notes": {foundDirectory("notes", time.Now().Add(-time.Hour))}}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func healthyContainer(module string) inventory.ResourceHealth {
|
|||||||
// tools' witness put its build back at once, and the send waits for the healthy module's own verdict — a pass,
|
// tools' witness put its build back at once, and the send waits for the healthy module's own verdict — a pass,
|
||||||
// counted over the gate's spacing — before it says its own. The broken one alone is put back.
|
// counted over the gate's spacing — before it says its own. The broken one alone is put back.
|
||||||
func TestAFaultBeforeTheSettleTimeWaitsForTheModuleBesideIt(t *testing.T) {
|
func TestAFaultBeforeTheSettleTimeWaitsForTheModuleBesideIt(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -99,6 +100,7 @@ const broker_runtime = "node-tools"
|
|||||||
// **A pass is counted only the gate's spacing after the one before** (review (c)): a send judged every tick
|
// **A pass is counted only the gate's spacing after the one before** (review (c)): a send judged every tick
|
||||||
// while a module beside it is not yet healthy counts the healthy one once.
|
// while a module beside it is not yet healthy counts the healthy one once.
|
||||||
func TestAPassIsCountedNoFasterThanTheGatesSpacing(t *testing.T) {
|
func TestAPassIsCountedNoFasterThanTheGatesSpacing(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
withGateBounds(t, time.Hour, time.Hour, time.Hour)
|
withGateBounds(t, time.Hour, time.Hour, time.Hour)
|
||||||
@@ -123,6 +125,7 @@ func TestAPassIsCountedNoFasterThanTheGatesSpacing(t *testing.T) {
|
|||||||
|
|
||||||
// **A carried build's pass carries its own wait, never another's** (review (e)).
|
// **A carried build's pass carries its own wait, never another's** (review (e)).
|
||||||
func TestACarriedPassCarriesOnlyItsOwnWait(t *testing.T) {
|
func TestACarriedPassCarriesOnlyItsOwnWait(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := b.open.inventory
|
inv := b.open.inventory
|
||||||
@@ -147,6 +150,7 @@ func TestACarriedPassCarriesOnlyItsOwnWait(t *testing.T) {
|
|||||||
// carried its own module, healthy, and a build waiting on that machine that never became healthy. At the
|
// carried its own module, healthy, and a build waiting on that machine that never became healthy. At the
|
||||||
// bound the waiting one is put back and marked; the plan's own module keeps its pass, so no walk waits on it.
|
// bound the waiting one is put back and marked; the plan's own module keeps its pass, so no walk waits on it.
|
||||||
func TestThePlansOwnModuleKeepsItsPassWhenItsSendFails(t *testing.T) {
|
func TestThePlansOwnModuleKeepsItsPassWhenItsSendFails(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
g := aGateMesh(t)
|
g := aGateMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := g.open.inventory
|
inv := g.open.inventory
|
||||||
@@ -227,6 +231,7 @@ func TestThePlansOwnModuleKeepsItsPassWhenItsSendFails(t *testing.T) {
|
|||||||
// **A provider waiting for a login says who waits on it** (review (g)): its consumer, failing a check that
|
// **A provider waiting for a login says who waits on it** (review (g)): its consumer, failing a check that
|
||||||
// needs it, is held under it, and the provider's relogin-needed condition lists it and is urgent.
|
// needs it, is held under it, and the provider's relogin-needed condition lists it and is urgent.
|
||||||
func TestAProviderWaitingForALoginSaysWhoWaitsOnIt(t *testing.T) {
|
func TestAProviderWaitingForALoginSaysWhoWaitsOnIt(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
@@ -289,6 +294,7 @@ func TestAProviderWaitingForALoginSaysWhoWaitsOnIt(t *testing.T) {
|
|||||||
// tools are marked and put back in the very judging that found them broken — their registered build is the
|
// tools are marked and put back in the very judging that found them broken — their registered build is the
|
||||||
// one before, and the laptop is sent it — while `app` goes on being judged, recovers, and keeps its own pass.
|
// one before, and the laptop is sent it — while `app` goes on being judged, recovers, and keeps its own pass.
|
||||||
func TestABrokenModuleIsPutBackAtOnceAndTheOneBesideItGetsItsOwnVerdict(t *testing.T) {
|
func TestABrokenModuleIsPutBackAtOnceAndTheOneBesideItGetsItsOwnVerdict(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -431,6 +437,7 @@ func TestABrokenModuleIsPutBackAtOnceAndTheOneBesideItGetsItsOwnVerdict(t *testi
|
|||||||
// **A move from a build not known excuses no wait** (issue 318 review): a plan's own module whose previous
|
// **A move from a build not known excuses no wait** (issue 318 review): a plan's own module whose previous
|
||||||
// build was not recorded, or whose previous manifest is not kept, cannot be shown to have added the group.
|
// build was not recorded, or whose previous manifest is not kept, cannot be shown to have added the group.
|
||||||
func TestAMoveFromAnUnknownBuildExcusesNoWait(t *testing.T) {
|
func TestAMoveFromAnUnknownBuildExcusesNoWait(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ func aGateMesh(t *testing.T) *gateMesh {
|
|||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
g := &gateMesh{open: open, health: map[string]health{}}
|
g := &gateMesh{open: open, health: map[string]health{}}
|
||||||
|
// The gate's own tests judge a first machine, as walks a controller before novox/hq ADR 0360 started still are.
|
||||||
|
wasGated := gateFirstMachine
|
||||||
|
gateFirstMachine = true
|
||||||
|
t.Cleanup(func() { gateFirstMachine = wasGated })
|
||||||
g.keeper, _ = withConditionsInMemory(t)
|
g.keeper, _ = withConditionsInMemory(t)
|
||||||
g.told = &conditions.Told{}
|
g.told = &conditions.Told{}
|
||||||
was := doctorFrom
|
was := doctorFrom
|
||||||
@@ -271,6 +275,34 @@ func TestABuildThatPassesItsGateRollsEverywhereUnattended(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A build rolls everywhere with no hand and no gate (novox/hq ADR 0360 §1): both machines in one send, no health
|
||||||
|
// judged, and the walk done once both reported on the send. Before, anchor was judged first, then laptop sent.
|
||||||
|
func TestABuildRollsToEveryMachineAtOnceWithNoGate(t *testing.T) {
|
||||||
|
g := aGateMesh(t)
|
||||||
|
gateFirstMachine = false
|
||||||
|
ctx := t.Context()
|
||||||
|
judged := false
|
||||||
|
wasGather := gatherGateFacts
|
||||||
|
gatherGateFacts = func(ctx context.Context, open *stores, component string) (gateFacts, error) {
|
||||||
|
judged = true
|
||||||
|
return wasGather(ctx, open, component)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { gatherGateFacts = wasGather })
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
advancePlans(ctx, g.open)
|
||||||
|
}
|
||||||
|
p := g.plan(t)
|
||||||
|
if !reflect.DeepEqual(g.sent, [][]string{{"anchor", "laptop"}}) {
|
||||||
|
t.Fatalf("sent %v, not both machines in one send", g.sent)
|
||||||
|
}
|
||||||
|
if s := p.Modules["app"]; p.State != inventory.PlanDone || s.Gate != nil || s.FirstAt != nil || judged {
|
||||||
|
t.Fatalf("the plan is %s (%s), app %+v, judged %v", p.State, p.Note, s, judged)
|
||||||
|
}
|
||||||
|
if obs, err := probeGates(ctx, doctorFrom); err != nil || len(obs) != 0 {
|
||||||
|
t.Fatalf("a rolled build left a condition: %+v %v", obs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A leaf module passes its gate on one reading, as soon as its first machine is healthy, without waiting out the
|
// A leaf module passes its gate on one reading, as soon as its first machine is healthy, without waiting out the
|
||||||
// core's settle time (novox/hq ADR 0282 decision 4 as ADR 0304 decision 3 changed it, issue 524).
|
// core's settle time (novox/hq ADR 0282 decision 4 as ADR 0304 decision 3 changed it, issue 524).
|
||||||
func TestALeafModulePassesOnOneHealthyReading(t *testing.T) {
|
func TestALeafModulePassesOnOneHealthyReading(t *testing.T) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
// another send reaches anchor, unreported; the gate still passes. And a send that moves a judged module
|
// another send reaches anchor, unreported; the gate still passes. And a send that moves a judged module
|
||||||
// to another build supersedes the judging: no verdict, nothing put back.
|
// to another build supersedes the judging: no verdict, nothing put back.
|
||||||
func TestReplay352(t *testing.T) {
|
func TestReplay352(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
t.Run("a newer send to the judged machine does not unreport the gate's", testANewerSendDoesNotUnreportTheGatesOwn)
|
t.Run("a newer send to the judged machine does not unreport the gate's", testANewerSendDoesNotUnreportTheGatesOwn)
|
||||||
t.Run("a send that moves the module supersedes the judging", testASendThatMovesTheModuleSupersedesTheJudging)
|
t.Run("a send that moves the module supersedes the judging", testASendThatMovesTheModuleSupersedesTheJudging)
|
||||||
@@ -67,6 +68,7 @@ func testANewerSendDoesNotUnreportTheGatesOwn(t *testing.T) {
|
|||||||
|
|
||||||
// A plan's own first send waits while a release judges the same module on that machine with another build.
|
// A plan's own first send waits while a release judges the same module on that machine with another build.
|
||||||
func TestAPlansFirstSendWaitsForAReleaseJudgingTheModuleThere(t *testing.T) {
|
func TestAPlansFirstSendWaitsForAReleaseJudgingTheModuleThere(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -82,6 +84,7 @@ func TestAPlansFirstSendWaitsForAReleaseJudgingTheModuleThere(t *testing.T) {
|
|||||||
|
|
||||||
// A merge plan's judging is superseded the same way: another send moved its module on the first machine.
|
// A merge plan's judging is superseded the same way: another send moved its module on the first machine.
|
||||||
func TestAPlansJudgingIsSupersededByASendThatMovesItsModule(t *testing.T) {
|
func TestAPlansJudgingIsSupersededByASendThatMovesItsModule(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
g := aGateMesh(t)
|
g := aGateMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -140,6 +143,7 @@ func testASendThatMovesTheModuleSupersedesTheJudging(t *testing.T) {
|
|||||||
// A report is on the gate's own send: the declaration itself, or one sequenced after it; a gate kept
|
// A report is on the gate's own send: the declaration itself, or one sequenced after it; a gate kept
|
||||||
// without its send reads the report against the send made last, as before. Pure.
|
// without its send reads the report against the send made last, as before. Pure.
|
||||||
func TestAReportIsHeldAgainstTheGatesOwnSend(t *testing.T) {
|
func TestAReportIsHeldAgainstTheGatesOwnSend(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
sent := inventory.SentDeclaration{Digest: "d-490", Sequence: 490}
|
sent := inventory.SentDeclaration{Digest: "d-490", Sequence: 490}
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
r inventory.Reported
|
r inventory.Reported
|
||||||
@@ -185,6 +189,7 @@ func TestAReportIsHeldAgainstTheGatesOwnSend(t *testing.T) {
|
|||||||
// A gate judges only the build the machine was last sent: last sent another build of the module, the
|
// A gate judges only the build the machine was last sent: last sent another build of the module, the
|
||||||
// judging is superseded, whatever the machine reports. Pure.
|
// judging is superseded, whatever the machine reports. Pure.
|
||||||
func TestAGateJudgesOnlyTheBuildTheMachineWasLastSent(t *testing.T) {
|
func TestAGateJudgesOnlyTheBuildTheMachineWasLastSent(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
at := time.Now()
|
at := time.Now()
|
||||||
f := gateFacts{now: at, reports: map[string]inventory.Reported{"anchor": {Node: "anchor", Outcome: inventory.OutcomeApplied,
|
f := gateFacts{now: at, reports: map[string]inventory.Reported{"anchor": {Node: "anchor", Outcome: inventory.OutcomeApplied,
|
||||||
At: &at, Current: true}}, engines: map[string]string{}, served: map[string]served{}, rolledBack: map[string][]lease.Rollback{},
|
At: &at, Current: true}}, engines: map[string]string{}, served: map[string]served{}, rolledBack: map[string][]lease.Rollback{},
|
||||||
@@ -212,6 +217,7 @@ func TestAGateJudgesOnlyTheBuildTheMachineWasLastSent(t *testing.T) {
|
|||||||
// A move of another build of a module to a machine where a release or a plan is judging that module
|
// A move of another build of a module to a machine where a release or a plan is judging that module
|
||||||
// waits for that judging; the same build to that machine is already there. Pure.
|
// waits for that judging; the same build to that machine is already there. Pure.
|
||||||
func TestAWalkWaitsForAJudgingOfTheSameModuleOnThatMachine(t *testing.T) {
|
func TestAWalkWaitsForAJudgingOfTheSameModuleOnThatMachine(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
at := time.Now()
|
at := time.Now()
|
||||||
release := inventory.Plan{ID: "release-1", State: inventory.PlanRolling, Release: &inventory.PlanRelease{
|
release := inventory.Plan{ID: "release-1", State: inventory.PlanRolling, Release: &inventory.PlanRelease{
|
||||||
Gate: &inventory.PlanGate{Machines: []string{"novox"}, Carried: []inventory.CarriedMove{
|
Gate: &inventory.PlanGate{Machines: []string{"novox"}, Carried: []inventory.CarriedMove{
|
||||||
@@ -249,6 +255,7 @@ func TestAWalkWaitsForAJudgingOfTheSameModuleOnThatMachine(t *testing.T) {
|
|||||||
// The controller is never put back to a build that reaches less of the store's schema than the store
|
// The controller is never put back to a build that reaches less of the store's schema than the store
|
||||||
// has, or to one that never said what it reaches: the current build is kept, and the condition says so.
|
// has, or to one that never said what it reaches: the current build is kept, and the condition says so.
|
||||||
func TestTheControllerIsNotPutBackToABuildOlderThanTheStoresSchema(t *testing.T) {
|
func TestTheControllerIsNotPutBackToABuildOlderThanTheStoresSchema(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
@@ -345,6 +352,7 @@ func TestTheControllerIsNotPutBackToABuildOlderThanTheStoresSchema(t *testing.T)
|
|||||||
|
|
||||||
// The lease's holder names the build the declaration told it it is, and the version stamp only without one.
|
// The lease's holder names the build the declaration told it it is, and the version stamp only without one.
|
||||||
func TestTheHolderNamesTheBuildTheDeclarationToldIt(t *testing.T) {
|
func TestTheHolderNamesTheBuildTheDeclarationToldIt(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
t.Setenv(RunningBuildVar, " ad62528c47c7 ")
|
t.Setenv(RunningBuildVar, " ad62528c47c7 ")
|
||||||
if h := holderOf("x"); h.Build != "ad62528c47c7" {
|
if h := holderOf("x"); h.Build != "ad62528c47c7" {
|
||||||
t.Fatalf("the holder's build is %q", h.Build)
|
t.Fatalf("the holder's build is %q", h.Build)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func TestANoMoveTierWaitsForNoNewReport(t *testing.T) {
|
|||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := g.open.inventory
|
inv := g.open.inventory
|
||||||
|
|
||||||
// app is the build machine, so a module of the next tier is built by it: the tier gates the next on app
|
// app holds the build seat, so a module of the next tier is built by it; since novox/hq ADR 0360 §3 that gates
|
||||||
// running on its machines.
|
// nothing.
|
||||||
for _, m := range []catalogue.Manifest{
|
for _, m := range []catalogue.Manifest{
|
||||||
{Module: "app", Version: "c2", Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}},
|
{Module: "app", Version: "c2", Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}},
|
||||||
{Module: "web", Version: "w1"},
|
{Module: "web", Version: "w1"},
|
||||||
@@ -71,7 +71,8 @@ func TestANoMoveTierWaitsForNoNewReport(t *testing.T) {
|
|||||||
t.Fatalf("the walk waits on a no-move tier for a report nothing will cause: tier %d, %s %q", p.Tier, p.State, p.Note)
|
t.Fatalf("the walk waits on a no-move tier for a report nothing will cause: tier %d, %s %q", p.Tier, p.State, p.Note)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A machine that has not answered its last send is still waited on: no move is not "never sent".
|
// A machine that has not answered its last send is not waited on either: a built-by edge gates no tier
|
||||||
|
// (novox/hq ADR 0360 §3). Before, the walk waited for app on laptop.
|
||||||
p.Tier, p.State, p.Note = 0, inventory.PlanRolling, ""
|
p.Tier, p.State, p.Note = 0, inventory.PlanRolling, ""
|
||||||
if err := inv.RecordSent(ctx, nodeID(t, g.open, "laptop"), "d-laptop-later", map[string]string{"app": "c1"}); err != nil {
|
if err := inv.RecordSent(ctx, nodeID(t, g.open, "laptop"), "d-laptop-later", map[string]string{"app": "c1"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -81,7 +82,7 @@ func TestANoMoveTierWaitsForNoNewReport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
advancePlans(ctx, g.open)
|
advancePlans(ctx, g.open)
|
||||||
p = g.plan(t)
|
p = g.plan(t)
|
||||||
if p.Tier != 0 || !strings.Contains(p.Note, "waiting for app on laptop") {
|
if p.Tier < 1 || strings.Contains(p.Note, "waiting for app") {
|
||||||
t.Fatalf("a machine that has not answered its last send was not waited on: tier %d, %q", p.Tier, p.Note)
|
t.Fatalf("the walk waits on a built-by edge: tier %d, %q", p.Tier, p.Note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq ADR 0360 §4, §5: a walk ends on its machines' reports — every machine sent has reported on its send,
|
||||||
|
// or is silent past ApplySilentAfter — and names a failed or refused apply; it never waits for a health reading.
|
||||||
|
func TestAWalkEndsOnItsMachinesReports(t *testing.T) {
|
||||||
|
now := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
|
||||||
|
sent := now.Add(-time.Minute)
|
||||||
|
long := now.Add(-inventory.ApplySilentAfter - time.Minute)
|
||||||
|
at := now.Add(-10 * time.Second)
|
||||||
|
p := inventory.Plan{Modules: map[string]*inventory.PlanModule{
|
||||||
|
"app": {Rest: map[string]inventory.SentDeclaration{
|
||||||
|
"ace": {Digest: "d-ace", At: sent},
|
||||||
|
"novox": {Digest: "d-novox", At: sent},
|
||||||
|
"shanks": {Digest: "d-shanks", At: sent},
|
||||||
|
"laptop": {Digest: "d-laptop", At: long},
|
||||||
|
}},
|
||||||
|
"proxy": {Rest: map[string]inventory.SentDeclaration{"ace": {Digest: "d-ace", At: sent}}},
|
||||||
|
}}
|
||||||
|
reports := []inventory.Reported{
|
||||||
|
{Node: "ace", Outcome: inventory.OutcomeApplied, At: &at, Declared: "d-ace"},
|
||||||
|
{Node: "novox", Outcome: inventory.OutcomeFailed, At: &at, Declared: "d-novox"},
|
||||||
|
{Node: "shanks", Outcome: inventory.OutcomeApplied, At: &at, Declared: "d-before"},
|
||||||
|
}
|
||||||
|
waiting, failed, silent := walkReports(p, reports, now)
|
||||||
|
if !slices.Equal(waiting, []string{"shanks"}) || !slices.Equal(failed, []string{"novox"}) ||
|
||||||
|
!slices.Equal(silent, []string{"laptop"}) {
|
||||||
|
t.Fatalf("waiting %v, failed %v, silent %v", waiting, failed, silent)
|
||||||
|
}
|
||||||
|
reports[2].Declared = "d-shanks"
|
||||||
|
if waiting, _, _ := walkReports(p, reports, now); len(waiting) != 0 {
|
||||||
|
t.Fatalf("the walk still waits once every machine reported: %v", waiting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withTheGate turns the first-machine gate on for a test of its own path: kept for the walks a controller before
|
||||||
|
// novox/hq ADR 0360 started.
|
||||||
|
func withTheGate(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
was := gateFirstMachine
|
||||||
|
gateFirstMachine = true
|
||||||
|
t.Cleanup(func() { gateFirstMachine = was })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every machine heard from is sent at once and passed on its applied report, with no judging (novox/hq ADR 0360 §7);
|
||||||
|
// before, the first machine was judged before the rest were sent (ADR 0353 §1).
|
||||||
|
func TestAReleaseSendsEveryMachineAtOnce(t *testing.T) {
|
||||||
|
judgeLeavesAsTheCore(t)
|
||||||
|
b := aBacklogOnThree(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
said := events(t)
|
||||||
|
for i := 0; i < 12; i++ {
|
||||||
|
advancePlans(ctx, b.open)
|
||||||
|
}
|
||||||
|
p := b.release(t)
|
||||||
|
if p.State != inventory.PlanDone || len(p.Release.Done) != 3 {
|
||||||
|
t.Fatalf("the release is %s: %s %+v", p.State, p.Note, p.Release)
|
||||||
|
}
|
||||||
|
for _, n := range []string{"desk", "laptop", "anchor"} {
|
||||||
|
if !slices.Contains(*said, "send "+n) {
|
||||||
|
t.Fatalf("%s was not sent: %v", n, *said)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(*said, "judge") {
|
||||||
|
t.Fatalf("a machine was judged by its health: %v", *said)
|
||||||
|
}
|
||||||
|
if v, found, err := b.open.inventory.GateOf(ctx, "build-late-c2"); err != nil || !found || v.Verdict != inventory.GatePassed {
|
||||||
|
t.Fatalf("late's pass on laptop was not kept: %+v %v %v", v, found, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A machine that reports its apply failed is named and fails the release, with nothing put back, and every other
|
||||||
|
// machine goes on to its own report (novox/hq ADR 0360 §5, §7); before, it was put back (ADR 0353 §3).
|
||||||
|
func TestAMachineThatFailsItsApplyIsNamedAndNothingIsPutBack(t *testing.T) {
|
||||||
|
judgeLeavesAsTheCore(t)
|
||||||
|
b := aBacklogOnThree(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := b.open.inventory
|
||||||
|
b.broken["desk"] = true // desk fails what it is sent
|
||||||
|
advancePlans(ctx, b.open)
|
||||||
|
for i := 0; i < 12 && b.release(t).Open(); i++ {
|
||||||
|
advancePlans(ctx, b.open)
|
||||||
|
}
|
||||||
|
p := b.release(t)
|
||||||
|
if p.State != inventory.PlanFailed || !slices.Equal(p.Release.Failed, []string{"desk"}) {
|
||||||
|
t.Fatalf("the release is %s: %s %+v", p.State, p.Note, p.Release)
|
||||||
|
}
|
||||||
|
if !slices.Contains(p.Release.Done, "laptop") || !slices.Contains(p.Release.Done, "anchor") {
|
||||||
|
t.Fatalf("the others have no verdict of their own: %+v", p.Release)
|
||||||
|
}
|
||||||
|
if v, found, err := inv.GateOf(ctx, "build-late-c2"); err != nil || !found || v.Verdict != inventory.GatePassed {
|
||||||
|
t.Fatalf("late's pass on laptop was not kept: %+v %v %v", v, found, err)
|
||||||
|
}
|
||||||
|
if sent, _, _ := inv.SentBuilds(ctx, "desk"); sent["app"] != "c2" {
|
||||||
|
t.Fatalf("desk was put back: %v", sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A release's send kept before it was made, and made, is judged by the controller that takes over without being
|
||||||
|
// made again (ADR 0353 §8), with every machine sent at once (ADR 0360 §7).
|
||||||
|
func TestAReleaseSendKeptAndMadeIsNotMadeAgainAtOnce(t *testing.T) {
|
||||||
|
judgeLeavesAsTheCore(t)
|
||||||
|
b := aBacklog(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := b.open.inventory
|
||||||
|
p, err := releaseBacklog(ctx, b.open, "")
|
||||||
|
if err != nil || p == nil {
|
||||||
|
t.Fatalf("no release opened: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := advanceRelease(ctx, b.open, p); err != nil || p.Release.Gates["anchor"] == nil || !p.Release.Gates["anchor"].Pending {
|
||||||
|
t.Fatalf("the send was not kept before it was made: %+v %v", p.Release, err)
|
||||||
|
}
|
||||||
|
if err := inv.SavePlan(ctx, p); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// The send is made, and the controller is replaced before it keeps it.
|
||||||
|
if _, err := sendRollout(ctx, b.open, []string{"anchor"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
advancePlans(ctx, b.open)
|
||||||
|
if n := len(slices.DeleteFunc(slices.Clone(b.sent), func(s []string) bool { return !slices.Equal(s, []string{"anchor"}) })); n != 1 {
|
||||||
|
t.Fatalf("anchor was sent again: %v", b.sent)
|
||||||
|
}
|
||||||
|
g := b.release(t).Release.Gates["anchor"]
|
||||||
|
if g == nil || g.Pending || g.Since == nil || len(g.Carried) == 0 {
|
||||||
|
t.Fatalf("anchor is not judged on the send made: %+v", g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A machine of the rest that says nothing on its send is left silent after fifteen minutes: not failed, not put
|
||||||
|
// back; a rest that went silent whole ends failed, said as a condition (ADR 0353 §4); its step also sent anchor, so the release is done, not failed (ADR 0360 §7).
|
||||||
|
func TestAMachineSilentPastItsBoundIsLeftAndSaidAtOnce(t *testing.T) {
|
||||||
|
judgeLeavesAsTheCore(t)
|
||||||
|
b := aBacklog(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := b.open.inventory
|
||||||
|
reporting := sendRollout
|
||||||
|
sendRollout = func(ctx context.Context, open *stores, names []string) ([]string, error) {
|
||||||
|
if slices.Equal(names, []string{"laptop"}) { // laptop sleeps: sent, and says nothing
|
||||||
|
b.sent = append(b.sent, names)
|
||||||
|
current, _ := open.inventory.CurrentBuilds(ctx)
|
||||||
|
return names, open.inventory.RecordSent(ctx, nodeID(t, open, "laptop"), "d-laptop-asleep",
|
||||||
|
map[string]string{"app": current["app"].Commit, "late": current["late"].Commit, "same": current["same"].Commit})
|
||||||
|
}
|
||||||
|
return reporting(ctx, open, names)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { sendRollout = reporting })
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
advancePlans(ctx, b.open)
|
||||||
|
}
|
||||||
|
p := b.release(t)
|
||||||
|
g := p.Release.Gates["laptop"]
|
||||||
|
if !p.Open() || g == nil || g.Verdict != "" || g.Silent {
|
||||||
|
t.Fatalf("laptop is judged before fifteen minutes: %s %s %+v", p.State, p.Note, g)
|
||||||
|
}
|
||||||
|
since := time.Now().Add(-inventory.ApplySilentAfter - time.Minute)
|
||||||
|
g.Since = &since
|
||||||
|
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
advancePlans(ctx, b.open)
|
||||||
|
p = b.release(t)
|
||||||
|
// anchor applied in the same step, so the release is done, laptop named silent (novox/hq ADR 0360 §7).
|
||||||
|
if p.State != inventory.PlanDone || !slices.Equal(p.Release.Silent, []string{"laptop"}) || len(p.Release.Failed) > 0 {
|
||||||
|
t.Fatalf("the release is %s: %s %+v", p.State, p.Note, p.Release)
|
||||||
|
}
|
||||||
|
if failed, _ := inv.GateFailed(ctx, "build-late-c2"); failed {
|
||||||
|
t.Fatal("a silent machine's build was marked failed")
|
||||||
|
}
|
||||||
|
if sent, _, _ := inv.SentBuilds(ctx, "laptop"); sent["late"] != "c2" {
|
||||||
|
t.Fatalf("laptop was put back: %v", sent)
|
||||||
|
}
|
||||||
|
obs, err := silentObservation(ctx, inv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(obs) != 1 || obs[0].Kind != "release-silent" || !slices.Equal(obs[0].Also, []string{"laptop"}) {
|
||||||
|
t.Fatalf("the silence is not said: %+v", obs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ func userUnit(module, account string) inventory.ResourceHealth {
|
|||||||
// Only what depends on the new login alone is a wait; anything else unhealthy of the module is judged as
|
// Only what depends on the new login alone is a wait; anything else unhealthy of the module is judged as
|
||||||
// before, so that a fault is never excused.
|
// before, so that a fault is never excused.
|
||||||
func TestOnlyWhatDependsOnTheNewLoginIsAWait(t *testing.T) {
|
func TestOnlyWhatDependsOnTheNewLoginIsAWait(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
notInGroup := relogin("lights", "operator")
|
notInGroup := relogin("lights", "operator")
|
||||||
notInGroup.Reason = "not in the group lights: the apply has not put operator there; its outcome says why"
|
notInGroup.Reason = "not in the group lights: the apply has not put operator there; its outcome says why"
|
||||||
systemUnit := userUnit("lights", "operator")
|
systemUnit := userUnit("lights", "operator")
|
||||||
@@ -74,6 +75,7 @@ func TestOnlyWhatDependsOnTheNewLoginIsAWait(t *testing.T) {
|
|||||||
// The gate reads a wait for a person as its own reading, never a fault, and passes with it; the same module
|
// The gate reads a wait for a person as its own reading, never a fault, and passes with it; the same module
|
||||||
// with a container down beside it is not yet healthy, as before.
|
// with a container down beside it is not yet healthy, as before.
|
||||||
func TestAWaitForAPersonIsAPassCarriedAlong(t *testing.T) {
|
func TestAWaitForAPersonIsAPassCarriedAlong(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
since := now.Add(-time.Minute)
|
since := now.Add(-time.Minute)
|
||||||
account := relogin("lights", "operator")
|
account := relogin("lights", "operator")
|
||||||
@@ -102,6 +104,7 @@ func TestAWaitForAPersonIsAPassCarriedAlong(t *testing.T) {
|
|||||||
// The module's condition says the wait in one sentence for a person, and clears on the first statement that
|
// The module's condition says the wait in one sentence for a person, and clears on the first statement that
|
||||||
// no longer says it — said as the module's own fault when its unit still fails after the new login.
|
// no longer says it — said as the module's own fault when its unit still fails after the new login.
|
||||||
func TestTheReloginConditionSaysTheWaitAndClearsAfterTheLogin(t *testing.T) {
|
func TestTheReloginConditionSaysTheWaitAndClearsAfterTheLogin(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
k, _ := withConditionsInMemory(t)
|
k, _ := withConditionsInMemory(t)
|
||||||
@@ -158,6 +161,7 @@ func TestTheReloginConditionSaysTheWaitAndClearsAfterTheLogin(t *testing.T) {
|
|||||||
// A module that is healthy on its own keeps its pass when another module of the same send fails at the
|
// A module that is healthy on its own keeps its pass when another module of the same send fails at the
|
||||||
// bound: it is neither put back nor left without a verdict for every other walk to wait on.
|
// bound: it is neither put back nor left without a verdict for every other walk to wait on.
|
||||||
func TestAModuleHealthyOnItsOwnKeepsItsPassWhenItsSendFails(t *testing.T) {
|
func TestAModuleHealthyOnItsOwnKeepsItsPassWhenItsSendFails(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := b.open.inventory
|
inv := b.open.inventory
|
||||||
@@ -207,6 +211,7 @@ func TestAModuleHealthyOnItsOwnKeepsItsPassWhenItsSendFails(t *testing.T) {
|
|||||||
// What the operator reads of the wait (ADR 0253, ADR 0254): it needs them, so it is never quiet, and it offers
|
// What the operator reads of the wait (ADR 0253, ADR 0254): it needs them, so it is never quiet, and it offers
|
||||||
// no button, since nothing but their own new login can do it.
|
// no button, since nothing but their own new login can do it.
|
||||||
func TestTheReloginConditionNeedsTheOperatorAndOffersNoButton(t *testing.T) {
|
func TestTheReloginConditionNeedsTheOperatorAndOffersNoButton(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
o := reloginObservation("openrazer", "g14", "relogin needed on g14: …", "operator", []inventory.ResourceHealth{
|
o := reloginObservation("openrazer", "g14", "relogin needed on g14: …", "operator", []inventory.ResourceHealth{
|
||||||
relogin("openrazer", "operator"), userUnit("openrazer", "operator")})
|
relogin("openrazer", "operator"), userUnit("openrazer", "operator")})
|
||||||
plainExample(t, o, "openrazer waits for a new login on g14",
|
plainExample(t, o, "openrazer waits for a new login on g14",
|
||||||
@@ -238,6 +243,7 @@ func TestTheReloginConditionNeedsTheOperatorAndOffersNoButton(t *testing.T) {
|
|||||||
// When a module not working turns into one waiting for a new login, and back, the clearing line says what it
|
// When a module not working turns into one waiting for a new login, and back, the clearing line says what it
|
||||||
// became, never that it works again (issue 318 review).
|
// became, never that it works again (issue 318 review).
|
||||||
func TestAConditionThatBecameTheOtherKindSaysSoWhenItClears(t *testing.T) {
|
func TestAConditionThatBecameTheOtherKindSaysSoWhenItClears(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
store := conditions.NewInMemory()
|
store := conditions.NewInMemory()
|
||||||
@@ -286,6 +292,7 @@ func TestAConditionThatBecameTheOtherKindSaysSoWhenItClears(t *testing.T) {
|
|||||||
|
|
||||||
// Which moves add an account group, read from the builds' manifests (issue 318 review).
|
// Which moves add an account group, read from the builds' manifests (issue 318 review).
|
||||||
func TestOnlyAMoveThatAddsAnAccountGroupCanBringAWait(t *testing.T) {
|
func TestOnlyAMoveThatAddsAnAccountGroupCanBringAWait(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
user := func(groups ...any) catalogue.Manifest {
|
user := func(groups ...any) catalogue.Manifest {
|
||||||
return catalogue.Manifest{Module: "lights", Resources: []map[string]any{{"id": "operator", "type": "user",
|
return catalogue.Manifest{Module: "lights", Resources: []map[string]any{{"id": "operator", "type": "user",
|
||||||
"name": "operator", "groups": groups}}}
|
"name": "operator", "groups": groups}}}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
|
|||||||
{"the route proxy's program: the route proxy alone", "mesh-controller",
|
{"the route proxy's program: the route proxy alone", "mesh-controller",
|
||||||
[]string{"examples/route-proxy/main.go"}, "route-proxy", true},
|
[]string{"examples/route-proxy/main.go"}, "route-proxy", true},
|
||||||
{"a package all three build from: all three", "mesh-controller",
|
{"a package all three build from: all three", "mesh-controller",
|
||||||
[]string{"internal/broker/broker.go"}, "mesh-controller | build-agent | route-proxy", false},
|
[]string{"internal/broker/broker.go"}, "mesh-controller,route-proxy | build-agent", false},
|
||||||
{"the route proxy's directory in the catalogue: it alone", "mesh-catalog",
|
{"the route proxy's directory in the catalogue: it alone", "mesh-catalog",
|
||||||
[]string{"modules/route-proxy/module.json"}, "route-proxy", false},
|
[]string{"modules/route-proxy/module.json"}, "route-proxy", false},
|
||||||
{"the build agent's directory: it alone, nothing it builds", "mesh-catalog",
|
{"the build agent's directory: it alone, nothing it builds", "mesh-catalog",
|
||||||
|
|||||||
@@ -145,8 +145,9 @@ func TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt(t *testing.T) {
|
|||||||
repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"},
|
repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"},
|
||||||
{what: "built-by never widens", edges: []inventory.Edge{dep("x", builtBy, "a")},
|
{what: "built-by never widens", edges: []inventory.Edge{dep("x", builtBy, "a")},
|
||||||
repo: "one", paths: []string{"modules/a/x"}, want: "a"},
|
repo: "one", paths: []string{"modules/a/x"}, want: "a"},
|
||||||
{what: "built-by orders", edges: []inventory.Edge{dep("x", builtBy, "a")},
|
// Whichever builder runs builds it (novox/hq ADR 0360 §3): built-by orders nothing, as before it ordered.
|
||||||
repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a | x"},
|
{what: "built-by does not order", edges: []inventory.Edge{dep("x", builtBy, "a")},
|
||||||
|
repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"},
|
||||||
{what: "worker-of never widens", edges: []inventory.Edge{dep("x", workerOf, "a")},
|
{what: "worker-of never widens", edges: []inventory.Edge{dep("x", workerOf, "a")},
|
||||||
repo: "one", paths: []string{"modules/a/x"}, want: "a"},
|
repo: "one", paths: []string{"modules/a/x"}, want: "a"},
|
||||||
{what: "worker-of orders", edges: []inventory.Edge{dep("x", workerOf, "a")},
|
{what: "worker-of orders", edges: []inventory.Edge{dep("x", workerOf, "a")},
|
||||||
@@ -296,18 +297,18 @@ func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
|
|||||||
[]string{"internal/inventory/migrations/0088-a-build-says-its-build-source.sql"}, read,
|
[]string{"internal/inventory/migrations/0088-a-build-says-its-build-source.sql"}, read,
|
||||||
"mesh-controller | build-agent", ""},
|
"mesh-controller | build-agent", ""},
|
||||||
// A package all three build from: all three; the build seat's holder after the controller whose worker
|
// A package all three build from: all three; the build seat's holder after the controller whose worker
|
||||||
// it binds (worker-of, issue 206), the proxy after the holder that builds it (built-by).
|
// it binds (worker-of, issue 206), the proxy beside the controller: built-by orders nothing (ADR 0360 §3).
|
||||||
{"a package all three build from: all three", "mesh-controller",
|
{"a package all three build from: all three", "mesh-controller",
|
||||||
[]string{"internal/broker/broker.go"}, read, "mesh-controller | build-agent | route-proxy", ""},
|
[]string{"internal/broker/broker.go"}, read, "mesh-controller,route-proxy | build-agent", ""},
|
||||||
{"a vendored package all three build from: all three", "mesh-controller",
|
{"a vendored package all three build from: all three", "mesh-controller",
|
||||||
[]string{"vendor/github.com/nats-io/nats.go/nats.go"}, read, "mesh-controller | build-agent | route-proxy", ""},
|
[]string{"vendor/github.com/nats-io/nats.go/nats.go"}, read, "mesh-controller,route-proxy | build-agent", ""},
|
||||||
{"go.sum: all three", "mesh-controller",
|
{"go.sum: all three", "mesh-controller",
|
||||||
[]string{"go.sum"}, read, "mesh-controller | build-agent | route-proxy", ""},
|
[]string{"go.sum"}, read, "mesh-controller,route-proxy | build-agent", ""},
|
||||||
{"a file added to the proxy's package: the proxy", "mesh-controller",
|
{"a file added to the proxy's package: the proxy", "mesh-controller",
|
||||||
[]string{"examples/route-proxy/new.go"}, read, "route-proxy", ""},
|
[]string{"examples/route-proxy/new.go"}, read, "route-proxy", ""},
|
||||||
// Before any build source is said: as before.
|
// Before any build source is said: as before.
|
||||||
{"no build source said: a README moves all three, as before", "mesh-controller",
|
{"no build source said: a README moves all three, as before", "mesh-controller",
|
||||||
[]string{"README.md"}, unsaid, "mesh-controller | build-agent | route-proxy", ""},
|
[]string{"README.md"}, unsaid, "mesh-controller,route-proxy | build-agent", ""},
|
||||||
// In the catalogue, where they live, each by its own build source.
|
// In the catalogue, where they live, each by its own build source.
|
||||||
{"the route proxy's recipe: it alone", "mesh-catalog",
|
{"the route proxy's recipe: it alone", "mesh-catalog",
|
||||||
[]string{"modules/route-proxy/Dockerfile"}, read, "route-proxy", ""},
|
[]string{"modules/route-proxy/Dockerfile"}, read, "route-proxy", ""},
|
||||||
@@ -331,7 +332,7 @@ func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
|
|||||||
// Files not all said: everything the repository builds, as before.
|
// Files not all said: everything the repository builds, as before.
|
||||||
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Commit: "head",
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Commit: "head",
|
||||||
Paths: []string{"README.md"}, PathsTruncated: true}
|
Paths: []string{"README.md"}, PathsTruncated: true}
|
||||||
if got := tiered(reachOfMerge(m, entries, read, edges).Plan.Tiers); got != "mesh-controller | build-agent | route-proxy" {
|
if got := tiered(reachOfMerge(m, entries, read, edges).Plan.Tiers); got != "mesh-controller,route-proxy | build-agent" {
|
||||||
t.Errorf("a merge whose files were not all said: planned %q, wanted all three", got)
|
t.Errorf("a merge whose files were not all said: planned %q, wanted all three", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,8 +453,9 @@ func TestAPlanIsTheTouchedModulesAndWhatIsReachableAlongTheWideningEdges(t *test
|
|||||||
kinds := []string{inventory.EdgeStandsOn, inventory.EdgeDeclared, inventory.EdgePackages,
|
kinds := []string{inventory.EdgeStandsOn, inventory.EdgeDeclared, inventory.EdgePackages,
|
||||||
inventory.EdgeBuiltBy, inventory.EdgeWorkerOf}
|
inventory.EdgeBuiltBy, inventory.EdgeWorkerOf}
|
||||||
widens := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true}
|
widens := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true}
|
||||||
|
// Built-by orders nothing since novox/hq ADR 0360 §3.
|
||||||
orders := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true,
|
orders := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true,
|
||||||
inventory.EdgeBuiltBy: true, inventory.EdgeWorkerOf: true}
|
inventory.EdgeWorkerOf: true}
|
||||||
// Directory names drawn from one pool, so two repositories hold directories of the same name, and one
|
// Directory names drawn from one pool, so two repositories hold directories of the same name, and one
|
||||||
// is a prefix of another.
|
// is a prefix of another.
|
||||||
dirs := []string{"a", "ab", "b", "c", "lib/x", "lib/y", "modules/a", "modules/a/sub"}
|
dirs := []string{"a", "ab", "b", "c", "lib/x", "lib/y", "modules/a", "modules/a/sub"}
|
||||||
|
|||||||
@@ -616,6 +616,7 @@ func TestKillAndPauseAskTheHolderOnTheMachine(t *testing.T) {
|
|||||||
// A plan that stopped at its first machine is retried: the module sent to that machine again, the
|
// A plan that stopped at its first machine is retried: the module sent to that machine again, the
|
||||||
// send recorded as the first anew, and the plan goes on — unless a newer plan holds the module.
|
// send recorded as the first anew, and the plan goes on — unless a newer plan holds the module.
|
||||||
func TestAPlanStoppedAtItsFirstMachineIsRetried(t *testing.T) {
|
func TestAPlanStoppedAtItsFirstMachineIsRetried(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate's own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
asked := asksRecorded(t)
|
asked := asksRecorded(t)
|
||||||
|
|||||||
@@ -750,6 +750,11 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
|
|||||||
if r.Rest {
|
if r.Rest {
|
||||||
return advanceRest(ctx, open, p, now)
|
return advanceRest(ctx, open, p, now)
|
||||||
}
|
}
|
||||||
|
if r.Gate == nil && !gateFirstMachine {
|
||||||
|
// **No first machine** (novox/hq ADR 0360 §7): every machine heard from is sent at once, in one step.
|
||||||
|
r.Rest = true
|
||||||
|
return advanceRest(ctx, open, p, now)
|
||||||
|
}
|
||||||
if r.Gate == nil {
|
if r.Gate == nil {
|
||||||
heard, err := releaseHeard(ctx, open)
|
heard, err := releaseHeard(ctx, open)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1015,6 +1020,25 @@ func advanceRest(ctx context.Context, open *stores, p *inventory.Plan, now time.
|
|||||||
judging = append(judging, node+": not yet reported on its send")
|
judging = append(judging, node+": not yet reported on its send")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !gateFirstMachine {
|
||||||
|
// **Judged by its report alone** (novox/hq ADR 0360 §7): applied passes; failed or refused is named and
|
||||||
|
// fails the walk, with nothing put back — apply-failing says it.
|
||||||
|
switch rep := byNode[node]; rep.Outcome {
|
||||||
|
case inventory.OutcomeApplied:
|
||||||
|
decide(g, inventory.GatePassed, node+" reported it applied its send (novox/hq ADR 0360)", now)
|
||||||
|
passCarried(ctx, open, p, g, "")
|
||||||
|
r.Done = append(r.Done, node)
|
||||||
|
changed = true
|
||||||
|
case inventory.OutcomeFailed, inventory.OutcomeRefused:
|
||||||
|
decide(g, inventory.GateFailed, fmt.Sprintf("%s %s its send", node, rep.Outcome), now)
|
||||||
|
r.Failed = append(r.Failed, node)
|
||||||
|
changed = true
|
||||||
|
notes = append(notes, fmt.Sprintf("%s %s its send: nothing put back, apply-failing says it", node, rep.Outcome))
|
||||||
|
default:
|
||||||
|
judging = append(judging, node+": not yet reported on its send")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
verdict, err := judgeMoves(ctx, open, g, nil, now)
|
verdict, err := judgeMoves(ctx, open, g, nil, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return changed, err
|
return changed, err
|
||||||
@@ -1099,6 +1123,9 @@ func advanceRest(ctx context.Context, open *stores, p *inventory.Plan, now time.
|
|||||||
// keptStepGrew says why a step's kept sends would now carry a build no gate passed that they were not kept with;
|
// keptStepGrew says why a step's kept sends would now carry a build no gate passed that they were not kept with;
|
||||||
// empty when none would.
|
// empty when none would.
|
||||||
func keptStepGrew(ctx context.Context, open *stores, r *inventory.PlanRelease, pending []string) (string, error) {
|
func keptStepGrew(ctx context.Context, open *stores, r *inventory.PlanRelease, pending []string) (string, error) {
|
||||||
|
if !gateFirstMachine {
|
||||||
|
return "", nil // every machine goes at once (novox/hq ADR 0360 §7): a send carries what waits when it is made
|
||||||
|
}
|
||||||
f, err := readMoveFacts(ctx, open.inventory)
|
f, err := readMoveFacts(ctx, open.inventory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -1207,7 +1234,11 @@ func openRestStep(ctx context.Context, open *stores, p *inventory.Plan) (bool, s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, "", err
|
||||||
}
|
}
|
||||||
step := restStep(nodes, waiting, f.gated, func(module string) bool { return oneAtATime(module, shelf[module]) })
|
// **Every machine at once** (novox/hq ADR 0360 §2, §7): no build waits for one machine to pass first.
|
||||||
|
step := nodes
|
||||||
|
if gateFirstMachine {
|
||||||
|
step = restStep(nodes, waiting, f.gated, func(module string) bool { return oneAtATime(module, shelf[module]) })
|
||||||
|
}
|
||||||
if len(step) == 0 {
|
if len(step) == 0 {
|
||||||
if len(walked) > 0 {
|
if len(walked) > 0 {
|
||||||
return false, "waiting before " + strings.Join(walked, "; "), nil
|
return false, "waiting before " + strings.Join(walked, "; "), nil
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/builder"
|
"github.com/novox/mesh-controller/internal/builder"
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
)
|
)
|
||||||
@@ -27,6 +28,10 @@ import (
|
|||||||
// alone cannot (a tier waiting for machines to report); a controller replaced mid-plan finds the
|
// alone cannot (a tier waiting for machines to report); a controller replaced mid-plan finds the
|
||||||
// plan where it left it.
|
// plan where it left it.
|
||||||
|
|
||||||
|
// gateFirstMachine sends a module that rolls out to one machine first and judges it there (ADR 0236). Off since
|
||||||
|
// novox/hq ADR 0360: the gate's code is kept for the walks a controller before it started, and its tests turn it on.
|
||||||
|
var gateFirstMachine = false
|
||||||
|
|
||||||
// planWaitBound is how long a plan may wait on one thing before `status` names it red.
|
// planWaitBound is how long a plan may wait on one thing before `status` names it red.
|
||||||
const planWaitBound = 30 * time.Minute
|
const planWaitBound = 30 * time.Minute
|
||||||
|
|
||||||
@@ -43,29 +48,15 @@ func tiersOf(set []string, edges []inventory.Edge) [][]string {
|
|||||||
for _, m := range set {
|
for _, m := range set {
|
||||||
deps[m] = map[string]bool{}
|
deps[m] = map[string]bool{}
|
||||||
}
|
}
|
||||||
// The build seat's holders follow the controller that defines their worker (EdgeWorkerOf,
|
|
||||||
// novox/hq issue 206), so the built-by edge from that controller to such a holder yields: the
|
|
||||||
// controller is built by whichever build machine is running, as the runtime image always was.
|
|
||||||
worker := map[string]map[string]bool{}
|
|
||||||
for _, e := range edges {
|
|
||||||
if e.Kind == inventory.EdgeWorkerOf && in[e.From] && in[e.To] {
|
|
||||||
if worker[e.To] == nil {
|
|
||||||
worker[e.To] = map[string]bool{}
|
|
||||||
}
|
|
||||||
worker[e.To][e.From] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, e := range edges {
|
for _, e := range edges {
|
||||||
// A code dependency — B packages A's source — rebuilds B with A, in the same tier: B's
|
// A code dependency — B packages A's source — rebuilds B with A, in the same tier: B's
|
||||||
// build needs nothing of A's first. The other kinds order: stands-on and declared after
|
// build needs nothing of A's first. Stands-on and declared order after the base is built, and the
|
||||||
// the base is built, built-by after the build machine is built and running — except for
|
// build seat's holder follows the controller that defines its worker (EdgeWorkerOf, issue 206).
|
||||||
// what the build machine itself stands on, and for the controller whose worker the build
|
|
||||||
// machine binds. The runtime image is built by the builder and the builder is built on the
|
|
||||||
// runtime image; the image comes first, built by the builder that is running.
|
|
||||||
if !in[e.From] || !in[e.To] || e.From == e.To || e.Kind == inventory.EdgePackages {
|
if !in[e.From] || !in[e.To] || e.From == e.To || e.Kind == inventory.EdgePackages {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if e.Kind == inventory.EdgeBuiltBy && (isBaseOf(e.From, e.To, edges, in) || worker[e.From][e.To]) {
|
// **Being built by the builder orders nothing** (novox/hq ADR 0360 §3): whichever builder runs builds it.
|
||||||
|
if e.Kind == inventory.EdgeBuiltBy {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
deps[e.From][e.To] = true
|
deps[e.From][e.To] = true
|
||||||
@@ -106,29 +97,6 @@ func tiersOf(set []string, edges []inventory.Edge) [][]string {
|
|||||||
return tiers
|
return tiers
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBaseOf says whether `to` stands on `base`, directly or through other bases in the set, along
|
|
||||||
// the build edges alone.
|
|
||||||
func isBaseOf(base, to string, edges []inventory.Edge, in map[string]bool) bool {
|
|
||||||
seen := map[string]bool{}
|
|
||||||
var walk func(string) bool
|
|
||||||
walk = func(m string) bool {
|
|
||||||
if m == base {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if seen[m] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
seen[m] = true
|
|
||||||
for _, e := range edges {
|
|
||||||
if e.From == m && in[e.To] && (e.Kind == inventory.EdgeStandsOn || e.Kind == inventory.EdgeDeclared) && walk(e.To) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return walk(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reachableFrom is the moved modules plus everything that depends on them, through every layer:
|
// reachableFrom is the moved modules plus everything that depends on them, through every layer:
|
||||||
// what a merge rebuilds. Along the code and build edges only: a module *built by* the build machine
|
// what a merge rebuilds. Along the code and build edges only: a module *built by* the build machine
|
||||||
// is not changed by a new build machine, so a built-by edge orders and gates a plan and never
|
// is not changed by a new build machine, so a built-by edge orders and gates a plan and never
|
||||||
@@ -164,7 +132,7 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hasCycle says whether the tiers' last tier holds modules that still depend on each other. A packages
|
// hasCycle says whether the tiers' last tier holds modules that still depend on each other. A packages
|
||||||
// edge orders nothing (tiersOf), so a module and what packages its source share a tier by rule: that is
|
// or built-by edge orders nothing (tiersOf), so a module and what packages its source share a tier by rule: that is
|
||||||
// no cycle, and saying one was is a false report in every such plan's log.
|
// no cycle, and saying one was is a false report in every such plan's log.
|
||||||
func hasCycle(tiers [][]string, edges []inventory.Edge) bool {
|
func hasCycle(tiers [][]string, edges []inventory.Edge) bool {
|
||||||
if len(tiers) == 0 {
|
if len(tiers) == 0 {
|
||||||
@@ -175,7 +143,7 @@ func hasCycle(tiers [][]string, edges []inventory.Edge) bool {
|
|||||||
last[m] = true
|
last[m] = true
|
||||||
}
|
}
|
||||||
for _, e := range edges {
|
for _, e := range edges {
|
||||||
if e.Kind == inventory.EdgePackages {
|
if e.Kind == inventory.EdgePackages || e.Kind == inventory.EdgeBuiltBy {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if last[e.From] && last[e.To] {
|
if last[e.From] && last[e.To] {
|
||||||
@@ -221,12 +189,6 @@ func gates(p inventory.Plan, edges []inventory.Edge, rollsOut func(string) bool)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
inTier := map[string]bool{}
|
inTier := map[string]bool{}
|
||||||
all := map[string]bool{}
|
|
||||||
for _, tier := range p.Tiers {
|
|
||||||
for _, m := range tier {
|
|
||||||
all[m] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, m := range p.Tiers[p.Tier] {
|
for _, m := range p.Tiers[p.Tier] {
|
||||||
inTier[m] = true
|
inTier[m] = true
|
||||||
}
|
}
|
||||||
@@ -241,8 +203,8 @@ func gates(p inventory.Plan, edges []inventory.Edge, rollsOut func(string) bool)
|
|||||||
for _, e := range edges {
|
for _, e := range edges {
|
||||||
// A delivery group's order inside a walk (novox/hq ADR 0276 decision 5) gates as built-by does: the
|
// A delivery group's order inside a walk (novox/hq ADR 0276 decision 5) gates as built-by does: the
|
||||||
// member before is running on its machines before the member after is asked.
|
// member before is running on its machines before the member after is asked.
|
||||||
ordered := e.Kind == edgeGroupOrder ||
|
// A built-by edge waits for nothing since novox/hq ADR 0360 §3.
|
||||||
e.Kind == inventory.EdgeBuiltBy && !isBaseOf(e.From, e.To, edges, all)
|
ordered := e.Kind == edgeGroupOrder
|
||||||
if later[e.From] && inTier[e.To] && ordered && !seen[e.To] && rollsOut(e.To) {
|
if later[e.From] && inTier[e.To] && ordered && !seen[e.To] && rollsOut(e.To) {
|
||||||
seen[e.To] = true
|
seen[e.To] = true
|
||||||
out = append(out, e.To)
|
out = append(out, e.To)
|
||||||
@@ -888,6 +850,12 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
// **Every machine at once** (novox/hq ADR 0360 §1, §2): a module that rolls out is sent to every machine
|
||||||
|
// running it in one step, with no first machine and no gate, whatever module it is. A module a controller
|
||||||
|
// before ADR 0360 already sent to its first machine finishes under its gate.
|
||||||
|
if !gateFirstMachine && state.FirstAt == nil {
|
||||||
|
policy.Together = true
|
||||||
|
}
|
||||||
var reports []inventory.Reported
|
var reports []inventory.Reported
|
||||||
if !policy.Together {
|
if !policy.Together {
|
||||||
// Read for the choice of the first machine as well as for its report.
|
// Read for the choice of the first machine as well as for its report.
|
||||||
@@ -1117,15 +1085,92 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
|
|||||||
return changed, nil
|
return changed, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **A walk ends on its machines' reports** (novox/hq ADR 0360 §4, §5): after its last tier, every machine it sent
|
||||||
|
// has reported on that send, or is silent past ApplySilentAfter; never a health reading.
|
||||||
|
end := ""
|
||||||
|
if p.Tier == len(p.Tiers)-1 {
|
||||||
|
reports, err := inv.LastReports(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
waiting, failed, silent := walkReports(*p, reports, time.Now().UTC())
|
||||||
|
if len(waiting) > 0 {
|
||||||
|
note := "sent; waiting for " + strings.Join(waiting, ", ") + " to report on their send"
|
||||||
|
changed := p.State != inventory.PlanRolling || p.Note != note
|
||||||
|
p.State = inventory.PlanRolling
|
||||||
|
p.Note = note
|
||||||
|
return changed, nil
|
||||||
|
}
|
||||||
|
var said []string
|
||||||
|
if len(failed) > 0 {
|
||||||
|
said = append(said, "failed or refused its send (apply-failing says it): "+strings.Join(failed, ", "))
|
||||||
|
}
|
||||||
|
if len(silent) > 0 {
|
||||||
|
said = append(said, fmt.Sprintf("said nothing on its send within %s: %s", inventory.ApplySilentAfter,
|
||||||
|
strings.Join(silent, ", ")))
|
||||||
|
}
|
||||||
|
end = strings.Join(said, "; ")
|
||||||
|
// **The new controller's user list follows its walk** (ADR 0236 §34, read without the gate since ADR 0360):
|
||||||
|
// once a walk that moved the controller ends, the bus's machine is sent the list it composes, if behind.
|
||||||
|
if s := p.Modules[catalogue.ControllerSeatName]; s != nil && s.SentAt != nil && s.Gate == nil && !noMove(s) {
|
||||||
|
carryUserList(ctx, open, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
p.Tier++
|
p.Tier++
|
||||||
p.State = inventory.PlanBuilding
|
p.State = inventory.PlanBuilding
|
||||||
p.Note = ""
|
p.Note = end
|
||||||
if p.Tier < len(p.Tiers) {
|
if p.Tier < len(p.Tiers) {
|
||||||
fmt.Printf("%s: tier %d done; asking tier %d: %s\n", p.ID, p.Tier-1, p.Tier, strings.Join(p.Tiers[p.Tier], ", "))
|
fmt.Printf("%s: tier %d done; asking tier %d: %s\n", p.ID, p.Tier-1, p.Tier, strings.Join(p.Tiers[p.Tier], ", "))
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// walkReports is, for every machine a walk sent its rest, whether it reported on that send (novox/hq ADR 0360 §4):
|
||||||
|
// those still waiting, those that reported a failed or refused apply, and those silent past ApplySilentAfter,
|
||||||
|
// each sorted. Pure.
|
||||||
|
func walkReports(p inventory.Plan, reports []inventory.Reported, now time.Time) (waiting, failed, silent []string) {
|
||||||
|
byNode := map[string]inventory.Reported{}
|
||||||
|
for _, r := range reports {
|
||||||
|
byNode[r.Node] = r
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, s := range p.Modules {
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for node, sent := range s.Rest {
|
||||||
|
if seen[node] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := byNode[node]
|
||||||
|
at := sent.At
|
||||||
|
if at.IsZero() && s.SentAt != nil {
|
||||||
|
at = *s.SentAt
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r.At != nil && sent.ReportsOn(r):
|
||||||
|
seen[node] = true
|
||||||
|
if r.Outcome == inventory.OutcomeFailed || r.Outcome == inventory.OutcomeRefused {
|
||||||
|
failed = append(failed, node)
|
||||||
|
}
|
||||||
|
case !at.IsZero() && now.Sub(at) > inventory.ApplySilentAfter:
|
||||||
|
seen[node] = true
|
||||||
|
silent = append(silent, node)
|
||||||
|
default:
|
||||||
|
if !slices.Contains(waiting, node) {
|
||||||
|
waiting = append(waiting, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A machine one module's send was reported on and another's not yet is the same send: reported.
|
||||||
|
waiting = slices.DeleteFunc(waiting, func(n string) bool { return seen[n] })
|
||||||
|
sort.Strings(waiting)
|
||||||
|
sort.Strings(failed)
|
||||||
|
sort.Strings(silent)
|
||||||
|
return waiting, failed, silent
|
||||||
|
}
|
||||||
|
|
||||||
// restOf is, for one module, every machine of its rest that the send reached and what it carried there.
|
// restOf is, for one module, every machine of its rest that the send reached and what it carried there.
|
||||||
func restOf(to, sent []string, what map[string]inventory.SentDeclaration) map[string]inventory.SentDeclaration {
|
func restOf(to, sent []string, what map[string]inventory.SentDeclaration) map[string]inventory.SentDeclaration {
|
||||||
out := map[string]inventory.SentDeclaration{}
|
out := map[string]inventory.SentDeclaration{}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -49,8 +50,10 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
|
|||||||
if pos["mesh-tools"] != 0 || pos["builder"] != 1 {
|
if pos["mesh-tools"] != 0 || pos["builder"] != 1 {
|
||||||
t.Fatalf("the runtime then the builder: %v", tiers)
|
t.Fatalf("the runtime then the builder: %v", tiers)
|
||||||
}
|
}
|
||||||
if !(pos["shop"] > pos["builder"] && pos["postgres"] > pos["builder"]) {
|
// Built-by orders nothing (novox/hq ADR 0360 §3): what the builder builds is beside it, after the runtime it
|
||||||
t.Fatalf("what the builder builds comes after the builder: %v", tiers)
|
// stands on.
|
||||||
|
if pos["shop"] != 1 || pos["postgres"] != 1 {
|
||||||
|
t.Fatalf("what the builder builds waits for the builder: %v", tiers)
|
||||||
}
|
}
|
||||||
if pos["shop-plugin"] <= pos["shop"] {
|
if pos["shop-plugin"] <= pos["shop"] {
|
||||||
t.Fatalf("a plugin after what it is declared on: %v", tiers)
|
t.Fatalf("a plugin after what it is declared on: %v", tiers)
|
||||||
@@ -67,24 +70,23 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
|
|||||||
// A change to a package all three build from moves all three. The builder holds
|
// A change to a package all three build from moves all three. The builder holds
|
||||||
// the build seat, whose worker the controller defines, so it follows the controller (worker-of,
|
// the build seat, whose worker the controller defines, so it follows the controller (worker-of,
|
||||||
// novox/hq issue 206), and the controller's built-by edge to it yields: the controller is built by the
|
// novox/hq issue 206), and the controller's built-by edge to it yields: the controller is built by the
|
||||||
// build machine that is running. The proxy is built by the new builder: the controller, the builder,
|
// build machine that is running. The proxy is built by whichever builder runs, so it goes beside the
|
||||||
// the proxy — the live plan of every controller merge. (This read "the builder, then the controller
|
// controller (novox/hq ADR 0360 §3; it read "the controller, the builder, the proxy" before).
|
||||||
// and the proxy together" before issue 206, and the fixture had no worker-of edge.)
|
|
||||||
small := reachableFrom([]string{"mesh-controller", "builder", "route-proxy"}, edges)
|
small := reachableFrom([]string{"mesh-controller", "builder", "route-proxy"}, edges)
|
||||||
smallTiers := tiersOf(small, edges)
|
smallTiers := tiersOf(small, edges)
|
||||||
if got := tiered(smallTiers); got != "mesh-controller | builder | route-proxy" {
|
if got := tiered(smallTiers); got != "mesh-controller,route-proxy | builder" {
|
||||||
t.Fatalf("the controller, then the builder, then the proxy: %v", smallTiers)
|
t.Fatalf("the controller and the proxy, then the builder: %v", smallTiers)
|
||||||
}
|
}
|
||||||
// The builder alone moved: the builder, and nothing it builds.
|
// The builder alone moved: the builder, and nothing it builds.
|
||||||
if only := reachableFrom([]string{"builder"}, edges); len(only) != 1 {
|
if only := reachableFrom([]string{"builder"}, edges); len(only) != 1 {
|
||||||
t.Fatalf("a build machine change rebuilds the build machine alone: %v", only)
|
t.Fatalf("a build machine change rebuilds the build machine alone: %v", only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only a runtime dependency gates on deployment, and only when the module rolls out.
|
// Nothing waits on deployment for a built-by edge (novox/hq ADR 0360 §3): the builder gates no tier after it.
|
||||||
p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-tools", Commit: "abc"}, []string{"mesh-tools"}, edges)
|
p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-tools", Commit: "abc"}, []string{"mesh-tools"}, edges)
|
||||||
p.Tier = pos["builder"]
|
p.Tier = pos["builder"]
|
||||||
rollsOut := func(m string) bool { return m == "builder" }
|
rollsOut := func(m string) bool { return m == "builder" }
|
||||||
if g := gates(p, edges, rollsOut); len(g) != 1 || g[0] != "builder" {
|
if g := gates(p, edges, rollsOut); len(g) != 0 {
|
||||||
t.Fatalf("the builder gates the tier after it: %v", g)
|
t.Fatalf("the builder gates the tier after it: %v", g)
|
||||||
}
|
}
|
||||||
p.Tier = 0
|
p.Tier = 0
|
||||||
@@ -253,10 +255,10 @@ func TestAChangeToTheBuildAgentRebuildsTheBuildAgentAlone(t *testing.T) {
|
|||||||
t.Fatalf("%v planned %v: the build agent alone", paths, p.Tiers)
|
t.Fatalf("%v planned %v: the build agent alone", paths, p.Tiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// With what it builds moved beside it, the agent comes first and they follow: an order, not a widening.
|
// With what it builds moved beside it, both go in one tier (novox/hq ADR 0360 §3): built-by orders nothing.
|
||||||
p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Commit: "abc"},
|
p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Commit: "abc"},
|
||||||
[]string{"build-agent", "redis"}, edges)
|
[]string{"build-agent", "redis"}, edges)
|
||||||
if len(p.Tiers) != 2 || p.Tiers[0][0] != "build-agent" || p.Tiers[1][0] != "redis" || len(p.Modules) != 2 {
|
if len(p.Tiers) != 1 || !slices.Equal(p.Tiers[0], []string{"build-agent", "redis"}) || len(p.Modules) != 2 {
|
||||||
t.Fatalf("the agent, then what moved beside it: %v", p.Tiers)
|
t.Fatalf("the agent and what moved beside it, in one tier: %v", p.Tiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,13 @@ func aBacklog(t *testing.T) *backlogMesh {
|
|||||||
if err := open.inventory.RecordSent(ctx, nodeID(t, open, node), digest, carried); err != nil {
|
if err := open.inventory.RecordSent(ctx, nodeID(t, open, node), digest, carried); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// A broken machine reports its apply failed (novox/hq ADR 0360 §7: the report is the verdict).
|
||||||
|
outcome := inventory.OutcomeApplied
|
||||||
|
if b.broken[node] {
|
||||||
|
outcome = inventory.OutcomeFailed
|
||||||
|
}
|
||||||
if _, err := open.inventory.RecordDoing(ctx, nodeID(t, open, node), inventory.Doing{Node: node,
|
if _, err := open.inventory.RecordDoing(ctx, nodeID(t, open, node), inventory.Doing{Node: node,
|
||||||
Outcome: inventory.OutcomeApplied, Declared: digest, Applied: 1, At: time.Now()}); err != nil {
|
Outcome: outcome, Declared: digest, Applied: 1, At: time.Now()}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,6 +169,7 @@ func (b *backlogMesh) release(t *testing.T) inventory.Plan {
|
|||||||
// The backlog goes out one machine at a time: the first judged before the second is sent, each build's
|
// The backlog goes out one machine at a time: the first judged before the second is sent, each build's
|
||||||
// pass kept; a rebuild with the same bytes is no move at all; and a send outside a gate is refused.
|
// pass kept; a rebuild with the same bytes is no move at all; and a send outside a gate is refused.
|
||||||
func TestTheBacklogIsReleasedOneMachineAtATimeEachJudged(t *testing.T) {
|
func TestTheBacklogIsReleasedOneMachineAtATimeEachJudged(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate's own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -219,6 +225,7 @@ func TestTheBacklogIsReleasedOneMachineAtATimeEachJudged(t *testing.T) {
|
|||||||
// A release that fails on its first machine puts back what it carried there, goes no further, and the
|
// A release that fails on its first machine puts back what it carried there, goes no further, and the
|
||||||
// next one waits for a person, said as a condition; a person releases it with why.
|
// next one waits for a person, said as a condition; a person releases it with why.
|
||||||
func TestAFailedReleaseIsPutBackAndTheNextWaitsForAPerson(t *testing.T) {
|
func TestAFailedReleaseIsPutBackAndTheNextWaitsForAPerson(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate's own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := b.open.inventory
|
inv := b.open.inventory
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
// nothing is put back, the walk goes on to the workstation, and openrazer's condition says, in one sentence
|
// nothing is put back, the walk goes on to the workstation, and openrazer's condition says, in one sentence
|
||||||
// for the operator, that a new login is needed on the laptop.
|
// for the operator, that a new login is needed on the laptop.
|
||||||
func TestReplay318(t *testing.T) {
|
func TestReplay318(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
// at its gate with the pass's own words, and put it back. Written with only what the controller had before
|
// at its gate with the pass's own words, and put it back. Written with only what the controller had before
|
||||||
// its fix, so it is laid over the commit before.
|
// its fix, so it is laid over the commit before.
|
||||||
func TestReplay335(t *testing.T) {
|
func TestReplay335(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
t.Run("the first machine's later reports", testAPassedGateIsNotJudgedAgainFromTheFirstMachinesLaterReports)
|
t.Run("the first machine's later reports", testAPassedGateIsNotJudgedAgainFromTheFirstMachinesLaterReports)
|
||||||
t.Run("a walk stopped after the pass", testAWalkStoppedAfterItsGatePassedKeepsThePass)
|
t.Run("a walk stopped after the pass", testAWalkStoppedAfterItsGatePassedKeepsThePass)
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ func testAWalkStoppedAfterItsGatePassedKeepsThePass(t *testing.T) {
|
|||||||
// the first send is older than the wait for a first machine's report. Read before the pass is taken over, the
|
// the first send is older than the wait for a first machine's report. Read before the pass is taken over, the
|
||||||
// carried module's step said the first machine never applied it, and the passed build was put back.
|
// carried module's step said the first machine never applied it, and the passed build was put back.
|
||||||
func TestACarriedModuleTakesItsLeadsPassBeforeItsStepIsRead(t *testing.T) {
|
func TestACarriedModuleTakesItsLeadsPassBeforeItsStepIsRead(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
tm := aTierMesh(t, "m01", "m02")
|
tm := aTierMesh(t, "m01", "m02")
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
// had before its fix — the stores, register, assign, a plan saved and advanced, the watchdogs' gathering and
|
// 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.
|
// seeing — so it is laid over the commit before.
|
||||||
func TestReplay336(t *testing.T) {
|
func TestReplay336(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func TestReplay273AConsumerBesideItsStoreStaysBoundToIt(t *testing.T) {
|
|||||||
// one, kept below. Heard as bytes, so an older controller reads them as it reads any report — this file is
|
// one, kept below. Heard as bytes, so an older controller reads them as it reads any report — this file is
|
||||||
// written only with what the controller had before the judging, for the prover to lay over that commit.
|
// written only with what the controller had before the judging, for the prover to lay over that commit.
|
||||||
func TestReplayCrashLoopFailsItsGateOnTheFirstMachine(t *testing.T) {
|
func TestReplayCrashLoopFailsItsGateOnTheFirstMachine(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate's own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
// novox/hq ADR 0353: after its first machine passes, a build goes to every other machine at once, each judged by
|
// novox/hq ADR 0353, kept behind gateFirstMachine for walks a controller before ADR 0360 started: after its first machine passes, a build goes to every other machine at once, each judged by
|
||||||
// its own gate and a failing one put back by itself; and a gate's readings outlive the controller that took them —
|
// its own gate and a failing one put back by itself; and a gate's readings outlive the controller that took them —
|
||||||
// a machine already sent what a send is for is not sent again, and its judging goes on from what was read.
|
// a machine already sent what a send is for is not sent again, and its judging goes on from what was read.
|
||||||
//
|
//
|
||||||
@@ -62,6 +62,7 @@ func events(t *testing.T) *[]string {
|
|||||||
// Once the first machine passed, the rest are both sent before either is judged (ADR 0353 §1); before, the second
|
// Once the first machine passed, the rest are both sent before either is judged (ADR 0353 §1); before, the second
|
||||||
// was judged to its verdict before the third was sent.
|
// was judged to its verdict before the third was sent.
|
||||||
func TestAReleaseSendsTheRestAtOnceAfterTheFirstPasses(t *testing.T) {
|
func TestAReleaseSendsTheRestAtOnceAfterTheFirstPasses(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklogOnThree(t)
|
b := aBacklogOnThree(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -89,6 +90,7 @@ func TestAReleaseSendsTheRestAtOnceAfterTheFirstPasses(t *testing.T) {
|
|||||||
// A machine of the rest whose gate fails is put back at once, and the other machine of the step goes on to its own
|
// A machine of the rest whose gate fails is put back at once, and the other machine of the step goes on to its own
|
||||||
// verdict (ADR 0353 §3); before, the walk stopped at the failure and the machine after it was never sent.
|
// verdict (ADR 0353 §3); before, the walk stopped at the failure and the machine after it was never sent.
|
||||||
func TestARestMachineThatFailsIsPutBackAloneAndTheOthersAreJudgedOn(t *testing.T) {
|
func TestARestMachineThatFailsIsPutBackAloneAndTheOthersAreJudgedOn(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklogOnThree(t)
|
b := aBacklogOnThree(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -125,6 +127,7 @@ func TestARestMachineThatFailsIsPutBackAloneAndTheOthersAreJudgedOn(t *testing.T
|
|||||||
// A walk whose first machine already runs its build under another walk's gate sends nothing and goes on from that
|
// A walk whose first machine already runs its build under another walk's gate sends nothing and goes on from that
|
||||||
// gate's readings (ADR 0353 §7); before, it sent the machine again and judged from its first reading.
|
// gate's readings (ADR 0353 §7); before, it sent the machine again and judged from its first reading.
|
||||||
func TestAWalkTakesOverTheReadingsOfAGateJudgingTheSameBuildThere(t *testing.T) {
|
func TestAWalkTakesOverTheReadingsOfAGateJudgingTheSameBuildThere(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -163,6 +166,7 @@ func TestAWalkTakesOverTheReadingsOfAGateJudgingTheSameBuildThere(t *testing.T)
|
|||||||
// A walk's first send that was made and not kept — the controller replaced between the two — is not made again
|
// A walk's first send that was made and not kept — the controller replaced between the two — is not made again
|
||||||
// by the controller that takes over: the gate counts from that send (ADR 0353 §5, §6).
|
// by the controller that takes over: the gate counts from that send (ADR 0353 §5, §6).
|
||||||
func TestAFirstSendMadeAndNotKeptIsNotSentAgain(t *testing.T) {
|
func TestAFirstSendMadeAndNotKeptIsNotSentAgain(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
g := aGateMesh(t)
|
g := aGateMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -200,6 +204,7 @@ func TestAFirstSendMadeAndNotKeptIsNotSentAgain(t *testing.T) {
|
|||||||
// A release's send kept before it was made, and made, is judged by the controller that takes over without being
|
// A release's send kept before it was made, and made, is judged by the controller that takes over without being
|
||||||
// made again (ADR 0353 §8).
|
// made again (ADR 0353 §8).
|
||||||
func TestAReleaseSendKeptAndMadeIsNotMadeAgain(t *testing.T) {
|
func TestAReleaseSendKeptAndMadeIsNotMadeAgain(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -231,6 +236,7 @@ func TestAReleaseSendKeptAndMadeIsNotMadeAgain(t *testing.T) {
|
|||||||
// A build no gate has passed goes to one machine of a step, its first; a machine that would be its second waits
|
// A build no gate has passed goes to one machine of a step, its first; a machine that would be its second waits
|
||||||
// for the next step, and one whose every build passed goes now (ADR 0353 §2).
|
// for the next step, and one whose every build passed goes now (ADR 0353 §2).
|
||||||
func TestABuildThatPassedNoGateGoesToOneMachineOfAStep(t *testing.T) {
|
func TestABuildThatPassedNoGateGoesToOneMachineOfAStep(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
waiting := map[string][]inventory.CarriedMove{
|
waiting := map[string][]inventory.CarriedMove{
|
||||||
"desk": {{Module: "app", To: "c2"}, {Module: "new", To: "n1"}},
|
"desk": {{Module: "app", To: "c2"}, {Module: "new", To: "n1"}},
|
||||||
"laptop": {{Module: "app", To: "c2"}, {Module: "new", To: "n1"}},
|
"laptop": {{Module: "app", To: "c2"}, {Module: "new", To: "n1"}},
|
||||||
@@ -251,6 +257,7 @@ func TestABuildThatPassedNoGateGoesToOneMachineOfAStep(t *testing.T) {
|
|||||||
// The node-engine and the mesh's resolver go to the rest one machine at a time, even passed (ADR 0353 §9): a step
|
// The node-engine and the mesh's resolver go to the rest one machine at a time, even passed (ADR 0353 §9): a step
|
||||||
// carries either to one machine only, while what else passed goes everywhere at once.
|
// carries either to one machine only, while what else passed goes everywhere at once.
|
||||||
func TestTheNodeEngineAndTheResolverGoToTheRestOneAtATime(t *testing.T) {
|
func TestTheNodeEngineAndTheResolverGoToTheRestOneAtATime(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
waiting := map[string][]inventory.CarriedMove{
|
waiting := map[string][]inventory.CarriedMove{
|
||||||
"desk": {{Module: hostModule, To: "h2"}},
|
"desk": {{Module: hostModule, To: "h2"}},
|
||||||
"laptop": {{Module: hostModule, To: "h2"}, {Module: "app", To: "c2"}},
|
"laptop": {{Module: hostModule, To: "h2"}, {Module: "app", To: "c2"}},
|
||||||
@@ -277,6 +284,7 @@ func TestTheNodeEngineAndTheResolverGoToTheRestOneAtATime(t *testing.T) {
|
|||||||
|
|
||||||
// A first machine that fails still stops the walk (ADR 0236 §5, kept by ADR 0353 §3): the rest are never sent.
|
// A first machine that fails still stops the walk (ADR 0236 §5, kept by ADR 0353 §3): the rest are never sent.
|
||||||
func TestAFirstMachineThatFailsStillStopsTheWalk(t *testing.T) {
|
func TestAFirstMachineThatFailsStillStopsTheWalk(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
b := aBacklogOnThree(t)
|
b := aBacklogOnThree(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
gateEvery = 0
|
gateEvery = 0
|
||||||
@@ -298,6 +306,7 @@ func TestAFirstMachineThatFailsStillStopsTheWalk(t *testing.T) {
|
|||||||
// The control node is sent last in a step of the rest (ADR 0353 §1): the machine holding the controller is
|
// The control node is sent last in a step of the rest (ADR 0353 §1): the machine holding the controller is
|
||||||
// ordered after every other.
|
// ordered after every other.
|
||||||
func TestTheControlNodeIsSentLastInAStep(t *testing.T) {
|
func TestTheControlNodeIsSentLastInAStep(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklogOnThree(t)
|
b := aBacklogOnThree(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -322,6 +331,7 @@ func TestTheControlNodeIsSentLastInAStep(t *testing.T) {
|
|||||||
// A machine of the rest that says nothing on its send is left silent after fifteen minutes: not failed, not put
|
// A machine of the rest that says nothing on its send is left silent after fifteen minutes: not failed, not put
|
||||||
// back; a rest that went silent whole ends failed, said as a condition (ADR 0353 §4).
|
// back; a rest that went silent whole ends failed, said as a condition (ADR 0353 §4).
|
||||||
func TestARestMachineSilentPastItsBoundIsLeftAndSaid(t *testing.T) {
|
func TestARestMachineSilentPastItsBoundIsLeftAndSaid(t *testing.T) {
|
||||||
|
withTheGate(t)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
b := aBacklog(t)
|
b := aBacklog(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ func (tm *tierMesh) raise(machine, kind, summary string) {
|
|||||||
// The incident's tier, small: a dozen modules whose first machine is the same are sent there in one
|
// The incident's tier, small: a dozen modules whose first machine is the same are sent there in one
|
||||||
// send, judged by one gate, and the rest of their machines are sent once — two sends, not two dozen.
|
// send, judged by one gate, and the rest of their machines are sent once — two sends, not two dozen.
|
||||||
func TestATierOfManyModulesIsSentToEachMachineOnce(t *testing.T) {
|
func TestATierOfManyModulesIsSentToEachMachineOnce(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
var modules []string
|
var modules []string
|
||||||
for i := 1; i <= 12; i++ {
|
for i := 1; i <= 12; i++ {
|
||||||
@@ -223,6 +224,7 @@ func TestATierOfManyModulesIsSentToEachMachineOnce(t *testing.T) {
|
|||||||
// settling, is the node tools' — inside their settle window, the gate's bound — and no failure of the
|
// settling, is the node tools' — inside their settle window, the gate's bound — and no failure of the
|
||||||
// modules sent with them: at the bound only the node tools are put back.
|
// modules sent with them: at the bound only the node tools are put back.
|
||||||
func TestACoreBehindWhileTheNodeToolsSettleFailsNoOtherModule(t *testing.T) {
|
func TestACoreBehindWhileTheNodeToolsSettleFailsNoOtherModule(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
tm := aTierMesh(t, "app1", "app2", broker.RuntimeModule)
|
tm := aTierMesh(t, "app1", "app2", broker.RuntimeModule)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := tm.open.inventory
|
inv := tm.open.inventory
|
||||||
@@ -276,6 +278,7 @@ func TestACoreBehindWhileTheNodeToolsSettleFailsNoOtherModule(t *testing.T) {
|
|||||||
// A machine-level condition about anything else holds the machine back as a whole: everything the send
|
// A machine-level condition about anything else holds the machine back as a whole: everything the send
|
||||||
// moved there fails together, once, with that reason — not one module for it.
|
// moved there fails together, once, with that reason — not one module for it.
|
||||||
func TestAMachineUnhealthyAsAWholeFailsTheWholeSendTogether(t *testing.T) {
|
func TestAMachineUnhealthyAsAWholeFailsTheWholeSendTogether(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
tm := aTierMesh(t, "app1", "app2", "app3")
|
tm := aTierMesh(t, "app1", "app2", "app3")
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -307,6 +310,7 @@ func TestAMachineUnhealthyAsAWholeFailsTheWholeSendTogether(t *testing.T) {
|
|||||||
// The machine-level conditions, sorted: one naming a module is that module's; the core behind is the
|
// The machine-level conditions, sorted: one naming a module is that module's; the core behind is the
|
||||||
// core's when the send moved it and nobody's when it did not; anything else is the machine's as a whole.
|
// core's when the send moved it and nobody's when it did not; anything else is the machine's as a whole.
|
||||||
func TestTheConditionsAboutAMachineItself(t *testing.T) {
|
func TestTheConditionsAboutAMachineItself(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
since := time.Now().Add(-time.Minute)
|
since := time.Now().Add(-time.Minute)
|
||||||
about := func(kind, id string) conditions.Condition {
|
about := func(kind, id string) conditions.Condition {
|
||||||
return conditions.Condition{Key: "machine." + id + "." + kind, Kind: kind, Raised: time.Now(),
|
return conditions.Condition{Key: "machine." + id + "." + kind, Kind: kind, Raised: time.Now(),
|
||||||
@@ -339,6 +343,7 @@ func TestTheConditionsAboutAMachineItself(t *testing.T) {
|
|||||||
// rollback that failed; and `plans retry` asks it again rather than refusing a build that was never
|
// rollback that failed; and `plans retry` asks it again rather than refusing a build that was never
|
||||||
// judged (the incident's "NOT put back: no build kept").
|
// judged (the incident's "NOT put back: no build kept").
|
||||||
func TestAModuleItsSendChangedNothingOfIsLeftAsItWasAndRetried(t *testing.T) {
|
func TestAModuleItsSendChangedNothingOfIsLeftAsItWasAndRetried(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
g := aGateMesh(t)
|
g := aGateMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -394,6 +399,7 @@ func TestAModuleItsSendChangedNothingOfIsLeftAsItWasAndRetried(t *testing.T) {
|
|||||||
// them, they are registered back and marked in the judging that finds them broken, the plan goes on judging
|
// them, they are registered back and marked in the judging that finds them broken, the plan goes on judging
|
||||||
// the other module to its own pass, and only then fails.
|
// the other module to its own pass, and only then fails.
|
||||||
func TestATierPutsABrokenModuleBackAtOnce(t *testing.T) {
|
func TestATierPutsABrokenModuleBackAtOnce(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
judgeLeavesAsTheCore(t)
|
judgeLeavesAsTheCore(t)
|
||||||
for _, order := range [][]string{{broker.RuntimeModule, "app1"}, {"app1", broker.RuntimeModule}} {
|
for _, order := range [][]string{{broker.RuntimeModule, "app1"}, {"app1", broker.RuntimeModule}} {
|
||||||
t.Run("gate kept on "+order[0], func(t *testing.T) {
|
t.Run("gate kept on "+order[0], func(t *testing.T) {
|
||||||
@@ -454,6 +460,7 @@ func TestATierPutsABrokenModuleBackAtOnce(t *testing.T) {
|
|||||||
// decision 4): the leaf beside the node tools has its one reading at once, but the send is judged by the tool
|
// decision 4): the leaf beside the node tools has its one reading at once, but the send is judged by the tool
|
||||||
// runner's rule: one reading, not before the node-engine's witness window for it has ended.
|
// runner's rule: one reading, not before the node-engine's witness window for it has ended.
|
||||||
func TestASendCarryingTheToolRunnerWaitsForItsWitnessWindow(t *testing.T) {
|
func TestASendCarryingTheToolRunnerWaitsForItsWitnessWindow(t *testing.T) {
|
||||||
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
||||||
tm := aTierMesh(t, broker.RuntimeModule, "app1")
|
tm := aTierMesh(t, broker.RuntimeModule, "app1")
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
// One reading counted per advance, as every test with a gate left open does: a gate judged with no time
|
// One reading counted per advance, as every test with a gate left open does: a gate judged with no time
|
||||||
|
|||||||
@@ -38,8 +38,9 @@ func TestTheBuildSeatsHolderFollowsTheControllerThatDefinesItsWorker(t *testing.
|
|||||||
if pos["build-agent"] <= pos["mesh-controller"] {
|
if pos["build-agent"] <= pos["mesh-controller"] {
|
||||||
t.Fatalf("the build machine after the controller that defines its worker: %v", tiers)
|
t.Fatalf("the build machine after the controller that defines its worker: %v", tiers)
|
||||||
}
|
}
|
||||||
if pos["route-proxy"] <= pos["build-agent"] {
|
// Built-by orders nothing (novox/hq ADR 0360 §3): whichever builder runs builds the proxy, beside the controller.
|
||||||
t.Fatalf("what the build machine builds comes after it: %v", tiers)
|
if pos["route-proxy"] != 0 {
|
||||||
|
t.Fatalf("what the builder builds waits for a new one: %v", tiers)
|
||||||
}
|
}
|
||||||
if hasCycle(tiers, edges) {
|
if hasCycle(tiers, edges) {
|
||||||
t.Fatalf("no cycle here: %v", tiers)
|
t.Fatalf("no cycle here: %v", tiers)
|
||||||
|
|||||||
Reference in New Issue
Block a user