A claim on a seat was checked against `SeatNamed` inside `ParseManifest`, and the build machine parses manifests too. It has no store, so there it answered from the set compiled into the binary — a copy of data the control plane owns (ADR 0122). When the two disagreed, that copy won where it mattered. The store's row said the bus seat answers for `amqp`; the binary's said `mesh-bus`; and a holder that provides `amqp` was refused at build time for not providing `mesh-bus`. The seat went unheld, the controller lost the address it composes through that seat, and the control plane crash-looped on a bus that was healthy the whole time. So the two checks that read the set — a seat's scope, and what its holder must provide — move to CatalogueProblems, which runs only in the control plane and only after UseSeats has replaced the set with the store's. The parser keeps what it can judge from the manifest alone, the reserved-namespace rule included. A test pins it: the same manifest, two different values in the store, and the answer follows the store both times. It fails if the check moves back.
255 lines
9.7 KiB
Go
255 lines
9.7 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Seats a module declares of its own (novox/hq ADR 0118).
|
|
//
|
|
// The set of seats a mesh has is **derived**: the mesh's own, in seats.go, plus those declared by
|
|
// every module it has registered. Still closed — a seat named nowhere is refused — but computed
|
|
// from the catalogue rather than written in the controller, which is what ADR 0110 actually
|
|
// needed and a hand-maintained table could not keep. Its own evidence: the enumeration done by
|
|
// hand while that record was written reported eleven claims where there were thirteen.
|
|
//
|
|
// **What can be checked from one manifest and what cannot.** A declaration's shape, its scope,
|
|
// and the reserved prefix are facts about the manifest in front of you. Whether a seat anybody
|
|
// names actually exists, whether two modules declared the same one, and whether a holder
|
|
// satisfies the protocol are facts about the *catalogue* — so they are checked at registration,
|
|
// by CatalogueProblems, which is the last moment the mesh can still say no.
|
|
|
|
// meshSeatPrefix is reserved to the mesh. The prefix *is* the reservation rule: no list of
|
|
// reserved names to maintain, no way for the mesh's own namespace to be colonised by a manifest,
|
|
// and nothing to keep in step when a mesh seat is added.
|
|
const meshSeatPrefix = "mesh-"
|
|
|
|
// A SeatDeclaration is a role a module offers on the bus: what may be sent to it, what it says,
|
|
// and what it answers. A caller declares that it uses the *seat*, never the module, so the
|
|
// implementation can be replaced under it.
|
|
type SeatDeclaration struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope,omitempty"`
|
|
|
|
// Accepts are the verbs others may submit work on. Each becomes a work-queue subject, and
|
|
// the holder is the only consumer — so exactly one worker does the job, by construction
|
|
// rather than by how carefully somebody wrote a subscribe call.
|
|
Accepts []string `json:"accepts,omitempty"`
|
|
// Emits are the verbs the holder publishes: 1:many, nobody obliged to act.
|
|
Emits []string `json:"emits,omitempty"`
|
|
// Serves are the verbs the holder answers: request and reply, awaited.
|
|
Serves []string `json:"serves,omitempty"`
|
|
|
|
// RetainSeconds is how long the inbound backlog survives with no holder, zero for the
|
|
// mesh's default. Retention belongs to whoever owns the namespace (design 29 §3) — a seat
|
|
// owns its own, which is why a seat is also the answer for a module that needs retention
|
|
// its events cannot have.
|
|
RetainSeconds int `json:"retain-seconds,omitempty"`
|
|
}
|
|
|
|
// At is this declaration's scope, with the default applied. Mesh by default, because a seat
|
|
// declared by a module is nearly always "there is one of these in the mesh" — a per-node worker
|
|
// is the deliberate case, and says so.
|
|
func (s SeatDeclaration) At() string {
|
|
if s.Scope == "" {
|
|
return ScopeMesh
|
|
}
|
|
return s.Scope
|
|
}
|
|
|
|
// verbs is everything the protocol names, for the checks that do not care which half.
|
|
func (s SeatDeclaration) verbs() []string {
|
|
out := append([]string{}, s.Accepts...)
|
|
out = append(out, s.Emits...)
|
|
return append(out, s.Serves...)
|
|
}
|
|
|
|
// declaredSeatProblems is what one manifest can be judged on alone.
|
|
func declaredSeatProblems(m Manifest) []string {
|
|
var problems []string
|
|
seen := map[string]bool{}
|
|
|
|
for _, s := range m.DefinesSeats {
|
|
switch {
|
|
case s.Name == "":
|
|
problems = append(problems, fmt.Sprintf("%s declares a seat with no name", m.Module))
|
|
continue
|
|
case !name.MatchString(s.Name):
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares a seat named %q, which is not a usable name", m.Module, s.Name))
|
|
continue
|
|
case strings.HasPrefix(s.Name, meshSeatPrefix):
|
|
// The mesh's own code dereferences its seats by name — the resolver *is* the thing
|
|
// that finds the store — so the prefix is not a convention, it is a namespace.
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares a seat named %q; %q is reserved to the mesh, which defines its own "+
|
|
"seats (novox/hq ADR 0118)", m.Module, s.Name, meshSeatPrefix+"*"))
|
|
continue
|
|
}
|
|
if seen[s.Name] {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares the seat %q twice", m.Module, s.Name))
|
|
continue
|
|
}
|
|
seen[s.Name] = true
|
|
|
|
if _, isMesh := SeatNamed(s.Name); isMesh {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares %q, which is a seat the mesh already defines", m.Module, s.Name))
|
|
}
|
|
switch s.At() {
|
|
case ScopeNode, ScopeSite, ScopeMesh:
|
|
default:
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares seat %s at scope %q; a seat is held per node, per site or per mesh",
|
|
m.Module, s.Name, s.Scope))
|
|
}
|
|
// A seat with an empty protocol is allowed, and is the mesh saying what a machine is:
|
|
// which module is this node's packet filter, or its showcase. Design 26 calls it a seat
|
|
// that delivers nothing, and that is most of the node-scoped ones. ADR 0126's "a declared
|
|
// seat carries a protocol" governs what a holder must satisfy, not that every seat offers
|
|
// something — a marker seat's protocol is satisfied by holding it. Nothing can reach this
|
|
// state by accident: a mistyped field name is refused by the parser above, so an empty
|
|
// protocol was written as one.
|
|
for _, v := range s.verbs() {
|
|
if !name.MatchString(v) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares %s.%s, which is not a usable verb", m.Module, s.Name, v))
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, u := range m.Uses {
|
|
if !name.MatchString(u) {
|
|
problems = append(problems, fmt.Sprintf("%s uses %q, which is not a usable seat name", m.Module, u))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// A Shelf is every manifest the mesh has registered, by module name.
|
|
type Shelf map[string]Manifest
|
|
|
|
// CatalogueProblems are the rules no single manifest can be judged against.
|
|
//
|
|
// Run at registration, which is the last moment the mesh can still refuse: after it, a caller is
|
|
// bound to a seat and a refusal is an outage rather than a conversation.
|
|
func CatalogueProblems(shelf Shelf) []string {
|
|
var problems []string
|
|
|
|
// Who declares what, and who declared it first.
|
|
declaredBy := map[string]string{}
|
|
declared := map[string]SeatDeclaration{}
|
|
for _, module := range shelfOrder(shelf) {
|
|
for _, s := range shelf[module].DefinesSeats {
|
|
if s.Name == "" {
|
|
continue
|
|
}
|
|
if first, taken := declaredBy[s.Name]; taken {
|
|
// The second loses. A seat name meaning two different protocols is the failure
|
|
// nobody could diagnose afterwards — a caller would bind to whichever happened
|
|
// to register first, and the symptom would appear in the other module.
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares the seat %q, which %s already declares; a seat name means one "+
|
|
"protocol", module, s.Name, first))
|
|
continue
|
|
}
|
|
declaredBy[s.Name] = module
|
|
declared[s.Name] = s
|
|
}
|
|
}
|
|
|
|
exists := func(seat string) bool {
|
|
if _, isMesh := SeatNamed(seat); isMesh {
|
|
return true
|
|
}
|
|
_, ok := declaredBy[seat]
|
|
return ok
|
|
}
|
|
|
|
for _, module := range shelfOrder(shelf) {
|
|
m := shelf[module]
|
|
|
|
// A `uses` naming nothing is where ADR 0110's guarantee lands under a derived set: the
|
|
// same refusal, at the same moment, from a set nobody maintains by hand.
|
|
for _, u := range m.Uses {
|
|
if !exists(u) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s uses the seat %q, which no module declares and the mesh does not define",
|
|
module, u))
|
|
}
|
|
}
|
|
|
|
for _, c := range m.Claims {
|
|
if !exists(c.Name) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s claims the seat %q, which no module declares and the mesh does not define",
|
|
module, c.Name))
|
|
continue
|
|
}
|
|
s, isModuleSeat := declared[c.Name]
|
|
if !isModuleSeat {
|
|
// **A mesh seat is judged here and nowhere else** (novox/hq ADR 0122): the set is
|
|
// the store's, and this is the only place that runs with the store's set loaded.
|
|
// The parser cannot do it — it also runs on the build machine, against whatever
|
|
// set that binary was compiled with.
|
|
seat, _ := SeatNamed(c.Name)
|
|
if c.At() != seat.Scope {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s claims %s at scope %q, and %s is a %s seat",
|
|
module, c.Name, c.At(), c.Name, seat.Scope))
|
|
}
|
|
if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope",
|
|
module, c.Name, seat.Delivers, module, seat.Delivers, seat.Scope))
|
|
}
|
|
continue
|
|
}
|
|
if c.At() != s.At() {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s claims %s at scope %q, and %s declares it at %s",
|
|
module, c.Name, c.At(), declaredBy[c.Name], s.At()))
|
|
}
|
|
// A holder that does not answer what the seat promises is a caller's timeout, found
|
|
// at assignment instead.
|
|
if missing := unserved(m, s); len(missing) > 0 {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s claims %s but does not serve %s, which that seat's protocol promises",
|
|
module, c.Name, strings.Join(missing, ", ")))
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(problems)
|
|
return problems
|
|
}
|
|
|
|
// unserved is what a seat's protocol promises and the claimant does not answer. Only the tools
|
|
// are checked: `accepts` and `emits` are wired by the runtime from the declaration, while a tool
|
|
// is code the module either has or has not written.
|
|
func unserved(m Manifest, s SeatDeclaration) []string {
|
|
has := map[string]bool{}
|
|
for _, t := range m.Tools {
|
|
has[t] = true
|
|
}
|
|
var missing []string
|
|
for _, t := range s.Serves {
|
|
if !has[t] {
|
|
missing = append(missing, t)
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
// shelfOrder is the catalogue in a stable order, so two runs report the same problems in the same
|
|
// sequence — a refusal that reorders itself is a refusal nobody can diff.
|
|
func shelfOrder(shelf Shelf) []string {
|
|
out := make([]string, 0, len(shelf))
|
|
for k := range shelf {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|