process: a bucket missing at a part's start is bound when the bus says it was created, keeping what was written meanwhile; buscompose grants each principal the creation of the buckets it may bind (issue 1790)
Since the grants part creates a manifest's buckets after a delivery (ADR 0547), a module restarted by that delivery can open a new state before it exists; OptionalBucket answered nil and the part never looked again (mesh-delivery's checker and builder, state hearing, 2026-10-26). AwaitBucket binds on the server's STREAM.CREATED advisory, or at the next use, and writes the latest value kept per key. The copied composer's agreement test leaves that one row out: the controller's list is no longer loaded since the take-over.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -14,6 +15,9 @@ import (
|
||||
// internal/broker (mesh-controller branch fix/1945-mesh-grants-publishes-memberships, and fix/1790-grants-create-buckets for the grants part creating declared streams), lists and memberships, from fixtures_test.go compiled there unchanged; composed.conf is the
|
||||
// controller's golden of the whole server configuration. A row that differs is the copy drifting from the controller,
|
||||
// which is a fault while both compose (until the controller loses its lease, ADR 0547 §13).
|
||||
// createdAdvisory is one subscription to a bucket's creation in a composed list, as stateCreatedHeard composes it.
|
||||
var createdAdvisory = regexp.MustCompile(`"\$JS\.EVENT\.ADVISORY\.STREAM\.CREATED\.KV_[^"]*", `)
|
||||
|
||||
func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", "controller-user-lists.json"))
|
||||
if err != nil {
|
||||
@@ -48,6 +52,11 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
if strings.Join(missing, ",") != strings.Join(want[i].Missing, ",") {
|
||||
t.Errorf("left out %v, the controller %v", missing, want[i].Missing)
|
||||
}
|
||||
// The one row the copy composes beyond the controller's, on purpose (novox/hq issue 1790, 2026-10-26, after
|
||||
// the take-over of ADR 0547 milestone 2): each principal hears its own and read buckets created, so a state
|
||||
// missing at its start is bound when it appears. The controller's composer is frozen and its list no longer
|
||||
// loaded; while it is not, this row is left out of the comparison and nothing else.
|
||||
list = createdAdvisory.ReplaceAllString(list, "")
|
||||
if list != want[i].List {
|
||||
t.Errorf("the list differs from the controller's:\n%s", list)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,22 @@ func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) {
|
||||
t.Errorf("%s may publish %s: %v, want %v", row.who, row.subject, got, row.may)
|
||||
}
|
||||
}
|
||||
// Issue 1790 (2026-10-26): a module hears the bus create each bucket it may bind, its own and those it reads, so a
|
||||
// state missing at its start is bound when it appears; no other bucket's, and no other advisory.
|
||||
for _, row := range []struct {
|
||||
who, subject string
|
||||
may bool
|
||||
}{
|
||||
{"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps", true},
|
||||
{GrantsModule, "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-vault_bus-accounts", true},
|
||||
{"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions", false},
|
||||
{"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.LOG_mesh-delivery-builder_builds", false},
|
||||
{"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.DELETED.KV_mesh-delivery-builder_steps", false},
|
||||
} {
|
||||
if got := MaySubscribe(perms[row.who], row.subject); got != row.may {
|
||||
t.Errorf("%s may hear %s: %v, want %v", row.who, row.subject, got, row.may)
|
||||
}
|
||||
}
|
||||
// A name that is no plain token is never granted, and a module other than the grants part is never granted one.
|
||||
if got := CreateStreamGrants([]string{"*", ">", "KV_a.b", "KV_a_b"}); len(got) != 2 {
|
||||
t.Errorf("grants for wildcard names: %v", got)
|
||||
|
||||
@@ -1085,8 +1085,12 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
|
||||
// 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and
|
||||
// watched, its own written too.
|
||||
pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State,
|
||||
PerMachine: p.PerMachine, Reads: p.Reads, KeyedReads: p.KeyedReads})...)
|
||||
kept := stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State, PerMachine: p.PerMachine, Reads: p.Reads,
|
||||
KeyedReads: p.KeyedReads}
|
||||
pub = append(pub, stateGrants(kept)...)
|
||||
// And it hears each of those buckets created, so one not on the bus at its start is bound when it appears
|
||||
// (novox/hq issue 1790; mesh-sdk process.AwaitBucket).
|
||||
sub = append(sub, stateCreatedHeard(kept)...)
|
||||
// And its logs (novox/hq ADR 0297): appended to and read directly, its own alone.
|
||||
pub = append(pub, logGrants(p.Module, p.Logs)...)
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ func processGrants(p Principal) (pub, sub []string, err error) {
|
||||
|
||||
// 3. Its module's own state, by the local names its bus names alone — never the whole of what the
|
||||
// module keeps (novox/hq ADR 0436 §1), read and written as stateGrants gives any module's own bucket.
|
||||
pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State})...)
|
||||
kept := stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State}
|
||||
pub = append(pub, stateGrants(kept)...)
|
||||
// And it hears each of them created, so one not on the bus at its start is bound when it appears (novox/hq issue
|
||||
// 1790; process.AwaitBucket).
|
||||
sub = append(sub, stateCreatedHeard(kept)...)
|
||||
|
||||
// 4. The router's records of its module's own asks (novox/hq ADR 0436 §1, ADR 0259 §3): the same
|
||||
// direct-get subjects a module's own principal reads for the seats its Uses names that keep them —
|
||||
|
||||
@@ -166,6 +166,49 @@ func stateGrants(a stateAccess) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// StreamCreatedAdvisory is the subject the bus says a stream was created on (the server's own advisory), here for a
|
||||
// bucket's stream KV_<bucket>.
|
||||
func StreamCreatedAdvisory(bucket string) string {
|
||||
return "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_" + bucket
|
||||
}
|
||||
|
||||
// stateCreatedHeard is what a principal subscribes to so that a bucket it may bind and that is not on the bus yet is
|
||||
// bound when it appears (novox/hq issue 1790, 2026-10-26: since the grants part creates a manifest's buckets, ADR 0547,
|
||||
// a module's delivery can start it before its new state exists): the bus's advisory that the bucket's stream was
|
||||
// created, for each bucket stateGrants lets it bind, and no other. Read-only: the server publishes it in the mesh's own
|
||||
// account, and hearing it changes nothing.
|
||||
func stateCreatedHeard(a stateAccess) []string {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
add := func(bucket string) {
|
||||
if !seen[bucket] {
|
||||
seen[bucket] = true
|
||||
out = append(out, StreamCreatedAdvisory(bucket))
|
||||
}
|
||||
}
|
||||
perMachine := map[string]bool{}
|
||||
for _, n := range a.PerMachine {
|
||||
perMachine[n] = true
|
||||
}
|
||||
for _, name := range a.Keeps {
|
||||
if !safeSubject.MatchString(name) || (perMachine[name] && !safeSubject.MatchString(a.Node)) {
|
||||
continue
|
||||
}
|
||||
add(BucketName(a.Module, name))
|
||||
}
|
||||
for _, r := range a.Reads {
|
||||
if bucket, ok := bucketOfRead(r); ok {
|
||||
add(bucket)
|
||||
}
|
||||
}
|
||||
for _, k := range a.KeyedReads {
|
||||
if bucket, ok := bucketOfRead(k.Read); ok && safeSubject.MatchString(k.Key) {
|
||||
add(bucket)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// KeyedRead is one key of another module's state a principal reads, and no other key of it.
|
||||
type KeyedRead struct {
|
||||
Read string `json:"read"`
|
||||
|
||||
Reference in New Issue
Block a user