Merge pull request 'mesh-grants: a pass composing part of the mesh only grows a membership (issue 1966, bootstrap recovery)' (#172) from fix/1966-grants-partial-pass-grows into main

This commit was merged in pull request #172.
This commit is contained in:
2026-11-07 23:39:26 +00:00
3 changed files with 121 additions and 10 deletions
+34 -2
View File
@@ -12,6 +12,7 @@ import (
"maps"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strings"
@@ -2473,6 +2474,29 @@ func TestGrants(t *testing.T) {
t.Errorf("%s: losses %v name no %q", d.name, grants.Losses(d.composed, inForce), d.loss)
}
}
// The membership a pass composing part of the mesh publishes is the union of the one published and the one composed.
for _, d := range []struct {
name, was, now, want string
kept []string
}{
{"narrows and adds: the union", `{"node":"a","emits":"e","seats":[{"seat":"s","verb":"v"}],"state":[{"name":"x"}]}`,
`{"node":"a","emits":"e","state":[{"name":"x"},{"name":"view-marks"}]}`,
`{"node":"a","emits":"e","seats":[{"seat":"s","verb":"v"}],"state":[{"name":"x"},{"name":"view-marks"}]}`, []string{`seats {"seat":"s","verb":"v"}`}},
{"adds only", `{"node":"a","state":[{"name":"x"}]}`, `{"node":"a","state":[{"name":"x"},{"name":"y"}]}`,
`{"node":"a","state":[{"name":"x"},{"name":"y"}]}`, nil},
{"narrows only: as published", `{"node":"a","state":[{"name":"x"},{"name":"y"}],"reaches":{"m.t":["s1"]}}`, `{"node":"a","state":[{"name":"x"}],"reaches":{"m.t":["s2"]}}`,
`{"node":"a","state":[{"name":"x"},{"name":"y"}],"reaches":{"m.t":["s1","s2"]}}`, []string{`reaches.m.t "s1"`, `state {"name":"y"}`}},
{"the private network and received are the pass's", `{"node":"a","mesh":["m1"],"receives":{"r":1}}`, `{"node":"a","mesh":["m2"]}`,
`{"node":"a","mesh":["m2"]}`, nil},
} {
grown, kept, err := grants.UnionMembership([]byte(d.was), []byte(d.now))
var g, w any
_ = json.Unmarshal(grown, &g)
_ = json.Unmarshal([]byte(d.want), &w)
if err != nil || !reflect.DeepEqual(g, w) || !slices.Equal(kept, d.kept) {
t.Errorf("%s: union %s kept %v (%v), want %s kept %v", d.name, grown, kept, err, d.want, d.kept)
}
}
for _, row := range []struct {
name string
bend func(*fakeBus, *grants.Part)
@@ -2525,6 +2549,7 @@ func TestGrants(t *testing.T) {
// The indexer's membership as published serves a tool more than any pass composes now (a tool runner's
// seat, say): a pass composing part of the mesh keeps it as published, a whole one publishes what it composes.
const extra = `{"subject":"mesh.tools.a.mesh-delivery-indexer.extra"}`
var lacks []byte
{
var m map[string]any
if err := json.Unmarshal(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], &m); err != nil {
@@ -2532,7 +2557,10 @@ func TestGrants(t *testing.T) {
}
var e any
_ = json.Unmarshal([]byte(extra), &e)
m["serves"] = append(m["serves"].([]any), e)
// The published one also lacks a tool the pass composes: the pass narrows it and adds to it at once (issue
// 1966, the assigner's view-marks), so it publishes the union and the addition is not held.
lacks, _ = json.Marshal(m["serves"].([]any)[0])
m["serves"] = append(m["serves"].([]any)[1:], e)
b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], _ = json.Marshal(m)
}
servesExtra := func() bool {
@@ -2564,10 +2592,14 @@ func TestGrants(t *testing.T) {
}
partial := row.name != "the ledger of a module whose manifest is refused"
if servesExtra() != partial || partial != slices.ContainsFunc(res.NotPublished, func(n string) bool {
return strings.HasPrefix(n, "a.mesh-delivery-indexer: kept as published: ") && strings.Contains(n, "mesh-delivery-indexer.extra")
return strings.HasPrefix(n, "a.mesh-delivery-indexer: kept beyond this pass: ") && strings.Contains(n, "mesh-delivery-indexer.extra")
}) {
t.Errorf("the published membership serving more: kept %v in a pass composing part of the mesh %v; not published %v", servesExtra(), partial, res.NotPublished)
}
if partial := row.name != "the ledger of a module whose manifest is refused"; partial &&
!strings.Contains(string(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"]), string(lacks)) {
t.Errorf("the pass narrowing and adding does not publish its addition %s", lacks)
}
if row.held != "" {
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, row.held) {
t.Fatalf("not held for %q: %+v", row.held, res.Sealed)
@@ -171,3 +171,74 @@ func uncovered(path string, a, b any) []string {
}
return nil
}
// UnionMembership is the membership last published (was) grown by the one composed now: every list of both, deduplicated, was's
// elements first and then what now adds; an object by key; a single value as now says it. The private network, the contributions
// received and the bindings are now's, which carry them over or the bus module writes. A pass composing part of the mesh publishes
// it, so it only grows (issue 1966): a membership kept whole could never receive what the pass that ends the hold needs. It also
// names, as MembershipNarrows does, what was has that now lacks. Pure.
func UnionMembership(was, now []byte) ([]byte, []string, error) {
var a, b map[string]any
if err := json.Unmarshal(was, &a); err != nil {
return nil, nil, err
}
if err := json.Unmarshal(now, &b); err != nil {
return nil, nil, err
}
out := map[string]any{}
for k, v := range a {
out[k] = v
}
for k, v := range b {
switch k {
case "mesh", "receives", "bindings":
out[k] = v
default:
out[k] = unionOf(a[k], v)
}
}
for _, k := range []string{"mesh", "receives", "bindings"} {
if _, ok := b[k]; !ok {
delete(out, k)
}
}
body, err := json.Marshal(out)
return body, MembershipNarrows(was, now), err
}
func unionOf(a, b any) any {
switch bv := b.(type) {
case map[string]any:
av, ok := a.(map[string]any)
if !ok {
return b
}
out := map[string]any{}
for k, v := range av {
out[k] = v
}
for k, v := range bv {
out[k] = unionOf(av[k], v)
}
return out
case []any:
av, ok := a.([]any)
if !ok {
return b
}
out := slices.Clone(av)
seen := map[string]bool{}
for _, e := range out {
j, _ := json.Marshal(e)
seen[string(j)] = true
}
for _, e := range bv {
if j, _ := json.Marshal(e); !seen[string(j)] {
seen[string(j)] = true
out = append(out, e)
}
}
return out
}
return b
}
+16 -8
View File
@@ -433,18 +433,26 @@ func (p *Part) Compose() Result {
res.NotPublished = append(res.NotPublished, key+": not published: the membership last published cannot be read from its own "+StateMemberships+": "+err.Error())
continue
}
m, now := publish(was, found, ms[key], holdings, seated)
// A pass that composes part of the mesh never narrows a published membership: it keeps the one published
// (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it serves.
if why := in.keepsLoaded(); now && found && why != "" {
if body, err := json.Marshal(m.Membership); err == nil {
if bare, err := membership.BareGrantsMembership(was); err == nil {
if narrower := MembershipNarrows(bare, body); len(narrower) > 0 {
m.Published, now = "kept as published: "+why+", and this pass composes it without "+strings.Join(narrower, ", "), false
// A pass that composes part of the mesh only grows a published membership: it publishes the union of the one last
// published and the one composed (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it
// serves and still receives what the pass that ends the hold needs.
if why := in.keepsLoaded(); found && why != "" {
if bare, err := membership.BareGrantsMembership(was); err == nil {
if body, err := json.Marshal(ms[key].Membership); err == nil {
if grown, kept, err := UnionMembership(bare, body); err == nil && len(kept) > 0 {
var u membership.Membership
if json.Unmarshal(grown, &u) == nil {
withUnion := ms[key]
withUnion.Membership = u
withUnion.Digest = digestOf(u)
ms[key] = withUnion
res.NotPublished = append(res.NotPublished, key+": kept beyond this pass: "+why+", this pass composes it without "+strings.Join(kept, ", "))
}
}
}
}
}
m, now := publish(was, found, ms[key], holdings, seated)
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
// published, so a membership held back never reaches a reader.
if now {