No change to a machine takes effect unseen (hq ADR 0217, to-be 44)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/no-change-takes-effect-unseen delivered: every member is delivered
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/no-change-takes-effect-unseen delivered: every member is delivered
Two incidents had one shape: a change took effect that nobody saw first (hq issues 241, 304). Three guards, each silent when nothing is at stake: - settings show [--history], and a set that answers each key it adds, changes and removes, and refuses a removal unless --replace; the replaced or cleared layer is kept in settings_history, in the same transaction as the write (migration 0078). - every send keeps a summary of what it sent (no file content), plan <node> --diff compares with it, and push with no machine is refused unless --all. - a push that would give a running container's mount another host directory holds that machine, naming the module, mount and both directories, until push <node> --move <module>; a named push's cascade is held the same way. Rebased onto main and fitted to it: the hold runs before the push says what it recreates, a whole push still says so first and leaves machines waiting for a gate, the verb's push with no machine is still --behind and a push still needs why. The verbs take plan diff, settings replace and history, and push move beside a machine, each refused where it cannot take effect.
This commit is contained in:
@@ -359,14 +359,31 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
// A push by hand is a repair, and says why (novox/hq to-be 45 §7): required through the seat,
|
||||
// recorded when given at a shell — see handacts.go for why a shell is not refused.
|
||||
why := addHandActFlags(set)
|
||||
// Every machine at once is said, not defaulted to (novox/hq ADR 0217): a bare `push` sent the
|
||||
// whole mesh on 2026-10-05 when its usage was wanted.
|
||||
all := set.Bool("all", false, "every machine")
|
||||
// A running module's data moving is sent only when said, per module (novox/hq ADR 0217).
|
||||
move := set.String("move", "", "modules whose data may move with this push, comma-separated")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = positionals
|
||||
if len(args) > 1 {
|
||||
return errors.New("push [<node>] [--behind] — one node, or all of them")
|
||||
return errors.New("push <node> | push --behind | push --all — one machine, the ones behind, or all of them")
|
||||
}
|
||||
if len(args) == 0 && !*behind && !*all {
|
||||
return errors.New("push names a machine, or says --behind (the ones not running what they " +
|
||||
"should) or --all (every machine) — a push to the whole mesh is not the default (novox/hq ADR 0217)")
|
||||
}
|
||||
if *all && (*behind || len(args) == 1) {
|
||||
return errors.New("push --all is every machine; it takes no machine and no --behind")
|
||||
}
|
||||
movable := map[string]bool{}
|
||||
for _, m := range splitModules(*move) {
|
||||
movable[m] = true
|
||||
}
|
||||
var heldBack []string
|
||||
if len(args) == 1 && *behind {
|
||||
// Naming a machine and asking for the ones that need it are two different requests, and
|
||||
// guessing which was meant would sometimes push to a machine somebody did not name.
|
||||
@@ -377,6 +394,12 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
if *behind {
|
||||
recorded = append(recorded, "--behind")
|
||||
}
|
||||
if *all {
|
||||
recorded = append(recorded, "--all")
|
||||
}
|
||||
if *move != "" {
|
||||
recorded = append(recorded, "--move", *move)
|
||||
}
|
||||
why.record(ctx, "push", recorded)
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
@@ -587,22 +610,28 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
})
|
||||
|
||||
defer release()
|
||||
// A machine whose declaration would move a running module's data is held, and only it (novox/hq
|
||||
// ADR 0217): said, with the command that sends it, and left out of everything below.
|
||||
toSend, err := holdingBack(ctx, inv, sending, movable, &heldBack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// **What it recreates, said before it is sent** (novox/hq ADR 0245): a person's push moves every
|
||||
// module whose build changed, and recreates what changed in it — a gated send said so, a push did not.
|
||||
sayWhatAPushRecreates(ctx, open, sending, os.Stdout)
|
||||
sayWhatAPushRecreates(ctx, open, toSend, os.Stdout)
|
||||
// Each machine's memberships first, then the declarations (novox/hq issue 249, ADR 0160): a push
|
||||
// is the one most operators run, and on 2026-10-01 it was the one path that issued none.
|
||||
bus := overTheBus{open: open, server: server, signer: ident}
|
||||
sentDigest, err := deliver(ctx, bus, holder, sending)
|
||||
sentDigest, err := deliver(ctx, bus, holder, toSend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
release()
|
||||
told := make([]string, 0, len(sending))
|
||||
for _, r := range sending {
|
||||
told := make([]string, 0, len(toSend))
|
||||
for _, r := range toSend {
|
||||
told = append(told, r.node)
|
||||
}
|
||||
fmt.Printf("\n%d node(s) told: %s\n", len(sending), strings.Join(told, ", "))
|
||||
fmt.Printf("\n%d node(s) told: %s\n", len(toSend), strings.Join(told, ", "))
|
||||
reportUnheldPushed(os.Stdout, len(args) == 1, asked, unheld)
|
||||
|
||||
// **A named push leaves the mesh consistent, not just the machine it named** (novox/hq
|
||||
@@ -624,7 +653,12 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
for n := range saidHeld {
|
||||
handled[n] = true
|
||||
}
|
||||
refused, err := flushBehind(ctx, open, nodes, handled, composeForPush(open, gens), bus, holder, os.Stdout)
|
||||
// The cascade is a push too, and a machine in it whose data would move is held the same way
|
||||
// (novox/hq ADR 0217).
|
||||
keep := keeping(func(held context.Context, sending []readyNode) ([]readyNode, error) {
|
||||
return holdingBack(held, inv, sending, movable, &heldBack)
|
||||
})
|
||||
refused, err := flushBehind(ctx, open, nodes, handled, composeForPush(open, gens), bus, holder, keep, os.Stdout)
|
||||
refusals = append(refusals, refused...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -639,6 +673,11 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(heldBack) > 0 {
|
||||
sort.Strings(heldBack)
|
||||
return fmt.Errorf("held %s: a running module's data would move — see above; nothing was sent "+
|
||||
"there (novox/hq ADR 0217)", strings.Join(heldBack, ", "))
|
||||
}
|
||||
return couldNotBeResolved(refusals, len(sending))
|
||||
}
|
||||
|
||||
@@ -747,7 +786,7 @@ func composeEach(names []string, allot func(node string) (order, error),
|
||||
// still sent. Their memberships go before their declarations, as every send's do (issue 249).
|
||||
func sendRound(ctx context.Context, open *stores, names []string,
|
||||
compose func(held context.Context, node string) (sendable, error),
|
||||
d delivery, holder string) ([]string, error) {
|
||||
d delivery, holder string, keep keeping) ([]string, error) {
|
||||
held, release, err := holdNodes(ctx, open, names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -756,12 +795,46 @@ func sendRound(ctx context.Context, open *stores, names []string,
|
||||
sending, refused := composeEach(names, allotting(held, open.inventory), func(node string) (sendable, error) {
|
||||
return compose(held, node)
|
||||
})
|
||||
if keep != nil {
|
||||
if sending, err = keep(held, sending); err != nil {
|
||||
return refused, err
|
||||
}
|
||||
}
|
||||
if _, err := deliver(held, d, holder, sending); err != nil {
|
||||
return refused, err
|
||||
}
|
||||
return refused, nil
|
||||
}
|
||||
|
||||
// keeping is what a round of sends keeps of the machines it composed, before any is delivered: nil
|
||||
// keeps them all. A push holds back the machines whose running modules' data would move (holdingBack).
|
||||
type keeping func(held context.Context, sending []readyNode) ([]readyNode, error)
|
||||
|
||||
// holdingBack leaves out each machine whose declaration would move a running module's data, says so,
|
||||
// and names it among those held back (novox/hq ADR 0217); the rest go on to be delivered, grants first
|
||||
// (issue 249). A declaration's body is the same bytes however often it is read.
|
||||
func holdingBack(ctx context.Context, inv *inventory.Inventory, sending []readyNode, movable map[string]bool,
|
||||
heldBack *[]string) ([]readyNode, error) {
|
||||
var out []readyNode
|
||||
for _, s := range sending {
|
||||
body, err := s.declared.Body()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moves, err := heldMoves(ctx, inv, s.node, body, movable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(moves) > 0 {
|
||||
sayDataHeld(os.Stdout, s.node, moves)
|
||||
*heldBack = append(*heldBack, s.node)
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// delivery is the two acts of sending machines what they should be, apart, so the order between
|
||||
// them is one function's and can be read and tested there (novox/hq issue 249).
|
||||
type delivery interface {
|
||||
@@ -1473,6 +1546,16 @@ func recordSent(ctx context.Context, inv *inventory.Inventory, node string, body
|
||||
if err := inv.RecordSentUnder(kept, record.ID, digest, builds, epoch); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// And what it was, summarised, so the next push can be compared with it (novox/hq ADR 0217).
|
||||
// Never a reason to fail a send that is already away: a summary that cannot be kept means the
|
||||
// next comparison has nothing to hold against, which is how every machine starts.
|
||||
if summary, err := summarize(body); err == nil {
|
||||
if raw, err := json.Marshal(summary); err == nil {
|
||||
if err := inv.RecordSentSummary(kept, record.ID, raw); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: what it was sent is not kept for comparison: %v\n", node, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user