The part hears the bus module's own stream on its durable consumer there. That consumer did not exist: since the controller ended (ADR 0590) nobody created a module's consumer on a module stream it hears (AccountConsumers left them to the controller), so it is created now with every account consumer, and a pass that created consumers wakes one more pass so the part's own hearing opens at once. Bindings are kept per module under bound.<module>. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
199 lines
7.4 KiB
Go
199 lines
7.4 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
|
"mesh-grants/internal/buscompose"
|
|
)
|
|
|
|
// **The binding travels on the bus module's events** (novox/hq ADR 0595 §1, §7; design 25 § 12.2; DLV-G1). The bus
|
|
// module (the holder of the seat mesh-broker) says `created` when it created one entry of a module's bus state,
|
|
// `recreated` when it rebuilt one, and `dropped` when it removed one; each carries the entry's names on the bus. The
|
|
// grants part hears them on its durable consumer of the bus module's own stream (its manifest's `consumes`), writes the
|
|
// binding into the module's membership on every machine the module is assigned, rewrites it on `recreated` and removes
|
|
// it on `dropped`. What it heard is kept in its own state memberships, one key per module (BoundKey), so a binding
|
|
// outlives the 30 days its event is kept and reaches a machine the module is assigned to later. A `refused` create
|
|
// hands no binding: only these three events are read.
|
|
|
|
// The bus module's events the part hears, by their local names (ADR 0595 §6; ADR 0620 §2: the holder's own events,
|
|
// never a seat's subject).
|
|
const (
|
|
BrokerCreated = "created"
|
|
BrokerRecreated = "recreated"
|
|
BrokerDropped = "dropped"
|
|
)
|
|
|
|
// BoundPrefix starts the key of the part's own state memberships that keeps one module's bindings, `bound.<module>`:
|
|
// never a `<node>.<module>` key, since no machine is named `bound` (a membership's runtime reads its own key alone).
|
|
const BoundPrefix = "bound."
|
|
|
|
// BoundKey is the key keeping module's bindings.
|
|
func BoundKey(module string) string { return BoundPrefix + module }
|
|
|
|
// BrokerEvent is the body of the bus module's `created`, `recreated` and `dropped`, the fields the part reads.
|
|
//
|
|
// It stands until the seat mesh-broker's definition is generated in mesh-contracts (ADR 0613 §1, §5; design 26): this
|
|
// part then reads the generated type, with the same fields.
|
|
type BrokerEvent struct {
|
|
V int `json:"v"`
|
|
Module string `json:"module"`
|
|
Local string `json:"local"`
|
|
Storage string `json:"storage"`
|
|
Template string `json:"template,omitempty"`
|
|
// Binding is the entry's names on the bus: whole on `created` and `recreated`; on `dropped` only its local name counts.
|
|
Binding membership.BusBinding `json:"binding"`
|
|
}
|
|
|
|
// Bound is the value of BoundKey: every binding of one module the bus module said it created and has not dropped, by
|
|
// local name.
|
|
type Bound struct {
|
|
V int `json:"v"`
|
|
Module string `json:"module"`
|
|
Bindings map[string]membership.BusBinding `json:"bindings"`
|
|
}
|
|
|
|
// brokerEvent is the local name of an event heard from the holder of mesh-broker, by its name or its bus subject, and
|
|
// whether it is one; an emitter that holds no mesh-broker seat is never one.
|
|
func brokerEvent(event string, brokers map[string]bool) (string, bool) {
|
|
emitter, name := "", ""
|
|
switch {
|
|
case buscompose.OnAModuleStream(event):
|
|
// mesh.stream.<module>.event.<event>
|
|
rest := strings.TrimPrefix(event, "mesh.stream.")
|
|
emitter, name, _ = strings.Cut(rest, ".event.")
|
|
case strings.HasPrefix(event, "mesh.mod."):
|
|
rest := strings.TrimPrefix(event, "mesh.mod.")
|
|
emitter, name, _ = strings.Cut(rest, ".event.")
|
|
default:
|
|
emitter, name, _ = strings.Cut(event, ".")
|
|
}
|
|
if !brokers[emitter] {
|
|
return "", false
|
|
}
|
|
switch name {
|
|
case BrokerCreated, BrokerRecreated, BrokerDropped:
|
|
return name, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// HearBroker applies one event heard to the kept bindings: a `created` or `recreated` writes the entry's binding, a
|
|
// `dropped` removes it. It answers whether the kept bindings changed (so a pass composes again), or why the event was
|
|
// not applied; an event that is no bus module's is no change and no error. brokers are the modules holding mesh-broker.
|
|
func (p *Part) HearBroker(event string, body []byte, brokers map[string]bool) (bool, error) {
|
|
name, ok := brokerEvent(event, brokers)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
var e BrokerEvent
|
|
if err := json.Unmarshal(body, &e); err != nil {
|
|
return false, fmt.Errorf("the bus module's %s does not read: %w", name, err)
|
|
}
|
|
local := e.Local
|
|
if local == "" {
|
|
local = e.Binding.Local
|
|
}
|
|
if e.Module == "" || local == "" || strings.Contains(e.Module, ".") {
|
|
return false, fmt.Errorf("the bus module's %s names no module or no entry (%q, %q)", name, e.Module, local)
|
|
}
|
|
b, err := p.bound(e.Module)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
was, had := b.Bindings[local]
|
|
switch name {
|
|
case BrokerDropped:
|
|
if !had {
|
|
return false, nil
|
|
}
|
|
delete(b.Bindings, local)
|
|
default:
|
|
bind := e.Binding
|
|
bind.Local = local
|
|
if bind.Storage == "" {
|
|
bind.Storage = e.Storage
|
|
}
|
|
if bind.Template == "" {
|
|
bind.Template = e.Template
|
|
}
|
|
if had && sameBinding(was, bind) {
|
|
return false, nil
|
|
}
|
|
b.Bindings[local] = bind
|
|
}
|
|
raw, _ := json.Marshal(b)
|
|
if _, err := p.Store.Put(StateMemberships, BoundKey(e.Module), raw); err != nil {
|
|
return false, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, BoundKey(e.Module), err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// bound reads the kept bindings of module; none kept is an empty value.
|
|
func (p *Part) bound(module string) (Bound, error) {
|
|
b := Bound{V: 1, Module: module, Bindings: map[string]membership.BusBinding{}}
|
|
raw, _, found, err := p.Store.Get(StateMemberships, BoundKey(module))
|
|
if err != nil {
|
|
return b, fmt.Errorf("its own %s %s cannot be read: %w", StateMemberships, BoundKey(module), err)
|
|
}
|
|
if found {
|
|
if err := json.Unmarshal(raw, &b); err != nil {
|
|
return b, fmt.Errorf("its own %s %s does not read: %w", StateMemberships, BoundKey(module), err)
|
|
}
|
|
if b.Bindings == nil {
|
|
b.Bindings = map[string]membership.BusBinding{}
|
|
}
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// bindingsOf is module's kept bindings in local-name order, for its memberships; nil when none.
|
|
func (p *Part) bindingsOf(module string) ([]membership.BusBinding, error) {
|
|
b, err := p.bound(module)
|
|
if err != nil || len(b.Bindings) == 0 {
|
|
return nil, err
|
|
}
|
|
locals := make([]string, 0, len(b.Bindings))
|
|
for l := range b.Bindings {
|
|
locals = append(locals, l)
|
|
}
|
|
sort.Strings(locals)
|
|
out := make([]membership.BusBinding, 0, len(locals))
|
|
for _, l := range locals {
|
|
out = append(out, b.Bindings[l])
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func sameBinding(a, b membership.BusBinding) bool {
|
|
ra, _ := json.Marshal(a)
|
|
rb, _ := json.Marshal(b)
|
|
return string(ra) == string(rb)
|
|
}
|
|
|
|
// OwnHears are the module streams the part's own membership on node names it hearing (its consumes of another module's
|
|
// stream, the bus module's among them), each with its durable consumer: read from its own state memberships, which the
|
|
// part writes; none yet is an error, so the hearing is opened at the next change.
|
|
func OwnHears(s Store, node string) ([]membership.HeardIssued, error) {
|
|
key := buscompose.MembershipKey(node, Module)
|
|
raw, _, found, err := s.Get(StateMemberships, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("its own membership %s cannot be read: %w", key, err)
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("its own membership %s is not composed yet, so the module streams it hears are not known", key)
|
|
}
|
|
bare, err := membership.BareGrantsMembership(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("its own membership %s does not read: %w", key, err)
|
|
}
|
|
var m membership.Membership
|
|
if err := json.Unmarshal(bare, &m); err != nil {
|
|
return nil, fmt.Errorf("its own membership %s does not read: %w", key, err)
|
|
}
|
|
return m.Hears, nil
|
|
}
|