Hear the keeper alone and stop reading the controller's lease in mesh-grants (issue 1126, B7b)
ADR 0590 §3 ends the choice of a condition source by the keeper's take-over key: the controller is unassigned and the keeper is the one source. The part still read the take-over key, fell back to the controller's condition events when it was absent, and read and watched the controller's lease, which nothing writes any more. It now hears the keeper's events alone and reads neither. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -187,7 +187,7 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
if n := said.Load(); n != 0 {
|
||||
t.Errorf("%d memberships were said on their subject", n)
|
||||
}
|
||||
for _, bucket := range []string{grants.Module + "_lease", grants.Module + "_takeover"} {
|
||||
for _, bucket := range []string{"mesh-controller_lease", grants.Module + "_lease", grants.Module + "_takeover"} {
|
||||
if _, err := js.KeyValue(ctx, bucket); !errors.Is(err, jetstream.ErrBucketNotFound) {
|
||||
t.Errorf("the bucket %s: %v, want none", bucket, err)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
|
||||
@@ -725,44 +724,24 @@ func TestGrants(t *testing.T) {
|
||||
waitFor(t, func() bool { return wb.count(grants.CallUsers) > 0 && handle != nil })
|
||||
passes := func() int { return wb.count(grants.CallUsers) }
|
||||
before := passes()
|
||||
handle(grants.EventConditionRaised, []byte(`{"kind":"disk-full","key":"machine.a.disk-full"}`))
|
||||
handle(grants.EventKeeperRaised, []byte(`{"kind":"disk-full","key":"machine.a.disk-full"}`))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if passes() != before {
|
||||
t.Errorf("a condition of another kind woke a pass")
|
||||
}
|
||||
// 1952/G1 (ADR 0564 §9): the keeper's events are not heard while the take-over key follows the controller
|
||||
handle("mesh.seat.mesh-conditions.event.condition-raised", []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if passes() != before || wf.Follows() != "mesh-controller" {
|
||||
t.Errorf("the keeper's event woke a pass while the part follows %s", wf.Follows())
|
||||
}
|
||||
handle(grants.EventConditionCleared, []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
||||
waitFor(t, func() bool { return passes() > before })
|
||||
// and from the take-over only the keeper's are heard, never both
|
||||
watcher := wf.Watch.(*fakeWatcher)
|
||||
waitFor(t, func() bool {
|
||||
watcher.mu.Lock()
|
||||
defer watcher.mu.Unlock()
|
||||
return watcher.changes[grants.ReadTakeover] != nil
|
||||
})
|
||||
watcher.mu.Lock()
|
||||
toKeeper := watcher.changes[grants.ReadTakeover]
|
||||
watcher.mu.Unlock()
|
||||
toKeeper(health.TakeoverKey, []byte(`{"v":1,"follow":"keeper","by":"operator","at":"2026-10-29T20:00:00Z"}`))
|
||||
if wf.Follows() != health.KeeperModule {
|
||||
t.Fatalf("the part follows %s after the take-over", wf.Follows())
|
||||
}
|
||||
before = passes()
|
||||
// 1126/B7b (ADR 0590 §3): only the keeper's events are heard, with no take-over key read; the controller's never.
|
||||
handle("mesh.seat.mesh-controller.event.condition-raised", []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if passes() != before {
|
||||
t.Errorf("the controller's event woke a pass after the take-over")
|
||||
t.Errorf("the controller's event woke a pass")
|
||||
}
|
||||
handle(grants.EventKeeperRaised, []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
||||
handle("mesh.seat.mesh-conditions.event.condition-raised", []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
||||
waitFor(t, func() bool { return passes() > before })
|
||||
toKeeper(health.TakeoverKey, nil) // handed back: the key deleted follows the controller
|
||||
if wf.Follows() != "mesh-controller" {
|
||||
t.Errorf("the part follows %s after the hand-back", wf.Follows())
|
||||
before = passes()
|
||||
handle(grants.EventKeeperCleared, []byte(`{"kind":"root-not-free","key":"machine.a.root-not-free"}`))
|
||||
waitFor(t, func() bool { return passes() > before })
|
||||
if slices.Contains(grants.Followed, "mesh-conditions.takeover") || slices.Contains(grants.Reads, "mesh-conditions.takeover") {
|
||||
t.Errorf("the part still reads the keeper's take-over key: %v", grants.Reads)
|
||||
}
|
||||
wcancel()
|
||||
<-wdone
|
||||
@@ -2042,7 +2021,7 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("memberships %v", got)
|
||||
}
|
||||
for _, r := range grants.Reads {
|
||||
if strings.HasPrefix(r, "mesh-controller.") && r != grants.ReadLease {
|
||||
if strings.HasPrefix(r, "mesh-controller.") {
|
||||
t.Errorf("the part reads %s", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,6 @@ import (
|
||||
// alone creates them, with the configuration the bus composer derives (buscompose.AccountConsumers), only where the bus
|
||||
// lacks one; one the bus holds is never changed or deleted by a pass, a difference is only said.
|
||||
|
||||
// ReadLease is the controller's lease as the part reads it (buscompose.NamedReaders names the vault and the part), and
|
||||
// LeaseKey its one key.
|
||||
const (
|
||||
ReadLease = buscompose.ControllerLease
|
||||
LeaseKey = buscompose.ControllerLeaseKey
|
||||
)
|
||||
|
||||
// ConsumerCreator creates one durable consumer the bus does not hold: the part's process alone, whose account is granted
|
||||
// the create of each account's consumer by stream and name. One the bus holds answers ErrHeld.
|
||||
type ConsumerCreator interface {
|
||||
@@ -88,9 +81,8 @@ func heldConsumers(c Caller, stream string) (map[string]heldConsumer, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ConsumersOn is why the part creates and matches the account consumers with no lease read: the controller ended by its
|
||||
// unassignment, never by the lease write, so its lease names no holder for good and the part is their only maker
|
||||
// (issue 1126; mesh-sdk's buscompose.LeaseHandedAway answers false for an absent key, so it is no longer asked here).
|
||||
// ConsumersOn is why the part creates and matches the account consumers: the controller ended by its unassignment, so
|
||||
// the part is their only maker (novox/hq ADR 0590, issue 1126).
|
||||
const ConsumersOn = "the grants part makes and matches the account consumers: the controller ended by its unassignment (issue 1126)"
|
||||
|
||||
// consumers compares the account consumers the records compose with those the bus holds, and creates the missing ones
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// Watcher watches another part's state: changed hears each change after what stands, ended hears the watch end. A watch
|
||||
@@ -23,8 +21,8 @@ type Watcher interface {
|
||||
//
|
||||
// And the seat mesh-enrolment's keys (ADR 0587 §4, §5): a key at live composes enrol.<node>, and its end, by the holder or by
|
||||
// the bus's expiry, is a change of the key that takes the user out; no event is needed beside the watch.
|
||||
var Followed = []string{ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadLease, ReadTakeover,
|
||||
ReadEnrolments, ReadNodeLogins}
|
||||
var Followed = []string{ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments,
|
||||
ReadNodeLogins}
|
||||
|
||||
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
|
||||
// open is said, composed without, and opened again at the next wake (any change it does hear, or a reconnect).
|
||||
@@ -34,8 +32,8 @@ type Follower struct {
|
||||
Logf func(string, ...any)
|
||||
// Asked watches the part's own ask to compose now (a tool's compose-now), waking at each; nil watches none.
|
||||
Asked func(wake func()) error
|
||||
// Hear hears the controller seat's condition events and the vault's account-issued (Consumes) and hands each, by event
|
||||
// name, with its body, to handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass:
|
||||
// Hear hears the keeper's condition events and the vault's account-issued (Consumes) and hands each, by event name,
|
||||
// with its body, to handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass:
|
||||
// a machine's root-free word changed; and so does an account the vault minted (ADR 0561 §1), whose hash the list carries.
|
||||
Hear func(handle func(event string, body []byte)) error
|
||||
|
||||
@@ -52,21 +50,6 @@ type Follower struct {
|
||||
held string
|
||||
notAsserted string
|
||||
refusedMans string
|
||||
// leaseOn is whether the lease was handed away at its last change heard, once leaseSeen.
|
||||
leaseOn, leaseSeen bool
|
||||
// follows is the seat whose condition events the part hears, as the take-over key names it: the controller's until
|
||||
// the operator's take-over (an absent key, or one not readable, follows the controller).
|
||||
follows string
|
||||
}
|
||||
|
||||
// Follows is the seat whose condition events the part hears now.
|
||||
func (f *Follower) Follows() string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.follows == "" {
|
||||
return "mesh-controller"
|
||||
}
|
||||
return f.follows
|
||||
}
|
||||
|
||||
func (f *Follower) init() {
|
||||
@@ -106,35 +89,6 @@ func (f *Follower) logf(format string, args ...any) {
|
||||
// heard says whether a change composes again: every change but a have's, and of the haves a machine's first and the
|
||||
// bus module's loaded list moving.
|
||||
func (f *Follower) heard(state, key string, value []byte) bool {
|
||||
if state == ReadTakeover {
|
||||
// Which seat's condition events are heard: no pass, since nothing composed reads it.
|
||||
if key != health.TakeoverKey {
|
||||
return false
|
||||
}
|
||||
t, err := health.DecodeTakeover(value, len(value) > 0)
|
||||
if err != nil {
|
||||
f.logf("the take-over key cannot be read (%v): the controller's condition events are heard", err)
|
||||
t = health.Takeover{Follow: health.FollowController}
|
||||
}
|
||||
f.mu.Lock()
|
||||
was := f.follows
|
||||
f.follows = t.Seat()
|
||||
f.mu.Unlock()
|
||||
if was != t.Seat() {
|
||||
f.logf("root-free words are heard from %s's condition events (the take-over key follows %s)", t.Seat(), t.Follow)
|
||||
}
|
||||
return false
|
||||
}
|
||||
if state == ReadLease {
|
||||
// Renewed every few seconds by its holder: only the write that hands it away, or back, composes again (ADR 0561 §6).
|
||||
// Once handed away nobody renews it, so any change is heard: it may be the lease altered (ADR 0582 clause 4).
|
||||
on, _ := buscompose.LeaseHandedAway(value, len(value) > 0, nil)
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
was, seen := f.leaseOn, f.leaseSeen
|
||||
f.leaseOn, f.leaseSeen = on, true
|
||||
return !seen || was != on || on
|
||||
}
|
||||
if state != ReadHave {
|
||||
return true
|
||||
}
|
||||
@@ -179,16 +133,13 @@ func (f *Follower) open() {
|
||||
f.Wake()
|
||||
return
|
||||
}
|
||||
// A condition event of the seat the take-over key does not name is not heard (ADR 0564 §9).
|
||||
if from := HeardFrom(event); from != "" && from != f.Follows() {
|
||||
return
|
||||
}
|
||||
if IsRootNotFree(body) {
|
||||
// A condition event is heard from the keeper alone (ADR 0590 §3).
|
||||
if FromKeeper(event) && IsRootNotFree(body) {
|
||||
f.Wake()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
f.logf("the controller's condition events cannot be heard yet (%v): tried again at the next change", err)
|
||||
f.logf("the keeper's condition events cannot be heard yet (%v): tried again at the next change", err)
|
||||
} else {
|
||||
f.heard_ = true
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const (
|
||||
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
|
||||
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
|
||||
var Reads = []string{ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||
ReadAccounts, ReadPlacement, ReadLease, ReadTakeover, ReadEnrolments, ReadNodeLogins}
|
||||
ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins}
|
||||
|
||||
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
||||
const (
|
||||
|
||||
@@ -18,30 +18,20 @@ import (
|
||||
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
|
||||
// clearing of its `root-not-free` condition. They are the controller seat's until the operator's take-over, and the
|
||||
// condition keeper's (mesh-conditions) from it: the part hears only the seat the take-over key names, never both (novox/hq
|
||||
// ADR 0564 §9, issue 1952 G1).
|
||||
// clearing of its `root-not-free` condition. They are the condition keeper's (mesh-conditions) alone, the one keeper
|
||||
// since the controller ended by its unassignment (novox/hq ADR 0590 §3, which ends ADR 0564 §9's selection by the
|
||||
// take-over key).
|
||||
const (
|
||||
EventConditionRaised = "mesh-controller.condition-raised"
|
||||
EventConditionCleared = "mesh-controller.condition-cleared"
|
||||
EventKeeperRaised = health.KeeperModule + ".condition-raised"
|
||||
EventKeeperCleared = health.KeeperModule + ".condition-cleared"
|
||||
EventKeeperRaised = health.KeeperModule + ".condition-raised"
|
||||
EventKeeperCleared = health.KeeperModule + ".condition-cleared"
|
||||
// KindRootNotFree is the condition kind whose events wake a pass.
|
||||
KindRootNotFree = "root-not-free"
|
||||
)
|
||||
|
||||
// ReadTakeover is the condition keeper's take-over key's state (`switch`): which seat's condition events the part hears.
|
||||
const ReadTakeover = health.KeeperModule + "." + health.TakeoverState
|
||||
|
||||
// HeardFrom is the seat a condition event heard comes from, by its name (`<seat>.<event>`) or its bus subject
|
||||
// (`mesh.seat.<seat>.event.<event>`); empty for any other.
|
||||
func HeardFrom(event string) string {
|
||||
for _, seat := range []string{health.KeeperModule, "mesh-controller"} {
|
||||
if strings.HasPrefix(event, seat+".") || strings.HasPrefix(event, "mesh.seat."+seat+".event.") {
|
||||
return seat
|
||||
}
|
||||
}
|
||||
return ""
|
||||
// FromKeeper says a condition event heard is the keeper's, by its name (`<seat>.<event>`) or its bus subject
|
||||
// (`mesh.seat.<seat>.event.<event>`).
|
||||
func FromKeeper(event string) bool {
|
||||
return strings.HasPrefix(event, health.KeeperModule+".") || strings.HasPrefix(event, "mesh.seat."+health.KeeperModule+".event.")
|
||||
}
|
||||
|
||||
// EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's
|
||||
@@ -49,8 +39,6 @@ func HeardFrom(event string) string {
|
||||
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
|
||||
|
||||
// Consumes is every event the part hears, as its manifest's consumes name them.
|
||||
//
|
||||
// Not the controller's condition events: the keeper's alone (novox/hq ADR 0564 §9, issue 774).
|
||||
var Consumes = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued}
|
||||
|
||||
// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject.
|
||||
@@ -58,11 +46,6 @@ func IsAccountIssued(event string) bool {
|
||||
return event == EventAccountIssued || event == buscompose.AccountIssuedSubject
|
||||
}
|
||||
|
||||
// EventSubject is the bus subject of a controller seat event named `mesh-controller.<event>`.
|
||||
func EventSubject(event string) string {
|
||||
return "mesh.seat.mesh-controller.event." + strings.TrimPrefix(event, "mesh-controller.")
|
||||
}
|
||||
|
||||
// IsRootNotFree says a condition event's body names the `root-not-free` kind (its `kind`, or the last part of its
|
||||
// `key`, `<scope>.<id>.<kind>`). An unreadable body wakes a pass too: a pass is cheap, a missed change is not.
|
||||
func IsRootNotFree(body []byte) bool {
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
"mesh-delivery-assigner.placement",
|
||||
"node-engine.have",
|
||||
"mesh-vault.bus-accounts",
|
||||
"mesh-controller.lease",
|
||||
"mesh-conditions.takeover",
|
||||
"mesh-enrolment.enrolments",
|
||||
"mesh-vault.node-logins"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user