Refuse a send that puts the controller or node-engine behind its machine's build (issue 201)
mesh/merge-gate out of date: judged against trunk f10aedac5880, the trunk is now f8daec01d3f0; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk f10aedac5880, the trunk is now f8daec01d3f0; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request

On 2026-10-02 a plan sent the control node a controller older than the one
running there, whose seat row the newer build had widened; the older binary
refused the row and the mesh went quiet for ten minutes. ADR 0185 took the
outage out of it, and the race stayed open.

Every send but a person's push now refuses a move of mesh-controller or
mesh-host to a build asked before the one its machine was last sent (the
builds' registration order, issue 219), whatever composed it — gated or not.
A gate's own put-back is the one exception, for the modules it puts back,
and says so in its send's scope. The refusal is the send's error, which the
plan keeps as its note, and a warning condition on the machine, cleared by
the next send there that carries no such move.
This commit is contained in:
jochen
2026-10-12 18:21:17 +02:00
parent 1e28342ede
commit 9038d4b0d3
5 changed files with 390 additions and 6 deletions
+34 -4
View File
@@ -51,6 +51,9 @@ type sendScope struct {
modules map[string]bool
// person is a person's act: it carries what it carries.
person bool
// puttingBack is a gate's own put-back of its modules: the one send that may carry a core module to a build
// older than the one its machine runs (novox/hq issue 201).
puttingBack bool
}
type sendScopeKey struct{}
@@ -248,9 +251,11 @@ func ungatedIn(ctx context.Context, open *stores, names []string, addedHolder st
if err != nil {
return nil, err
}
var kept, refused []string
var kept, refused, older []string
for _, n := range names {
moves, err := machineMoves(ctx, open, f, n, false)
// Every move, gated or not: a move to a build that passed a gate elsewhere is no gate's concern here, and
// may still be a core module's downgrade (novox/hq issue 201).
every, err := machineMoves(ctx, open, f, n, true)
if err != nil {
return nil, err
}
@@ -262,11 +267,33 @@ func ungatedIn(ctx context.Context, open *stores, names []string, addedHolder st
return nil, err
}
}
var waiting []string
for _, mv := range moves {
var made []inventory.CarriedMove
for _, mv := range every {
if was, kept := keeps[mv.Module]; kept && sameCommit(was, mv.From) {
continue
}
made = append(made, mv)
}
// **No send but a gate's own put-back moves the controller or the node-engine to an older build than
// the one its machine runs** (novox/hq issue 201).
downgrades, err := refuseCoreDowngrades(ctx, open.inventory, scope, n, made)
if err != nil {
return nil, err
}
if len(downgrades) > 0 {
if n == addedHolder {
fmt.Printf("%s holds the bus and its user list changed, and it is not sent now: %s\n", n,
strings.Join(downgrades, "; "))
continue
}
older = append(older, downgrades...)
continue
}
var waiting []string
for _, mv := range made {
if f.gated(mv.Module, mv.To) {
continue
}
if !scope.judged[n] && !scope.modules[mv.Module] {
waiting = append(waiting, fmt.Sprintf("%s %s → %s", mv.Module, short(mv.From), short(mv.To)))
}
@@ -281,6 +308,9 @@ func ungatedIn(ctx context.Context, open *stores, names []string, addedHolder st
refused = append(refused, fmt.Sprintf("%s (%s)", n, strings.Join(waiting, ", ")))
}
}
if len(older) > 0 {
return nil, fmt.Errorf("%w: %s", errCoreDowngrade, strings.Join(older, "; "))
}
if len(refused) > 0 {
return nil, fmt.Errorf("%w: %s — a walk sends them, one machine at a time, each judged (novox/hq ADR "+
"0236); `upgrade backlog` lists them", errUngated, strings.Join(refused, "; "))