Merge pull request 'The assigner, builder and grants read off the controller; upgrade compare by watch (design 47 L5, L7)' (#130) from feat/l5-l7-main into main

This commit was merged in pull request #130.
This commit is contained in:
2026-11-01 13:53:33 +00:00
31 changed files with 184 additions and 183 deletions
@@ -407,7 +407,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
for _, fp := range []string{fNew, fOld} { for _, fp := range []string{fNew, fOld} {
gkey, _ := delivery.ControllerGrantKey("web", fp) gkey, _ := delivery.ControllerGrantKey("web", fp)
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
} }
runs := map[string]string{"ace": fOld, "g14": "build-9", "shanks": fOld} runs := map[string]string{"ace": fOld, "g14": "build-9", "shanks": fOld}
for _, node := range []string{"ace", "g14", "shanks"} { for _, node := range []string{"ace", "g14", "shanks"} {
@@ -617,7 +617,7 @@ func TestTheAssignerReplacedResumesWithNothingLostOrRepeated(t *testing.T) {
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
gkey, _ := delivery.ControllerGrantKey(name, fp) gkey, _ := delivery.ControllerGrantKey(name, fp)
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
b.compose(t, "ace", name, fp, 'c', delivery.JudgedNeither) b.compose(t, "ace", name, fp, 'c', delivery.JudgedNeither)
b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(fmt.Sprintf(`{"node":"ace","module":%q,"build":"b-%d"}`, name, i))) b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(fmt.Sprintf(`{"node":"ace","module":%q,"build":"b-%d"}`, name, i)))
} }
@@ -663,7 +663,7 @@ func TestAWantWaitsForItsComposition(t *testing.T) {
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
gkey, _ := delivery.ControllerGrantKey("web", fNew) gkey, _ := delivery.ControllerGrantKey("web", fNew)
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
on, _ := delivery.EncodePresence(delivery.Presence{Online: true}) on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
b.putJSON(t, delivery.PresenceBucket, "ace", on) b.putJSON(t, delivery.PresenceBucket, "ace", on)
b.putJSON(t, provider.AssignmentsBucket, "ace.web", []byte(`{"node":"ace","module":"web","build":"b-1"}`)) b.putJSON(t, provider.AssignmentsBucket, "ace.web", []byte(`{"node":"ace","module":"web","build":"b-1"}`))
@@ -712,7 +712,7 @@ func (b *bed) fixture(t *testing.T, name, fp, manifest string) {
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
gkey, _ := delivery.ControllerGrantKey(name, fp) gkey, _ := delivery.ControllerGrantKey(name, fp)
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
on, _ := delivery.EncodePresence(delivery.Presence{Online: true}) on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
b.putJSON(t, delivery.PresenceBucket, "ace", on) b.putJSON(t, delivery.PresenceBucket, "ace", on)
b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(`{"node":"ace","module":"`+name+`","build":"b-1"}`)) b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(`{"node":"ace","module":"`+name+`","build":"b-1"}`))
@@ -59,7 +59,6 @@ func main() {
var readBuckets = []string{ var readBuckets = []string{
buscompose.ResolvedBucket, buscompose.ResolvedBucket,
provider.AssignmentsBucket, provider.AssignmentsBucket,
delivery.ControllerGrantsBucket,
delivery.PartGrantsBucket, delivery.PartGrantsBucket,
buscompose.LeaseBucket, buscompose.LeaseBucket,
delivery.ControllerComposedBucket, delivery.ControllerComposedBucket,
@@ -219,8 +218,8 @@ func startsWithout(name string) bool {
return slices.Contains(seatBuckets, name) || sourceOfGrants(name) return slices.Contains(seatBuckets, name) || sourceOfGrants(name)
} }
// sourceOfGrants says the bucket is one of the two that choose the grants' source, the grants part's grants and the // sourceOfGrants says the bucket is the grants part's grants or the controller's lease: neither stops the assigner when it
// controller's lease: neither stops the assigner when it is not there (delivery.GrantSource). // is not there.
func sourceOfGrants(name string) bool { func sourceOfGrants(name string) bool {
return name == delivery.PartGrantsBucket || name == buscompose.LeaseBucket return name == delivery.PartGrantsBucket || name == buscompose.LeaseBucket
} }
@@ -333,7 +332,6 @@ func (e emitting) Emit(eventType string, body any) error { return e.Conn.Emit(ev
var watched = []string{ var watched = []string{
buscompose.ResolvedBucket, buscompose.ResolvedBucket,
provider.AssignmentsBucket, provider.AssignmentsBucket,
delivery.ControllerGrantsBucket,
delivery.PartGrantsBucket, delivery.PartGrantsBucket,
buscompose.LeaseBucket, buscompose.LeaseBucket,
delivery.ControllerComposedBucket, delivery.ControllerComposedBucket,
@@ -419,11 +417,10 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
if e.Operation() == jetstream.KeyValuePut { if e.Operation() == jetstream.KeyValuePut {
value = e.Value() value = e.Value()
} }
before := w.grantsBucket() before := w.handedAway()
w.putEntry(name, e.Key(), value, e.Revision(), e.Created()) w.putEntry(name, e.Key(), value, e.Revision(), e.Created())
if name == buscompose.LeaseBucket && w.grantsBucket() == before { if name == buscompose.LeaseBucket && w.handedAway() == before {
// The controller renews its lease every few seconds: only a change of where grants are read from // The controller renews its lease every few seconds: only the lease's being handed away makes a pass.
// makes a pass (delivery.GrantSource).
continue continue
} }
wake() wake()
@@ -272,7 +272,7 @@ func putOfTheBus(t *testing.T) {
world(delivery.ControllerComposedBucket, ckey, cd) world(delivery.ControllerComposedBucket, ckey, cd)
gkey, _ := delivery.ControllerGrantKey("nats", fp) gkey, _ := delivery.ControllerGrantKey("nats", fp)
g, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) g, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{})
world(delivery.ControllerGrantsBucket, gkey, g) world(delivery.PartGrantsBucket, gkey, g)
got := step() got := step()
akey, _ := delivery.RecordAskKey("novox", "nats", digest) akey, _ := delivery.RecordAskKey("novox", "nats", digest)
if got.Step != delivery.PutPerson || got.Ask != akey { if got.Step != delivery.PutPerson || got.Ask != akey {
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"reflect"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -15,6 +16,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-sdk/go/provisions"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
@@ -400,6 +402,47 @@ func wantsVerbs(t *testing.T) {
if splits := out.(map[string]any)["splits"].([]assign.Split); len(splits) != 1 || splits[0].Module != "web" || out.(map[string]any)["findings"] == nil { if splits := out.(map[string]any)["splits"].([]assign.Split); len(splits) != 1 || splits[0].Module != "web" || out.(map[string]any)["findings"] == nil {
t.Errorf("splits = %+v", splits) t.Errorf("splits = %+v", splits)
} }
// Read by watch (issue 1953 L5f): the same answer, each watched state started once, and no get per key of the
// large states (the modules, their manifests, the composed record).
watches := map[string]int{}
byWatch := newWantsTools(st, nil, nil)
byWatch.watched = stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
watches[state]++
for k, v := range st[state].values {
_ = handle(stdio.StateChange{Key: k, Op: stdio.OpPut, Value: v, Current: true})
}
return nil
}, func(state, key string) ([]byte, bool, error) {
watches[state+" by key"]++
v, ok := st[state].values[key]
return v, ok, nil
})
for range 2 {
wout, werr := byWatch.doUpgrade(map[string]any{"compare": "true"}, "op")
if werr != nil {
t.Fatal(werr)
}
if ws := wout.(map[string]any)["splits"].([]assign.Split); len(ws) != 1 || ws[0].Module != "web" {
t.Errorf("splits by watch = %+v", ws)
}
}
if n := watches[manifestsState+" by key"]; n == 0 || n > len(st[manifestsState].values) {
t.Errorf("the manifests were read by key %d times, want each once", n)
}
for _, state := range []string{modulesState, composedState, health.FindingsState} {
if watches[state] != 1 {
t.Errorf("%s was watched %d times, want once", state, watches[state])
}
}
// ADR 0580 clause 3: the compare reads through mesh-sdk's watched reader and the seat keeps no mirror of its own.
for f := range reflect.TypeFor[wantsTools]().Fields() {
if f.Type.Kind() == reflect.Map || (f.Type.Kind() == reflect.Pointer && f.Type.Elem().Kind() == reflect.Map) {
t.Errorf("wantsTools keeps %s, a copy of its own beside the watched reader", f.Name)
}
}
if watches[policiesState] != 0 {
t.Errorf("the seat's own small policies state was watched: %v", watches)
}
// The process says the split as its finding, and deletes it once they agree. // The process says the split as its finding, and deletes it once they agree.
findings := map[string][]byte{} findings := map[string][]byte{}
p := &passer{w: newWorld(), logf: func(string, ...any) {}, findings: assign.Findings{ p := &passer{w: newWorld(), logf: func(string, ...any) {}, findings: assign.Findings{
@@ -352,10 +352,12 @@ func serveTools() error {
} }
}() }()
tools := append(s.tools(), newSeatTools(runtimeStore{}, stdio.Ask).tools()...) tools := append(s.tools(), newSeatTools(runtimeStore{}, stdio.Ask).tools()...)
tools = append(tools, newWantsTools(runtimeStore{}, stdio.Ask, func(e, id string, b any) error { wants := newWantsTools(runtimeStore{}, stdio.Ask, func(e, id string, b any) error {
// The seat's event, said on the seat: a claimed seat's event is never the module's own. // The seat's event, said on the seat: a claimed seat's event is never the module's own.
return stdio.SeatPublish(wantsEventSubject(e), b, id) return stdio.SeatPublish(wantsEventSubject(e), b, id)
}).tools()...) })
wants.watched = stdio.NewWatchedStates() // the large states read whole from one watched copy (ADR 0580 clause 3)
tools = append(tools, wants.tools()...)
return stdio.Serve("", tools) return stdio.Serve("", tools)
} }
@@ -41,6 +41,10 @@ type wantsTools struct {
ask func(key string, body any) (json.RawMessage, error) ask func(key string, body any) (json.RawMessage, error)
emit func(event, id string, body any) error emit func(event, id string, body any) error
now func() time.Time now func() time.Time
// watched is the copy of the large states it reads whole, each by one watch started on its first read and kept by
// its changes after (mesh-sdk's WatchedStates, novox/hq ADR 0580 clause 3), never a get per key, which timed out on
// a large state (issue 1953 L5f). Nil reads key by key.
watched *stdio.WatchedStates
} }
// wantsEventSubject is the subject of one of the seat's events. // wantsEventSubject is the subject of one of the seat's events.
@@ -59,8 +63,16 @@ func (s *wantsTools) leased() bool {
return away return away
} }
// values reads every key of a state. // values reads every key of a state: from the watched copy when there is one, else key by key.
func (s *wantsTools) values(state string) (map[string][]byte, error) { func (s *wantsTools) values(state string) (map[string][]byte, error) {
if s.watched != nil {
return s.watched.Values(state)
}
return s.direct(state)
}
// direct reads a state key by key, for a small state of the seat's own.
func (s *wantsTools) direct(state string) (map[string][]byte, error) {
keys, err := s.st.keys(state) keys, err := s.st.keys(state)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s cannot be listed: %w", state, err) return nil, fmt.Errorf("%s cannot be listed: %w", state, err)
@@ -78,6 +90,16 @@ func (s *wantsTools) values(state string) (map[string][]byte, error) {
return out, nil return out, nil
} }
// manifest reads a manifest by its digest: kept once read when there is a watched copy, a manifest never changing
// under its digest.
func (s *wantsTools) manifest(digest string) ([]byte, bool, error) {
if s.watched != nil {
return s.watched.Once(manifestsState, digest)
}
raw, _, found, err := s.st.get(manifestsState, digest)
return raw, found, err
}
// manifests is every module the indexer holds, with its trunk manifest and fingerprint. // manifests is every module the indexer holds, with its trunk manifest and fingerprint.
func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]delivery.Module, error) { func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]delivery.Module, error) {
mods, err := s.values(modulesState) mods, err := s.values(modulesState)
@@ -94,7 +116,7 @@ func (s *wantsTools) manifests() (map[string]catalogue.Manifest, map[string]deli
if m.Manifest == "" { if m.Manifest == "" {
continue continue
} }
mraw, _, found, err := s.st.get(manifestsState, m.Manifest) mraw, found, err := s.manifest(m.Manifest)
if err != nil || !found { if err != nil || !found {
continue continue
} }
@@ -126,7 +148,7 @@ func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
raw, err := s.values(policiesState) raw, err := s.direct(policiesState) // the seat's own small state, read whole after its own writes
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -584,15 +584,9 @@ func (w *world) value(bucket, key string) []byte {
return w.values[bucket][key] return w.values[bucket][key]
} }
// grantsBucket is the bucket the grants are read from now (delivery.GrantSource): the grants part's while the controller's // grantsBucket is the bucket the grants are read from: the grants part's state `grants`, always (ADR 0571 clause 9); no
// lease is handed away, as the lease bucket's key says it (buscompose.LeaseHandedAway), the controller's in every other // manifest but the controller's reads the controller's grants.
// case, a lease not held here included. func (w *world) grantsBucket() string { return delivery.PartGrantsBucket }
func (w *world) grantsBucket() string {
raw := w.value(buscompose.LeaseBucket, buscompose.ControllerLeaseKey)
away, _ := buscompose.LeaseHandedAway(raw, raw != nil, nil)
bucket, _ := delivery.GrantSource(away)
return bucket
}
// has says whether a bucket holds a key now. // has says whether a bucket holds a key now.
func (w *world) has(bucket, key string) bool { func (w *world) has(bucket, key string) bool {
@@ -128,24 +128,22 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) {
if got := w.changed(delivery.HaveBucket, "gone"); got != 0 { if got := w.changed(delivery.HaveBucket, "gone"); got != 0 {
t.Errorf("changed = %d for a key never seen", got) t.Errorf("changed = %d for a key never seen", got)
} }
// Issue 774: grants are read from the grants part's bucket only while the controller's lease is handed away: a grant // ADR 0571 clause 9: grants are read from the grants part's bucket in every case, a lease held, handed away, absent or
// only the part holds lets a node's put through then, and with a controller's lease (or none, or an unreadable one) it // unreadable alike, and the controller's own grants count for nothing.
// holds it, a grant only the controller holds being the one that counts instead.
fp := delivery.SourcePrefix2 + strings.Repeat("d", 64) fp := delivery.SourcePrefix2 + strings.Repeat("d", 64)
gkey, _ := delivery.ControllerGrantKey("web", fp) gkey, _ := delivery.ControllerGrantKey("web", fp)
grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}}) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}})
for _, row := range []struct { for _, row := range []struct {
name, lease string name, lease string
inPart, inController bool inPart, inController bool
bucket string
granted bool granted bool
}{ }{
{"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, delivery.PartGrantsBucket, true}, {"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, true},
{"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, delivery.PartGrantsBucket, false}, {"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, false},
{"a controller holds it, only the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, delivery.ControllerGrantsBucket, false}, {"a controller holds it, the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, true},
{"a controller holds it, its own grant", `{"instance":"novox/1"}`, false, true, delivery.ControllerGrantsBucket, true}, {"a controller holds it, only its own grant", `{"instance":"novox/1"}`, false, true, false},
{"no lease read", ``, true, false, delivery.ControllerGrantsBucket, false}, {"no lease read", ``, true, false, true},
{"an unreadable lease", `[1]`, true, false, delivery.ControllerGrantsBucket, false}, {"an unreadable lease", `[1]`, true, false, true},
} { } {
p := newPasser(buckets{}, time.Now()) p := newPasser(buckets{}, time.Now())
if row.lease != "" { if row.lease != "" {
@@ -157,8 +155,8 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) {
if row.inController { if row.inController {
p.w.put(delivery.ControllerGrantsBucket, gkey, grant) p.w.put(delivery.ControllerGrantsBucket, gkey, grant)
} }
if got := p.w.grantsBucket(); got != row.bucket { if got := p.w.grantsBucket(); got != delivery.PartGrantsBucket {
t.Errorf("%s: grants are read from %s, want %s", row.name, got, row.bucket) t.Errorf("%s: grants are read from %s, want the part's", row.name, got)
} }
if _, granted := p.composedFor("ace", "web", fp); granted != row.granted { if _, granted := p.composedFor("ace", "web", fp); granted != row.granted {
t.Errorf("%s: granted = %v, want %v", row.name, granted, row.granted) t.Errorf("%s: granted = %v, want %v", row.name, granted, row.granted)
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-assigner
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
) )
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -33,7 +33,7 @@ func TestManifest(t *testing.T) {
} }
reads := slices.Clone(m.Reads) reads := slices.Clone(m.Reads)
slices.Sort(reads) slices.Sort(reads)
want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "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.composed", "mesh-controller.lease",
"mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.manifests", "mesh-delivery-builder.build-log", "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"} "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) { if !reflect.DeepEqual(reads, want) {
@@ -59,7 +59,7 @@ func TestManifest(t *testing.T) {
t.Errorf("uses = %v, want exactly node-build-go and operator-channel", m.Uses) t.Errorf("uses = %v, want exactly node-build-go and operator-channel", m.Uses)
} }
// ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the // ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the
// module), says each put's step as the event put, and calls the controller's warranted and the bus node's backup holder. // module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder.
// ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream // ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream
// placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its // placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its
// hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches. // hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches.
@@ -82,9 +82,9 @@ func TestManifest(t *testing.T) {
!slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) { !slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) {
t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays) t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays)
} }
if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.hand-act", "seat:mesh-controller.pin", if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.pin",
"seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin", "seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin",
"seat:mesh-controller.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat, the controller's only while it has no holder "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone
"seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { "seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want) t.Errorf("invokes = %v, want %v", m.Invokes, want)
} }
@@ -32,7 +32,6 @@
"mesh-config.resolved", "mesh-config.resolved",
"mesh-controller.assignments", "mesh-controller.assignments",
"mesh-controller.composed", "mesh-controller.composed",
"mesh-controller.grants",
"mesh-controller.lease", "mesh-controller.lease",
"mesh-delivery-builder.build-log", "mesh-delivery-builder.build-log",
"mesh-delivery-builder.builds", "mesh-delivery-builder.builds",
@@ -51,12 +50,10 @@
], ],
"invokes": [ "invokes": [
"seat:mesh-controller.assign", "seat:mesh-controller.assign",
"seat:mesh-controller.hand-act",
"seat:mesh-controller.pin", "seat:mesh-controller.pin",
"seat:mesh-controller.placement-export", "seat:mesh-controller.placement-export",
"seat:mesh-controller.unassign", "seat:mesh-controller.unassign",
"seat:mesh-controller.unpin", "seat:mesh-controller.unpin",
"seat:mesh-controller.warranted",
"seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-act",
"seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.hand-acts",
"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.warranted",
@@ -12,6 +12,7 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
) )
@@ -19,7 +20,7 @@ import (
// (novox/hq issue 1937): on 2026-10-24 the store's 03:30 collection deleted the bare blobs of three done keys the // (novox/hq issue 1937): on 2026-10-24 the store's 03:30 collection deleted the bare blobs of three done keys the
// controller had never taken in, and the builder kept them done. // controller had never taken in, and the builder kept them done.
// heldVerb answers the controller seat's verb held on the bus, as the serving controller does: {result: {output, ok, // heldVerb answers the seat mesh-artifact-sweep's verb held on the bus, as its holder does: {result: {output, ok,
// answer}}; a reference is gone while gone says so. // answer}}; a reference is gone while gone says so.
type heldVerb struct { type heldVerb struct {
mu sync.Mutex mu sync.Mutex
@@ -29,7 +30,7 @@ type heldVerb struct {
func (h *heldVerb) serve(t *testing.T, b *bed) { func (h *heldVerb) serve(t *testing.T, b *bed) {
t.Helper() t.Helper()
sub, err := b.js.Conn().Subscribe("mesh.seat.mesh-controller.call.held.>", func(m *nats.Msg) { sub, err := b.js.Conn().Subscribe("mesh.seat."+catalogue.ArtifactSweepSeat+".call.held.>", func(m *nats.Msg) {
var args struct { var args struct {
References []string `json:"references"` References []string `json:"references"`
} }
@@ -11,7 +11,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"mesh-delivery-builder/internal/build" "mesh-delivery-builder/internal/build"
) )
@@ -26,19 +25,14 @@ const SeatHeld = "seat:" + catalogue.ArtifactSweepSeat + ".held"
// toolCaller calls a tool or a seat verb as the builder's own account (mesh-sdk process.Conn.Call). // toolCaller calls a tool or a seat verb as the builder's own account (mesh-sdk process.Conn.Call).
type toolCaller func(key string, body any) (json.RawMessage, error) type toolCaller func(key string, body any) (json.RawMessage, error)
// storedVia is build.Builder's Stored: it asks the controller's verb held (mesh-sdk delivery.HeldVerb; novox/hq issue // storedVia is build.Builder's Stored: it asks the seat mesh-artifact-sweep's `held` (novox/hq ADR 0571 §2, issue 1937)
// 1937) and answers the references the store no longer holds. One it could not ask about, with none gone, is an error: // and answers the references the store no longer holds. One it could not ask about, with none gone, is an error: the key
// the key is then left done, and the controller's take-in still refuses a gone blob. // is then left done, and the controller's take-in still refuses a gone blob. The controller's `held` is not asked: it
// // is no longer a fallback (issue 1955).
// It asks the seat mesh-artifact-sweep's `held` (novox/hq ADR 0571 §2), and the controller's while that seat has no holder that
// answers (a named exception of clause 10, until the lease write).
func storedVia(call toolCaller) func(ctx context.Context, references []string) ([]string, error) { func storedVia(call toolCaller) func(ctx context.Context, references []string) ([]string, error) {
return func(_ context.Context, references []string) ([]string, error) { return func(_ context.Context, references []string) ([]string, error) {
body := map[string]any{"references": references} body := map[string]any{"references": references}
raw, err := call(SeatHeld, body) raw, err := call(SeatHeld, body)
if handacts.NoResponder(err) {
raw, err = call(delivery.HeldVerb, body)
}
if err != nil { if err != nil {
return nil, fmt.Errorf("asking held: %w", err) return nil, fmt.Errorf("asking held: %w", err)
} }
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-builder
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
) )
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -61,8 +61,8 @@ type ClaimOptions struct {
// a package's reference. A key queued waiting for a version (BuilderBuild.Waits) is taken only once it is (ADR 0500 // a package's reference. A key queued waiting for a version (BuilderBuild.Waits) is taken only once it is (ADR 0500
// clause 6). Nil means none is. // clause 6). Nil means none is.
Published func(ref string) bool Published func(ref string) bool
// Stored answers which of the references given the artifact store no longer holds (the controller's verb held, // Stored answers which of the references given the artifact store no longer holds (the seat mesh-artifact-sweep's verb held,
// mesh-sdk delivery.HeldVerb; novox/hq issue 1937). A done key of the trunk's fingerprint is asked before it is left // novox/hq ADR 0571 §2, issue 1937). A done key of the trunk's fingerprint is asked before it is left
// done: the store's nightly collector deletes a blob nothing holds, and a key the controller never took in holds // done: the store's nightly collector deletes a blob nothing holds, and a key the controller never took in holds
// nothing, so a key whose blob is gone is claimed building as a new revision and built again. Nil asks nothing. // nothing, so a key whose blob is gone is claimed building as a new revision and built again. Nil asks nothing.
Stored func(ctx context.Context, references []string) (gone []string, err error) Stored func(ctx context.Context, references []string) (gone []string, err error)
@@ -51,7 +51,7 @@ type Builder struct {
// Answered is told the build module whose step answered a reference (issue 1886): what the builder says it writes // Answered is told the build module whose step answered a reference (issue 1886): what the builder says it writes
// rests on it. Nil tells nobody. // rests on it. Nil tells nobody.
Answered func(buildModule string) Answered func(buildModule string)
// Stored answers which references the artifact store no longer holds (the controller's verb held; issue 1937): a done // Stored answers which references the artifact store no longer holds (the seat mesh-artifact-sweep's verb held; issue 1937): a done
// key of the trunk's fingerprint is asked before it is left done (ClaimOptions.Stored). Nil asks nothing. // key of the trunk's fingerprint is asked before it is left done (ClaimOptions.Stored). Nil asks nothing.
Stored func(ctx context.Context, references []string) (gone []string, err error) Stored func(ctx context.Context, references []string) (gone []string, err error)
// Trunk answers the head of a repository in the gate's source revision rev (source.main at that revision): the trunk a // Trunk answers the head of a repository in the gate's source revision rev (source.main at that revision): the trunk a
@@ -107,8 +107,8 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
} }
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no // 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
// other tool; it holds no capacity. // other tool; it holds no capacity.
// 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's only while the seat has no holder (clause 10). // 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955).
if want := []string{"seat:mesh-artifact-sweep.held", "seat:mesh-controller.held"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 { if want := []string{"seat:mesh-artifact-sweep.held"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want) t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want)
} }
// The builder is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it // The builder is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it
+1 -2
View File
@@ -20,8 +20,7 @@
"mesh-controller.artifacts-gone" "mesh-controller.artifacts-gone"
], ],
"invokes": [ "invokes": [
"seat:mesh-artifact-sweep.held", "seat:mesh-artifact-sweep.held"
"seat:mesh-controller.held"
], ],
"state": [ "state": [
{ "name": "builds", "history": 5 }, { "name": "builds", "history": 5 },
+3 -3
View File
@@ -65,9 +65,9 @@ func schema(properties map[string]string, required ...string) map[string]any {
// toolsOf are the part's tools over a part (grants.Tools names them). // toolsOf are the part's tools over a part (grants.Tools names them).
func toolsOf(p *grants.Part) []stdio.Tool { func toolsOf(p *grants.Part) []stdio.Tool {
return []stdio.Tool{ return []stdio.Tool{
{Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 6): in this milestone it " + {Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 5, ADR 0571 clause 9): the grants it wrote " +
"writes none (grants.<module>.<fp> from the third milestone, clause 5), and shows beside it the grants the controller " + "(grants.<module>.<fp> in its state grants, the one key the assigner reads), all or one module's with the accounts each names. " +
"wrote (mesh-controller_grants), all or one module's with the accounts each names. Read-only.", "Read-only.",
Input: schema(map[string]string{"module": "one module"}), Input: schema(map[string]string{"module": "one module"}),
Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }}, Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }},
{Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " + {Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " +
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-grants
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
) )
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+12 -12
View File
@@ -59,7 +59,7 @@ type fakeBus struct {
refuseCreate bool refuseCreate bool
// refuseUsers refuses the bus module's nats_users: the loaded list cannot be read. // refuseUsers refuses the bus module's nats_users: the loaded list cannot be read.
refuseUsers bool refuseUsers bool
// rootFree answers the controller's root-free verb: nil is no answerer at all (no responders). // rootFree answers a machine's engine's root-free verb: nil is no answerer at all (no responders).
rootFree func(machines []string) (any, error) rootFree func(machines []string) (any, error)
// consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and // consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and
// consumerCreates each consumer the part asked to create, `<stream>.<name>`. // consumerCreates each consumer the part asked to create, `<stream>.<name>`.
@@ -191,8 +191,13 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
} }
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
b.calls = append(b.calls, key)
args, _ := body.(map[string]any) args, _ := body.(map[string]any)
// A node seat's verb is asked of one machine: `<verb>@<machine>`; the call is kept and judged by the verb alone.
machine := ""
if base, m, ok := strings.Cut(key, "@"); ok {
key, machine = base, m
}
b.calls = append(b.calls, key)
switch key { switch key {
case grants.CallUsers: case grants.CallUsers:
if b.refuseUsers { if b.refuseUsers {
@@ -222,7 +227,7 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
if b.rootFree == nil { if b.rootFree == nil {
return nil, errors.New("nats: no responders available for request") return nil, errors.New("nats: no responders available for request")
} }
out, err := b.rootFree(args["machines"].([]string)) out, err := b.rootFree([]string{machine})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -350,7 +355,6 @@ func fixture() *fakeBus {
b.put(grants.ReadHave, "b.node", map[string]any{"v": 1}) b.put(grants.ReadHave, "b.node", map[string]any{"v": 1})
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}}) UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}})
b.put(grants.ReadControllerGrant, "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123", delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}})
return b return b
} }
@@ -613,7 +617,7 @@ func TestGrants(t *testing.T) {
} }
}) })
t.Run("root-free: the controller's word fills the composition, failing closed (issue 1945)", func(t *testing.T) { t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` + const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` + `"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}` `"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
@@ -1317,7 +1321,7 @@ func TestGrants(t *testing.T) {
t.Errorf("membership = %v", ms) t.Errorf("membership = %v", ms)
} }
gr, _ := p.Grants("mod-x") gr, _ := p.Grants("mod-x")
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller_count"] != 1 { if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller"] != nil || g["controller_count"] != nil {
t.Errorf("grants = %v", gr) t.Errorf("grants = %v", gr)
} }
@@ -2064,9 +2068,6 @@ func TestGrants(t *testing.T) {
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1}) b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1})
} }
held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) } held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) }
// The controller's grant for this fingerprint, beside the stale one the fixture holds for another.
delete(b.states[grants.ReadControllerGrant], "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123")
b.put(grants.ReadControllerGrant, key("mod-x"), delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}})
hash("a.mod-x") hash("a.mod-x")
hash("a.nats") hash("a.nats")
res := p.Compose() res := p.Compose()
@@ -2091,11 +2092,10 @@ func TestGrants(t *testing.T) {
if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 { if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 {
t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said()) t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said())
} }
// The tool: equal on mod-x, and nats is the part's alone (the controller's fixture holds none). // The tool answers what the part wrote, and reads nothing of the controller's.
got, err := p.Grants("") got, err := p.Grants("")
gm, _ := got.(map[string]any) gm, _ := got.(map[string]any)
diff, _ := gm["differences"].([]grants.GrantDifference) if _, ok := gm["controller"]; err != nil || gm["count"] != 2 || ok {
if err != nil || gm["equal"] != false || len(diff) != 1 || diff[0].Key != key("nats") || diff[0].Kind != "only the part's" {
t.Errorf("grants = %+v, %v", got, err) t.Errorf("grants = %+v, %v", got, err)
} }
// A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted. // A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted.
+1 -36
View File
@@ -17,7 +17,7 @@ import (
// the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the // the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the
// controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key. // controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key.
// Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's // Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's
// (delivery.GrantSource), and the tool grants says every difference, so the switch is made on a measured equality. //, and the tool grants says every difference, so the switch is made on a measured equality.
// //
// A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has // A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has
// one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the // one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the
@@ -469,38 +469,3 @@ func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant {
g.V = delivery.Version g.V = delivery.Version
return g return g
} }
// GrantDifference is one key on which the part's grants and the controller's differ.
type GrantDifference struct {
Key string `json:"key"`
// Kind is `only the controller's`, `only the part's` or `accounts differ`.
Kind string `json:"kind"`
Controller []string `json:"controller_accounts,omitempty"`
Part []string `json:"part_accounts,omitempty"`
}
// CompareGrants lists every key the two sets differ on, in key order: a key held by one alone, or held by both with other
// accounts. Pure.
func CompareGrants(controller, part map[string]delivery.ControllerGrant) []GrantDifference {
keys := map[string]bool{}
for k := range controller {
keys[k] = true
}
for k := range part {
keys[k] = true
}
out := []GrantDifference{}
for _, k := range sortedKeys(keys) {
c, inC := controller[k]
g, inP := part[k]
switch {
case inC && !inP:
out = append(out, GrantDifference{Key: k, Kind: "only the controller's", Controller: c.Accounts})
case inP && !inC:
out = append(out, GrantDifference{Key: k, Kind: "only the part's", Part: g.Accounts})
case !delivery.SameControllerGrant(withVersion(c), withVersion(g)):
out = append(out, GrantDifference{Key: k, Kind: "accounts differ", Controller: c.Accounts, Part: g.Accounts})
}
}
return out
}
+9 -10
View File
@@ -24,14 +24,13 @@ import (
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`). // The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
const ( const (
ReadAssignments = "mesh-controller.assignments" ReadAssignments = "mesh-controller.assignments"
ReadControllerGrant = "mesh-controller.grants" ReadModules = "mesh-delivery-indexer.modules"
ReadModules = "mesh-delivery-indexer.modules" ReadManifests = "mesh-delivery-indexer.manifests"
ReadManifests = "mesh-delivery-indexer.manifests" ReadHave = "node-engine.have"
ReadHave = "node-engine.have" ReadResolved = "mesh-config.resolved"
ReadResolved = "mesh-config.resolved" ReadWants = "mesh-delivery-assigner.wants"
ReadWants = "mesh-delivery-assigner.wants" ReadBuilds = "mesh-delivery-builder.builds"
ReadBuilds = "mesh-delivery-builder.builds"
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9). // ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
ReadAccounts = "mesh-vault.bus-accounts" ReadAccounts = "mesh-vault.bus-accounts"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats // ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
@@ -42,7 +41,7 @@ const (
// Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the // Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the
// controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write // controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write
// they place nothing, and the first change after it takes them out (../assignments-readers names the part until then). // they place nothing, and the first change after it takes them out (../assignments-readers names the part until then).
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds, var Reads = []string{ReadAssignments, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
ReadAccounts, ReadPlacement, ReadLease, ReadTakeover} ReadAccounts, ReadPlacement, ReadLease, ReadTakeover}
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each. // The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
@@ -53,7 +52,7 @@ const (
) )
// Invokes is every tool the part calls. // Invokes is every tool the part calls.
var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant, CallRecordWarrantController} var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant}
// Reader reads another part's state, named `<module>.<state>`. A refusal (a read not granted yet, a bucket not there) is // Reader reads another part's state, named `<module>.<state>`. A refusal (a read not granted yet, a bucket not there) is
// an error the part names as a gap and composes past: it never stops for one. // an error the part names as a gap and composes past: it never stops for one.
@@ -103,7 +103,7 @@ var EndedBy = map[string]string{
DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read", DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read",
DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts", DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts",
DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts", DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts",
DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: root-free is the self-check's probe", DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)",
DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's", DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's",
DependsRead + " mesh-controller.declared-packages": "ADR 0565 §6: each node agent writes its own node's packages", DependsRead + " mesh-controller.declared-packages": "ADR 0565 §6: each node agent writes its own node's packages",
} }
+29 -18
View File
@@ -11,9 +11,11 @@ import (
"git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/health"
) )
// CallRootFree is the controller seat's verb that judges machines root-free (novox/hq ADR 0259 §8): the one judgement // CallRootFree is the node seat's verb that judges one machine root-free (novox/hq ADR 0259 §8, ADR 0563 §6), asked of
// the composer needs and the part cannot make from the bus (issue 1945). Only the controller answers it, on purpose. // that machine's own engine as `seat:node-engine.root-free@<machine>`: the judgement the composer needs and the part
const CallRootFree = "seat:mesh-controller.root-free" // cannot make from the bus (issue 1945). Only that machine's engine answers it, and the controller takes no part in it
// (ADR 0571 §11, issue 1955). The messenger asks the same verb the same way.
const CallRootFree = "seat:node-engine.root-free"
// The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the // The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the
// clearing of its `root-not-free` condition. They are the controller seat's until the operator's take-over, and the // clearing of its `root-not-free` condition. They are the controller seat's until the operator's take-over, and the
@@ -99,36 +101,45 @@ func RootFreeNodes(r buscompose.Records) []string {
return nodes return nodes
} }
// verdict is one machine's answer of the controller's root-free verb. // verdict is one machine's answer of its engine's root-free verb.
type verdict struct { type verdict struct {
Machine string `json:"machine"` Machine string `json:"machine"`
Free bool `json:"free"` Free bool `json:"free"`
Why string `json:"why"` Why string `json:"why"`
} }
// WithRootFree fills r.RootFree for the machines the composition needs by asking the controller once, and answers a // WithRootFree fills r.RootFree for the machines the composition needs by asking each machine's own engine about that
// line for each machine that is not free, `<machine>: <why>`. It fails closed: an error, no answer or a machine // machine, and answers a line for each machine that is not free, `<machine>: <why>`. It fails closed: an error, no
// left out is not free, and says so. Pure but for the one call; the answer is kept for this pass alone. // answer or a verdict an engine gives on another machine is not free, and says so. Pure but for the calls; the answers
// are kept for this pass alone.
func WithRootFree(r *buscompose.Records, call Caller) []string { func WithRootFree(r *buscompose.Records, call Caller) []string {
nodes := RootFreeNodes(*r) nodes := RootFreeNodes(*r)
if len(nodes) == 0 { if len(nodes) == 0 {
return nil return nil
} }
free := map[string]verdict{} free := map[string]verdict{}
var failure string failures := map[string]string{}
if call == nil { for _, n := range nodes {
failure = "no way to ask the controller here" if call == nil {
} else if raw, err := call.Call(CallRootFree, map[string]any{"machines": nodes}); err != nil { failures[n] = "no way to ask the machine's engine here"
failure = "the controller's root-free word could not be asked: " + err.Error() continue
} else { }
raw, err := call.Call(CallRootFree+"@"+n, map[string]any{})
if err != nil {
failures[n] = "the engine's root-free word could not be asked: " + err.Error()
continue
}
var body struct { var body struct {
Machines []verdict `json:"machines"` Machines []verdict `json:"machines"`
} }
if err := json.Unmarshal(raw, &body); err != nil { if err := json.Unmarshal(raw, &body); err != nil {
failure = "the controller's root-free answer could not be read: " + err.Error() failures[n] = "the engine's root-free answer could not be read: " + err.Error()
continue
} }
for _, v := range body.Machines { for _, v := range body.Machines {
free[v.Machine] = v if v.Machine == n {
free[n] = v
}
} }
} }
r.RootFree = map[string]bool{} r.RootFree = map[string]bool{}
@@ -138,10 +149,10 @@ func WithRootFree(r *buscompose.Records, call Caller) []string {
switch { switch {
case answered && v.Free: case answered && v.Free:
r.RootFree[n] = true r.RootFree[n] = true
case failure != "": case failures[n] != "":
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failure)) notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failures[n]))
case !answered: case !answered:
notes = append(notes, fmt.Sprintf("%s: not free, the controller named no verdict for it", n)) notes = append(notes, fmt.Sprintf("%s: not free, its engine named no verdict for it", n))
default: default:
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why)) notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why))
} }
@@ -519,12 +519,8 @@ func (p *Part) TakeOverState() (any, error) {
// Module is the grants part's module, the asker of its take-over and hand-back. // Module is the grants part's module, the asker of its take-over and hand-back.
const Module = "mesh-grants" const Module = "mesh-grants"
// The hand-act log's verbs the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's, and the // The hand-act log's verb the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's.
// controller's while the seat has no holder and the controller holds its lease (handacts.Recorder). const CallRecordWarrant = "seat:" + handacts.Seat + ".warranted"
const (
CallRecordWarrant = "seat:" + handacts.Seat + ".warranted"
CallRecordWarrantController = "seat:mesh-controller.warranted"
)
// recordWarrant records, in the hand-act log, that a take-over or a hand-back was made on the operator's warrant for ask. // recordWarrant records, in the hand-act log, that a take-over or a hand-back was made on the operator's warrant for ask.
// Said, never fatal: the switch stands either way. // Said, never fatal: the switch stands either way.
+4 -19
View File
@@ -31,12 +31,11 @@ const (
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState, var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes, ToolLeaseCheck} ToolAccounts, ToolConsumerMatch, ToolAcceptHashes, ToolLeaseCheck}
// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the // WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's
// controller's lease is handed away (delivery.GrantSource). // accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5).
const WritesGrants = "in shadow: the grants part writes grants.<module>.<fp> into its state grants, which the assigner reads only once the controller's lease is handed away (ADR 0547 clause 5); the controller's are beside, with every difference" const WritesGrants = "the grants part writes grants.<module>.<fp> into its state grants, once the list the bus loaded carries that build's accounts; it is the one key the assigner reads (ADR 0571 clause 9)"
// Grants answers the tool grants: the grants the part wrote, the controller's, and every difference between them, one // Grants answers the tool grants: the grants the part wrote, one module's or all.
// module's or all.
func (p *Part) Grants(module string) (any, error) { func (p *Part) Grants(module string) (any, error) {
out := map[string]any{"writes": WritesGrants} out := map[string]any{"writes": WritesGrants}
read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) { read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) {
@@ -65,10 +64,6 @@ func (p *Part) Grants(module string) (any, error) {
} }
return m, nil return m, nil
} }
controller, cerr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
keys, err := p.Read.Keys(ReadControllerGrant)
return keys, func(k string) ([]byte, bool, error) { return p.Read.Get(ReadControllerGrant, k) }, err
})
mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) { mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
keys, err := p.Store.Keys(StateGrants) keys, err := p.Store.Keys(StateGrants)
return keys, func(k string) ([]byte, bool, error) { return keys, func(k string) ([]byte, bool, error) {
@@ -89,21 +84,11 @@ func (p *Part) Grants(module string) (any, error) {
} }
return out return out
} }
if cerr != nil {
out["controller_unread"] = cerr.Error()
} else {
out["controller"], out["controller_count"] = list(controller), len(controller)
}
if perr != nil { if perr != nil {
out["written_unread"] = perr.Error() out["written_unread"] = perr.Error()
} else { } else {
out["written"], out["count"] = list(mine), len(mine) out["written"], out["count"] = list(mine), len(mine)
} }
if cerr == nil && perr == nil {
diff := CompareGrants(controller, mine)
out["differences"], out["differing"] = diff, len(diff)
out["equal"] = len(diff) == 0
}
if last := p.Last(); last.Grants != nil { if last := p.Last(); last.Grants != nil {
out["last_pass"] = last.Grants out["last_pass"] = last.Grants
} }
+2 -4
View File
@@ -5,7 +5,6 @@
"reads": [ "reads": [
"mesh-config.resolved", "mesh-config.resolved",
"mesh-controller.assignments", "mesh-controller.assignments",
"mesh-controller.grants",
"mesh-delivery-assigner.wants", "mesh-delivery-assigner.wants",
"mesh-delivery-builder.builds", "mesh-delivery-builder.builds",
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.manifests",
@@ -30,9 +29,8 @@
"nats.nats_streams", "nats.nats_streams",
"nats.nats_user_can", "nats.nats_user_can",
"nats.nats_users", "nats.nats_users",
"seat:mesh-controller.root-free", "seat:node-engine.root-free",
"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.warranted"
"seat:mesh-controller.warranted"
], ],
"emits": [ "emits": [
"asserted", "asserted",