Read memberships from the grants part's state, what the mesh holds from the indexer and placement, and give bundles newest, since and watch on logs, so the tool runner asks the controller for nothing it keeps (issue 1955, ADR 0571 clauses 1, 4, 5, 7)
Pins mesh-sdk bb993dd. The assignments stream stays the membership's fallback until the lease write; a log another module keeps is read as <module>.<log>.
This commit is contained in:
+129
-24
@@ -10,6 +10,7 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -180,9 +182,12 @@ type Conn struct {
|
||||
cred Credential
|
||||
mu sync.Mutex
|
||||
issued map[string]*Membership // module → membership; present with nil = followed, none issued
|
||||
onNew []*func(Membership)
|
||||
subs []*nats.Subscription
|
||||
Logf func(format string, args ...any)
|
||||
// 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)
|
||||
// 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
|
||||
@@ -326,7 +331,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{}, Logf: log.Printf, answering: map[string]*answered{}}
|
||||
issued: map[string]*Membership{}, fromGrants: map[string]bool{}, Logf: log.Printf, answering: map[string]*answered{}}
|
||||
c.Follow(cred.Module)
|
||||
return c, nil
|
||||
}
|
||||
@@ -346,7 +351,10 @@ func MembershipSubject(node, module string) string {
|
||||
}
|
||||
|
||||
// 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).
|
||||
// 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.
|
||||
func (c *Conn) Follow(module string) {
|
||||
c.mu.Lock()
|
||||
if c.node == "" {
|
||||
@@ -359,16 +367,21 @@ 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 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()
|
||||
c.issued[module] = &m
|
||||
c.mu.Unlock()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,24 +394,116 @@ func (c *Conn) Follow(module string) {
|
||||
c.Logf("[mesh-tools] a membership arrived that is not one: %v", err)
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.issued[module] = &m
|
||||
handlers := make([]func(Membership), 0, len(c.onNew))
|
||||
for _, h := range c.onNew {
|
||||
handlers = append(handlers, *h)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.Logf("[mesh-tools] %s on %s was issued a new membership; re-serving on it", module, c.node)
|
||||
for _, h := range handlers {
|
||||
h(m)
|
||||
}
|
||||
_ = c.nc.Flush()
|
||||
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) {
|
||||
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 {
|
||||
handlers = append(handlers, *h)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.Logf("[mesh-tools] %s on %s was issued a new membership; re-serving on it", module, c.node)
|
||||
for _, h := range handlers {
|
||||
h(m)
|
||||
}
|
||||
_ = c.nc.Flush()
|
||||
}
|
||||
|
||||
// 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.
|
||||
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,
|
||||
// answers at once, where binding the bucket would wait out its bound on a refusal.
|
||||
bucket := buscompose.GrantsMembershipsBucket
|
||||
probe, err := c.nc.Request("$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+key, nil, GrantsPatience)
|
||||
if err != nil || probe.Header.Get("Status") == "503" {
|
||||
why := "it did not answer"
|
||||
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)
|
||||
return
|
||||
}
|
||||
js, err := jetstream.New(c.nc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
|
||||
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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
apply := func(e jetstream.KeyValueEntry) {
|
||||
if e.Operation() != jetstream.KeyValuePut {
|
||||
return // a membership withdrawn is the stream's, or the module's unassignment's, to say
|
||||
}
|
||||
var m Membership
|
||||
if err := json.Unmarshal(e.Value(), &m); err != nil {
|
||||
c.Logf("[mesh-tools] the membership at %s is not one: %v", key, err)
|
||||
return
|
||||
}
|
||||
c.issue(module, m, true)
|
||||
}
|
||||
timer := time.NewTimer(StateTimeout)
|
||||
defer timer.Stop()
|
||||
for current := true; current; {
|
||||
select {
|
||||
case e, ok := <-w.Updates():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
current = false
|
||||
continue
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
for e := range w.Updates() {
|
||||
if e != nil {
|
||||
apply(e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Membership is what the mesh issued a module here, or nil when nothing has been issued.
|
||||
func (c *Conn) Membership(module string) *Membership {
|
||||
c.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user