issue 974: a rollout advances on events (a look asked by each plan saved, report and expired silent key), the 30 s ticker goes, and each tier send is said as sent

This commit is contained in:
2026-10-17 13:28:34 +02:00
parent fd522f4322
commit 8af77c2c75
13 changed files with 931 additions and 64 deletions
+32 -50
View File
@@ -24,9 +24,9 @@ import (
//
// The handler that hears the merge computes the plan from the catalogue's one dependency relation,
// writes it to the store, asks the first tier and returns — the receive loop is never held by a
// build. Every outcome taken in advances the plan it belongs to; a ticker advances what outcomes
// alone cannot (a tier waiting for machines to report); a controller replaced mid-plan finds the
// plan where it left it.
// build. Every outcome taken in advances the plan it belongs to; a look at the plans, asked for by each event that can
// move one (plan_looks.go, ADR 0406 §5), advances what outcomes alone cannot (a tier waiting for machines to report);
// a controller replaced mid-plan finds the plan where it left it and looks first.
// 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.
@@ -668,29 +668,6 @@ func askedAny(p inventory.Plan) bool {
return false
}
// advancePlans moves every open plan as far as the facts allow: a tier whose modules are all built
// and whose gates are applied gives way to the next; the last tier done is the plan done. Called
// after every outcome and on a timer, so a plan waiting on a machine's report moves when it comes.
//
// **One controller at a time** (novox/hq issue 213). A plan is read, changed and saved whole; two
// controllers — the old and the new while a machine hands its controller over — would each ask a
// tier the other had just asked. Taken without waiting: whoever holds the plans is moving them.
func advancePlans(ctx context.Context, open *stores) {
release, err := open.inventory.HoldPlans(ctx, false)
if err != nil {
if !errors.Is(err, inventory.ErrPlansBusy) {
fmt.Printf("plans: cannot hold them: %v\n", err)
}
return
}
defer release()
// What waits for a gate, released one machine first, then the rest at once (ADR 0236, ADR 0353).
if _, err := releaseBacklog(ctx, open, ""); err != nil {
fmt.Printf("plans: what waits for a gate could not be looked at: %v\n", err)
}
advanceHeld(ctx, open)
}
// keepWalkPhases keeps where the walks that ended lately spent their time (novox/hq ADR 0282), outside the hold
// on the plans so it never lengthens it: measured, never acted on, and an error only said.
func keepWalkPhases(ctx context.Context, open *stores) {
@@ -699,8 +676,13 @@ func keepWalkPhases(ctx context.Context, open *stores) {
}
}
// advanceHeld is advancePlans for a caller already holding the plans.
func advanceHeld(ctx context.Context, open *stores) {
// advanceHeld moves every open plan as far as the facts allow, for a caller holding the plans: a tier whose modules
// are all built and whose gates are applied gives way to the next; the last tier done is the plan done. It says
// whether a step failed, which the look at the plans (lookAtPlans) retries.
//
// **One controller at a time** (novox/hq issue 213). A plan is read, changed and saved whole; two controllers — the
// old and the new while a machine hands its controller over — would each ask a tier the other had just asked.
func advanceHeld(ctx context.Context, open *stores) (failed bool) {
inv := open.inventory
// A walk that ended lets the next batch be cut at once, not at a window's next expiry (novox/hq ADR 0276, ADR 0407).
defer func() {
@@ -711,15 +693,15 @@ func advanceHeld(ctx context.Context, open *stores) {
plans, err := inv.OpenPlans(ctx)
if err != nil {
fmt.Printf("plans: cannot read them: %v\n", err)
return
return true
}
if len(plans) == 0 {
return
return false
}
edges, err := inv.Dependencies(ctx)
if err != nil {
fmt.Printf("plans: cannot read the dependencies: %v\n", err)
return
return true
}
rollsOut := func(module string) bool {
u, err := inv.UpgradeOf(ctx, module)
@@ -733,10 +715,12 @@ func advanceHeld(ctx context.Context, open *stores) {
// stood still for half an hour was written every few seconds, its revision moved, `plan-moved`
// said on the bus, and its last save read as when it began building.
before := planSnapshot(*p)
moved, err := advanceOnce(ctx, open, p, edges, rollsOut)
sends := &sendsMade{}
moved, err := advanceOnce(withSendsMade(ctx, sends), open, p, edges, rollsOut)
if err != nil {
failed = true
// Kept in the plan, so `plans` says why it has not moved rather than the log alone;
// the state is left as it was and the step is tried again on the next tick. Said and
// the state is left as it was and the step is tried again by the next look. Said and
// kept when it is new: the same refusal on every tick is one fact, not one per tick.
p.Note = "tier " + fmt.Sprint(p.Tier) + ": " + err.Error() + " — tried again"
// A refusal for the bus's planned step is a person's to end: S17 says it from its first moment.
@@ -757,14 +741,18 @@ func advanceHeld(ctx context.Context, open *stores) {
if planSnapshot(*p) != before {
if err := inv.SavePlan(ctx, p); err != nil {
fmt.Printf("%s: cannot keep the plan: %v\n", p.ID, err)
failed = true
break
}
// Said once the send is kept (ADR 0406 §5, issue 974): what an outsider hears of a tier sent.
saySends(ctx, *p, sends)
}
if !moved {
break
}
}
}
return failed
}
// planSnapshot is a plan as a save would write it, to compare before and after a step: what the store
@@ -1066,6 +1054,16 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
p.Modules[m].SentAt = &now
p.Modules[m].Rest = restOf(restTo[m], sent, sentWhat)
}
// One `sent` for each machine, with what it was sent (ADR 0406 §5, issue 974).
for _, n := range sent {
var carried []string
for _, m := range rest {
if slices.Contains(restTo[m], n) {
carried = append(carried, m)
}
}
noteSends(ctx, p.Tier, []string{n}, carried, now)
}
fmt.Printf("%s: tier %d built; sent %s to %s, one send each\n", p.ID, p.Tier, strings.Join(rest, ", "),
strings.Join(sent, ", "))
return true, nil
@@ -1338,6 +1336,7 @@ func firstSend(ctx context.Context, open *stores, p *inventory.Plan, node string
s.GatedBy = lead
}
}
noteSends(ctx, p.Tier, sent, modules, now)
p.State = inventory.PlanRolling
p.Note = fmt.Sprintf("tier %d built; sent %s to %s first, in one send", p.Tier, strings.Join(modules, ", "),
strings.Join(sent, ", "))
@@ -1652,23 +1651,6 @@ func sayUnsent(p *inventory.Plan, rollsOut func(string) bool) {
p.Note += "; built and never sent: " + strings.Join(unsent, ", ") + " — `push --behind` sends them"
}
// planTicker advances open plans on a timer, for the steps outcomes alone cannot take.
func planTicker(ctx context.Context, open *stores) {
advancePlans(ctx, open)
keepWalkPhases(ctx, open)
tick := time.NewTicker(30 * time.Second)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
advancePlans(ctx, open)
keepWalkPhases(ctx, open)
}
}
}
// planLine is one plan as `status` says it, given the least bound of a tier.
func planLine(p inventory.Plan, now time.Time) string {
return planLineWith(p, now, pauseView{}, tierAtLeast)