Files
mesh-delivery/modules/mesh-grants/internal/grants/bindings.go
T
jschoubben 7cd5bfb31b DLV-G1: the grants part writes the bus module's binding into the module's memberships on created, rewrites it on recreated, removes it on dropped, kept in memberships (ADR 0595 §1, §7)
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
2026-11-04 12:34:50 +01:00

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
}