push: the wait is the call's or the controller's setting push-wait, never a length in the code (issue 629)
mesh/merge-gate head f09c5ebdec6c merged into trunk 584aa2d7d7a5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head f09c5ebdec6c merged into trunk 584aa2d7d7a5 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head f09c5ebdec6c merged into trunk 584aa2d7d7a5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head f09c5ebdec6c merged into trunk 584aa2d7d7a5 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator's rule: a wait is never hard-coded, at most a default is configured. The push verb takes `wait`; without it the controller's setting `push-wait` (module default 2m) applies; a setting that cannot be read waits for nothing, as a push did before, and says so.
This commit is contained in:
@@ -55,7 +55,7 @@ func TestARepairByHandCarriesItsReason(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"push", map[string]any{"node": "anchor", "why": "stuck", "cause": "sent-not-reported"},
|
{"push", map[string]any{"node": "anchor", "why": "stuck", "cause": "sent-not-reported"},
|
||||||
"push anchor --wait 2m0s --why stuck --cause sent-not-reported"},
|
"push anchor --wait 0s --why stuck --cause sent-not-reported"},
|
||||||
{"plans", map[string]any{"close": "plan-1", "why": "the report will not come"},
|
{"plans", map[string]any{"close": "plan-1", "why": "the report will not come"},
|
||||||
"plans close plan-1 --why the report will not come"},
|
"plans close plan-1 --why the report will not come"},
|
||||||
{"plans", map[string]any{"retry": "plan-1"}, "plans retry plan-1"},
|
{"plans", map[string]any{"retry": "plan-1"}, "plans retry plan-1"},
|
||||||
|
|||||||
@@ -522,9 +522,11 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
|||||||
case "push":
|
case "push":
|
||||||
// A push to a named machine waits for that machine's own answer, so the call's record says
|
// A push to a named machine waits for that machine's own answer, so the call's record says
|
||||||
// how it went: applied, refused with the machine's reason, or which resources failed and why
|
// how it went: applied, refused with the machine's reason, or which resources failed and why
|
||||||
// (novox/hq issue 629). The caller is answered before the push starts (answersFirst), so the
|
// (novox/hq issue 629). How long is the call's `wait`, else the controller's setting `push-wait`;
|
||||||
// wait holds nobody; it is what `calls` then shows. A sweep (--behind) still does not wait: it
|
// neither given waits for nothing, as before. The caller is answered before the push starts
|
||||||
// sends many machines, and the slowest would hold back what is said of the others.
|
// (answersFirst) and the machines are released once sent, so the wait holds nobody; it is what
|
||||||
|
// `calls` then shows. A sweep (--behind) does not wait: the slowest machine would hold back
|
||||||
|
// what is said of the others.
|
||||||
// A push through the seat is a push by hand, and says why (novox/hq to-be 45 §7).
|
// A push through the seat is a push by hand, and says why (novox/hq to-be 45 §7).
|
||||||
if err := need("why"); err != nil {
|
if err := need("why"); err != nil {
|
||||||
return nil, fmt.Errorf("%w: a push by hand is a repair, recorded in the hand-act log with why", err)
|
return nil, fmt.Errorf("%w: a push by hand is a repair, recorded in the hand-act log with why", err)
|
||||||
@@ -537,7 +539,11 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
|||||||
// behind is not read here: given with a machine, it is refused as passed over — naming
|
// behind is not read here: given with a machine, it is refused as passed over — naming
|
||||||
// a machine and asking for every machine behind are two requests, and guessing one
|
// a machine and asking for every machine behind are two requests, and guessing one
|
||||||
// would push a machine nobody named, or not push one somebody did.
|
// would push a machine nobody named, or not push one somebody did.
|
||||||
argv := []string{"push", n, "--wait", seatPushWait.String()}
|
wait, err := pushWait(str("wait"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
argv := []string{"push", n, "--wait", wait.String()}
|
||||||
// A running module's data moving is sent only when said, per module (novox/hq ADR 0217).
|
// A running module's data moving is sent only when said, per module (novox/hq ADR 0217).
|
||||||
if m := str("move"); m != "" {
|
if m := str("move"); m != "" {
|
||||||
argv = append(argv, "--move", m)
|
argv = append(argv, "--move", m)
|
||||||
@@ -1331,10 +1337,46 @@ var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true,
|
|||||||
// 0259 §8): the router asks it before an approval.
|
// 0259 §8): the router asks it before an approval.
|
||||||
"root-free": true}
|
"root-free": true}
|
||||||
|
|
||||||
// seatPushWait is how long a push through the seat waits for the machine it names to report applying what it
|
// pushFileVar names the file the controller's push settings are written to (module.json, `push-wait`).
|
||||||
// was sent (novox/hq issue 629): inside the call's bound with room for the send itself, which took two minutes
|
const pushFileVar = "MESH_PUSH_FILE"
|
||||||
// on 2026-10-13 with four pushes at once.
|
|
||||||
const seatPushWait = 2 * time.Minute
|
// pushWait is how long a push to a named machine waits for its answer (novox/hq issue 629): the call's own
|
||||||
|
// `wait` when given, else the controller's setting `push-wait`, else nothing. No length is written here: the
|
||||||
|
// setting's default is the module's, and a setting that cannot be read waits for nothing, as a push did
|
||||||
|
// before, and is said.
|
||||||
|
func pushWait(given string) (time.Duration, error) {
|
||||||
|
if strings.TrimSpace(given) != "" {
|
||||||
|
d, ok := durationSetting(given)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("push: wait %q is no duration: say it as \"90s\", \"2m\", or \"0\" to wait for nothing", given)
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
path := os.Getenv(pushFileVar)
|
||||||
|
if path == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "the push settings cannot be read (%v); a push waits for nothing\n", err)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
var values map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &values); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "the push settings are not JSON (%v); a push waits for nothing\n", err)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
v, has := values["push-wait"]
|
||||||
|
if !has || v == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
d, ok := durationSetting(v)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "the setting push-wait=%v is no duration; a push waits for nothing\n", v)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
// answersFirst is a command line whose caller is answered before it runs: a push, by its verb or
|
// answersFirst is a command line whose caller is answered before it runs: a push, by its verb or
|
||||||
// through `command`. A push sends the machine holding the bus first when its user list changed, the
|
// through `command`. A push sends the machine holding the bus first when its user list changed, the
|
||||||
|
|||||||
@@ -146,8 +146,7 @@ func TestAnArgumentAVerbDoesNotDeclareIsRefused(t *testing.T) {
|
|||||||
// naming one beside behind is refused rather than one of the two guessed.
|
// naming one beside behind is refused rather than one of the two guessed.
|
||||||
func TestAPushIsOneMachineOrSaysItIsTheWholeMesh(t *testing.T) {
|
func TestAPushIsOneMachineOrSaysItIsTheWholeMesh(t *testing.T) {
|
||||||
argv, err := argvFor("push", map[string]any{"node": "g1", "why": "w"})
|
argv, err := argvFor("push", map[string]any{"node": "g1", "why": "w"})
|
||||||
// A named push waits for the machine's own answer, so the call says how it went (novox/hq issue 629).
|
if err != nil || strings.Join(argv, " ") != "push g1 --wait 0s --why w" {
|
||||||
if err != nil || strings.Join(argv, " ") != "push g1 --wait 2m0s --why w" {
|
|
||||||
t.Fatalf("a named push: %v %v", argv, err)
|
t.Fatalf("a named push: %v %v", argv, err)
|
||||||
}
|
}
|
||||||
for _, args := range []map[string]any{{"why": "w"}, {"behind": "true", "why": "w"}} {
|
for _, args := range []map[string]any{{"why": "w"}, {"behind": "true", "why": "w"}} {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -170,8 +172,8 @@ func TestAVerbMissingWhatItNeedsIsRefused(t *testing.T) {
|
|||||||
// repository given as a forge path is said to be one (issue 176).
|
// repository given as a forge path is said to be one (issue 176).
|
||||||
func TestActsDoNotBlockTheCall(t *testing.T) {
|
func TestActsDoNotBlockTheCall(t *testing.T) {
|
||||||
argv, _ := argvFor("push", map[string]any{"node": "one", "why": "w"})
|
argv, _ := argvFor("push", map[string]any{"node": "one", "why": "w"})
|
||||||
if strings.Join(argv, " ") != "push one --wait 2m0s --why w" || !answersFirst(argv) {
|
if strings.Join(argv, " ") != "push one --wait 0s --why w" || !answersFirst(argv) {
|
||||||
t.Fatalf("a named push does not wait for the machine's answer, or holds its caller: %v", argv)
|
t.Fatalf("a named push with no setting waits, or holds its caller: %v", argv)
|
||||||
}
|
}
|
||||||
argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"})
|
argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"})
|
||||||
if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" {
|
if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" {
|
||||||
@@ -462,3 +464,40 @@ func TestAQuotedValueRoundTrips(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A named push waits as long as the call says, else as long as the controller's setting push-wait says, else
|
||||||
|
// not at all: no length is the code's own (novox/hq issue 629).
|
||||||
|
func TestAPushWaitsAsTheCallOrTheSettingSays(t *testing.T) {
|
||||||
|
file := filepath.Join(t.TempDir(), "push.json")
|
||||||
|
if err := os.WriteFile(file, []byte(`{"push-wait": "2m"}`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv(pushFileVar, file)
|
||||||
|
argv, err := argvFor("push", map[string]any{"node": "one", "why": "w"})
|
||||||
|
if err != nil || strings.Join(argv, " ") != "push one --wait 2m0s --why w" {
|
||||||
|
t.Fatalf("the setting was not the default: %v %v", argv, err)
|
||||||
|
}
|
||||||
|
argv, err = argvFor("push", map[string]any{"node": "one", "why": "w", "wait": "30s"})
|
||||||
|
if err != nil || strings.Join(argv, " ") != "push one --wait 30s --why w" {
|
||||||
|
t.Fatalf("the call's wait did not win over the setting: %v %v", argv, err)
|
||||||
|
}
|
||||||
|
argv, err = argvFor("push", map[string]any{"node": "one", "why": "w", "wait": "0"})
|
||||||
|
if err != nil || strings.Join(argv, " ") != "push one --wait 0s --why w" {
|
||||||
|
t.Fatalf("a wait of 0 did not wait for nothing: %v %v", argv, err)
|
||||||
|
}
|
||||||
|
if _, err := argvFor("push", map[string]any{"node": "one", "why": "w", "wait": "soon"}); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "no duration") {
|
||||||
|
t.Fatalf("a wait that is no duration was taken: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(file, []byte(`{"push-wait": "later"}`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
argv, err = argvFor("push", map[string]any{"node": "one", "why": "w"})
|
||||||
|
if err != nil || strings.Join(argv, " ") != "push one --wait 0s --why w" {
|
||||||
|
t.Fatalf("an unreadable setting did not fall back to waiting for nothing: %v %v", argv, err)
|
||||||
|
}
|
||||||
|
argv, _ = argvFor("push", map[string]any{"why": "w"})
|
||||||
|
if strings.Join(argv, " ") != "push --behind --wait 0 --why w" {
|
||||||
|
t.Fatalf("a sweep waits: %v", argv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func TestTheVerbsCarryReadReplaceAndMove(t *testing.T) {
|
|||||||
{"settings", map[string]any{"module": "plex", "history": "true"}, []string{"settings", "show", "plex", "--history"}},
|
{"settings", map[string]any{"module": "plex", "history": "true"}, []string{"settings", "show", "plex", "--history"}},
|
||||||
{"settings", map[string]any{"module": "plex", "values": "{}", "replace": "true"}, []string{"settings", "set", "plex", "{}", "--replace"}},
|
{"settings", map[string]any{"module": "plex", "values": "{}", "replace": "true"}, []string{"settings", "set", "plex", "{}", "--replace"}},
|
||||||
{"push", map[string]any{"node": "home", "move": "plex", "why": "w"},
|
{"push", map[string]any{"node": "home", "move": "plex", "why": "w"},
|
||||||
[]string{"push", "home", "--wait", "2m0s", "--move", "plex", "--why", "w"}},
|
[]string{"push", "home", "--wait", "0s", "--move", "plex", "--why", "w"}},
|
||||||
{"push", map[string]any{"why": "w"}, []string{"push", "--behind", "--wait", "0", "--why", "w"}},
|
{"push", map[string]any{"why": "w"}, []string{"push", "--behind", "--wait", "0", "--why", "w"}},
|
||||||
{"plan", map[string]any{"node": "home", "diff": "true"}, []string{"plan", "home", "--diff"}},
|
{"plan", map[string]any{"node": "home", "diff": "true"}, []string{"plan", "home", "--diff"}},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -217,14 +217,16 @@ var ControllerVerbs = []Verb{
|
|||||||
Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})},
|
Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})},
|
||||||
{Name: "push", Description: "Send one machine everything it should be. With no machine named it is a push of the " +
|
{Name: "push", Description: "Send one machine everything it should be. With no machine named it is a push of the " +
|
||||||
"WHOLE mesh — every machine that is behind — and the answer says so first; behind says that outright. " +
|
"WHOLE mesh — every machine that is behind — and the answer says so first; behind says that outright. " +
|
||||||
"Answers at once that it is running, with a call id: `calls` with that id says what it sent " +
|
"Answers at once that it is running, with a call id: `calls` with that id says what it sent and, for a " +
|
||||||
"(a push can reload the bus, which then refuses any answer still to come). A push by hand is a repair, " +
|
"machine named, how that machine applied it — applied, refused with its reason, or each failed resource — " +
|
||||||
|
"once it reports within wait (a push can reload the bus, which then refuses any answer still to come). A push by hand is a repair, " +
|
||||||
"and says why: recorded in the hand-act log (novox/hq to-be 45 §7). A machine whose push would give a " +
|
"and says why: recorded in the hand-act log (novox/hq to-be 45 §7). A machine whose push would give a " +
|
||||||
"running module's data another directory is held and named, the others sent (novox/hq ADR 0217): read " +
|
"running module's data another directory is held and named, the others sent (novox/hq ADR 0217): read " +
|
||||||
"`plan` with diff, and name the machine with move to send it.",
|
"`plan` with diff, and name the machine with move to send it.",
|
||||||
Input: schema(map[string]string{
|
Input: schema(map[string]string{
|
||||||
"node": "the machine's name; without it, every machine that is behind",
|
"node": "the machine's name; without it, every machine that is behind",
|
||||||
"move": "with node: the modules whose data may move with this push, comma-separated (optional)",
|
"move": "with node: the modules whose data may move with this push, comma-separated (optional)",
|
||||||
|
"wait": "with node: how long the call waits for the machine to report how it applied what it was sent, as \"90s\" or \"2m\"; \"0\" waits for nothing; without it, the controller's setting push-wait (optional)",
|
||||||
"behind": "\"true\": every machine that is behind, the whole mesh — the same as naming none, said outright; not with node",
|
"behind": "\"true\": every machine that is behind, the whole mesh — the same as naming none, said outright; not with node",
|
||||||
"why": "why this is pushed by hand: recorded in the hand-act log",
|
"why": "why this is pushed by hand: recorded in the hand-act log",
|
||||||
"cause": "the cause in a word, or a condition's kind — the word a second push for the same reason uses (optional)",
|
"cause": "the cause in a word, or a condition's kind — the word a second push for the same reason uses (optional)",
|
||||||
|
|||||||
+15
-1
@@ -35,6 +35,11 @@
|
|||||||
"kind": "preference",
|
"kind": "preference",
|
||||||
"default": "10m",
|
"default": "10m",
|
||||||
"why": "a busy period never holds a batch longer than this after its first merge (novox/hq ADR 0276)"
|
"why": "a busy period never holds a batch longer than this after its first merge (novox/hq ADR 0276)"
|
||||||
|
},
|
||||||
|
"push-wait": {
|
||||||
|
"kind": "preference",
|
||||||
|
"default": "2m",
|
||||||
|
"why": "how long a push to a named machine waits for that machine's answer, so its call says how it went; a call's own wait overrides it, and 0 waits for nothing (novox/hq issue 629)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": [
|
"tools": [
|
||||||
@@ -120,6 +125,14 @@
|
|||||||
"owner": "mesh-controller",
|
"owner": "mesh-controller",
|
||||||
"content": "{\n \"merge-window\": \"${setting:merge-window}\",\n \"merge-window-at-most\": \"${setting:merge-window-at-most}\"\n}\n"
|
"content": "{\n \"merge-window\": \"${setting:merge-window}\",\n \"merge-window-at-most\": \"${setting:merge-window-at-most}\"\n}\n"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "push",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:mesh-state}/push.json",
|
||||||
|
"mode": "0600",
|
||||||
|
"owner": "mesh-controller",
|
||||||
|
"content": "{\n \"push-wait\": \"${setting:push-wait}\"\n}\n"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "controller",
|
"id": "controller",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
@@ -144,7 +157,8 @@
|
|||||||
"MESH_BROKER_ADDRESS_PORT": "${seat:mesh-broker:5671}",
|
"MESH_BROKER_ADDRESS_PORT": "${seat:mesh-broker:5671}",
|
||||||
"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus",
|
"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus",
|
||||||
"MESH_CONTROLLER_VERSION": "${version}",
|
"MESH_CONTROLLER_VERSION": "${version}",
|
||||||
"MESH_MERGE_WINDOW_FILE": "${dir:mesh-state}/merge-window.json"
|
"MESH_MERGE_WINDOW_FILE": "${dir:mesh-state}/merge-window.json",
|
||||||
|
"MESH_PUSH_FILE": "${dir:mesh-state}/push.json"
|
||||||
},
|
},
|
||||||
"replaces": [
|
"replaces": [
|
||||||
"server"
|
"server"
|
||||||
|
|||||||
Reference in New Issue
Block a user