A provider now waits for a person before retiring more than three consumers or half of what it holds, and deletes only when asked. The controller is that person's way in: it keeps waiting and rejected sets as conditions, answers them with retire approve|reject, lists and deletes retired consumers through the provider's own tools on its machine, records each act in the hand-act log, and probes for anything retired longer than thirty days (D11).
200 lines
8.1 KiB
Go
200 lines
8.1 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// What a module may call an event, and what a consumer may ask for.
|
|
//
|
|
// A module names an event **locally**: `order.placed`, not a subject and not a routing key
|
|
// (design 29 §1). A consumer names the emitter and the event: `billing.order.placed`. The mesh
|
|
// derives the subject from those, so reorganising the subject space leaves every manifest correct.
|
|
//
|
|
// **Nothing checked this until every manifest in the catalogue was wrong the same way**
|
|
// (novox/hq 04-ISSUES/127). All thirty-seven kept the old bus's routing key —
|
|
// `module.<module>.<verb>` — which the derivation read as "a module called `module`", so every
|
|
// cross-module subscription in the mesh pointed at a namespace nobody publishes to. Nothing failed:
|
|
// the services started and none of them reacted. The documentation on these fields taught the old
|
|
// form too, which is why the drift was uniform rather than scattered.
|
|
|
|
// eventName is one name in a local event: lower-case, and no wildcard.
|
|
var eventName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
|
|
|
// The wildcards a consumer may use, spelled the mesh's way and derived to whatever the transport
|
|
// spells them as.
|
|
//
|
|
// **A manifest holds no transport token**, which is the whole point of naming locally: the bus the
|
|
// mesh runs on today spells these `*` and `#`, and the one being built spells them `*` and `>`. A
|
|
// manifest that said either would be a manifest that stopped being true when the wire changed.
|
|
const (
|
|
// OneName stands for exactly one name.
|
|
OneName = "*"
|
|
// TheRest stands for one or more names, and may only come last.
|
|
TheRest = "**"
|
|
)
|
|
|
|
// EventProblems is what is wrong with a manifest's events.
|
|
//
|
|
// Refused at registration, because the alternative is a module that installs, starts, connects and
|
|
// reacts to nothing — and every log line says it is fine.
|
|
func EventProblems(m Manifest) []string {
|
|
var problems []string
|
|
|
|
for _, e := range m.Emits {
|
|
if was, stale := staleEventForm(e, m.Module); stale {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s emits %q, which is the old bus's routing key. An event is named locally now, so "+
|
|
"write %q — the mesh derives the subject (novox/hq design 29 §1)",
|
|
m.Module, was, strings.TrimPrefix(was, "module."+m.Module+".")))
|
|
continue
|
|
}
|
|
if strings.HasPrefix(e, "module.") {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s emits %q: `module.` is reserved, because it is how the old bus spelled a "+
|
|
"routing key and an event named that way derives into a namespace nobody owns",
|
|
m.Module, e))
|
|
continue
|
|
}
|
|
if err := localName(e); err != nil {
|
|
problems = append(problems, fmt.Sprintf("%s emits %q: %v", m.Module, e, err))
|
|
continue
|
|
}
|
|
// **Its own name, never another's.** The bus enforces that a namespace belongs to the module
|
|
// it is named for, so an event named for somebody else cannot be published at all. If the
|
|
// event is about a role rather than about this module, it belongs on the seat: a name that
|
|
// is stable across whoever fills it (04-ISSUES/127).
|
|
if first, _, split := strings.Cut(e, "."); split && isAModuleNameOtherThan(first, m.Module) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s emits %q, which reads as another module's event. A module publishes under its "+
|
|
"own name only. If this is about a role rather than about %s, declare it on that "+
|
|
"seat, where the name survives the holder changing",
|
|
m.Module, e, m.Module))
|
|
}
|
|
}
|
|
|
|
for _, c := range m.Consumes {
|
|
if strings.HasPrefix(c, "module.") {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s consumes %q, which is the old bus's pattern. A consumed event names its emitter "+
|
|
"and the event: write %q", m.Module, c, strings.TrimPrefix(c, "module.")))
|
|
continue
|
|
}
|
|
if c == "#" {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s consumes %q, which is the old bus's wildcard for everything. Write %q",
|
|
m.Module, c, TheRest))
|
|
continue
|
|
}
|
|
if err := consumePattern(c); err != nil {
|
|
problems = append(problems, fmt.Sprintf("%s consumes %q: %v", m.Module, c, err))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// staleEventForm says an emitted name is this module's own old routing key, and what it was.
|
|
func staleEventForm(event, module string) (string, bool) {
|
|
return event, module != "" && strings.HasPrefix(event, "module."+module+".")
|
|
}
|
|
|
|
// isAModuleNameOtherThan says a first token names some module of this mesh that is not this one.
|
|
//
|
|
// Only the mesh's own seats and the catalogue could answer this properly, and neither is reachable
|
|
// from a parser given one manifest. So this catches the case that actually happened — a name that
|
|
// is a *provision* the mesh defines, which is where "another module's event" comes from in practice
|
|
// — and the whole-catalogue check catches the rest.
|
|
func isAModuleNameOtherThan(first, module string) bool {
|
|
if first == module || first == "" {
|
|
return false
|
|
}
|
|
if _, isASeat := SeatNamed(first); isASeat {
|
|
return true
|
|
}
|
|
if _, isASeat := SeatDelivering(first); isASeat {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// localName checks one event name: dot-separated names, no wildcards, nothing else.
|
|
func localName(event string) error {
|
|
if event == "" {
|
|
return fmt.Errorf("an event needs a name")
|
|
}
|
|
for _, part := range strings.Split(event, ".") {
|
|
if part == OneName || part == TheRest {
|
|
return fmt.Errorf("an emitted event names one event, so it carries no wildcard")
|
|
}
|
|
if !eventName.MatchString(part) {
|
|
return fmt.Errorf("%q is not a usable name: lower-case letters, digits and dashes", part)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// consumePattern checks a consumed pattern: the emitter, then the event, with wildcards.
|
|
func consumePattern(pattern string) error {
|
|
if pattern == "" {
|
|
return fmt.Errorf("a consumed event needs an emitter and an event")
|
|
}
|
|
parts := strings.Split(pattern, ".")
|
|
for i, part := range parts {
|
|
switch {
|
|
case part == TheRest:
|
|
if i != len(parts)-1 {
|
|
return fmt.Errorf("%q stands for the rest of a name, so nothing may follow it", TheRest)
|
|
}
|
|
case part == OneName:
|
|
case !eventName.MatchString(part):
|
|
return fmt.Errorf("%q is not a usable name: lower-case letters, digits and dashes", part)
|
|
}
|
|
}
|
|
// `**` alone is every event from every module, which the audit logger wants and says plainly.
|
|
if len(parts) == 1 && parts[0] != TheRest {
|
|
return fmt.Errorf(
|
|
"%q names an emitter and no event. Write <emitter>.<event>, or %q for every event",
|
|
pattern, TheRest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The events a provider says about its consumers (novox/hq ADR 0224): a consumer it has failed
|
|
// without one success for minutes, and that consumer succeeding again or being withdrawn. The
|
|
// controller follows them from every module and `status` names a consumer failing until it recovers.
|
|
//
|
|
// **And what becomes of a consumer the mesh stopped asking for** (novox/hq ADR 0230): retired — its
|
|
// access disabled and its data kept — after the same answer in five passes, waiting for a person when
|
|
// more would go than the bound allows, re-enabled when asked for again, and deleted only by a person's
|
|
// `cleanup delete`. One event, its `change` saying which.
|
|
const (
|
|
ProvisionerFailing = "provisioner.failing"
|
|
ProvisionerRecovered = "provisioner.recovered"
|
|
ProvisionerRetirement = "provisioner.retirement"
|
|
)
|
|
|
|
// ProvisionerEvents are all three, in the order they are said.
|
|
var ProvisionerEvents = []string{ProvisionerFailing, ProvisionerRecovered, ProvisionerRetirement}
|
|
|
|
// EmitsAll is every event a module may publish: what it declares and, for a module that receives
|
|
// contributions — a provider, running a provisioner over them — the provider's standing events.
|
|
//
|
|
// **Derived, not declared**, because they are the mesh's rule about every provider rather than
|
|
// anything one module chose to say: a provider whose manifest forgot them would fail its consumers
|
|
// as silently as on 2026-10-05, with its announcement refused by the bus (novox/hq issue 179). Every
|
|
// grant of a module's publishing reads this, never the declared list alone.
|
|
func (m Manifest) EmitsAll() []string {
|
|
out := append([]string(nil), m.Emits...)
|
|
if len(m.Receives) == 0 {
|
|
return out
|
|
}
|
|
for _, e := range ProvisionerEvents {
|
|
if !slices.Contains(out, e) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|