diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index 65b4b71e..8cc79fdd 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -250,7 +250,11 @@ func (b *bed) startStoppable(t *testing.T) (<-chan error, func()) { if err := os.WriteFile(broker, cred, 0o600); err != nil { t.Fatal(err) } - env := map[string]string{"MESH_BROKER_FILE": broker, "MESH_ASSIGNER_OUTSIDE_SETTLE_SECONDS": "1"} + settle := filepath.Join(b.dir, "outside-settle-seconds") + if err := os.WriteFile(settle, []byte("1"), 0o600); err != nil { + t.Fatal(err) + } + env := map[string]string{"MESH_BROKER_FILE": broker, "MESH_ASSIGNER_OUTSIDE_SETTLE_FILE": settle} ctx, stop := context.WithCancel(b.ctx) done := make(chan error, 1) go func() { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index fc0366c0..baa43787 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -526,7 +526,10 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, // The wait on an outside cause (outside.go, design 47 G §706): the keeper's three events say when to judge again, and the // seat mesh-wants' wait-held and wait-ended are said; registered before the hub starts. if bus != nil && kvs[buckets.Waits] != nil { - bound, settle := outsideSettings(getenv) + bound, settle, err := outsideSettings(getenv) + if err != nil { + return err + } p.outside = newOutsideActs(bus.CallKept, wake, logger.Printf, bound, settle) for _, subject := range []string{mcseat.SubjectConditionRaised, mcseat.SubjectConditionChanged, mcseat.SubjectConditionCleared} { subject := subject diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go index 6857216e..c8d18551 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go @@ -3,7 +3,10 @@ package main import ( "context" "encoding/json" + "errors" "fmt" + "io/fs" + "os" "strconv" "strings" "sync" @@ -15,9 +18,9 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" - "git.novox.be/novox/mesh-delivery/sdk/catalogkey" meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" + "git.novox.be/novox/mesh-delivery/sdk/catalogkey" "git.novox.be/novox/mesh-sdk/go/seatdef" "git.novox.be/novox/mesh-sdk/go/views" @@ -94,15 +97,42 @@ func newOutsideActs(call func(string, any) (json.RawMessage, error), wake func() } // outsideSettings are the bounds of the wait, the settings outside-wait-minutes and outside-settle-seconds (design 47 G §633, §706), -// from the environment the manifest's config fills; zero for one not set, which is its default. -func outsideSettings(getenv func(string) string) (bound, settle time.Duration) { - if n, err := strconv.Atoi(strings.TrimSpace(getenv("MESH_ASSIGNER_OUTSIDE_WAIT_MINUTES"))); err == nil && n > 0 { - bound = time.Duration(n) * time.Minute +// each read at the start from the file the manifest declares with the content ${config:}, which the environment names +// (A-112, as the builder reads its own); zero for one whose file is missing or empty, which is its default, and an error +// naming the file for a content that is no positive whole number. +func outsideSettings(getenv func(string) string) (bound, settle time.Duration, err error) { + minutes, err := settingFile(getenv("MESH_ASSIGNER_OUTSIDE_WAIT_FILE")) + if err != nil { + return 0, 0, err } - if n, err := strconv.Atoi(strings.TrimSpace(getenv("MESH_ASSIGNER_OUTSIDE_SETTLE_SECONDS"))); err == nil && n > 0 { - settle = time.Duration(n) * time.Second + seconds, err := settingFile(getenv("MESH_ASSIGNER_OUTSIDE_SETTLE_FILE")) + if err != nil { + return 0, 0, err } - return bound, settle + return time.Duration(minutes) * time.Minute, time.Duration(seconds) * time.Second, nil +} + +// settingFile reads one setting's file: 0 for no path, no file or an empty one. +func settingFile(path string) (int, error) { + if path == "" { + return 0, nil + } + raw, err := os.ReadFile(path) + if errors.Is(err, fs.ErrNotExist) { + return 0, nil + } + if err != nil { + return 0, err + } + v := strings.TrimSpace(string(raw)) + if v == "" { + return 0, nil + } + n, err := strconv.Atoi(v) + if err != nil || n < 1 { + return 0, fmt.Errorf("%s holds %q, which is not a positive whole number", path, v) + } + return n, nil } // readStart reads the open conditions of every machine once at the start, for the machines whose own network is held. diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside_test.go index f61e7ba1..690716b4 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside_test.go @@ -3,6 +3,8 @@ package main import ( "context" "encoding/json" + "os" + "path/filepath" "strings" "testing" "time" @@ -209,6 +211,28 @@ func TestTheWaitOnAnOutsideCause(t *testing.T) { t.Errorf("events %v", r.events) } }) + t.Run("its bounds are read at the start from the files the manifest declares (A-112)", func(t *testing.T) { + dir := t.TempDir() + write := func(name, content string) string { + p := filepath.Join(dir, name) + if err := os.WriteFile(p, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + return p + } + env := map[string]string{"MESH_ASSIGNER_OUTSIDE_WAIT_FILE": write("wait", "45\n"), "MESH_ASSIGNER_OUTSIDE_SETTLE_FILE": write("settle", "7")} + if bound, settle, err := outsideSettings(func(k string) string { return env[k] }); err != nil || bound != 45*time.Minute || settle != 7*time.Second { + t.Errorf("bound %v settle %v err %v, want 45m and 7s", bound, settle, err) + } + env["MESH_ASSIGNER_OUTSIDE_WAIT_FILE"], env["MESH_ASSIGNER_OUTSIDE_SETTLE_FILE"] = filepath.Join(dir, "missing"), write("empty", "") + if bound, settle, err := outsideSettings(func(k string) string { return env[k] }); err != nil || bound != 0 || settle != 0 { + t.Errorf("bound %v settle %v err %v, want both defaults", bound, settle, err) + } + env["MESH_ASSIGNER_OUTSIDE_SETTLE_FILE"] = write("bad", "soon") + if _, _, err := outsideSettings(func(k string) string { return env[k] }); err == nil || !strings.Contains(err.Error(), "bad") { + t.Errorf("err %v, want one naming the file", err) + } + }) } func TestAMachineWhoseOwnNetworkIsOpenGetsNoWant(t *testing.T) { diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index 159cfe40..5139cf28 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -142,7 +142,8 @@ func TestManifest(t *testing.T) { "seat:operator-channel.asks"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { // ADR 0668 §4: the ended asks at bind t.Errorf("invokes = %v, want %v", m.Invokes, want) } - // design 47 G §633, §706: the bounds of the wait on an outside cause are `config` keys filling the process's environment. + // design 47 G §633, §706, A-112: the bounds of the wait on an outside cause are `config` keys, each filling a file the + // manifest declares, which the process reads at its start; the environment names only the files. var raw struct { Config map[string]struct { Default any `json:"default"` @@ -152,9 +153,17 @@ func TestManifest(t *testing.T) { if err := json.Unmarshal(file, &raw); err != nil || raw.Config["outside-wait-minutes"].Default != float64(30) || raw.Config["outside-settle-seconds"].Default != float64(90) || len(raw.Config) != 2 { t.Errorf("config = %+v (%v), want outside-wait-minutes of 30 and outside-settle-seconds of 90", raw.Config, err) } - if !strings.Contains(string(file), `"MESH_ASSIGNER_OUTSIDE_WAIT_MINUTES": "${config:outside-wait-minutes}"`) || - !strings.Contains(string(file), `"MESH_ASSIGNER_OUTSIDE_SETTLE_SECONDS": "${config:outside-settle-seconds}"`) { - t.Errorf("the process's env does not carry the two settings from config") + for _, f := range []struct{ env, path, key string }{ + {"MESH_ASSIGNER_OUTSIDE_WAIT_FILE", "${dir:state}/outside-wait-minutes", "outside-wait-minutes"}, + {"MESH_ASSIGNER_OUTSIDE_SETTLE_FILE", "${dir:state}/outside-settle-seconds", "outside-settle-seconds"}, + } { + if !strings.Contains(string(file), `"`+f.env+`": "`+f.path+`"`) || !strings.Contains(string(file), `"path": "`+f.path+`"`) || + !strings.Contains(string(file), `"content": "${config:`+f.key+`}"`) { + t.Errorf("the setting %s is not a file %s the env names as %s", f.key, f.path, f.env) + } + } + if strings.Contains(string(file), `": "${config:outside-`) && strings.Contains(string(file), `"MESH_ASSIGNER_OUTSIDE_WAIT_MINUTES"`) { + t.Errorf("the process's env still carries a setting") } if want := []string{"assignments-copied", "put", "views.asks", "views.copies", "views.differences", "views.findings", "views.assignments", "views.seat-holders", "views.wants"}; !slices.Equal(m.Emits, want) { t.Errorf("emits = %v, want the copy, put and its six views", m.Emits) diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 51e09caf..fe290015 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -180,6 +180,22 @@ "place": ".", "unknown": "program" }, + { + "id": "outside-wait", + "type": "file", + "path": "${dir:state}/outside-wait-minutes", + "owner": "mesh-delivery-assigner", + "mode": "0600", + "content": "${config:outside-wait-minutes}" + }, + { + "id": "outside-settle", + "type": "file", + "path": "${dir:state}/outside-settle-seconds", + "owner": "mesh-delivery-assigner", + "mode": "0600", + "content": "${config:outside-settle-seconds}" + }, { "id": "assigner", "type": "process", @@ -190,8 +206,8 @@ ], "env": { "MESH_BROKER_FILE": "${dir:state}/broker", - "MESH_ASSIGNER_OUTSIDE_WAIT_MINUTES": "${config:outside-wait-minutes}", - "MESH_ASSIGNER_OUTSIDE_SETTLE_SECONDS": "${config:outside-settle-seconds}" + "MESH_ASSIGNER_OUTSIDE_WAIT_FILE": "${dir:state}/outside-wait-minutes", + "MESH_ASSIGNER_OUTSIDE_SETTLE_FILE": "${dir:state}/outside-settle-seconds" }, "user": "mesh-delivery-assigner", "health": {