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 { continue // a mesh seat: already judged by claimProblems } 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 }