package grants import ( "encoding/json" "fmt" "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" "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 as the seat's events, `mesh-broker.` on the shared events stream (its manifest's // `consumes`), on its durable consumer; 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 7 days the shared events stream keeps its event 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 on the seat mesh-broker (ADR 0595 §6). 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.`: // never a `.` 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,omitempty"` Module string `json:"module"` // LocalName and StorageType are the seat's contract's names (mesh-contracts meshbroker.Said); Local and Storage the // same read where an older body names them so. LocalName string `json:"local-name,omitempty"` Local string `json:"local,omitempty"` StorageType string `json:"storage-type,omitempty"` Storage string `json:"storage,omitempty"` 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 bus subject, and whether it is // one: the seat's own subject alone (`mesh.seat.mesh-broker.event.`, the lead's ruling, registry milestone 13:0x), // where only its holder publishes, and only while a module holds the seat. The bus module's own stream is its history // for its tool, never what the part hears. func brokerEvent(event string, brokers map[string]bool) (string, bool) { name, onTheSeat := strings.CutPrefix(event, "mesh.seat."+meshbroker.Seat+".event.") if !onTheSeat || len(brokers) == 0 { 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 := firstOf(e.LocalName, e.Local, e.Binding.Local) storage := firstOf(e.StorageType, e.Storage) 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 = 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 } // boundStreams are, by module, the streams on the bus of every binding kept (BoundKey): what the bus module's process // may still define, an orphan's among them (buscompose.BusStateNames). func (p *Part) boundStreams() (map[string][]string, error) { keys, err := p.Store.Keys(StateMemberships) if err != nil { return nil, fmt.Errorf("its own %s cannot be listed: %w", StateMemberships, err) } out := map[string][]string{} for _, key := range keys { module, ok := strings.CutPrefix(key, BoundPrefix) if !ok { continue } b, err := p.bound(module) if err != nil { return nil, err } for _, bind := range b.Bindings { out[module] = append(out[module], bindingStreams(bind)...) } } return out, nil } // bindingStreams are the streams on the bus a binding names: a stream's, a bucket's KV_, an object store's OBJ_, a ledger's // history, its snapshot stream and its state and index buckets. func bindingStreams(b membership.BusBinding) []string { var out []string if b.Stream != "" { out = append(out, b.Stream) } if b.Bucket != "" { out = append(out, "KV_"+b.Bucket) } if b.ObjectStore != "" { out = append(out, "OBJ_"+b.ObjectStore) } if b.History != "" { out = append(out, b.History, b.History+"_snapshot") } if b.State != "" { out = append(out, "KV_"+b.State) } for _, ix := range b.Indexes { out = append(out, "KV_"+ix) } return out } func firstOf(values ...string) string { for _, v := range values { if v != "" { return v } } return "" }