A-112: the bus module's snapshots-cap and snapshot-keep-days are config values in a settings file the keeper reads at its start

Builds: design 47 G §633, §681, lead answer A-112

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:02:13 +01:00
parent 6277067465
commit 8d6b7fbbc9
5 changed files with 127 additions and 15 deletions
+32 -11
View File
@@ -227,17 +227,10 @@ func percent(getenv func(string) string, name string, fallback int64) int64 {
return fallback
}
// number reads a positive whole number from the environment, or the fallback.
func number(getenv func(string) string, name string, fallback int64) int64 {
if n, err := strconv.ParseInt(strings.TrimSpace(getenv(name)), 10, 64); err == nil && n > 0 {
return n
}
return fallback
}
// runKeeper is the keeper's process: it returns when ctx ends, or when it has no credential.
func runKeeper(ctx context.Context, getenv func(string) string, logOut io.Writer) error {
logger := log.New(logOut, "nats-keeper: ", log.LstdFlags)
settings := readSettings(getenv("MESH_NATS_SNAPSHOTS_DIR") + "/settings.json")
account, err := process.ReadBrokerAccount(getenv)
if err != nil {
return err
@@ -276,9 +269,10 @@ func runKeeper(ctx context.Context, getenv func(string) string, logOut io.Writer
Warrant: WarrantFrom(conn.Ask), Finding: writer,
Logf: logger.Printf,
Raise: percent(getenv, "MESH_NATS_FULL_RAISE", 75), Clear: percent(getenv, "MESH_NATS_FULL_CLEAR", 70),
// The settings snapshots-cap (20 GiB) and snapshot-keep-days (90) of the manifest (design 47 G §681).
SnapshotsCap: number(getenv, "MESH_NATS_SNAPSHOTS_CAP", keeper.DefaultSnapshotsCap),
SnapshotKeepDays: int(number(getenv, "MESH_NATS_SNAPSHOT_KEEP_DAYS", keeper.DefaultSnapshotKeepDays)),
// The settings snapshots-cap (20 GiB) and snapshot-keep-days (90) of the manifest, read from the file it declares
// at the start, its defaults when the file is missing (design 47 G §681, A-112).
SnapshotsCap: settings.SnapshotsCap,
SnapshotKeepDays: settings.SnapshotKeepDays,
}
// The process answers the seat mesh-broker's snapshot verbs itself (design 47 G §681, A-107), with the caller the bus proved.
stopAnswers, err := conn.AnswerSeat(meshbroker.Seat, snapshotAnswers(k, heldRequest(k.Store), keeperSnapshots(nc, logger.Printf)))
@@ -454,3 +448,30 @@ func requestsOf(c *process.Conn) func(context.Context, func()) error {
})
}
}
// keeperSettings are the keeper's settings its manifest declares as a file (design 47 G A-112): the content of
// `${config:<key>}`, read at its start.
type keeperSettings struct {
SnapshotsCap int64 `json:"snapshots-cap"`
SnapshotKeepDays int `json:"snapshot-keep-days"`
}
// readSettings reads the settings file at path; a file that is missing, or a value that is not above 0, gives its default.
func readSettings(path string) keeperSettings {
s := keeperSettings{SnapshotsCap: keeper.DefaultSnapshotsCap, SnapshotKeepDays: keeper.DefaultSnapshotKeepDays}
raw, err := os.ReadFile(path)
if err != nil {
return s
}
var f keeperSettings
if json.Unmarshal(raw, &f) != nil {
return s
}
if f.SnapshotsCap > 0 {
s.SnapshotsCap = f.SnapshotsCap
}
if f.SnapshotKeepDays > 0 {
s.SnapshotKeepDays = f.SnapshotKeepDays
}
return s
}
+14 -1
View File
@@ -6,6 +6,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
@@ -196,7 +197,19 @@ func allTools() []stdio.Tool {
out = append(out, keeperHealthTool(StatusInState, HeldInState, jsz))
out = append(out, readTools(jsz, LiveMsgGet(LiveBus()))...)
rs := RequestsInState()
out = append(out, snapshotListTool(rs, int(number(os.Getenv, "MESH_NATS_SNAPSHOT_KEEP_DAYS", keeper.DefaultSnapshotKeepDays)))...)
out = append(out, snapshotListTool(rs, bundleKeepDays())...)
out = append(out, plansTool(PlansInState))
return append(out, requestTools(rs)...)
}
// bundleKeepDays is the setting snapshot-keep-days as this bundle's configuration holds it (design 47 G A-112), its
// default when the runtime gives none.
func bundleKeepDays() int {
if raw, err := stdio.Config("snapshot-keep-days"); err == nil {
var n int
if json.Unmarshal(raw, &n) == nil && n > 0 {
return n
}
}
return keeper.DefaultSnapshotKeepDays
}
@@ -8,6 +8,8 @@ import (
"slices"
"strings"
"testing"
"nats-tools/internal/keeper"
)
// The shape ADR 0480 clauses 1, 2, 3 and 5 give the bus's manifest: the upstream image by digest, the mesh's two
@@ -315,3 +317,50 @@ func TestTheKeepersProcessServesTheVerbsItAnswers(t *testing.T) {
t.Errorf("the keeper process serves %v; its code answers %v", served, want)
}
}
// The keeper's snapshot settings are config values of the manifest, written into a file the keeper reads at its start,
// never environment (design 47 G A-112, §681): a missing file, or a value not above 0, gives the default.
func TestTheKeepersSnapshotSettingsAreAFileItsManifestDeclares(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Config map[string]struct{ Default float64 } `json:"config"`
Resources []struct {
ID string `json:"id"`
Path string `json:"path"`
Owner string `json:"owner"`
TakesConfig []string `json:"takes-config"`
Env map[string]string `json:"env"`
} `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Config["snapshots-cap"].Default != 21474836480 || m.Config["snapshot-keep-days"].Default != 90 {
t.Errorf("config %v: snapshots-cap 20 GiB and snapshot-keep-days 90 are the defaults", m.Config)
}
found := false
for _, r := range m.Resources {
if r.ID == "keeper-settings" {
found = r.Path == "${dir:keeper-state}/settings.json" && r.Owner == "nats-keeper" && slices.Equal(r.TakesConfig, []string{"snapshots-cap", "snapshot-keep-days"})
}
for k := range r.Env {
if strings.Contains(k, "SNAPSHOTS_CAP") || strings.Contains(k, "SNAPSHOT_KEEP_DAYS") {
t.Errorf("%s still sets %s in the environment", r.ID, k)
}
}
}
if !found {
t.Errorf("the keeper has no settings file taking snapshots-cap and snapshot-keep-days")
}
dir := t.TempDir()
if got := readSettings(dir + "/none.json"); got.SnapshotsCap != keeper.DefaultSnapshotsCap || got.SnapshotKeepDays != keeper.DefaultSnapshotKeepDays {
t.Errorf("a missing file gives %+v", got)
}
_ = os.WriteFile(dir+"/s.json", []byte(`{"snapshots-cap": 1000, "snapshot-keep-days": 0}`), 0o600)
if got := readSettings(dir + "/s.json"); got.SnapshotsCap != 1000 || got.SnapshotKeepDays != keeper.DefaultSnapshotKeepDays {
t.Errorf("the file gives %+v: the cap it names, the day count's default for 0", got)
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"snapshots-cap": 21474836480,
"snapshot-keep-days": 90
}
+28 -3
View File
@@ -71,6 +71,20 @@
"uses": [
"operator-channel"
],
"config": {
"snapshots-cap": {
"type": "number",
"description": "How many bytes the keeper's snapshots directory may hold (design 47 G §681): past 80 % of it the keeper tells a finding, and it never takes a snapshot past it. The keeper reads it at its start from its settings file.",
"minimum": 1,
"default": 21474836480
},
"snapshot-keep-days": {
"type": "number",
"description": "How many days a snapshot the keeper took before a drop or recreate, and its record, are kept (design 47 G §681, A-112). The keeper reads it at its start from its settings file; the tool nats_snapshots reads it as its configuration.",
"minimum": 1,
"default": 90
}
},
"streams": [
{
"name": "broker-events",
@@ -236,6 +250,20 @@
"place": "mesh",
"unknown": "program"
},
{
"id": "keeper-settings",
"trusted": false,
"type": "file",
"path": "${dir:keeper-state}/settings.json",
"mode": "0600",
"owner": "nats-keeper",
"takes-config": [
"snapshots-cap",
"snapshot-keep-days"
],
"merge": "json",
"template": "files/settings.json"
},
{
"id": "server-conf",
"type": "file",
@@ -278,8 +306,6 @@
"MESH_BROKER_FILE": "${dir:keeper-state}/broker-keeper",
"MESH_NATS_FULL_RAISE": "75",
"MESH_NATS_FULL_CLEAR": "70",
"MESH_NATS_SNAPSHOTS_CAP": "21474836480",
"MESH_NATS_SNAPSHOT_KEEP_DAYS": "90",
"MESH_NATS_SNAPSHOTS_DIR": "${dir:keeper-state}"
},
"serves": [
@@ -389,7 +415,6 @@
"MESH_NATS_CONTAINER": "mesh-broker-nats",
"MESH_NATS_USERS_FILE": "/etc/nats/accounts.conf",
"MESH_NATS_SNAPSHOTS_DIR": "${dir:snapshots}",
"MESH_NATS_SNAPSHOT_KEEP_DAYS": "90",
"NATS_TOOLS_CREDENTIAL": "${dir:tools-secret}/broker-nats-tools"
}
},