Let a module declare a stream of its own and restrict who hears it by a trusted config value (hq issue 718, ADR 0363)
mesh-mail's events must live on a stream of their own and reach only the modules the operator granted an account (ADR 0359 §2, §4). The controller now creates a module's declared stream, publishes its events on mesh.stream.<module>.event.<event>, and composes a consumer's durable consumer, filters and bus grants on a restricted stream only where its consumes asks and the provider's trusted value names it; a consumer that loses its grant has its consumer removed on the next assertion. Nothing changes while no manifest declares a stream.
This commit is contained in:
@@ -60,9 +60,19 @@ func assertBusObjects(ctx context.Context, inv *inventory.Inventory, r broker.Ra
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// And every module's own stream (novox/hq ADR 0363), from the catalogue, before the consumers on it.
|
||||
modStreams, err := inv.DeclaredStreams(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Every one tried, and every failure named: one module's consumer the bus refuses is no reason
|
||||
// the modules after it in the list hear nothing (novox/hq issue 208, where this runs on each send).
|
||||
var failed []error
|
||||
for _, s := range broker.ModuleStreams(modStreams) {
|
||||
if err := r.EnsureStream(s); err != nil {
|
||||
failed = append(failed, fmt.Errorf("the module stream %s: %w", s.Name, err))
|
||||
}
|
||||
}
|
||||
for _, s := range trafficStreams {
|
||||
if err := r.EnsureStream(s); err != nil {
|
||||
failed = append(failed, fmt.Errorf("the work queue %s: %w", s.Name, err))
|
||||
@@ -78,6 +88,21 @@ func assertBusObjects(ctx context.Context, inv *inventory.Inventory, r broker.Ra
|
||||
failed = append(failed, fmt.Errorf("how %s on %s hears what it consumes: %w", c.Module, c.Node, err))
|
||||
}
|
||||
}
|
||||
// **And a consumer no longer granted loses what it reads through** (novox/hq ADR 0363 §6): every consumer
|
||||
// on a module's stream the composition no longer asks for is removed, on this very send.
|
||||
if p, prunes := r.(broker.Pruner); prunes && len(modStreams) > 0 {
|
||||
var wanted []broker.Consumer
|
||||
for _, c := range consumers {
|
||||
wanted = append(wanted, c.Consumer)
|
||||
}
|
||||
removed, err := broker.PruneHeard(p, modStreams, wanted)
|
||||
if err != nil {
|
||||
failed = append(failed, err)
|
||||
}
|
||||
for _, gone := range removed {
|
||||
fmt.Printf(" removed the consumer %s: its module is no longer granted that stream (novox/hq ADR 0363)\n", gone)
|
||||
}
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
return nil, errors.Join(failed...)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
@@ -1386,6 +1387,13 @@ func issueMemberships(ctx context.Context, open *stores, server *link.Server, se
|
||||
if issued > 0 {
|
||||
fmt.Printf(" issued %d membership(s)\n", issued)
|
||||
}
|
||||
// **And what every other machine hears of a module's stream** (novox/hq ADR 0363 §6): a grant the provider's
|
||||
// setting changed reaches a consumer on a machine this send does not reach. Its membership is issued again
|
||||
// as last issued, with only what it hears changed, so nothing else of it moves before its own send.
|
||||
reheard := reissueHearing(ctx, bus, records, where, sent)
|
||||
if reheard > 0 {
|
||||
fmt.Printf(" issued %d membership(s) again for what they hear of a module's stream\n", reheard)
|
||||
}
|
||||
if len(refused) > 0 {
|
||||
// Returned, never passed over (novox/hq issue 249): the declarations of the machines they are
|
||||
// for are not sent, and the rollout that asked is tried again rather than waiting for a push.
|
||||
@@ -1744,3 +1752,78 @@ func schemaReach() (int, error) {
|
||||
}
|
||||
return reach, nil
|
||||
}
|
||||
|
||||
// reissueHearing issues again, on every machine not just sent, the membership of each assignment whose
|
||||
// `consumes` names a module owning a stream, when what it hears changed: read as last issued from the bus,
|
||||
// its `hears` replaced, and published. One that was never issued is left to its own send. Failures are said
|
||||
// and passed over: the send that carries the grant is not the consumer's, and its own next send issues it.
|
||||
func reissueHearing(ctx context.Context, bus link.OverNATS, records broker.Records, where broker.Placements,
|
||||
sent []readyNode) int {
|
||||
reached := map[string]bool{}
|
||||
for _, s := range sent {
|
||||
reached[s.node] = true
|
||||
}
|
||||
issued := 0
|
||||
for node, declared := range records.Assigned {
|
||||
if reached[node] {
|
||||
continue
|
||||
}
|
||||
for _, d := range declared {
|
||||
if !d.HearsAStream {
|
||||
continue
|
||||
}
|
||||
subject := broker.MembershipSubject(node, d.Module)
|
||||
last, err := bus.JS.GetLastMsg(broker.AssignmentsStream, subject)
|
||||
if err != nil {
|
||||
if !errors.Is(err, nats.ErrMsgNotFound) {
|
||||
fmt.Printf(" %s on %s: its membership could not be read to issue what it hears again: %v\n",
|
||||
d.Module, node, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
body, changed, err := withHearing(last.Data, broker.HeardIssuedFor(node, d))
|
||||
if err != nil {
|
||||
fmt.Printf(" %s on %s: its membership could not be read to issue what it hears again: %v\n",
|
||||
d.Module, node, err)
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
if err := bus.PublishMembership(ctx, node, d.Module, body); err != nil {
|
||||
fmt.Printf(" %s on %s: what it hears could not be issued again: %v\n", d.Module, node, err)
|
||||
continue
|
||||
}
|
||||
issued++
|
||||
}
|
||||
}
|
||||
return issued
|
||||
}
|
||||
|
||||
// withHearing is a membership as issued with its `hears` replaced, and whether that changed it. Pure.
|
||||
func withHearing(issued []byte, hears []broker.HeardIssued) ([]byte, bool, error) {
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(issued, &fields); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var had []broker.HeardIssued
|
||||
if raw, ok := fields["hears"]; ok {
|
||||
if err := json.Unmarshal(raw, &had); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
if slices.Equal(had, hears) {
|
||||
return issued, false, nil
|
||||
}
|
||||
if len(hears) == 0 {
|
||||
delete(fields, "hears")
|
||||
} else {
|
||||
raw, err := json.Marshal(hears)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
fields["hears"] = raw
|
||||
}
|
||||
body, err := json.Marshal(fields)
|
||||
return body, true, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
)
|
||||
|
||||
// A membership issued again for what it hears changes nothing else of it (novox/hq ADR 0363 §6).
|
||||
func TestAMembershipIssuedAgainChangesOnlyWhatItHears(t *testing.T) {
|
||||
issued := []byte(`{"node":"ace","module":"reader","receives":{"x":[1]},"emits":"mesh.mod.reader.event.{event}"}`)
|
||||
hears := []broker.HeardIssued{{Stream: "MAIL", Consumer: "ace_reader"}}
|
||||
body, changed, err := withHearing(issued, hears)
|
||||
if err != nil || !changed {
|
||||
t.Fatalf("changed %v, %v", changed, err)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(body, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got["receives"] == nil || got["emits"] != "mesh.mod.reader.event.{event}" || got["hears"] == nil {
|
||||
t.Fatalf("issued again as %s", body)
|
||||
}
|
||||
if _, again, _ := withHearing(body, hears); again {
|
||||
t.Fatal("the same hearing issued again counts as a change")
|
||||
}
|
||||
gone, changed, err := withHearing(body, nil)
|
||||
if err != nil || !changed {
|
||||
t.Fatalf("a withdrawal: %v, %v", changed, err)
|
||||
}
|
||||
got = nil
|
||||
if err := json.Unmarshal(gone, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, still := got["hears"]; still {
|
||||
t.Fatalf("a withdrawn hearing is still issued: %s", gone)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user