Hold a node from composing to sending, so a push composed before converge or adopt is never sent after it (hq ADR 0100)

This commit is contained in:
2026-09-22 18:10:04 +02:00
parent 1eff586a40
commit 689c2c6d33
6 changed files with 220 additions and 4 deletions
+30 -4
View File
@@ -282,8 +282,14 @@ func pushCommand(ctx context.Context, args []string) error {
asked = append(asked, n.Name)
}
// Held from composing to sending, so a converge on one of them cannot send between the two
// and be overtaken by what was composed before it (novox/hq ADR 0100).
held, release, err := holdNodes(ctx, open, asked)
if err != nil {
return err
}
sending, refusals := composeEach(asked, func(node string) (sendable, error) {
plan, settings, err := planFor(ctx, open, node)
plan, settings, err := planFor(held, open, node)
if err != nil {
return sendable{}, err
}
@@ -294,10 +300,11 @@ func pushCommand(ctx context.Context, args []string) error {
// The private network is in here with everything else. It used to be composed separately
// and prepended, which meant every machine with an address was on it and no machine could
// be kept off. It is a module now, so it arrives the way a module does.
return declarationWith(ctx, open, node, plan, settings, gens, Allocating)
return declarationWith(held, open, node, plan, settings, gens, Allocating)
})
sentDigest := map[string]string{}
defer release()
for _, s := range sending {
body, err := s.declared.Body()
if err != nil {
@@ -319,6 +326,7 @@ func pushCommand(ctx context.Context, args []string) error {
sentDigest[s.node] = digest
fmt.Printf("sent %s %d resource(s)\n", s.node, len(s.declared.Resources))
}
release()
fmt.Printf("\n%d node(s) told\n", len(sending))
// **A named push leaves the mesh consistent, not just the machine it named** (novox/hq
@@ -373,13 +381,19 @@ func pushCommand(ctx context.Context, args []string) error {
// (novox/hq ADR 0066). The earlier cut routed these through sendTo, which is
// all-or-nothing — so one swept machine's compose error failed the operator's named
// push and skipped its --wait, the very intolerance the main path exists to avoid.
// Held for this round only, and after the last round's were given back, so two pushes
// cascading into each other's machines never each wait on the other.
held, release, err := holdNodes(ctx, open, also)
if err != nil {
return err
}
sending, refused := composeEach(also, func(node string) (sendable, error) {
plan, settings, err := planFor(ctx, open, node)
plan, settings, err := planFor(held, open, node)
if err != nil {
return sendable{}, err
}
reportUnhostable(node, plan)
return declarationWith(ctx, open, node, plan, settings, gens, Allocating)
return declarationWith(held, open, node, plan, settings, gens, Allocating)
})
refusals = append(refusals, refused...)
for _, s := range sending {
@@ -388,17 +402,21 @@ func pushCommand(ctx context.Context, args []string) error {
return err
}
if err := link.Declare(ctx, server.Channel(), ident, s.node, body, 15*time.Second); err != nil {
release()
return err
}
record, err := inv.NodeByName(ctx, s.node)
if err != nil {
release()
return err
}
if err := inv.RecordSent(ctx, record.ID, digestOf(body)); err != nil {
release()
return err
}
fmt.Printf("sent %s %d resource(s)\n", s.node, len(s.declared.Resources))
}
release()
// Every candidate this round is marked handled — the sent ones so they are not
// re-listed, and the refused ones so a machine that cannot be composed does not make
// the loop spin on it for ever. Its refusal is already in the report.
@@ -532,6 +550,14 @@ func sendTo(ctx context.Context, open *stores, names []string) error {
return err
}
// Held from composing to sending (novox/hq ADR 0100); a caller that holds them already —
// converge, which flips the node and then sends it — is not made to wait on itself.
ctx, release, err := holdNodes(ctx, open, names)
if err != nil {
return err
}
defer release()
var sending []readyNode
var refusals []string
for _, name := range names {