The controller ended by its unassignment (ADR 0590): its user, its lease's cut and reads, the operator's hand path to its compositions and its writers' rows go; an enrolling node enrols through mesh-enrolment alone; the desk's prompt stays the vault's alone (DeskOnly). The golden lists are now this composer's own, taken first from the controller's with its user removed. The bus module's tools may remove the controller's leftovers by name, consumers, CONTROL, ASSIGNMENTS, its 17 buckets and each node's declaration files' bucket, under the word controller-leftovers (clause 6). The provisioner client reads placement in place of the assignments mirror. Issue 1126 B3a, B5. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
199 lines
9.8 KiB
Go
199 lines
9.8 KiB
Go
package buscompose
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// **The health holders' grants** (novox/hq ADR 0564 clauses 2, 5 and 8, Limits 2, 4 and 8; issue 1952 S5; ADR 0590).
|
|
//
|
|
// - Nobody reads mesh-healers' `would` (clause 8): what the healers would have done reaches no condition. Every
|
|
// observer's `findings`, the healers' `attempts`, the wants and the haves are read through `reads` as any announced
|
|
// state is; and no module writes another's state, so an observer can never write the keeper's buckets (clause 2).
|
|
// - The watchman makes its own consumer on DEAD_LETTER_NOTICES, by its own name, pulled, and reads whether the ended
|
|
// controller's consumer `controller` still stands; it may neither delete nor change that consumer (clause 5).
|
|
// - The bus module's tools run on an account of their own, declared by their bundle's `bus` with `removes`
|
|
// (catalogue.BusAccounts), which may remove only keys of the node-engine's acts bucket (Limits 4) and the ended
|
|
// controller's leftovers (ADR 0590 clause 6), never the backup's account `broker`, which only reads (ADR 0235).
|
|
|
|
// HealersWould is what mesh-healers would have done while a controller was placed: read by nobody (ADR 0564 clause 8).
|
|
var HealersWould = health.HealersModule + "." + health.WouldState
|
|
|
|
// ControllerLeftoverConsumers are the ended controller's durable consumers left on the bus, and one leftover of the
|
|
// module mesh-delivery placed nowhere (issue 1126, milestone section 3), each `<stream>.<consumer>`.
|
|
var ControllerLeftoverConsumers = []string{
|
|
"EVENTS." + ControllerName, "EVENTS." + MergesConsumerName, "EVENTS.novox_mesh-delivery",
|
|
"CONTROL." + ControllerName, DeadLetterNoticesStream + "." + ControllerName,
|
|
}
|
|
|
|
// ControllerLeftoverBuckets are the ended controller's buckets, each by its local name (`KV_mesh-controller_<name>`).
|
|
var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "calls", "checks-running", "composed",
|
|
"condition-history", "conditions", "declared-config", "declared-packages", "delivery-merges", "grants", "hand-acts",
|
|
"lease", "predefined", "rollouts", "states-announced"}
|
|
|
|
// ControllerLeftoverStreams are the ended controller's streams, by their names: CONTROL, ASSIGNMENTS, each of its buckets'
|
|
// and, for each node given, the declaration files' object bucket the controller wrote (ADR 0486 §2).
|
|
func ControllerLeftoverStreams(nodes []string) []string {
|
|
out := []string{"CONTROL", AssignmentsStream}
|
|
for _, b := range ControllerLeftoverBuckets {
|
|
out = append(out, "KV_"+BucketName(catalogue.EndedControllerName, b))
|
|
}
|
|
for _, n := range nodes {
|
|
if safeSubject.MatchString(n) {
|
|
out = append(out, "OBJ_"+DeclarationFilesBucket+n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BusRemovalGrants are the whole authority of an account whose bus declares removals (ADR 0564 Limits 4, ADR 0590 clause
|
|
// 6), each removal its grant: the node-engine's acts bucket read (STREAM.INFO with a subject filter lists what a removal
|
|
// would take, so the tools read before they remove) and its keys removed, which nothing else may empty when it fills; and
|
|
// the ended controller's leftovers, each consumer and each stream read and deleted by its name, for the nodes given (the
|
|
// declaration files' object buckets). A removal outside the set is refused, as the indexer's judge refuses it in a
|
|
// manifest (novox/hq ADR 0578 §1).
|
|
func BusRemovalGrants(removes, nodes []string) ([]string, error) {
|
|
var out []string
|
|
for _, r := range removes {
|
|
switch r {
|
|
case catalogue.RemovesEngineActs:
|
|
out = append(out, "$JS.API.STREAM.INFO.KV_"+EngineActsBucket, "$KV."+EngineActsBucket+".>")
|
|
case catalogue.RemovesControllerLeftovers:
|
|
for _, c := range ControllerLeftoverConsumers {
|
|
out = append(out, "$JS.API.CONSUMER.INFO."+c, "$JS.API.CONSUMER.DELETE."+c)
|
|
}
|
|
for _, s := range ControllerLeftoverStreams(nodes) {
|
|
out = append(out, "$JS.API.STREAM.INFO."+s, "$JS.API.STREAM.DELETE."+s)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("a bus account removes %q, which is not one of %s (novox/hq ADR 0564 Limits 4, ADR 0590)", r,
|
|
strings.Join([]string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}, ", "))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// removesLeftover is an account declared to remove the ended controller's leftovers deleting one of its streams: the
|
|
// writers table's one exemption from stream definitions being the grants part's (ADR 0590 clause 6).
|
|
func removesLeftover(p Principal, pattern string) bool {
|
|
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.DELETE.")
|
|
return ok && p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesControllerLeftovers) &&
|
|
slices.Contains(ControllerLeftoverStreams(p.FilesFor), name)
|
|
}
|
|
|
|
// removesEngineActs is an account declared to remove keys of the node-engine's acts bucket: the writers table's one
|
|
// exemption from a node's engine being the only writer of its keys, for removal (ADR 0564 Limits 4).
|
|
func removesEngineActs(p Principal, pattern string) bool {
|
|
return p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesEngineActs) && pattern == "$KV."+EngineActsBucket+".>"
|
|
}
|
|
|
|
// WatchmanDeadLetterConsumer is the watchman's own consumer on DEAD_LETTER_NOTICES, made only once the lease names no
|
|
// controller and `controller` is gone (ADR 0564 clause 5).
|
|
const WatchmanDeadLetterConsumer = WatchmanModule
|
|
|
|
// WatchmanDeadLetterGrants are what the watchman may publish for the dead letters in L4 (ADR 0564 clause 5), at least
|
|
// privilege: its own pulled consumer on DEAD_LETTER_NOTICES (made, read, pulled, acknowledged); whether `controller`
|
|
// still stands (read only); a read of DEAD_LETTERS (its information and one kept message); a copy kept
|
|
// (`mesh.events.dead.>`); and on each stream a notice can name (sources: EVENTS, the seats' work queues and the
|
|
// modules' own), its information, for a source's age (ADR 0376), and the message given up on, each stream by its name.
|
|
// Never a wildcard over the streams and never a bucket's or an object store's stream: a read of any message would read
|
|
// every bucket (the lead, 2026-10-29, on issue 1952 S5). Delivering again and dropping stay refused in L4, so it may
|
|
// neither delete a message nor publish `mesh.again.>`.
|
|
func WatchmanDeadLetterGrants(sources []string) []string {
|
|
own := DeadLetterNoticesStream + "." + WatchmanDeadLetterConsumer
|
|
out := []string{
|
|
"$JS.API.STREAM.INFO." + DeadLetterNoticesStream,
|
|
"$JS.API.CONSUMER.INFO." + DeadLetterNoticesStream + "." + ControllerName,
|
|
"$JS.API.CONSUMER.CREATE." + own,
|
|
"$JS.API.CONSUMER.INFO." + own,
|
|
"$JS.API.CONSUMER.MSG.NEXT." + own,
|
|
"$JS.ACK." + own + ".>",
|
|
"$JS.API.STREAM.INFO." + DeadLettersStream,
|
|
"$JS.API.STREAM.MSG.GET." + DeadLettersStream,
|
|
deadLetterPrefix + ">",
|
|
// And the provisioner clients' reports, the newest per subject read at its start (S21, ADR 0362 §3).
|
|
"$JS.API.STREAM.INFO." + ProvAppliedStream,
|
|
"$JS.API.STREAM.MSG.GET." + ProvAppliedStream,
|
|
"$JS.API.DIRECT.GET." + ProvAppliedStream + ".>",
|
|
}
|
|
for _, s := range DeadLetterSources(sources) {
|
|
out = append(out, "$JS.API.STREAM.INFO."+s, "$JS.API.STREAM.MSG.GET."+s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// WatchmanHears are the core subjects the watchman subscribes, with no durable consumer (ADR 0564 clauses 4, 5 and
|
|
// Limits 3, 11): the power seat's words (S1, ADR 0211), the providers' failing and recovered words (S8), each tool
|
|
// runner's beat (S11), the self-check's checked (S10), a consumer deleted and the bus module's slow consumers and closed
|
|
// connections (S9), the provisioner clients' reports (S21), the dead letters kept, and the lease write's events. Never the bus's API audit
|
|
// (`$JS.EVENT.ADVISORY.API`): it carries every API call's request and answer in the account, the vault's hashes among
|
|
// them (the lead, 2026-10-29).
|
|
func WatchmanHears() []string {
|
|
return []string{
|
|
"mesh.mod.power.event.*",
|
|
"mesh.mod.*.event.provisioner.failing",
|
|
"mesh.mod.*.event.provisioner.recovered",
|
|
"mesh.control.*.tools-alive",
|
|
"mesh.seat.mesh-self-check.event." + health.EventChecked,
|
|
"$JS.EVENT.ADVISORY.CONSUMER.DELETED.>",
|
|
"mesh.mod.nats.event.bus.slow-consumer",
|
|
"mesh.mod.nats.event.bus.connection-closed",
|
|
ProvAppliedSubjects,
|
|
deadLetterPrefix + ">",
|
|
}
|
|
}
|
|
|
|
// DeadLetterSources are the streams of names a dead letter may come from, sorted, none twice: each a plain name, never a
|
|
// bucket's (KV_) or an object store's (OBJ_) stream, the dead letters' own two streams, or a pattern.
|
|
func DeadLetterSources(names []string) []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, n := range names {
|
|
switch {
|
|
case seen[n], !safeSubject.MatchString(n), strings.HasPrefix(n, "KV_"), strings.HasPrefix(n, "OBJ_"),
|
|
n == DeadLettersStream, n == DeadLetterNoticesStream:
|
|
continue
|
|
}
|
|
seen[n] = true
|
|
out = append(out, n)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// deadLetterSourcesOf are the streams the records' consumers give up on: EVENTS, the work queue of every seat that
|
|
// accepts work, and every module's and process's own work queue.
|
|
func deadLetterSourcesOf(r Records) []string {
|
|
out := []string{EventsStream}
|
|
for _, mods := range r.Assigned {
|
|
for _, d := range mods {
|
|
for _, s := range append(append([]Seat(nil), d.Holds...), d.Uses...) {
|
|
if len(s.Accepts) > 0 {
|
|
out = append(out, seatStreamName(s.Name))
|
|
}
|
|
}
|
|
for _, q := range d.Queues {
|
|
out = append(out, q.Stream)
|
|
}
|
|
for _, pr := range d.Processes {
|
|
for _, q := range pr.Queues {
|
|
out = append(out, q.Stream)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return DeadLetterSources(out)
|
|
}
|
|
|
|
// keepsDeadLetters is the watchman keeping the dead letters: a second writer of the copies beside the controller, which
|
|
// keeps them until its lease is handed away and the watchman after (ADR 0564 clause 5). Delivering again stays the
|
|
// controller's in L4.
|
|
func keepsDeadLetters(p Principal, pattern string) bool {
|
|
return p.Kind == KindModule && p.Module == WatchmanModule && pattern == deadLetterPrefix+">"
|
|
}
|