`seat <name> --to <node>/<module>` makes one assignment the holder of a seat in the same write that removes the previous one. The row is new (migration 0039); without one, the resolver derives the holder as it always did — the sole eligible assignment, two refused — so nothing changes for a mesh that never hands a seat over. With one, the recorded assignment holds and any other whose module could hold the seat is eligible and silent: not refused, not holding. That is what lets the next holder run beside the current one until the switch (hq design 26, design 28 task 5.3, ADR 0131). Why: the controller finds its own bus through a seat, and the day that seat was left with nobody in it — because two eligible holders could not coexist and the old one's claim was taken away — the control plane looped for two hours while every service stayed up. A handover that is never empty in between is the fix, not a workaround for it. `CanHold` is the one judgement of whether a module may hold a seat — claims it at its scope, provides what it delivers, against the store's row — shared by registration and the handover so they cannot drift apart. The holding belongs to the assignment and goes when it does, so a seat never points at nothing running. Tests: the resolver with and without a record, on the same and another machine, under a former name; the store's row replaced not added, refused for an unassigned target, removed with its assignment; CanHold's four answers and that they follow the store. Full suite green against a real NATS and store.
248 lines
9.4 KiB
Go
248 lines
9.4 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 err := CanHold(m, seat); err != nil {
|
|
problems = append(problems, err.Error())
|
|
}
|
|
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
|
|
}
|