Seat list reader: its status for a module's health, and the foundation's bootstrapped definitions (ADR 0635 §3; issue 1966)

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-05 12:49:36 +01:00
parent 50258215b4
commit 7ea960fe20
11 changed files with 2559 additions and 4 deletions
+42
View File
@@ -0,0 +1,42 @@
package seatlist
import (
"embed"
"fmt"
"sort"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// The foundation's bootstrapped definitions (novox/hq ADR 0608 §2, ADR 0635 §3): the seats the foundation's parts claim
// and invoke before the indexer has run, kept as the definitions' own files (`bootstrap/<seat>.json`, the format's own
// text, whole or at the seat list's grain) and embedded as data. A reader holds them until the owner answers, and says so
// (Status). **They are copies:** a seat's definition is the one in its contracts module, and a test there holds each of
// these files equal to it; where a seat moves or changes, its file here is replaced by the same change. Which seats are
// the foundation's, and how a fresh mesh gets them without a copy, is research 075's.
//
//go:embed bootstrap/*.json
var bootstrapFiles embed.FS
// Bootstrapped are the foundation's definitions, by seat name. It panics on a file that does not parse, which the
// package's test refuses to let ship.
func Bootstrapped() []seatdef.Definition {
entries, err := bootstrapFiles.ReadDir("bootstrap")
if err != nil {
panic(err)
}
var out []seatdef.Definition
for _, e := range entries {
raw, err := bootstrapFiles.ReadFile("bootstrap/" + e.Name())
if err != nil {
panic(err)
}
d, err := seatdef.Parse(raw)
if err != nil {
panic(fmt.Sprintf("seatlist: bootstrap/%s does not parse: %v", e.Name(), err))
}
out = append(out, d)
}
sort.Slice(out, func(i, j int) bool { return out[i].Seat < out[j].Seat })
return out
}
+569
View File
@@ -0,0 +1,569 @@
{
"format": 1,
"seat": "mesh-broker",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0079",
"delivers": "mesh-bus",
"description": "The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone.",
"verbs": [
{
"name": "list",
"description": "Every entry the bus module created, its template, its settings and its fill against its cap.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "one module (optional)"
}
}
},
"answer": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/defs/Held"
},
"description": "every entry"
}
},
"required": [
"entries"
]
},
"optional": true
},
{
"name": "show",
"description": "One entry, its template, settings and fill.",
"callers": "any caller whose role grants it",
"input": {
"$ref": "#/defs/Named"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "check",
"description": "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "one module (optional)"
}
}
},
"answer": {
"type": "object",
"properties": {
"differences": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"local-name": {
"type": "string",
"description": "the entry"
},
"asked": {
"type": "object",
"description": "asked"
},
"held": {
"type": "object",
"description": "held"
},
"grows": {
"type": "boolean",
"description": "the difference only grows: apply may act on it"
}
},
"required": [
"module",
"local-name"
]
},
"description": "every difference"
}
},
"required": [
"differences"
]
},
"optional": true
},
{
"name": "orphans",
"description": "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {},
"description": "no arguments"
},
"answer": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/defs/Held"
},
"description": "every orphan"
}
},
"required": [
"entries"
]
},
"optional": true
},
{
"name": "create",
"description": "Create one entry now.",
"callers": "the bus module's own process only",
"input": {
"$ref": "#/defs/Entry"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "apply",
"description": "Apply a difference that only grows: a larger cap, a longer age.",
"callers": "the bus module's own process only",
"input": {
"$ref": "#/defs/Named"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "recreate",
"description": "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.",
"callers": "the bus module's process, on the operator's warrant",
"input": {
"$ref": "#/defs/Warranted"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "drop",
"description": "Remove one orphan, after a stream snapshot.",
"callers": "the bus module's process, on the operator's warrant",
"input": {
"$ref": "#/defs/Warranted"
},
"answer": {
"type": "object",
"properties": {
"dropped": {
"type": "boolean",
"description": "it is gone"
},
"snapshot": {
"type": "string",
"description": "the snapshot taken first"
}
},
"required": [
"dropped"
]
},
"optional": true
}
],
"events": [
{
"name": "created",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "refused",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "drifted",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "drift-cleared",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "full",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "full-cleared",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "orphaned",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "recreated",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "dropped",
"body": {
"$ref": "#/defs/Said"
}
}
],
"family": {
"name": "mesh-broker.events",
"max-age-days": 7
},
"provisions": [
{
"name": "bus-state",
"form": {
"$ref": "#/defs/Binding"
}
}
],
"defs": {
"Entry": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module asking it"
},
"local-name": {
"type": "string",
"description": "its local name in the module's manifest"
},
"storage-type": {
"type": "string",
"enum": [
"stream",
"key-value",
"object-store",
"ledger"
],
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"enum": [
"state",
"events",
"work-queue",
"log",
"objects",
"ledger",
""
],
"description": "its template; empty for a configuration of its own"
},
"options": {
"type": "object",
"description": "the template's options the module set, within their bounds"
},
"configuration": {
"type": "object",
"description": "its own configuration, where no template fits"
}
},
"required": [
"module",
"local-name",
"storage-type"
],
"description": "one piece of bus state as a manifest asks it"
},
"Fill": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"description": "messages, keys or objects held"
},
"bytes": {
"type": "integer",
"description": "bytes held"
},
"cap": {
"type": "integer",
"description": "its cap in bytes"
}
},
"required": [
"count",
"bytes",
"cap"
]
},
"Held": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module asking it"
},
"local-name": {
"type": "string",
"description": "its local name in the module's manifest"
},
"storage-type": {
"type": "string",
"enum": [
"stream",
"key-value",
"object-store",
"ledger"
],
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"enum": [
"state",
"events",
"work-queue",
"log",
"objects",
"ledger",
""
],
"description": "its template; empty for a configuration of its own"
},
"options": {
"type": "object",
"description": "the template's options the module set, within their bounds"
},
"configuration": {
"type": "object",
"description": "its own configuration, where no template fits"
},
"bus-name": {
"type": "string",
"description": "its name on the bus, <module>_<local name>"
},
"settings": {
"type": "object",
"description": "the settings the bus holds"
},
"fill": {
"$ref": "#/defs/Fill"
},
"state": {
"type": "string",
"enum": [
"held",
"refused",
"waiting",
"drifted",
"full",
"orphaned"
],
"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)"
},
"why": {
"type": "string",
"description": "why it is in its state, in plain words; empty when held as asked"
}
},
"required": [
"module",
"local-name",
"storage-type",
"bus-name"
],
"description": "one entry as the bus holds it"
},
"Said": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module asking it"
},
"local-name": {
"type": "string",
"description": "its local name in the module's manifest"
},
"storage-type": {
"type": "string",
"enum": [
"stream",
"key-value",
"object-store",
"ledger"
],
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"enum": [
"state",
"events",
"work-queue",
"log",
"objects",
"ledger",
""
],
"description": "its template; empty for a configuration of its own"
},
"options": {
"type": "object",
"description": "the template's options the module set, within their bounds"
},
"configuration": {
"type": "object",
"description": "its own configuration, where no template fits"
},
"bus-name": {
"type": "string",
"description": "its name on the bus"
},
"asked": {
"type": "object",
"description": "the settings asked"
},
"held": {
"type": "object",
"description": "the settings held"
},
"why": {
"type": "string",
"description": "why: a refusal's cause, a drift's difference, a drop's or recreate's reason"
},
"fill": {
"$ref": "#/defs/Fill"
},
"snapshot": {
"type": "string",
"description": "the stream snapshot taken before a recreate or drop"
},
"binding": {
"$ref": "#/defs/Binding"
},
"revision": {
"type": "integer",
"description": "the indexer's revision of the verdict the bus module acted on (ADR 0622 §2), so a reader orders what it hears"
}
},
"required": [
"module",
"local-name",
"bus-name"
],
"description": "the body of every event of the seat"
},
"Named": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"local-name": {
"type": "string",
"description": "the entry's local name"
}
},
"required": [
"module",
"local-name"
]
},
"Warranted": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"local-name": {
"type": "string",
"description": "the entry's local name"
},
"warrant": {
"type": "string",
"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)"
},
"why": {
"type": "string",
"description": "why, kept in the record"
}
},
"required": [
"module",
"local-name",
"warrant",
"why"
]
},
"Binding": {
"type": "object",
"properties": {
"bus-name": {
"type": "string",
"description": "the entry's name on the bus"
},
"storage-type": {
"type": "string",
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"description": "its template"
},
"subjects": {
"type": "array",
"items": {
"type": "string"
},
"description": "its subjects, for a stream"
}
},
"required": [
"bus-name",
"storage-type"
],
"description": "the binding a module receives in its membership for one entry (ADR 0595 §1, §7)"
}
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"format": 1,
"seat": "mesh-catalog",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0604",
"verbs": [
{
"name": "modules",
"description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "module",
"description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.",
"optional": true,
"input": {
"properties": {
"module": {
"description": "the module's name",
"type": "string"
}
},
"required": [
"module"
],
"type": "object"
}
},
{
"name": "provides",
"description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.",
"optional": true,
"input": {
"properties": {
"provision": {
"description": "the provision, such as postgres-database",
"type": "string"
}
},
"required": [
"provision"
],
"type": "object"
}
},
{
"name": "dependents",
"description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.",
"optional": true,
"input": {
"properties": {
"module": {
"description": "the module's name",
"type": "string"
}
},
"required": [
"module"
],
"type": "object"
}
}
]
}
+222
View File
@@ -0,0 +1,222 @@
{
"format": 1,
"seat": "mesh-config",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0348, ADR 0565",
"verbs": [
{
"name": "config",
"description": "Set, clear, list or read the history of config values: mesh config or node config for a predefined key, or a module's own value on one node, each set with why, validated, and naming each module on each node whose value moves.",
"optional": true,
"input": {
"properties": {
"action": {
"description": "list (when not given), set, clear or history",
"type": "string"
},
"by": {
"description": "who gave the value at the controller's terminal",
"type": "string"
},
"key": {
"description": "the config key",
"type": "string"
},
"limit": {
"description": "for history: at most this many, newest",
"type": "number"
},
"module": {
"description": "a module: its own value, with node",
"type": "string"
},
"node": {
"description": "a node",
"type": "string"
},
"value": {
"description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list"
},
"why": {
"description": "for set and clear: why",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the values asked, or the one set or cleared, or its history"
}
},
{
"name": "renames",
"optional": true,
"description": "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.",
"input": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "one module (optional)"
}
}
},
"callers": "any caller whose role grants it; the checker asks it",
"answer": {
"type": "object",
"properties": {
"renames": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"from": {
"type": "string",
"description": "the old name"
},
"to": {
"type": "string",
"description": "the new name"
},
"at": {
"type": "string",
"description": "when it moved, RFC 3339"
},
"open": {
"type": "array",
"items": {
"type": "string"
},
"description": "the nodes still holding the old name open"
}
},
"required": [
"module",
"from",
"to"
]
},
"description": "every open rename"
}
},
"required": [
"renames"
]
}
}
],
"description": "The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2).",
"holder-events": [
{
"name": "config-changed",
"subject": "mesh.mod.<holder>.event.config-changed"
},
{
"name": "renamed",
"subject": "mesh.stream.<holder>.event.renamed",
"family": "config.events",
"body": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"from": {
"type": "string",
"description": "the old name"
},
"to": {
"type": "string",
"description": "the new name"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"module",
"from",
"to",
"at"
]
}
},
{
"name": "retiring",
"subject": "mesh.stream.<holder>.event.retiring",
"family": "config.events",
"body": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"key": {
"type": "string",
"description": "the value's or secret's name"
},
"kind": {
"type": "string",
"enum": [
"value",
"secret"
]
},
"since": {
"type": "string",
"description": "since when, RFC 3339"
},
"ask": {
"type": "string",
"description": "the ask's id"
}
},
"required": [
"module",
"key",
"kind",
"since"
]
}
},
{
"name": "retired",
"subject": "mesh.stream.<holder>.event.retired",
"family": "config.events",
"body": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"name": {
"type": "string",
"description": "the name"
},
"node": {
"type": "string",
"description": "the node"
},
"ask": {
"type": "string",
"description": "the ask's id"
}
},
"required": [
"module",
"name"
]
}
}
]
}
@@ -0,0 +1,245 @@
{
"format": 1,
"seat": "mesh-delivery-checker",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0547",
"verbs": [
{
"name": "try",
"description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.",
"optional": true,
"replaces": [
"sh merge-check.sh",
"docker run postgres"
],
"input": {
"properties": {
"head": {
"description": "the full commit to try, 40 or 64 hex digits",
"type": "string"
},
"level": {
"description": "unit or integration: only this level; both when absent",
"type": "string"
},
"repository": {
"description": "<owner>/<name>",
"type": "string"
},
"tests": {
"description": "a go test -run pattern: only those tests run, and nothing is kept",
"type": "string"
},
"trunk": {
"description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent",
"type": "string"
}
},
"required": [
"repository",
"head"
],
"type": "object"
},
"answer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "the try's id"
},
"state": {
"type": "string",
"description": "its state"
}
},
"required": [
"id"
]
}
},
{
"name": "try-read",
"description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the try's id, try-<digits>",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the try: its state, its steps and its verdict when it has one"
}
},
{
"name": "try-end",
"description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the try's id, try-<digits>",
"type": "string"
},
"why": {
"description": "why it is ended, in words",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
},
"answer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "the try"
},
"ended": {
"type": "boolean",
"description": "it is ended"
}
},
"required": [
"id",
"ended"
]
}
}
],
"description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).",
"holder-events": [
{
"name": "verdict",
"subject": "mesh.stream.<holder>.event.verdict",
"family": "checker.asks",
"body": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "owner/repo"
},
"pull": {
"type": "integer",
"description": "the pull request's number, for a pull request's check"
},
"head": {
"type": "string",
"description": "the commit checked"
},
"verdict": {
"type": "string",
"description": "pass or fail"
},
"out-of-step": {
"type": "array",
"items": {
"type": "string"
},
"description": "the generated files out of step with their seat definitions, each path (design 26)"
},
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module whose artifact's series changes"
},
"artifact": {
"type": "string",
"description": "its artifact, or the package's ecosystem name as the module's packages name it"
},
"from": {
"type": "string",
"description": "the series on trunk"
},
"to": {
"type": "string",
"description": "the series the checked head gives it"
}
},
"required": [
"module",
"artifact",
"from",
"to"
]
},
"description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none"
}
},
"required": [
"repository",
"head",
"verdict"
]
}
},
{
"name": "tag-wanted",
"subject": "mesh.stream.<holder>.event.tag-wanted",
"family": "checker.asks",
"body": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "owner/repo"
},
"pull": {
"type": "integer",
"description": "the pull request"
},
"head": {
"type": "string",
"description": "its head"
},
"branch": {
"type": "string",
"description": "the pull request's branch: its tag is removed when the branch ends"
},
"module": {
"type": "string",
"description": "the group member"
},
"package": {
"type": "string",
"description": "the package it declares"
},
"tag": {
"type": "string",
"description": "the tag wanted"
}
},
"required": [
"repository",
"head",
"branch",
"module",
"package"
]
}
},
{
"name": "preview.written",
"subject": "mesh.stream.<holder>.event.preview.written",
"family": "checks"
}
]
}
+97
View File
@@ -0,0 +1,97 @@
{
"format": 1,
"seat": "mesh-hand-acts",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0565",
"verbs": [
{
"name": "hand-act",
"description": "Record one act done by hand: what, why, its cause in a word or a condition's kind (the word a second act for the same reason uses) and the condition it addresses. Who did it is the caller the runtime proves, never said by the caller. Said as act-recorded.",
"input": {
"properties": {
"cause": {
"description": "the cause in a word, or a condition's kind",
"type": "string"
},
"condition": {
"description": "the key of the condition it addressed, if any (optional)",
"type": "string"
},
"verb": {
"description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own",
"type": "string"
},
"what": {
"description": "what was done, in a line",
"type": "string"
},
"why": {
"description": "why it had to be done by hand",
"type": "string"
}
},
"required": [
"what",
"why"
],
"type": "object"
}
},
{
"name": "hand-acts",
"description": "What was done by hand lately, over this log and the controller's: who, why and the cause of each, and the causes recorded twice within a fortnight (a healer is wanted).",
"input": {
"properties": {
"cause": {
"description": "only this cause",
"type": "string"
},
"days": {
"description": "how many days back (default 14)",
"type": "string"
},
"warrants": {
"description": "\"true\": every warrant recorded",
"enum": [
"true",
"false"
],
"type": "string"
},
"who": {
"description": "only acts whose who contains this",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "warranted",
"description": "Record what a module did on the operator's warrant: who chose, through which channel, with which proofs and which answer read from the router's own record of that module's ask, never from the caller; once per ask. Refused for an ask still open, ended without a choice, or not that module's.",
"input": {
"properties": {
"ask": {
"description": "its ask's id",
"type": "string"
},
"asker": {
"description": "the module that asked",
"type": "string"
}
},
"required": [
"asker",
"ask"
],
"type": "object"
}
}
],
"events": [
{
"name": "act-recorded"
}
]
}
+93
View File
@@ -0,0 +1,93 @@
{
"format": 1,
"seat": "mesh-watchdogs",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0564",
"verbs": [
{
"name": "signals",
"description": "Every watchdog of the signals table the watchman keeps, each a key that expires unless its signal renews it: its row, what renews it, its bound, the age of its newest signal, and the finding its expiry wrote.",
"input": {
"properties": {
"row": {
"description": "one row, e.g. S1",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "stuck",
"description": "Every want, build or step the watchman says stopped moving past its bound, with since when and the words of its finding.",
"input": {
"properties": {
"machine": {
"description": "only those on this machine",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "presence",
"description": "Every machine and tool runner the watchman sees connected, with its last word, and those it says silent; a machine that said it sleeps is shown as sleeping, not silent.",
"input": {
"properties": {
"machine": {
"description": "one machine",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "dead-letters",
"description": "Every message a consumer on the bus gave up on, kept in DEAD_LETTERS: whose consumer, the subject, how often it was handed over and when it was given up, newest first. With id: that one whole. With deliver or drop, a hand act with why.",
"input": {
"properties": {
"cause": {
"description": "with deliver or drop: the cause in a word (dead-letter when absent)",
"type": "string"
},
"consumer": {
"description": "a consumer's name, or \u003cstream\u003e.\u003cconsumer\u003e: only what that one gave up on",
"type": "string"
},
"deliver": {
"description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)",
"type": "string"
},
"drop": {
"description": "a dead letter's id: drop it for good (needs why)",
"type": "string"
},
"id": {
"description": "a dead letter's id, as the list gives it: that one whole, with its message",
"type": "string"
},
"limit": {
"description": "how many to list, newest first (default 50); only when listing",
"type": "string"
},
"why": {
"description": "with deliver or drop: why, required and recorded",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "resync",
"description": "Rebuild presence from the bus's connections, and the pulses and findings from the keys the watchman watches, and take every dead-letter notice waiting: the recovery of a watchman that missed a change.",
"input": {
"properties": {},
"type": "object"
}
}
]
}
+80
View File
@@ -0,0 +1,80 @@
{
"format": 1,
"seat": "node-backup",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0214",
"verbs": [
{
"name": "backed-up",
"description": "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.",
"replaces": [
"restic snapshots"
],
"input": {
"properties": {
"module": {
"description": "one module (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "now",
"description": "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.",
"replaces": [
"restic backup"
],
"input": {
"properties": {
"module": {
"description": "one module (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "restore",
"description": "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as \u003cpath\u003e.restored-\u003cdate\u003e. Swapping it in is a person's act. Lists the restore points when none is named.",
"replaces": [
"restic restore"
],
"input": {
"properties": {
"module": {
"description": "the module",
"type": "string"
},
"path": {
"description": "one of the module's directories (all of them when omitted)",
"type": "string"
},
"snapshot": {
"description": "the restore point (from `backed-up`; the newest when omitted)",
"type": "string"
}
},
"required": [
"module"
],
"type": "object"
}
}
],
"receives": [
{
"kind": "backup",
"comment": "#",
"dirs": true
},
{
"kind": "data",
"comment": "#",
"dirs": true
}
]
}
File diff suppressed because it is too large Load Diff
+59 -4
View File
@@ -18,6 +18,7 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
@@ -92,6 +93,52 @@ type Reader struct {
mu sync.Mutex
defs map[string]seatdef.Definition // by seat
boot map[string]bool // seats held from the bootstrap, not yet from the owner
// what the last ask of the owner came to (Status)
spoken bool
revision uint64
lastErr string
}
// Status is what the reader holds and what it waits for (ADR 0635 §3), for the host's health and its tool: Heard says
// the owner has answered, Waiting are the seats still held from the bootstrap alone (the owner has not named them),
// LastError is why the last ask failed, and Revision is the owner's state revision of the last listing.
type Status struct {
Heard bool `json:"heard"`
Held int `json:"held"`
Waiting []string `json:"waiting,omitempty"`
LastError string `json:"last-error,omitempty"`
Revision uint64 `json:"revision,omitempty"`
}
// Status is the reader's state of mind.
func (r *Reader) Status() Status {
r.mu.Lock()
defer r.mu.Unlock()
st := Status{Heard: r.spoken, Held: len(r.defs), LastError: r.lastErr, Revision: r.revision}
for seat := range r.boot {
st.Waiting = append(st.Waiting, seat)
}
sort.Strings(st.Waiting)
return st
}
// Health is Status as the one line a module's health says: empty when the owner has been heard and nothing waits.
func (st Status) Health() string {
switch {
case !st.Heard:
return fmt.Sprintf("the seat list is held from the foundation's bootstrap (%d seats): the definitions' owner has not answered%s",
st.Held, withError(st.LastError))
case len(st.Waiting) > 0:
return fmt.Sprintf("%d seats are held from the bootstrap alone, the owner names none of them: %s", len(st.Waiting), strings.Join(st.Waiting, ", "))
}
return ""
}
func withError(e string) string {
if e == "" {
return ""
}
return " (" + e + ")"
}
// Start returns a reader holding the bootstrap, asks the owner for every definition and follows its event. A first
@@ -148,11 +195,11 @@ func (r *Reader) seatsLocked() []catalogue.Seat {
func (r *Reader) Reload() error {
raw, err := r.cfg.Bus.Ask(r.cfg.Owner+"."+Tool, map[string]any{})
if err != nil {
return fmt.Errorf("seatlist: asking %s.%s: %w", r.cfg.Owner, Tool, err)
return r.failed(fmt.Errorf("seatlist: asking %s.%s: %w", r.cfg.Owner, Tool, err))
}
var l Listing
if err := json.Unmarshal(raw, &l); err != nil {
return fmt.Errorf("seatlist: %s.%s answered what is not a listing: %w", r.cfg.Owner, Tool, err)
return r.failed(fmt.Errorf("seatlist: %s.%s answered what is not a listing: %w", r.cfg.Owner, Tool, err))
}
parsed := map[string]seatdef.Definition{}
for _, e := range l.Definitions {
@@ -161,11 +208,12 @@ func (r *Reader) Reload() error {
}
d, err := seatdef.Parse(e.Definition)
if err != nil {
return fmt.Errorf("seatlist: the definition of %s does not parse: %w", e.Seat, err)
return r.failed(fmt.Errorf("seatlist: the definition of %s does not parse: %w", e.Seat, err))
}
parsed[d.Seat] = d
}
r.mu.Lock()
r.spoken, r.revision, r.lastErr = true, l.Revision, ""
for seat := range r.defs {
if !r.boot[seat] {
delete(r.defs, seat)
@@ -192,7 +240,7 @@ func (r *Reader) heard(_ string, body json.RawMessage) error {
}
raw, err := r.cfg.Bus.Ask(r.cfg.Owner+"."+Tool, map[string]any{"seat": c.Seat})
if err != nil {
return err
return r.failed(err)
}
var a Answer
if err := json.Unmarshal(raw, &a); err != nil {
@@ -220,6 +268,13 @@ func (r *Reader) heard(_ string, body json.RawMessage) error {
return nil
}
func (r *Reader) failed(err error) error {
r.mu.Lock()
r.lastErr = err.Error()
r.mu.Unlock()
return err
}
func (r *Reader) changed() {
if r.cfg.OnChange != nil {
r.cfg.OnChange(r.Seats())
+28
View File
@@ -127,3 +127,31 @@ func TestStartRefusesWhatCannotRead(t *testing.T) {
}
}
}
// ADR 0635 §3: before the owner is heard the reader holds the foundation's definitions and says it waits.
func TestTheBootstrapParsesAndIsHeldUntilTheOwnerAnswers(t *testing.T) {
boot := Bootstrapped()
if len(boot) == 0 {
t.Fatal("no bootstrapped definition")
}
owner := &fakeOwner{down: true}
r, err := Start(Config{Owner: "mesh-owner", Bus: owner, Bootstrap: boot})
if err != nil {
t.Fatal(err)
}
if got := len(r.Seats()); got != len(boot) {
t.Fatalf("the reader holds %d seats, the bootstrap has %d", got, len(boot))
}
st := r.Status()
if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" {
t.Fatalf("status %+v: want the owner not heard, every bootstrapped seat waiting and the error said", st)
}
owner.down = false
owner.table = map[string]string{"node-b": def("node-b", "node", "put")}
if err := r.Reload(); err != nil {
t.Fatal(err)
}
if st := r.Status(); !st.Heard || len(st.Waiting) != len(boot) || st.LastError != "" || st.Revision != 7 {
t.Fatalf("status after the owner answered %+v: want heard, the bootstrap's seats the owner does not hold still waiting, no error, revision 7", st)
}
}