The tool runner reads its membership from the grants part's state alone: no stream read, no subject heard (ADR 0571 §10)
Follow no longer asks the ASSIGNMENTS stream for a copy nor subscribes mesh.assignment; a module with no membership key is said so and served in its derived shape. The test mesh issues memberships where the grants part keeps them. Tests: none added; the stream-fallback assertions of two existing tests are replaced by their state-only counterparts, and the test mesh no longer makes the stream. The two integration tests of the bus were run locally against the embedded bus. (issue 774, 774/S32) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -37,8 +37,6 @@ import (
|
||||
// verb still runs, so a caller is never left guessing past this (novox/hq issue 265).
|
||||
const RequestTimeout = 30 * time.Second
|
||||
|
||||
const assignmentsStream = "ASSIGNMENTS"
|
||||
|
||||
// Credential is a broker credential as the mesh delivers it (novox/hq ADR 0120).
|
||||
type Credential struct {
|
||||
URL string `json:"url"`
|
||||
@@ -183,12 +181,9 @@ type Conn struct {
|
||||
cred Credential
|
||||
mu sync.Mutex
|
||||
issued map[string]*Membership // module → membership; present with nil = followed, none issued
|
||||
// fromGrants is every module whose membership the grants part's state memberships has given (novox/hq ADR 0571
|
||||
// clause 7): its assignments stream copy is then a fallback no longer applied.
|
||||
fromGrants map[string]bool
|
||||
onNew []*func(Membership)
|
||||
subs []*nats.Subscription
|
||||
Logf func(format string, args ...any)
|
||||
onNew []*func(Membership)
|
||||
subs []*nats.Subscription
|
||||
Logf func(format string, args ...any)
|
||||
// answering is every subject this runtime answers on, so a subscription the bus refused can be
|
||||
// asked for again (novox/hq issue 222).
|
||||
answering map[string]*answered
|
||||
@@ -332,7 +327,7 @@ func Connect(cred Credential) (*Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
c = &Conn{nc: nc, js: js, self: cred.Module, node: cred.Node, cred: cred,
|
||||
issued: map[string]*Membership{}, fromGrants: map[string]bool{}, Logf: log.Printf, answering: map[string]*answered{}}
|
||||
issued: map[string]*Membership{}, Logf: log.Printf, answering: map[string]*answered{}}
|
||||
c.Follow(cred.Module)
|
||||
return c, nil
|
||||
}
|
||||
@@ -346,16 +341,10 @@ func (c *Conn) Node() string { return c.node }
|
||||
// Credential is what this connection was opened with.
|
||||
func (c *Conn) Credential() Credential { return c.cred }
|
||||
|
||||
// MembershipSubject is the one address a runtime derives for an assignment (ADR 0160).
|
||||
func MembershipSubject(node, module string) string {
|
||||
return "mesh.assignment." + node + "." + module
|
||||
}
|
||||
|
||||
// Follow reads a module's membership on this machine once and follows it live, so its tools are
|
||||
// served where the mesh issued them (ADR 0175). It is read from the grants part's state memberships, its key
|
||||
// `<node>.<module>` (novox/hq ADR 0571 clause 7, mesh-sdk buscompose.GrantsMembershipsBucket); the controller's
|
||||
// assignments stream is read beside it as the fallback until the lease write, and applied only while the state has given
|
||||
// the module no membership.
|
||||
// `<node>.<module>` (novox/hq ADR 0571 clause 7 and 10, mesh-sdk buscompose.GrantsMembershipsBucket), and nowhere else:
|
||||
// the controller's assignments stream is not read and its subject is not heard.
|
||||
func (c *Conn) Follow(module string) {
|
||||
c.mu.Lock()
|
||||
if c.node == "" {
|
||||
@@ -369,53 +358,17 @@ func (c *Conn) Follow(module string) {
|
||||
c.issued[module] = nil
|
||||
c.mu.Unlock()
|
||||
c.followGrants(module)
|
||||
subject := MembershipSubject(c.node, module)
|
||||
// The subject-addressed direct get: the one address the mesh grants this account on the
|
||||
// stream's API.
|
||||
if c.Membership(module) == nil {
|
||||
if got, err := c.nc.Request("$JS.API.DIRECT.GET."+assignmentsStream+"."+subject, nil, 5*time.Second); err == nil {
|
||||
if got.Header.Get("Status") == "" && len(got.Data) > 0 {
|
||||
var m Membership
|
||||
if json.Unmarshal(got.Data, &m) == nil {
|
||||
c.mu.Lock()
|
||||
if !c.fromGrants[module] {
|
||||
c.issued[module] = &m
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Membership(module) == nil {
|
||||
c.Logf("[mesh-tools] no membership issued for %s on %s yet; serving the derived shape until one arrives", module, c.node)
|
||||
}
|
||||
sub, err := c.nc.Subscribe(subject, func(msg *nats.Msg) {
|
||||
var m Membership
|
||||
if err := json.Unmarshal(msg.Data, &m); err != nil {
|
||||
c.Logf("[mesh-tools] a membership arrived that is not one: %v", err)
|
||||
return
|
||||
}
|
||||
c.issue(module, m, false)
|
||||
})
|
||||
if err == nil {
|
||||
c.track(sub)
|
||||
}
|
||||
}
|
||||
|
||||
// GrantsPatience bounds the first read of a membership from the grants part's state.
|
||||
var GrantsPatience = 2 * time.Second
|
||||
|
||||
// issue applies a membership that arrived for module, live, and re-serves on it. One from the assignments stream is
|
||||
// passed over once the grants part's state has given the module one.
|
||||
func (c *Conn) issue(module string, m Membership, fromGrants bool) {
|
||||
// issue applies a membership that arrived for module, live, and re-serves on it.
|
||||
func (c *Conn) issue(module string, m Membership) {
|
||||
c.mu.Lock()
|
||||
if !fromGrants && c.fromGrants[module] {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if fromGrants {
|
||||
c.fromGrants[module] = true
|
||||
}
|
||||
c.issued[module] = &m
|
||||
handlers := make([]func(Membership), 0, len(c.onNew))
|
||||
for _, h := range c.onNew {
|
||||
@@ -431,7 +384,7 @@ func (c *Conn) issue(module string, m Membership, fromGrants bool) {
|
||||
|
||||
// followGrants reads module's key of the grants part's state memberships and watches it: the value there now is applied
|
||||
// before Follow returns, each change after it live. A state that is not there yet, or not readable, is said once, and
|
||||
// the assignments stream answers alone.
|
||||
// the module has no membership until it can be read.
|
||||
func (c *Conn) followGrants(module string) {
|
||||
key := buscompose.MembershipKey(c.node, module)
|
||||
// First the key's direct get, which the account is granted alone: a bus that refuses it, or a state not there yet,
|
||||
@@ -443,8 +396,8 @@ func (c *Conn) followGrants(module string) {
|
||||
if err != nil {
|
||||
why = err.Error()
|
||||
}
|
||||
c.Logf("[mesh-tools] the grants part's memberships (%s) cannot be read (%s); %s's membership is read from the "+
|
||||
"assignments stream alone", bucket, why, module)
|
||||
c.Logf("[mesh-tools] the grants part's memberships (%s) cannot be read (%s); %s has no membership "+
|
||||
"until it can be", bucket, why, module)
|
||||
return
|
||||
}
|
||||
js, err := jetstream.New(c.nc)
|
||||
@@ -455,14 +408,14 @@ func (c *Conn) followGrants(module string) {
|
||||
defer cancel()
|
||||
kv, err := js.KeyValue(ctx, bucket)
|
||||
if err != nil {
|
||||
c.Logf("[mesh-tools] the grants part's memberships (%s) cannot be read (%v); %s's membership is read from the "+
|
||||
"assignments stream alone", buscompose.GrantsMembershipsBucket, err, module)
|
||||
c.Logf("[mesh-tools] the grants part's memberships (%s) cannot be read (%v); %s has no membership "+
|
||||
"until it can be", buscompose.GrantsMembershipsBucket, err, module)
|
||||
return
|
||||
}
|
||||
w, err := kv.Watch(context.Background(), key)
|
||||
if err != nil {
|
||||
c.Logf("[mesh-tools] %s's key %s of the grants part's memberships cannot be watched (%v); read from the "+
|
||||
"assignments stream alone", module, key, err)
|
||||
c.Logf("[mesh-tools] %s's key %s of the grants part's memberships cannot be watched (%v); %s has no membership "+
|
||||
"until it can be", module, key, err, module)
|
||||
return
|
||||
}
|
||||
apply := func(e jetstream.KeyValueEntry) {
|
||||
@@ -471,7 +424,7 @@ func (c *Conn) followGrants(module string) {
|
||||
}
|
||||
// Read through mesh-sdk's one codec of this state, the grants part's writer's (issue 774): it takes the bare
|
||||
// membership and a wrapper the part wrote before of one published or unchanged, and refuses a held-back one's
|
||||
// wrapper or a value naming no node or module, which then leaves the assignments stream's copy in force instead.
|
||||
// wrapper or a value naming no node or module, which then leaves the module with no membership.
|
||||
// A refused value arriving after a good one keeps the last good membership: nothing here clears it.
|
||||
var m Membership
|
||||
body, err := membership.BareGrantsMembership(e.Value())
|
||||
@@ -482,7 +435,7 @@ func (c *Conn) followGrants(module string) {
|
||||
c.Logf("[mesh-tools] the membership at %s is not one: %v", key, err)
|
||||
return
|
||||
}
|
||||
c.issue(module, m, true)
|
||||
c.issue(module, m)
|
||||
}
|
||||
timer := time.NewTimer(StateTimeout)
|
||||
defer timer.Stop()
|
||||
@@ -499,8 +452,8 @@ func (c *Conn) followGrants(module string) {
|
||||
apply(e)
|
||||
case <-timer.C:
|
||||
_ = w.Stop()
|
||||
c.Logf("[mesh-tools] %s's key %s of the grants part's memberships did not answer in %s; read from the "+
|
||||
"assignments stream alone", module, key, StateTimeout)
|
||||
c.Logf("[mesh-tools] %s's key %s of the grants part's memberships did not answer in %s; %s has no membership "+
|
||||
"until it can be", module, key, StateTimeout, module)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user