From 64a5074d58a0324e0b6c34b8c3b22aeef798fb0c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 22:43:27 +0100 Subject: [PATCH] =?UTF-8?q?Assigner:=20lay=20each=20held=20value=20of=20th?= =?UTF-8?q?e=20group's=20own=20pull=20requests=20at=20their=20heads=20over?= =?UTF-8?q?=20its=20module's=20values=20in=20the=20composition=20check,=20?= =?UTF-8?q?judged=20against=20the=20head's=20declaration=20and=20named,=20?= =?UTF-8?q?so=20a=20check=20reads=20the=20held=20values=20itself=20(issue?= =?UTF-8?q?=201953=20D7,=20ADR=200565=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-assigner/main.go | 11 +++- .../cmd/mesh-delivery-assigner/world.go | 8 +++ .../internal/assign/compose.go | 63 +++++++++++++++++++ .../internal/assign/compose_test.go | 28 +++++++++ .../mesh-delivery-assigner/manifest_test.go | 2 +- modules/mesh-delivery-assigner/module.json | 1 + 6 files changed, 110 insertions(+), 3 deletions(-) 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 b7d75eb..7c232b3 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -73,6 +73,7 @@ var readBuckets = []string{ delivery.StepsBucket(goBuildModule), takeoverBucket, provisions.ProvisionsBucket, + keptBucket, } // takeoverBucket is the condition keeper's take-over key's bucket (novox/hq ADR 0564 §9): whether the assigner's said @@ -159,6 +160,11 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any logf("the bucket %s cannot be opened (%v): it is bound once the bus says it was made", name, err) continue } + if name == keptBucket && err != nil { + // mesh-config's held values (ADR 0565 §5): without them a composition lays none, said in the journal. + logf("the bucket %s cannot be opened (%v): no held value is laid on a composition until it is bound", name, err) + continue + } if name == provisions.ProvisionsBucket && err != nil { // The seat mesh-provisioner's state, with its own delivery (ADR 0570 §4): bound once the bus says it was made; // until then no want waits for a provision, as before. @@ -322,6 +328,7 @@ var watched = []string{ delivery.AssignerWantsBucket, assign.PoliciesBucket, provisions.ProvisionsBucket, + keptBucket, } // follow watches every bucket a pass reads and makes a pass at start, once every watch handed what stands, and after @@ -418,7 +425,7 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, } started := 0 for _, name := range watched { - if kvs[name] == nil && (slices.Contains(seatBuckets, name) || sourceOfGrants(name) || name == provider.AssignmentsBucket || name == assign.PoliciesBucket || name == provisions.ProvisionsBucket) { + if kvs[name] == nil && (slices.Contains(seatBuckets, name) || sourceOfGrants(name) || name == provider.AssignmentsBucket || name == assign.PoliciesBucket || name == provisions.ProvisionsBucket || name == keptBucket) { continue // not on the bus at this start (open said so) } started++ @@ -448,7 +455,7 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, } } // And the two that choose where grants are read from, with the same delivery as the grants part that writes one. - for _, name := range []string{delivery.PartGrantsBucket, buscompose.LeaseBucket, assign.PoliciesBucket, provisions.ProvisionsBucket} { + for _, name := range []string{delivery.PartGrantsBucket, buscompose.LeaseBucket, assign.PoliciesBucket, provisions.ProvisionsBucket, keptBucket} { if kvs[name] == nil && js != nil { name := name process.AwaitBucket(ctx, js, nc, name, "until then the bucket is not read", diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 258e526..8495a3d 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -432,6 +432,11 @@ func (r worldReader) Served(ctx context.Context, node, module string, keys []str func (w *world) change(ctx context.Context, manifests getter, c delivery.Candidate, trunk map[string]delivery.Module) (assign.Change, string) { ch := assign.Change{Group: c.Group, Heads: c.HeadsHash, Manifests: map[string]catalogue.Manifest{}, Unreadable: map[string]string{}, Twice: map[string][]string{}, Digests: map[string]string{}, Members: map[string]string{}} + heads := map[string]string{} + for _, p := range c.Pulls { + heads[fmt.Sprintf("%s#%d", p.Repository, p.Number)] = p.Head.Head + } + ch.Held = assign.HeldOf(w.read(keptBucket), heads) definedIn := map[string]map[string]bool{} for _, p := range c.Pulls { if p.Repository == assign.SDKRepository { @@ -675,3 +680,6 @@ func (w *world) runningCommits(ctx context.Context, get getter, module string) [ sort.Strings(out) return out } + +// keptBucket is mesh-config's `kept`: the approved held values of open pull requests (ADR 0565 §5). +var keptBucket = "mesh-config_" + delivery.MeshConfigKeptState diff --git a/modules/mesh-delivery-assigner/internal/assign/compose.go b/modules/mesh-delivery-assigner/internal/assign/compose.go index 65c1481..35cb850 100644 --- a/modules/mesh-delivery-assigner/internal/assign/compose.go +++ b/modules/mesh-delivery-assigner/internal/assign/compose.go @@ -5,6 +5,7 @@ package assign import ( + "encoding/json" "fmt" "slices" "sort" @@ -59,6 +60,63 @@ type Change struct { // Members the head of each member by repository: what a composer at the group's mesh-sdk reads (issues 1622, 1623). Digests map[string]string Members map[string]string + // Held are the approved held values of the group's pull requests at the heads judged (mesh-config's `kept`, novox/hq ADR + // 0565 §5, ADR 0502 §4): each laid over its module's values on its node in the change's composition, and named. + Held []delivery.KeptValue +} + +// HeldOf are the held values kept for the pull requests the candidate names, each at the head it names: one of another +// pull request, or at another head, is never laid (ADR 0502 §4). kept is mesh-config's `kept` by key. +func HeldOf(kept map[string][]byte, heads map[string]string) []delivery.KeptValue { + var out []delivery.KeptValue + for _, raw := range kept { + var k delivery.KeptValue + if json.Unmarshal(raw, &k) != nil || k.Node == "" || k.Module == "" || k.Key == "" { + continue + } + if head, ok := heads[k.Pull]; ok && head == k.Head { + out = append(out, k) + } + } + sort.Slice(out, func(i, j int) bool { + a, b := out[i], out[j] + return a.Pull+a.Node+a.Module+a.Key < b.Pull+b.Node+b.Module+b.Key + }) + return out +} + +// layHeld lays each held value over its module's values on its node, as a layer of its own after the rest, and answers +// the words that name each; judged first against the head's declaration of its key (catalogue.ValueProblems), a value the +// declaration refuses, or a key it does not declare, is a failure. +func layHeld(in *compose.Input, held []delivery.KeptValue) (said, fails []string) { + for _, k := range held { + var v any + if err := json.Unmarshal(k.Value, &v); err != nil { + said = append(said, fmt.Sprintf("the held value %s of %s on %s for %s cannot be read, so it is not laid: %v", k.Key, k.Module, k.Node, k.Pull, err)) + continue + } + d, declared := in.Catalogue[k.Module].Settings[k.Key] + if !declared { + fails = append(fails, fmt.Sprintf("the held value %s of %s on %s for %s: the head declares no such key", k.Key, k.Module, k.Node, k.Pull)) + continue + } + if problems := catalogue.ValueProblems(d, v); len(problems) > 0 { + fails = append(fails, fmt.Sprintf("the held value %s of %s on %s for %s does not hold to the head's declaration: %s", k.Key, k.Module, + k.Node, k.Pull, strings.Join(problems, "; "))) + continue + } + at := slices.IndexFunc(in.Nodes, func(n compose.Node) bool { return n.Name == k.Node }) + if at < 0 { + said = append(said, fmt.Sprintf("the held value %s of %s for %s names %s, which runs no module of the mesh: not laid", k.Key, k.Module, k.Pull, k.Node)) + continue + } + if in.Nodes[at].Settings == nil { + in.Nodes[at].Settings = map[string][]catalogue.Layer{} + } + in.Nodes[at].Settings[k.Module] = append(in.Nodes[at].Settings[k.Module], catalogue.Layer{From: "held for " + k.Pull, Values: map[string]any{k.Key: v}}) + said = append(said, fmt.Sprintf("held value laid: %s of %s on %s, held for %s at %.12s, approved by %s (novox/hq ADR 0502 §4)", k.Key, k.Module, k.Node, k.Pull, k.Head, k.Approved)) + } + return said, fails } // Touched is the machines a change touches (ADR 0446 clause 3): those running a module the change moves, adds or @@ -171,6 +229,11 @@ func Compose(m Mesh, c Change, sdk string, took time.Duration) delivery.Composit delete(changed, name) } after := input(changed, m.Assigned) + laid, heldFails := layHeld(&after, c.Held) + v.Warnings = append(v.Warnings, laid...) + for _, f := range heldFails { + failAt("", f) + } for _, node := range machines { _, before := compose.Resolve(trunk, node) _, now := compose.Resolve(after, node) diff --git a/modules/mesh-delivery-assigner/internal/assign/compose_test.go b/modules/mesh-delivery-assigner/internal/assign/compose_test.go index 96c46ab..86e217b 100644 --- a/modules/mesh-delivery-assigner/internal/assign/compose_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/compose_test.go @@ -1,7 +1,9 @@ package assign import ( + "encoding/json" "slices" + "strconv" "strings" "testing" @@ -38,6 +40,32 @@ func TestAChangeOfOneModuleComposesItsMachine(t *testing.T) { if _, err := delivery.EncodeCompositionVerdict(v); err != nil { t.Errorf("the verdict cannot be written: %v", err) } + // 1953/D7 (ADR 0565 §5, ADR 0502 §4): the group's own pull request's held value at its head is laid and named, judged + // against the head's declaration; one of another pull request or another head is not laid. + web, err := catalogue.ParseManifest([]byte(`{"module":"web","version":"2","config":{"port":{"type":"number","default":80,"description":"the port, a number","runtime":true}}}`)) + if err != nil { + t.Fatal(err) + } + kept := map[string][]byte{} + for i, k := range []delivery.KeptValue{ + {Pull: "novox/x#1", Head: "h1", Node: "g14", Module: "web", Key: "port", Value: json.RawMessage(`81`), Approved: "op"}, + {Pull: "novox/x#2", Head: "h2", Node: "g14", Module: "web", Key: "port", Value: json.RawMessage(`82`)}, + {Pull: "novox/x#1", Head: "old", Node: "g14", Module: "web", Key: "other", Value: json.RawMessage(`1`)}, + } { + kept[strconv.Itoa(i)], _ = json.Marshal(k) + } + held := HeldOf(kept, map[string]string{"novox/x#1": "h1"}) + if len(held) != 1 || string(held[0].Value) != "81" { + t.Fatalf("held = %+v, want #1's at its head alone", held) + } + c = Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"web": web}, Held: held} + if v := Compose(meshOf(), c, "x", 0); v.State != delivery.CompositionPass || !hasWarning(v, "held value laid: port of web on g14, held for novox/x#1") { + t.Errorf("composition = %+v", v) + } + c.Held[0].Value = json.RawMessage(`"eighty"`) + if v := Compose(meshOf(), c, "x", 0); v.State != delivery.CompositionFail || !strings.Contains(v.Words, "does not hold to the head's declaration") { + t.Errorf("a held value the declaration refuses: %+v", v) + } } // 1297/AC2: a change that removes a module a machine runs fails, naming the machine and the module. diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index 0003bae..84bee99 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -33,7 +33,7 @@ func TestManifest(t *testing.T) { } reads := slices.Clone(m.Reads) slices.Sort(reads) - want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.builds", "mesh-controller.composed", "mesh-controller.grants", "mesh-controller.lease", + want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.builds", "mesh-controller.composed", "mesh-controller.grants", "mesh-controller.lease", "mesh-delivery-builder.builds", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-provisioner.provisions", "node-engine.have"} if !reflect.DeepEqual(reads, want) { diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index ecbdc4b..ea1b4b7 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -28,6 +28,7 @@ "reads": [ "mesh-build-go.steps", "mesh-conditions.takeover", + "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.builds",