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:
@@ -12,6 +12,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"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)
|
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 {
|
for _, row := range []struct {
|
||||||
name string
|
name string
|
||||||
bend func(*fakeBus, *grants.Part)
|
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
|
// 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.
|
// 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"}`
|
const extra = `{"subject":"mesh.tools.a.mesh-delivery-indexer.extra"}`
|
||||||
|
var lacks []byte
|
||||||
{
|
{
|
||||||
var m map[string]any
|
var m map[string]any
|
||||||
if err := json.Unmarshal(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], &m); err != nil {
|
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
|
var e any
|
||||||
_ = json.Unmarshal([]byte(extra), &e)
|
_ = 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)
|
b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], _ = json.Marshal(m)
|
||||||
}
|
}
|
||||||
servesExtra := func() bool {
|
servesExtra := func() bool {
|
||||||
@@ -2564,10 +2592,14 @@ func TestGrants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
partial := row.name != "the ledger of a module whose manifest is refused"
|
partial := row.name != "the ledger of a module whose manifest is refused"
|
||||||
if servesExtra() != partial || partial != slices.ContainsFunc(res.NotPublished, func(n string) bool {
|
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)
|
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 row.held != "" {
|
||||||
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, row.held) {
|
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, row.held) {
|
||||||
t.Fatalf("not held for %q: %+v", row.held, res.Sealed)
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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())
|
res.NotPublished = append(res.NotPublished, key+": not published: the membership last published cannot be read from its own "+StateMemberships+": "+err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m, now := publish(was, found, ms[key], holdings, seated)
|
// A pass that composes part of the mesh only grows a published membership: it publishes the union of the one last
|
||||||
// A pass that composes part of the mesh never narrows a published membership: it keeps the one published
|
// published and the one composed (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it
|
||||||
// (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it serves.
|
// serves and still receives what the pass that ends the hold needs.
|
||||||
if why := in.keepsLoaded(); now && found && why != "" {
|
if why := in.keepsLoaded(); found && why != "" {
|
||||||
if body, err := json.Marshal(m.Membership); err == nil {
|
if bare, err := membership.BareGrantsMembership(was); err == nil {
|
||||||
if bare, err := membership.BareGrantsMembership(was); err == nil {
|
if body, err := json.Marshal(ms[key].Membership); err == nil {
|
||||||
if narrower := MembershipNarrows(bare, body); len(narrower) > 0 {
|
if grown, kept, err := UnionMembership(bare, body); err == nil && len(kept) > 0 {
|
||||||
m.Published, now = "kept as published: "+why+", and this pass composes it without "+strings.Join(narrower, ", "), false
|
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 is written to the state, where every runtime reads it (process.FollowMembership); only one
|
||||||
// published, so a membership held back never reaches a reader.
|
// published, so a membership held back never reaches a reader.
|
||||||
if now {
|
if now {
|
||||||
|
|||||||
Reference in New Issue
Block a user