mesh/delivery merged: source.main moved to novox/mesh-sdk@036266daff85
mesh/delivery-group group feat/1126-removal merged: every member is merged
mesh/check pass abf5eace7791 on 4a4fac090e9e | unit 6915 run, 1941 kept, 59 skipped | integration 6945 run, 2096 kept, 65 skipped | over bound: unit, …
novox_mesh-catalog follows the ended controller's built-before and each <node>_mesh-host restic's backed-up; no part composes either and nothing reads them (novox_mesh-catalog 10036 behind, each mesh-host 331 never delivered), so the removal after the delivery takes them with the controller's (issue 1126). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
213 lines
11 KiB
Go
213 lines
11 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, one leftover of the module
|
|
// mesh-delivery placed nowhere (issue 1126, milestone section 3), and mesh-catalog's of the controller's built-before,
|
|
// which its manifest no longer consumes and no part composes; each `<stream>.<consumer>`.
|
|
var ControllerLeftoverConsumers = []string{
|
|
"EVENTS." + ControllerName, "EVENTS." + MergesConsumerName, "EVENTS.novox_mesh-delivery", "EVENTS.novox_mesh-catalog",
|
|
"CONTROL." + ControllerName, DeadLetterNoticesStream + "." + ControllerName,
|
|
}
|
|
|
|
// ControllerLeftoverConsumersOn are ControllerLeftoverConsumers and, for each node given, the consumer the controller
|
|
// made for the node's engine (`EVENTS.<node>_mesh-host`, of restic's backed-up): the engine hears that event without a
|
|
// durable consumer, and the grants part composes none for it, so nothing reads it (issue 1126).
|
|
func ControllerLeftoverConsumersOn(nodes []string) []string {
|
|
out := slices.Clone(ControllerLeftoverConsumers)
|
|
for _, n := range nodes {
|
|
if safeSubject.MatchString(n) {
|
|
out = append(out, "EVENTS."+n+"_"+EngineModule)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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 ControllerLeftoverConsumersOn(nodes) {
|
|
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+">"
|
|
}
|