Make the rollout tests wait for the plan look to return before their cleanups restore what it reads, which raced (issue 1338)
mesh/merge-gate head c07f604047be merged into trunk 70cb217601f9 — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head c07f604047be merged into trunk 70cb217601f9 — pass: its merge-check.sh passed
mesh/delivery delivered

This commit is contained in:
2026-10-19 23:23:23 +00:00
parent b893cc829c
commit c07f604047
4 changed files with 30 additions and 10 deletions
+3 -1
View File
@@ -174,7 +174,9 @@ func TestAPlanGoesOnWithoutAMachineAwayWhenItsTierIsSent(t *testing.T) {
}} }}
first, stopFirst := context.WithCancel(ctx) first, stopFirst := context.WithCancel(ctx)
defer stopFirst() defer stopFirst()
startRolloutEvents(first, open, js) waitFirst := startRolloutEvents(first, open, js)
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
t.Cleanup(func() { stopFirst(); waitFirst() })
if err := inv.SavePlan(ctx, &p); err != nil { if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+15 -5
View File
@@ -138,7 +138,9 @@ func askToLook() {
// startPlanLooks starts the look at the open plans, and asks for the first: the controller took the lease, and a // startPlanLooks starts the look at the open plans, and asks for the first: the controller took the lease, and a
// plan left by a replaced controller resumes from there (ADR 0406 §6). It replaces the rollout's 30 second ticker. // plan left by a replaced controller resumes from there (ADR 0406 §6). It replaces the rollout's 30 second ticker.
func startPlanLooks(ctx context.Context, open *stores) { // It answers a channel that is closed once the look has returned after the context ended, for a test that restores what
// the look reads (issue 1338).
func startPlanLooks(ctx context.Context, open *stores) <-chan struct{} {
l := newLooker(func(ctx context.Context) bool { return lookAtPlans(ctx, open) }) l := newLooker(func(ctx context.Context) bool { return lookAtPlans(ctx, open) })
l.first = func(ctx context.Context) { rearmWalkEnds(ctx, open) } l.first = func(ctx context.Context) { rearmWalkEnds(ctx, open) }
lookerMu.Lock() lookerMu.Lock()
@@ -152,8 +154,13 @@ func startPlanLooks(ctx context.Context, open *stores) {
} }
lookerMu.Unlock() lookerMu.Unlock()
}() }()
go l.run(ctx) done := make(chan struct{})
go func() {
defer close(done)
l.run(ctx)
}()
l.ask() l.ask()
return done
} }
// applySilentAfter is how long a machine may say nothing on a send before the plan names it silent: the mesh's // applySilentAfter is how long a machine may say nothing on a send before the plan names it silent: the mesh's
@@ -163,8 +170,10 @@ var applySilentAfter = inventory.ApplySilentAfter
// startRolloutEvents starts what a rollout advances on, for as long as the context lives (novox/hq ADR 0406 §5, issue 974): // startRolloutEvents starts what a rollout advances on, for as long as the context lives (novox/hq ADR 0406 §5, issue 974):
// the wait for each machine's report as a key on the bus that expires (where the bus will not take it a timer of its own // the wait for each machine's report as a key on the bus that expires (where the bus will not take it a timer of its own
// for that wait looks instead), a provisioner client's words, and the look at the plans, asked for first so a plan left // for that wait looks instead), a provisioner client's words, and the look at the plans, asked for first so a plan left
// by a replaced controller resumes. The serving controller's start and the tests of a rollout call this one. // by a replaced controller resumes. The serving controller's start and the tests of a rollout call this one. It answers
func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream) { // a function that waits until the look at the plans has returned, which only a test that ends the context needs (issue
// 1338): the serving controller ignores it.
func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream) (wait func()) {
if err := startRolloutSilent(ctx, js); err != nil { if err := startRolloutSilent(ctx, js); err != nil {
fmt.Printf("plans: the wait for the machines' reports could not be put on the bus (%v); each is looked at by a timer of its own\n", err) fmt.Printf("plans: the wait for the machines' reports could not be put on the bus (%v); each is looked at by a timer of its own\n", err)
} }
@@ -174,7 +183,8 @@ func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream)
if err := startPlanMovedLooks(ctx, js); err != nil { if err := startPlanMovedLooks(ctx, js); err != nil {
fmt.Printf("plans: the plans moved by other processes could not be followed (%v)\n", err) fmt.Printf("plans: the plans moved by other processes could not be followed (%v)\n", err)
} }
startPlanLooks(ctx, open) done := startPlanLooks(ctx, open)
return func() { <-done }
} }
// servingPlanSaved is what the serving controller does for each plan it keeps: said as plan-moved, looked at (a build, a // servingPlanSaved is what the serving controller does for each plan it keeps: said as plan-moved, looked at (a build, a
+9 -3
View File
@@ -179,7 +179,7 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) {
// The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything. // The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything.
first, stopFirst := context.WithCancel(ctx) first, stopFirst := context.WithCancel(ctx)
defer stopFirst() defer stopFirst()
startRolloutEvents(first, open, js) waitFirst := startRolloutEvents(first, open, js)
built := time.Now().UTC() built := time.Now().UTC()
p := inventory.Plan{ID: "plan-974", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2", Created: built, p := inventory.Plan{ID: "plan-974", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2", Created: built,
State: inventory.PlanBuilding, Tiers: [][]string{{"a"}, {"b"}}, State: inventory.PlanBuilding, Tiers: [][]string{{"a"}, {"b"}},
@@ -199,12 +199,15 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) {
// The controller is replaced: its looks and its watcher end. anchor reports meanwhile, to nobody. // The controller is replaced: its looks and its watcher end. anchor reports meanwhile, to nobody.
stopFirst() stopFirst()
waitFirst()
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
reports("anchor") reports("anchor")
second, stopSecond := context.WithCancel(ctx) second, stopSecond := context.WithCancel(ctx)
defer stopSecond() defer stopSecond()
startRolloutEvents(second, open, js) waitSecond := startRolloutEvents(second, open, js)
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
t.Cleanup(func() { stopSecond(); waitSecond() })
// Its first look finds anchor reported, so its key goes; laptop's stays, and is the new controller's to hear expire. // Its first look finds anchor reported, so its key goes; laptop's stays, and is the new controller's to hear expire.
eventually(t, "the new controller's first look deletes the key of the machine that reported", func() bool { eventually(t, "the new controller's first look deletes the key of the machine that reported", func() bool {
h := heldKeys() h := heldKeys()
@@ -321,7 +324,7 @@ func TestAWalkLetGoByTheDeliverVerbIsAskedWithNoOtherEvent(t *testing.T) {
t.Cleanup(func() { inventory.PlanSaved = wasSaved }) t.Cleanup(func() { inventory.PlanSaved = wasSaved })
started, stop := context.WithCancel(ctx) started, stop := context.WithCancel(ctx)
defer stop() defer stop()
startRolloutEvents(started, open, js) wait := startRolloutEvents(started, open, js)
time.Sleep(500 * time.Millisecond) // the first look: the walk waits, and is asked nothing time.Sleep(500 * time.Millisecond) // the first look: the walk waits, and is asked nothing
// The verb as its child process runs it: the delivery command, whose saves are not the serving controller's. // The verb as its child process runs it: the delivery command, whose saves are not the serving controller's.
@@ -333,6 +336,9 @@ func TestAWalkLetGoByTheDeliverVerbIsAskedWithNoOtherEvent(t *testing.T) {
return verbAnswer{}, deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"}) return verbAnswer{}, deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"})
} }
t.Cleanup(func() { runTheVerb = wasRun }) t.Cleanup(func() { runTheVerb = wasRun })
// Registered after every variable cleanup above, so it runs before them (last in, first out): the look at the plans
// has returned when checkEvents, inventory.PlanSaved and runTheVerb are restored (issue 1338).
t.Cleanup(func() { stop(); wait() })
handlers, _, err := seatToolHandlers() handlers, _, err := seatToolHandlers()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -171,7 +171,9 @@ func TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToT
}} }}
first, stopFirst := context.WithCancel(ctx) first, stopFirst := context.WithCancel(ctx)
defer stopFirst() defer stopFirst()
startRolloutEvents(first, open, js) waitFirst := startRolloutEvents(first, open, js)
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
t.Cleanup(func() { stopFirst(); waitFirst() })
if err := inv.SavePlan(ctx, &p); err != nil { if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err) t.Fatal(err)
} }