150 lines
6.7 KiB
Go
150 lines
6.7 KiB
Go
package broker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What the mesh issues an assignment to serve and to reach (novox/hq ADR 0160).
|
|
//
|
|
// A module's code names its tools and its events; **where they land is the mesh's to decide**, and
|
|
// it decided it twice — once in the runtime, once here, by one rule compiled into both. Now the
|
|
// controller composes a membership for every module on every machine and publishes it to a subject
|
|
// only that assignment reads; the runtime serves exactly what the membership says, and the account's
|
|
// grant is the same composition read the other way. The shape issued today is the shape the mesh
|
|
// already had, so nothing moves when a membership first arrives; only who decides it moves.
|
|
|
|
// Membership is one assignment's subjects: what this instance of a module on this machine serves,
|
|
// and what it may reach.
|
|
type Membership struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
// Serves is every address a tool of this instance answers on. `{tool}` stands for the tool's
|
|
// own name, which the module knows and the mesh does not need to: the mesh issues the address,
|
|
// the runtime fills the name. An address with a queue is shared with the module's other
|
|
// instances, and the bus hands each call to one of them; an address without is this instance's.
|
|
Serves []Served `json:"serves"`
|
|
// Seats is every verb of a seat this instance holds, at the subject the seat's callers use.
|
|
Seats []SeatServed `json:"seats,omitempty"`
|
|
// Emits is where an event of this module lands; `{event}` stands for the event's name.
|
|
Emits string `json:"emits"`
|
|
// Reaches is each tool this module may call, `<module>.<tool>`, to the subjects that reach it:
|
|
// the first is whichever instance answers, when the mesh issued one; the rest name a machine.
|
|
Reaches map[string][]string `json:"reaches,omitempty"`
|
|
// Tools is where this instance answers what it serves — the runtime's one verb of its own.
|
|
Tools string `json:"tools"`
|
|
// Receives is what this assignment is given for each requirement it receives, by requirement:
|
|
// the contributions of every module that asked for it, as the catalogue composed them (novox/hq
|
|
// ADR 0167). The same list its received file is written from, so the two cannot disagree; a
|
|
// requirement nobody contributed to is an empty list, never absent. Kept as JSON because the
|
|
// catalogue owns the shape of a contribution and the bus only carries it.
|
|
Receives map[string]json.RawMessage `json:"receives,omitempty"`
|
|
// Mesh is every machine's address on the private network — what a rule saying "from the mesh"
|
|
// resolves to in the packet filter, issued here from the same list (novox/hq ADR 0167). A
|
|
// module that must tell the mesh from the world, the route proxy serving an internal name, reads
|
|
// it here rather than keeping a definition of its own.
|
|
Mesh []string `json:"mesh,omitempty"`
|
|
// State is every bucket this module's code may reach, by the name it uses for each, and whether
|
|
// it may write it (novox/hq ADR 0201): the runtime answers a bundle's state verbs from this list
|
|
// and refuses, with the reason, what is not on it — the bus enforces only the union over every
|
|
// module on the machine.
|
|
State []StateIssued `json:"state,omitempty"`
|
|
}
|
|
|
|
// Served is one address a tool is answered on.
|
|
type Served struct {
|
|
Subject string `json:"subject"`
|
|
Queue string `json:"queue,omitempty"`
|
|
}
|
|
|
|
// SeatServed is one verb of a held seat, where its callers ask.
|
|
type SeatServed struct {
|
|
Seat string `json:"seat"`
|
|
Verb string `json:"verb"`
|
|
Subject string `json:"subject"`
|
|
}
|
|
|
|
// MembershipSubject is the one address a runtime derives for itself: where its own membership is
|
|
// published, from the two names its credential carries. Everything else is in the membership.
|
|
func MembershipSubject(node, module string) string {
|
|
return "mesh.assignment." + node + "." + module
|
|
}
|
|
|
|
// Placements is where every module runs, for deciding which instance answers for the module.
|
|
type Placements struct {
|
|
// Nodes is each module's machines.
|
|
Nodes map[string][]string
|
|
// Interchangeable is each module whose definition says its instances are the same anywhere,
|
|
// so the module's plain subject is issued to all of them in one queue.
|
|
Interchangeable map[string]bool
|
|
}
|
|
|
|
// AnswersForTheModule says whether an instance of a module on one machine is issued the module's
|
|
// plain subject: when it is the only instance, or when the definition says instances are
|
|
// interchangeable. A stateful module on two machines gets only its machines' subjects, so a call
|
|
// that names none reaches nothing rather than the wrong store.
|
|
func (p Placements) AnswersForTheModule(module string) bool {
|
|
return len(p.Nodes[module]) <= 1 || p.Interchangeable[module]
|
|
}
|
|
|
|
// MembershipFor composes one assignment's membership from what it declared and where everything
|
|
// runs. The subjects are the ones PermissionsFor grants, derived here once more only until the
|
|
// grant itself is read from the membership — which is the next step, not this one.
|
|
func MembershipFor(node string, d Declared, where Placements) Membership {
|
|
own := "mesh.mod." + d.Module
|
|
m := Membership{
|
|
Node: node, Module: d.Module,
|
|
Emits: own + ".event.{event}",
|
|
Tools: own + ".tool.tools",
|
|
}
|
|
// This machine's address always; the module's when this instance answers for the module.
|
|
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}." + node})
|
|
if where.AnswersForTheModule(d.Module) {
|
|
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module})
|
|
}
|
|
for _, s := range d.Holds {
|
|
for _, verb := range s.Serves {
|
|
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
|
|
}
|
|
}
|
|
m.State = stateIssuedFor(d)
|
|
if len(d.Invokes) > 0 {
|
|
m.Reaches = map[string][]string{}
|
|
for _, t := range d.Invokes {
|
|
if t == "*" || strings.HasPrefix(t, "seat:") {
|
|
continue // every tool, or a role's: addressed by name, not resolved per instance
|
|
}
|
|
module, tool, ok := strings.Cut(t, ".")
|
|
if !ok {
|
|
continue
|
|
}
|
|
var reach []string
|
|
if where.AnswersForTheModule(module) {
|
|
reach = append(reach, "mesh.mod."+module+".tool."+tool)
|
|
}
|
|
nodes := append([]string{}, where.Nodes[module]...)
|
|
sort.Strings(nodes)
|
|
for _, n := range nodes {
|
|
reach = append(reach, "mesh.mod."+module+".tool."+tool+"."+n)
|
|
}
|
|
m.Reaches[t] = reach
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// PlacementsOf reads where everything runs from the records the bus's accounts are composed from.
|
|
func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
|
|
p := Placements{Nodes: map[string][]string{}, Interchangeable: interchangeable}
|
|
for node, declared := range r.Assigned {
|
|
for _, d := range declared {
|
|
p.Nodes[d.Module] = append(p.Nodes[d.Module], node)
|
|
}
|
|
}
|
|
for _, nodes := range p.Nodes {
|
|
sort.Strings(nodes)
|
|
}
|
|
return p
|
|
}
|