From c28c645ca4069cc925b64217dd3055a75d06c011 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 00:05:38 +0100 Subject: [PATCH] Name the node-engine's data items state, so mesh-host writes and disk-load reads one shape (ADR 0577) The contract (state name, key form, value) is in delivery; buscompose grants the engine its own node's keys of mesh-host_data and each node's disk-load a read of its own node's keys alone, the same way the engine's other states are granted. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/buscompose/buscompose_test.go | 19 ++++++--- go/buscompose/engine.go | 8 ++++ go/buscompose/state.go | 11 +++++ go/buscompose/userlist_switch.go | 5 ++- go/buscompose/writers.go | 4 ++ go/delivery/enginedata.go | 72 ++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 go/delivery/enginedata.go diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 42c4312..8e3bbc5 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -99,7 +99,9 @@ func engineBeyond(user, g string) bool { // And its own health report (issue 774). strings.Contains(g, "KV_"+delivery.NodeHealthBucket) || g == "$KV."+delivery.NodeHealthBucket+"."+node || // And the provisions of its node's modules, read (novox/hq ADR 0570 clause 6). - strings.Contains(g, "KV_"+provisions.ProvisionsBucket) + strings.Contains(g, "KV_"+provisions.ProvisionsBucket) || + // And its node's placed modules' data items, its own node's keys (novox/hq ADR 0577 clause 1). + strings.Contains(g, "KV_"+EngineDataBucket) || strings.HasPrefix(g, "$KV."+EngineDataBucket+"."+node+".") } // createsAnAccountsConsumer is the grants part's create and read of a durable consumer an account needs, by its stream @@ -657,6 +659,8 @@ func healthChecks(t *testing.T) { // ADR 0565 clauses 10, 11 (issue 1953 L5-S4): mesh-hand-acts reads the controller's log beside its own. {"mesh-hand-acts", ControllerHandActs, true}, {"mesh-conditions", "mesh-healers.attempts", true}, {"mesh-conditions", HealersWould, false}, {"mesh-delivery-view", HealersWould, false}, + // ADR 0577 clause 1: the node-engine's data items are disk-load's alone to read. + {"disk-load", delivery.EngineDataRead, true}, {"restic", delivery.EngineDataRead, false}, } { granted, _ := ReadsAnnouncedBy(c.reader, []string{c.read}, announced) if (len(granted) == 1) != c.may { @@ -686,8 +690,9 @@ func healthChecks(t *testing.T) { Reads: []string{delivery.SeatHoldersRead}}, {Module: delivery.AssignerModule, State: []Bucket{bucket(delivery.AssignerModule, delivery.SeatHoldersState)}}, // ADR 0571 clause 3: disk-load keeps the readings; the backup holder reads them. - {Module: "disk-load", State: []Bucket{bucket("disk-load", "readings")}}, - {Module: "restic", Reads: []string{"disk-load.readings"}, State: []Bucket{bucket("restic", "good")}}, + // ADR 0577 clause 1: and it reads its own node's keys of the node-engine's data items, which restic may not. + {Module: "disk-load", State: []Bucket{bucket("disk-load", "readings")}, Reads: []string{delivery.EngineDataRead}}, + {Module: "restic", Reads: []string{"disk-load.readings", delivery.EngineDataRead}, State: []Bucket{bucket("restic", "good")}}, // ADR 0571 clauses 1, 9: the indexer reads the builder's log through its own consumer; the catalogue module asks // the builder's tool. {Module: delivery.BuilderModule, Logs: []Log{{Module: delivery.BuilderModule, Name: delivery.BuilderLog, MaxAgeDays: 30}}}, @@ -708,7 +713,8 @@ func healthChecks(t *testing.T) { "a.mesh-vault": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak"}, "a.mesh-hand-acts": {"$KV.mesh-hand-acts_warrants.mesh-config.ask-1", "mesh.log.mesh-hand-acts.acts.act-1"}, - "a.disk-load": {"$KV.disk-load_readings.a.pg.data"}, + "a.disk-load": {"$KV.disk-load_readings.a.pg.data", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.a.pg", + "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.c1.$KV.mesh-host_data.a.*"}, "provisioner.a": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.c1.$KV.mesh-provisioner_plans.a.keycloak", // Every machine's plans, as the client V3 watches them (issue 774, 2026-10-30). @@ -748,7 +754,8 @@ func healthChecks(t *testing.T) { "a.mesh-vault": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak", "mesh.seat.mesh-artifact-sweep.event.artifacts-gone"}, // Each writes its own state alone, and these users are composed. - "a.disk-load": {"$KV.restic_good.a.pg.data"}, + "a.disk-load": {"$KV.restic_good.a.pg.data", "$KV.mesh-host_data.a.pg", + "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.b.pg", "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.c1.$KV.mesh-host_data.b.*"}, "a.mesh-delivery-builder": {"$KV.disk-load_readings.a.pg.data"}, // It reads every machine's plans (a provider's consumers are bound there, ADR 0373) but writes none, nor another // machine's needs, which it keeps for the client before V3 until the lease write. @@ -756,7 +763,7 @@ func healthChecks(t *testing.T) { // Only the assigner writes seat-holders. "a.artifact-store-tools": {"$KV.mesh-delivery-assigner_seat-holders.mesh-broker"}, // Only disk-load writes its readings. - "a.restic": {"$KV.disk-load_readings.a.pg.data"}, + "a.restic": {"$KV.disk-load_readings.a.pg.data", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.a.pg"}, "a.postgres": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak"}, // Nor mesh-hand-acts' log and warrants, which it alone writes (ADR 0565 clause 10). "a.mesh-healers": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch", diff --git a/go/buscompose/engine.go b/go/buscompose/engine.go index 9ccb790..6426db7 100644 --- a/go/buscompose/engine.go +++ b/go/buscompose/engine.go @@ -28,6 +28,10 @@ var EngineActsBucket = BucketName(EngineModule, EngineActsState) // node's name, written by that node's engine alone. var EnginePackagesBucket = BucketName(EngineModule, delivery.EnginePackagesState) +// EngineDataBucket is the engine's state of its node's placed modules' data items (novox/hq ADR 0577 clause 1): one +// key `.` per module, written by that node's engine alone. +var EngineDataBucket = delivery.EngineDataBucket + // EnginePackagesGrants is what a node's engine publishes to keep its node's key of the packages bucket: the key alone, // and its read. func EnginePackagesGrants(node string) []string { @@ -66,6 +70,10 @@ func engineGrants(p Principal) (pub, sub []string) { pub = append(pub, append([]string{"$KV." + delivery.NodeHealthBucket + "." + p.Node}, readGrants(delivery.NodeHealthBucket, p.Node)...)...) } + // And its node's placed modules' data items, its own node's keys alone (ADR 0577 clause 1). + if own, err := NodeKeysGrants(EngineDataBucket, p.Node); err == nil { + pub = append(pub, own...) + } // And its node's declared packages, its own key alone (ADR 0565 clause 6). if safeSubject.MatchString(p.Node) { pub = append(pub, EnginePackagesGrants(p.Node)...) diff --git a/go/buscompose/state.go b/go/buscompose/state.go index 017d05b..e0077a0 100644 --- a/go/buscompose/state.go +++ b/go/buscompose/state.go @@ -156,6 +156,12 @@ func stateGrants(a stateAccess) []string { } for _, r := range a.Reads { if bucket, ok := bucketOfRead(r); ok { + if NodeKeyedReads[r] { + if machineKey != "" { + read(bucket, machineKey+".>") + } + continue + } read(bucket, "") } } @@ -167,6 +173,11 @@ func stateGrants(a stateAccess) []string { return out } +// NodeKeyedReads are the states keyed `.>` whose reader reads its own machine's keys alone, whatever its `reads` +// names: the node-engine's data items, each node's disk-load reading its own node's (novox/hq ADR 0577 clause 1). A +// machine with no usable name reaches none of them. +var NodeKeyedReads = map[string]bool{delivery.EngineDataRead: true} + // StreamCreatedAdvisory is the subject the bus says a stream was created on (the server's own advisory), here for a // bucket's stream KV_. func StreamCreatedAdvisory(bucket string) string { diff --git a/go/buscompose/userlist_switch.go b/go/buscompose/userlist_switch.go index fece755..5f9d7b1 100644 --- a/go/buscompose/userlist_switch.go +++ b/go/buscompose/userlist_switch.go @@ -101,7 +101,10 @@ var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}, GrantsA ArtifactStoreTools, // ADR 0561 ยง3: mesh-config takes over the config only once the lease is handed away (its takeover.go leaseAway). ConfigModule}, - ControllerConditions: {health.KeeperModule}, ControllerHandActs: {health.HealersModule, handacts.Module}, HealersWould: {}} + ControllerConditions: {health.KeeperModule}, ControllerHandActs: {health.HealersModule, handacts.Module}, HealersWould: {}, + // ADR 0577 clause 1: the node-engine's data items are read by disk-load alone, each node's its own node's keys + // (NodeKeyedReads); the engine announces nothing of its own, so its readers are named here. + delivery.EngineDataRead: {health.DiskLoadModule}} // ReaderMay says whether module may read the state read (`.`) by the named readers: always, for a state // that names none. diff --git a/go/buscompose/writers.go b/go/buscompose/writers.go index 1239564..fc83892 100644 --- a/go/buscompose/writers.go +++ b/go/buscompose/writers.go @@ -259,6 +259,10 @@ var WritersTable = []WriterRow{ {State: "a machine's declared packages", Writer: "the node-engine of the machine, its key `` alone", KeptIn: "key-value " + EnginePackagesBucket, Others: "read by pacman through `reads`", Subjects: kvOf(EnginePackagesBucket), Writes: ownNodeKeys}, + // A node's placed modules' data items (novox/hq ADR 0577 clause 1): its engine's own keys, read by that node's disk-load. + {State: "a machine's data items", Writer: "the node-engine of the machine, under its own keys `.>`", + KeptIn: "key-value " + EngineDataBucket, Others: "read by the node's disk-load through `reads`, its own node's keys alone", + Subjects: kvOf(EngineDataBucket), Writes: ownNodeKeys}, {State: "a machine's hand acts", Writer: "the node-engine of the machine, under its own keys `.>`, compare-and-set", KeptIn: "key-value " + EngineActsBucket + ", the node-engine's declared state acts", Others: "read through the node seat's verb acts", Subjects: kvOf(EngineActsBucket), diff --git a/go/delivery/enginedata.go b/go/delivery/enginedata.go new file mode 100644 index 0000000..19273d3 --- /dev/null +++ b/go/delivery/enginedata.go @@ -0,0 +1,72 @@ +package delivery + +import ( + "encoding/json" + "fmt" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// **The data items of each module placed on a node, as its engine composed them** (novox/hq ADR 0577 clauses 1 and 4): +// the node-engine's state `data` (EngineDataBucket), one key `.` (EngineDataKey) per module placed on the +// node that declares data items, written by that node's engine alone, when the module's composition changes and only +// when the value differs, and deleted when the module leaves the node. That node's disk-load watches its node's keys +// (EngineDataNodeKeys) and measures what they name; it composes nothing itself. Only the contract is here: the state's +// names, its key and its value; the engine composes the items and disk-load measures them. + +// EngineDataState, EngineDataBucket and EngineDataRead are the node-engine's state data, by its local name, as the bus +// names it and as a reader declares it. +const ( + EngineDataState = "data" + EngineDataBucket = "mesh-host_" + EngineDataState + EngineDataRead = "mesh-host." + EngineDataState +) + +// EngineDataVersion is the version of EngineData's layout. +const EngineDataVersion = 1 + +// EngineData is one key's value: the module's data items on the node as the engine composed them (each item's id, its +// path resolved on that node, its measure, class, protection and what covers it), and the digest of the module's +// composition they came from (ComposedDigestOf). +type EngineData struct { + V int `json:"v"` + Node string `json:"node"` + Module string `json:"module"` + Digest string `json:"digest"` + Items []catalogue.PlacedDataItem `json:"items"` +} + +// EngineDataKey is a module's key on a node, `.`, its module part escaped as a have's is (HaveKey). +func EngineDataKey(node, module string) (string, error) { return HaveKey(node, module) } + +// EngineDataNodeKeys is the pattern of every key of a node, `.*`: what that node's disk-load watches. +func EngineDataNodeKeys(node string) (string, error) { + if err := checkName("node", node); err != nil { + return "", err + } + return node + ".*", nil +} + +// EncodeEngineData writes a value at this layout's version. +func EncodeEngineData(d EngineData) ([]byte, error) { + d.V = EngineDataVersion + if _, err := EngineDataKey(d.Node, d.Module); err != nil { + return nil, err + } + if d.Items == nil { + d.Items = []catalogue.PlacedDataItem{} + } + return json.Marshal(d) +} + +// DecodeEngineData reads a value; it refuses another version. +func DecodeEngineData(raw []byte) (EngineData, error) { + var d EngineData + if err := json.Unmarshal(raw, &d); err != nil { + return EngineData{}, fmt.Errorf("delivery: a module's data items are not readable: %w", err) + } + if d.V != EngineDataVersion { + return EngineData{}, fmt.Errorf("delivery: a module's data items of version %d, and this reads %d", d.V, EngineDataVersion) + } + return d, nil +}