Z4 zfs-manifest-bus: the events stream, states and ledger by template, the keeper as the module's one process bus account

The three root processes declare no bus and reach the keeper's socket; the module invokes the service manager's start. The judge refuses the keeper's bus emits and state until the validator accepts what the module asks for in requires (A-38).

Builds: design 42 zfs-owns-zfs §25, §26, §27, §28, A-6, A-19, A-36, A-37, A-38
This commit is contained in:
2026-11-13 01:05:19 +01:00
parent ec27f12524
commit 8927d68900
2 changed files with 247 additions and 7 deletions
+83 -2
View File
@@ -76,8 +76,8 @@ func TestTheManifestNamesTheToolsTheBundleServesAndWhatEachReplaces(t *testing.T
if len(m.Claims) != 0 {
t.Errorf("the module claims no seat: %v", m.Claims)
}
if strings.Join(m.Emits, ",") != "snapshot.taken,"+ScrubEvent {
t.Errorf("emits %v", m.Emits)
if len(m.Emits) != 0 {
t.Errorf("the events are asked for by template in requires: emits %v", m.Emits)
}
if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen)\b|/home/`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" {
t.Errorf("the manifest says %q", hit)
@@ -459,3 +459,84 @@ func TestTheBundleRunsOnlyTheFormsOfADR0679(t *testing.T) {
t.Errorf("found %d call sites; the test no longer sees them", sites)
}
}
// §25, §26 (A-19, A-36), A-37, A-38, §28: one events stream by template, 30 days, every kind of §25; the states applied,
// schedule and findings; the ledger snapshots of A-36 with a fixed cap and no index; the keeper alone holds the process bus, as a user of
// the module's own, naming the module's events and states and invoking the three; the three root processes declare no bus and
// reach the keeper's socket; the module invokes the service manager's start; the tools list holds §28's.
func TestTheManifestsBusStateKeeperAndTools(t *testing.T) {
tree, byID, _ := manifestOf(t)
reqs := map[string]map[string]any{}
for _, r := range tree["requires"].([]any) {
e := r.(map[string]any)
reqs[e["name"].(string)] = e
}
ev := reqs["events"]
opts, _ := ev["options"].(map[string]any)
var kinds []string
for _, k := range opts["events"].([]any) {
kinds = append(kinds, k.(string))
}
if ev["storage"] != "stream" || ev["template"] != "events" || opts["max-age-days"] != float64(30) || strings.Join(kinds, ",") != strings.Join(EventKinds, ",") {
t.Errorf("the events stream: %v", ev)
}
for _, n := range []string{"applied", "schedule", "findings"} {
if e := reqs[n]; e == nil || e["storage"] != "key-value" || e["template"] != "state" {
t.Errorf("the state %s: %v", n, e)
}
}
if reqs["findings"]["options"] != nil {
t.Errorf("findings declares no stream of its own (A-32): %v", reqs["findings"])
}
l := reqs[LedgerName]
lo, _ := l["options"].(map[string]any)
types, _ := l["types"].([]any)
if l["storage"] != "ledger" || l["template"] != "ledger" || lo["max-mib"] == nil || len(types) != 1 {
t.Fatalf("the ledger: %v", l)
}
ty := types[0].(map[string]any)
ops, _ := json.Marshal(ty["operations"])
if ty["name"] != "snapshot" || ty["indexes"] != nil ||
string(ops) != `[{"effect":"set-entry","fields":["dataset","name","kind","at"],"into":"taken","key":"name","name":"taken"},{"effect":"remove","into":"taken","key":"name","name":"removed"}]` {
t.Errorf("the ledger's type: %v", ty)
}
k := byID["keeper"]
bus, _ := k["bus"].(map[string]any)
acct := byID["keeper-account"]
if k["user"] != "zfs-keeper" || acct["type"] != "user" || acct["name"] != "zfs-keeper" || bus == nil ||
fmt.Sprint(bus["state"]) != "[applied schedule findings]" ||
fmt.Sprint(bus["invokes"]) != "["+ToolRecord+" "+ToolSchedule+" seat:node-service-manager.start]" {
t.Errorf("the keeper: %v %v", k, acct)
}
if got := fmt.Sprint(bus["emits"]); strings.Contains(got, ScrubEvent) || !strings.Contains(got, EventApplied) || !strings.Contains(got, EventTakenSchedule) {
t.Errorf("the keeper's events: %v", got)
}
for _, id := range []string{"apply", "events", "run"} {
r := byID[id]
if r["bus"] != nil || fmt.Sprint(r["env"]) != "map["+KeeperSocketEnv+":${dir:keeper-state}/keeper.sock]" {
t.Errorf("%s declares a bus or does not reach the keeper's socket: %v", id, r)
}
}
for _, id := range []string{"watch", "keeper-state"} {
if byID[id]["bus"] != nil {
t.Errorf("%s", id)
}
}
if byID["keeper-state"]["owner"] != "zfs-keeper" {
t.Errorf("the keeper's folder: %v", byID["keeper-state"])
}
if fmt.Sprint(tree["invokes"]) != "[seat:node-service-manager.start]" {
t.Errorf("invokes: %v", tree["invokes"])
}
m, _ := readManifest(t)
for _, tool := range []string{"zfs_config", "zfs_config_apply", "zfs_snapshots_schedule", "zfs_snapshots_run_now", "zfs_snapshots_prune_now", "zfs_snapshots_pause",
"zfs_snapshots_resume", "zfs_module_events", "zfs_snapshots_check", "zfs_findings", "zfs_snapshots_record"} {
found := false
for _, x := range m.Tools {
found = found || x == tool
}
if !found {
t.Errorf("%s is not in tools", tool)
}
}
}
+164 -5
View File
@@ -5,9 +5,103 @@
"package-manager",
"service-manager"
],
"emits": [
"snapshot.taken",
"scrub.requested"
"invokes": [
"seat:node-service-manager.start"
],
"own-secrets": {
"broker-keeper": {
"path": "${dir:keeper-state}/broker-keeper.secret",
"owner": "zfs-keeper"
}
},
"requires": [
{
"name": "events",
"storage": "stream",
"template": "events",
"options": {
"events": [
"config.applied",
"config.refused",
"config.waiting",
"snapshot.taken.person",
"snapshot.taken.schedule",
"snapshot.removed.person",
"snapshot.removed.schedule",
"snapshot.skipped.person",
"snapshot.skipped.schedule",
"run.skipped",
"scrub.requested"
],
"max-age-days": 30,
"max-mib": 16
}
},
{
"name": "applied",
"storage": "key-value",
"template": "state"
},
{
"name": "schedule",
"storage": "key-value",
"template": "state"
},
{
"name": "findings",
"storage": "key-value",
"template": "state"
},
{
"name": "snapshots",
"storage": "ledger",
"template": "ledger",
"options": {
"max-mib": 64
},
"types": [
{
"name": "snapshot",
"schema": {
"dataset": {
"type": "string"
},
"name": {
"type": "string"
},
"kind": {
"type": "string"
},
"at": {
"type": "string"
},
"taken": {
"type": "object"
}
},
"operations": [
{
"name": "taken",
"effect": "set-entry",
"into": "taken",
"key": "name",
"fields": [
"dataset",
"name",
"kind",
"at"
]
},
{
"name": "removed",
"effect": "remove",
"into": "taken",
"key": "name"
}
]
}
]
}
],
"config": {
"pool": {
@@ -260,6 +354,20 @@
"mode": "0755",
"unknown": "program"
},
{
"id": "keeper-account",
"type": "user",
"name": "zfs-keeper",
"shell": "/usr/bin/nologin"
},
{
"id": "keeper-state",
"type": "directory",
"mode": "0711",
"owner": "zfs-keeper",
"place": "mesh",
"unknown": "program"
},
{
"id": "config-dir",
"type": "directory",
@@ -432,6 +540,48 @@
"zed=off"
]
},
{
"id": "keeper",
"type": "process",
"name": "zfs-keeper",
"artifact": "tools",
"run": [
"./zfs-tools",
"keeper"
],
"user": "zfs-keeper",
"env": {
"MESH_BROKER_FILE": "${dir:keeper-state}/broker-keeper.secret",
"ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock"
},
"bus": {
"state": [
"applied",
"schedule",
"findings"
],
"emits": [
"config.applied",
"config.refused",
"config.waiting",
"snapshot.taken.person",
"snapshot.taken.schedule",
"snapshot.removed.person",
"snapshot.removed.schedule",
"snapshot.skipped.person",
"snapshot.skipped.schedule",
"run.skipped"
],
"invokes": [
"zfs.zfs_snapshots_record",
"zfs.zfs_snapshots_schedule",
"seat:node-service-manager.start"
]
},
"health": {
"kind": "unit"
}
},
{
"id": "apply",
"type": "process",
@@ -444,7 +594,10 @@
"run-once": true,
"restart-on": [
"config"
]
],
"env": {
"ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock"
}
},
{
"id": "watch",
@@ -476,6 +629,9 @@
],
"health": {
"kind": "unit"
},
"env": {
"ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock"
}
},
{
@@ -487,7 +643,10 @@
"./zfs-tools",
"run"
],
"schedule": "0,15,30,45 * * * *"
"schedule": "0,15,30,45 * * * *",
"env": {
"ZFS_KEEPER_SOCKET": "${dir:keeper-state}/keeper.sock"
}
},
{
"id": "distribution-weekly",