A gate judges its own send and the build it sent, and never puts the controller back behind its store (hq issue 352)
On 2026-10-09 a release's gate on the control node read the machine's
report against a newer send another plan had just made there, failed
three builds the machine had reported healthy, and put them back on
every machine to a controller older than the store's schema; that
controller then passed the newer plan's gate from its own health.
- A gate keeps what its send carried (digest, sequence) and reads the
report against it; a report on the last send is on it too.
- A gate judges only the build the machine was last sent: another build
there supersedes the judging — no verdict, nothing put back.
- A controller is told its build (MESH_CONTROLLER_VERSION, ${version}
in a process's env) and records how far it reads the store's schema;
a put-back to a build that reaches less, or never said, is refused
and the current build kept, said as urgent.
- A release's open gate holds other sends of its modules there, and a
plan's own first send waits on it.
This commit is contained in:
@@ -180,12 +180,35 @@ func (f moveFacts) moves(node string, modules []string, sent map[string]string,
|
||||
return out
|
||||
}
|
||||
|
||||
// walkedBy is the open plan that has started walking a module's build — sent it to a first machine,
|
||||
// not yet passed — other than to this machine; empty when none does.
|
||||
func (f moveFacts) walkedBy(module, node string) string {
|
||||
// walkedBy is the open plan that has started walking a module's build — sent it to a first machine, not
|
||||
// yet passed — and whose walk this move would cross; empty when none does. A move of the same build to a
|
||||
// machine that plan already sent it is not a crossing: the build is there. A move of **another** build of
|
||||
// the module to that machine is (novox/hq issue 352): on 2026-10-09 a merge's plan sent the control node a
|
||||
// newer controller while a release's gate was judging the controller there, the release's gate read the
|
||||
// machine's report against the newer send, failed three builds and put them back under the new plan's
|
||||
// feet. A release keeps no module records: its open gate's carried moves are its walk.
|
||||
func (f moveFacts) walkedBy(module, node, to string) string {
|
||||
for _, p := range f.plans {
|
||||
if !p.Open() {
|
||||
continue
|
||||
}
|
||||
if p.Release != nil {
|
||||
g := p.Release.Gate
|
||||
if g == nil || g.Verdict != "" {
|
||||
continue
|
||||
}
|
||||
for _, c := range g.Carried {
|
||||
if c.Module == module && !(slices.Contains(g.Machines, node) && sameCommit(c.To, to)) {
|
||||
return p.ID
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
s, holds := p.Modules[module]
|
||||
if !p.Open() || !holds || s == nil || s.FirstAt == nil || s.SentAt != nil || slices.Contains(s.First, node) {
|
||||
if !holds || s == nil || s.FirstAt == nil || s.SentAt != nil {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(s.First, node) && sameCommit(s.Commit, to) {
|
||||
continue
|
||||
}
|
||||
if s.Gate != nil && s.Gate.Verdict == inventory.GatePassed {
|
||||
@@ -277,11 +300,19 @@ func gatedSend(ctx context.Context, open *stores, node string, owns []inventory.
|
||||
if own(mv.Module) {
|
||||
continue
|
||||
}
|
||||
if id := f.walkedBy(mv.Module, node); id != "" {
|
||||
if id := f.walkedBy(mv.Module, node, mv.To); id != "" {
|
||||
return nil, nil, fmt.Errorf("%w: %s's build %s waits on %s, which %s is walking", errWalkedElsewhere,
|
||||
mv.Module, short(mv.To), node, id)
|
||||
}
|
||||
}
|
||||
// **And the plan's own modules wait too** (novox/hq issue 352): a walk of the same module by another
|
||||
// plan, or a release, on this machine is not crossed with a newer build; this send waits for its gate.
|
||||
for _, o := range owns {
|
||||
if id := f.walkedBy(o.Module, node, o.To); id != "" {
|
||||
return nil, nil, fmt.Errorf("%w: %s's build %s waits on %s, which %s is walking", errWalkedElsewhere,
|
||||
o.Module, short(o.To), node, id)
|
||||
}
|
||||
}
|
||||
for _, o := range owns {
|
||||
i := slices.IndexFunc(moves, func(mv inventory.CarriedMove) bool { return mv.Module == o.Module })
|
||||
switch {
|
||||
@@ -526,7 +557,7 @@ func waitingMoves(ctx context.Context, open *stores, all bool) (map[string][]inv
|
||||
return nil, err
|
||||
}
|
||||
for _, mv := range moves {
|
||||
if f.walkedBy(mv.Module, n.Name) == "" {
|
||||
if f.walkedBy(mv.Module, n.Name, mv.To) == "" {
|
||||
out[n.Name] = append(out[n.Name], mv)
|
||||
}
|
||||
}
|
||||
@@ -658,7 +689,7 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
|
||||
r.Next++
|
||||
continue
|
||||
}
|
||||
r.Gate = &inventory.PlanGate{Machines: sent, Since: &now, Carried: moves}
|
||||
r.Gate = &inventory.PlanGate{Machines: sent, Since: &now, Carried: moves, Sent: sentNow(ctx, open.inventory, sent)}
|
||||
p.Note = fmt.Sprintf("sent %s %d build(s) that waited for a gate; judging them there", node, len(moves))
|
||||
if said := recreationsSaid(moves); said != "" {
|
||||
p.Note += "; " + said
|
||||
@@ -693,6 +724,15 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
|
||||
r.Next++
|
||||
r.Gate = nil
|
||||
return true, nil
|
||||
case inventory.GateSuperseded:
|
||||
// Another send moved a judged module on the judged machine (novox/hq issue 352): no verdict on what
|
||||
// was carried, nothing put back, and this release ends; the builds still waiting are released again
|
||||
// by the next pass, judged afresh.
|
||||
p.State = inventory.PlanSuperseded
|
||||
p.Note = fmt.Sprintf("superseded on %s: %s — nothing judged, nothing put back; what still waits is released again",
|
||||
strings.Join(g.Machines, ", "), g.Why)
|
||||
fmt.Printf("%s: %s\n", p.ID, p.Note)
|
||||
return true, nil
|
||||
}
|
||||
p.Note = ""
|
||||
batched, back := batchingRollbacks(ctx)
|
||||
|
||||
Reference in New Issue
Block a user