Files
mesh-delivery/modules/mesh-grants/internal/grants/leasecheck.go
T
jschoubben 2dd5239726
mesh/delivery merged: source.main moved to novox/mesh-delivery@e559798972b4
mesh/check being carried to 2dd5239726ca on e559798972b4 | from d409bdf20f48
Refuse the lease write's check when the indexer's manifests were not all read (issue 774)
Review of 09791f5: with the indexer's modules bucket readable but empty, nothing was read, so
nothing was found and both phases passed; the operator could have handed the lease away while an
unkept read still stood. Judge now refuses when no manifest was read, or fewer of the indexer's
modules than it holds (a provided manifest does not count for a missing one), saying why; a
missing or refused manifest is named as before, now under test.

The controller's grants leave the keep list: ADR 0571 §9 has no manifest read them from L7's
delivery, changing ADR 0567 §10 and ADR 0547 §5's shadow compare, so the assigner's and the grants
part's reads are refused, and a refusal names the record that ended a declaration where one does.

Quick fix: the counts in the grants part's Judge, as here. Thorough fix: the same coverage rule
in the shared mesh-sdk derivation once the keep list moves there; the quick one stands until then.
2026-10-30 23:41:11 +01:00

480 lines
21 KiB
Go

package grants
import (
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// **The controller's lease, what still depends on the controller, and a dry run of the lease write's check** (issue 774,
// design 47 L3; novox/hq ADR 0547 §11, ADR 0561 §3, ADR 0571 §13). One read-only answer from bus state: the lease key,
// every manifest the indexer holds, and the bus module's streams. It writes nothing, and asks the controller nothing.
//
// It lives on the grants part because the part is where the lease write takes most of its effect and already reads all
// it needs: the lease (buscompose.NamedReaders names it), every manifest at the indexer's trunk, placement, and the bus's
// streams through the bus module's read tools. No record names the home of the lease write's check; ADR 0571 §13 names
// its test in three repositories, and this is the same judgement over the manifests the mesh holds now.
// ToolLeaseCheck is the tool's name.
const ToolLeaseCheck = "grants_lease_check"
// Controller is the module and the seat the check is about.
const Controller = "mesh-controller"
// The kinds of a dependency on the controller, as a manifest declares them.
const (
DependsRead = "read"
DependsInvoke = "invoke"
DependsConsume = "consume"
DependsUses = "uses"
DependsWaits = "waits"
)
// The phases of the lease write's check (ADR 0571 §13): before the lease write's own delivery the whole named list is
// allowed; that delivery drops every entry but the three kept to the write itself, and its check runs with those alone.
const (
PhaseBefore = "before-delivery"
PhaseWrite = "write"
)
// Dependency is one declaration of a manifest other than the controller's on the controller: a read of one of its
// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, or a send to its seat.
type Dependency struct {
Module string `json:"module"`
// By is the account declaring it when it is not the module's own: `process <id>` or `tools <binary>`.
By string `json:"by,omitempty"`
Kind string `json:"kind"`
What string `json:"what"`
// Kept is the record that keeps it until the lease write, from the named list; empty when none does.
Kept string `json:"kept,omitempty"`
// ToTheWrite says it is one of the three kept to the write itself, which the lease write's own delivery keeps.
ToTheWrite bool `json:"to_the_write,omitempty"`
}
// Kept is one entry of the named, temporary list ADR 0571 §13 allows: a read, invoke or consume another accepted record
// keeps until the lease write, with that record. ToTheWrite marks the three kept to the write itself.
type Kept struct {
Module, Kind, What, Record string
ToTheWrite bool
}
// KeepList is the named list. Each entry names the clause that keeps it; a dependency not on it fails the check. The
// third entry kept to the write, the bus node agent's following of the controller's user list (ADR 0571 §8, ADR 0547
// §8), is no manifest's declaration but the node-engine's own, so no manifest matches it; it stays named here so the
// list reads as the record has it.
var KeepList = []Kept{
// Kept to the write itself (ADR 0571 §4, §8, §13).
{Module: "mesh-delivery-builder", Kind: DependsConsume, What: "mesh-controller.artifacts-gone", Record: "ADR 0571 §4, §13", ToTheWrite: true},
{Module: "artifact-store-tools", Kind: DependsInvoke, What: "seat:mesh-controller.collect", Record: "ADR 0571 §4, §13", ToTheWrite: true},
{Module: "node-engine", Kind: DependsRead, What: "the controller's user list of the bus module's composition", Record: "ADR 0571 §8, §13; ADR 0547 §8", ToTheWrite: true},
// Kept until the lease write's own delivery.
{Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.placement-export", Record: "ADR 0567 §7"},
{Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.assign", Record: "ADR 0567 §8"},
{Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.unassign", Record: "ADR 0567 §8"},
{Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.pin", Record: "ADR 0567 §8"},
{Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.unpin", Record: "ADR 0567 §8"},
{Module: "mesh-delivery-assigner", Kind: DependsRead, What: "mesh-controller.assignments", Record: "ADR 0567 §8"},
{Module: "mesh-delivery-assigner", Kind: DependsRead, What: "mesh-controller.composed", Record: "ADR 0571 §8"},
{Module: "mesh-grants", Kind: DependsRead, What: "mesh-controller.assignments", Record: "ADR 0549 §5"},
{Module: "mesh-grants", Kind: DependsConsume, What: "mesh-controller.condition-raised", Record: "ADR 0564 §9"},
{Module: "mesh-grants", Kind: DependsConsume, What: "mesh-controller.condition-cleared", Record: "ADR 0564 §9"},
{Module: "messenger", Kind: DependsConsume, What: "mesh-controller.condition-raised", Record: "ADR 0564 §9"},
{Module: "messenger", Kind: DependsConsume, What: "mesh-controller.condition-cleared", Record: "ADR 0564 §9"},
{Module: "messenger", Kind: DependsConsume, What: "mesh-controller.condition-changed", Record: "ADR 0564 §9"},
{Module: "messenger", Kind: DependsInvoke, What: "seat:mesh-controller.conditions", Record: "ADR 0564 §9"},
{Module: "mesh-watcher", Kind: DependsConsume, What: "mesh-controller.doctor-heartbeat", Record: "ADR 0564 §9"},
{Module: "mesh-conditions", Kind: DependsRead, What: "mesh-controller.conditions", Record: "ADR 0564 §9"},
{Module: "mesh-config", Kind: DependsRead, What: "mesh-controller.declared-config", Record: "ADR 0565 §4"},
{Module: "mesh-config", Kind: DependsRead, What: "mesh-controller.predefined", Record: "ADR 0565 §4"},
{Module: "mesh-healers", Kind: DependsRead, What: "mesh-controller.hand-acts", Record: "ADR 0565 §11"},
{Module: "mesh-hand-acts", Kind: DependsRead, What: "mesh-controller.hand-acts", Record: "ADR 0565 §11"},
}
// EndedBy names, for a declaration no entry keeps, the record that already ended it, so a refusal says where it moved.
// The controller's `grants` is one: ADR 0571 §9 has no manifest read it from L7's delivery, which changes ADR 0567 §10
// and ADR 0547 §5's shadow compare, so neither the assigner's nor the grants part's read of it is kept.
var EndedBy = map[string]string{
DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read",
DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts",
DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts",
DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: root-free is the self-check's probe",
DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's",
DependsRead + " mesh-controller.declared-packages": "ADR 0565 §6: each node agent writes its own node's packages",
}
// keptFor is the list's entry for one dependency, or nil.
func keptFor(d Dependency) *Kept {
for i, k := range KeepList {
if k.Module == d.Module && k.Kind == d.Kind && k.What == d.What {
return &KeepList[i]
}
}
return nil
}
// onController says whether a name `<owner>.<rest>` is the controller's.
func onController(name string) bool { return strings.HasPrefix(name, Controller+".") }
// invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none:
// it is the console's grant to every tool, judged by no record as a dependency.
func invokesController(invoke string) bool {
if seat, _, ok := catalogue.SeatInvoke(invoke); ok {
return seat == Controller
}
return onController(invoke)
}
// Dependencies are every declaration on the controller in the manifests given, by module, each with the list's entry
// that keeps it. The controller's own manifest is not judged, nor a read of its lease key: it names the lease's holder,
// whoever that is, and parts read it to learn that the write happened (ADR 0571 §13).
func Dependencies(manifests map[string]catalogue.Manifest) []Dependency {
var out []Dependency
add := func(module, by, kind, what string) {
d := Dependency{Module: module, By: by, Kind: kind, What: what}
if k := keptFor(d); k != nil {
d.Kept, d.ToTheWrite = k.Record, k.ToTheWrite
}
if !slices.Contains(out, d) {
out = append(out, d)
}
}
for name, m := range manifests {
if name == Controller || m.Module == Controller {
continue
}
for _, r := range m.Reads {
if onController(r) && r != buscompose.ControllerLease {
add(name, "", DependsRead, r)
}
}
for _, i := range m.Invokes {
if invokesController(i) {
add(name, "", DependsInvoke, i)
}
}
for _, c := range m.Consumes {
if onController(c) {
add(name, "", DependsConsume, c)
}
}
for _, w := range m.Waits {
if onController(w) {
add(name, "", DependsWaits, w)
}
}
for _, u := range m.Uses {
if u == Controller {
add(name, "", DependsUses, u)
}
}
for _, a := range m.BusAccounts() {
by := "process " + a.ID
if a.Tools {
by = "tools " + a.ID
}
for _, i := range a.Bus.Invokes {
if invokesController(i) {
add(name, by, DependsInvoke, i)
}
}
}
}
sort.Slice(out, func(i, j int) bool {
a, b := out[i], out[j]
if a.Module != b.Module {
return a.Module < b.Module
}
if a.Kind != b.Kind {
return a.Kind < b.Kind
}
if a.What != b.What {
return a.What < b.What
}
return a.By < b.By
})
return out
}
// Verdict is the lease write's check in one phase.
type Verdict struct {
Phase string `json:"phase"`
Passes bool `json:"passes"`
Reasons []string `json:"reasons"`
}
// Coverage is how much of the indexer the dependencies were derived from: the manifests read, the modules the indexer
// holds (-1 when its keys could not be listed, which Unread then names), and each manifest that could not be read.
type Coverage struct {
Read, Indexed int
Unread []Gap
}
// Judge runs the lease write's check over the dependencies (ADR 0571 §13) in one phase: before the lease write's own
// delivery, every dependency must be on the named list; at the write, only the three kept to the write itself may stay.
// It passes only over every module the indexer holds: no manifest read, fewer read than the indexer holds, or one that
// could not be read fails it, since what was not read could hide a dependency. A lease already handed away leaves
// nothing to write.
func Judge(deps []Dependency, phase string, cov Coverage, handedAway bool, leaseSays string) Verdict {
v := Verdict{Phase: phase, Reasons: []string{}}
if handedAway {
v.Reasons = append(v.Reasons, "there is nothing to write: "+leaseSays)
}
switch {
case cov.Read == 0:
v.Reasons = append(v.Reasons, fmt.Sprintf("no manifest was read from the indexer (it holds %d modules): with nothing read, nothing on the controller can be judged absent", max(cov.Indexed, 0)))
case cov.Indexed >= 0 && cov.Read < cov.Indexed:
v.Reasons = append(v.Reasons, fmt.Sprintf("only %d of the %d modules the indexer holds had their manifest read, so what the others declare on the controller is not known", cov.Read, cov.Indexed))
}
for _, d := range deps {
who := d.Module
if d.By != "" {
who += " (" + d.By + ")"
}
switch {
case d.Kept == "":
why := fmt.Sprintf("%s declares %s %s, which no record keeps until the lease write (ADR 0571 §13)", who, d.Kind, d.What)
if ended := EndedBy[d.Kind+" "+d.What]; ended != "" {
why += "; ended by " + ended
}
v.Reasons = append(v.Reasons, why)
case phase == PhaseWrite && !d.ToTheWrite:
v.Reasons = append(v.Reasons, fmt.Sprintf("%s declares %s %s, kept by %s only until the lease write's own delivery, which drops it", who, d.Kind, d.What, d.Kept))
}
}
for _, g := range cov.Unread {
v.Reasons = append(v.Reasons, fmt.Sprintf("%s could not be read (%s), so what it declares on the controller is not known", g.Input, g.Why))
}
v.Passes = len(v.Reasons) == 0
return v
}
// LeaseNow is who holds the lease and what follows from it.
type LeaseNow struct {
HandedAway bool `json:"handed_away"`
Says string `json:"says"`
Holder *buscompose.LeaseHolder `json:"holder,omitempty"`
Since *time.Time `json:"since,omitempty"`
Does []string `json:"does"`
}
// While the controller holds the lease, and once it is handed away: what the holder and the parts watching the key do
// because of it, by the records that say so.
var (
DoesWhileController = []string{
"the controller mints every new bus account; the vault mints none and says what it holds back (ADR 0561 §3)",
"the grants part creates no durable consumer an account needs, and only compares them (ADR 0561 §6)",
"the grants part publishes memberships on the controller's assignments stream beside its own state (ADR 0571 §10)",
"mesh-healers writes each act it would take in `would`, and acts on none (ADR 0564 §8)",
"the artifact sweep lets go of nothing; the controller's take-in sweeps and says `artifacts-gone` (ADR 0571 §4)",
"the watchman only lists dead letters; the controller keeps DEAD_LETTERS through its consumer of DEAD_LETTER_NOTICES (ADR 0564 §5)",
"placement proposals take only assignments, pins and config values, and the assigner keeps the controller's assignments in step (ADR 0567 §8, §9)",
"the controller keeps composing for the bus node, whose agent follows its user list, so the hand-backs of config, conditions, placement and the user list still answer (ADR 0547 §8, ADR 0564 §9, ADR 0565 §4, ADR 0567 §11)",
}
DoesHandedAway = []string{
"the vault mints every new bus account (ADR 0561 §1)",
"the grants part creates the durable consumers an account needs, and stops publishing on the assignments stream (ADR 0561 §6, ADR 0571 §10)",
"mesh-healers acts (ADR 0564 §8)",
"the artifact sweep sweeps and records every deletion in the hand-act log (ADR 0571 §4)",
"the watchman keeps the dead letters (ADR 0564 §5)",
"every hand-back is answered as gone (ADR 0547 §11, ADR 0564 §9, ADR 0565 §4, ADR 0567 §11)",
}
)
// Lease reads the lease key and says who holds it, since when, and what follows.
func Lease(r Reader) LeaseNow {
raw, found, err := r.Get(ReadLease, LeaseKey)
away, says := buscompose.LeaseHandedAway(raw, found, err)
out := LeaseNow{HandedAway: away, Says: says, Does: DoesWhileController}
if away {
out.Does = DoesHandedAway
}
if err == nil && found && len(raw) > 0 {
var h buscompose.LeaseHolder
if json.Unmarshal(raw, &h) == nil {
out.Holder = &h
if !h.Taken.IsZero() {
t := h.Taken
out.Since = &t
}
}
}
return out
}
// StreamUse is one of the controller's streams (its buckets, the assignments stream, the dead-letter notices) as the bus
// module answers it: who consumes it now, and what it last carried.
type StreamUse struct {
Stream string `json:"stream"`
Consumers int `json:"consumers"`
Durable []string `json:"durable,omitempty"`
Messages int64 `json:"messages"`
Subjects int64 `json:"subjects_held,omitempty"`
Last string `json:"last,omitempty"`
}
// The streams of the controller's that are not its buckets: the assignments stream it publishes memberships on, and the
// notices its consumer `controller` turns into dead letters until the lease write (ADR 0564 §5).
const (
StreamAssignments = "ASSIGNMENTS"
StreamDeadLetterNotices = "DEAD_LETTER_NOTICES"
)
// controllerStream says whether a stream is the controller's.
func controllerStream(name string) bool {
return strings.HasPrefix(name, "KV_"+Controller+"_") || name == StreamAssignments || name == StreamDeadLetterNotices
}
// Duties reads, from the bus module's streams, every stream of the controller's someone consumes, with its durable
// consumers by name (a watcher of a bucket is ephemeral and names nobody). Streams with no consumer are counted apart.
func Duties(c Caller) (used []StreamUse, idle []string, err error) {
raw, err := c.Call(CallStreams, map[string]any{})
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", CallStreams, err)
}
type consumer struct {
Name string `json:"name"`
Kind string `json:"kind"`
Max int `json:"max_deliver"`
Ack string `json:"ack_policy"`
}
type stream struct {
Name string `json:"name"`
Consumers int `json:"consumers"`
Messages int64 `json:"messages"`
Held int64 `json:"subjects_held"`
Last string `json:"last"`
Detail []consumer `json:"consumer_detail"`
}
var got struct {
Detail []stream `json:"detail"`
}
if err := json.Unmarshal(unwrap(raw), &got); err != nil {
return nil, nil, fmt.Errorf("%s answered no streams: %w", CallStreams, err)
}
for _, s := range got.Detail {
if !controllerStream(s.Name) {
continue
}
if s.Consumers == 0 {
idle = append(idle, s.Name)
continue
}
u := StreamUse{Stream: s.Name, Consumers: s.Consumers, Messages: s.Messages, Subjects: s.Held, Last: s.Last}
if one, err := c.Call(CallStreams, map[string]any{"stream": s.Name}); err == nil {
var d struct {
Detail []stream `json:"detail"`
}
if json.Unmarshal(unwrap(one), &d) == nil && len(d.Detail) > 0 {
for _, cs := range d.Detail[0].Detail {
if durable(cs.Name) {
u.Durable = append(u.Durable, cs.Name)
}
}
}
}
used = append(used, u)
}
sort.Slice(used, func(i, j int) bool { return used[i].Stream < used[j].Stream })
sort.Strings(idle)
return used, idle, nil
}
// durable says whether a consumer's name is one a part gave it: the bus names an ephemeral watcher with eight random
// letters and digits, which no part of the mesh uses.
func durable(name string) bool {
if len(name) != 8 {
return true
}
for _, r := range name {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
return true
}
}
return false
}
// LeaseAnswer is the tool's answer.
type LeaseAnswer struct {
Lease LeaseNow `json:"lease"`
Dependencies []Dependency `json:"dependencies"`
// Unkept are the dependencies no record keeps, by module: what the check refuses in every phase.
Unkept map[string][]string `json:"unkept"`
Manifests int `json:"manifests_read"`
// Indexed is how many modules the indexer holds; -1 when its keys could not be listed.
Indexed int `json:"modules_indexed"`
Unread []Gap `json:"manifests_unread,omitempty"`
// Streams are the controller's streams someone consumes now; Idle those no one does.
Streams []StreamUse `json:"streams,omitempty"`
Idle []string `json:"streams_idle,omitempty"`
StreamsError string `json:"streams_unread,omitempty"`
// Assignments is how many of the controller's assignments stand, which it publishes memberships and grants for.
Assignments int `json:"assignments_standing"`
// Check is the dry run at the write (the three alone), Before the same over the whole list before the write's
// delivery.
Check Verdict `json:"check"`
Before Verdict `json:"check_before_delivery"`
Beside []string `json:"not_judged_here"`
}
// NotJudgedHere are the lease write's other conditions, each with the tool that answers it (design 47 L3, ADR 0547 §11).
var NotJudgedHere = []string{
"L4 to L7 delivered: the issue tracker's milestones 1952, 1953, 1954 and 1955",
"every node's record trusts the vault's key: mesh-delivery-view.node_trusts (ADR 0550 §2)",
"mesh-config's compare empty and its take-over done, and mesh-wants.upgrade compare empty: mesh-config.compare, mesh-wants.upgrade (ADR 0565 §14)",
"the condition keeper's take-over done: mesh-conditions.compare (ADR 0564 §9)",
}
// LeaseCheck answers the tool: who holds the lease and what follows, every dependency on the controller with what keeps
// it, the controller's streams in use, and the lease write's check run dry. Read-only.
func (p *Part) LeaseCheck() (any, error) {
in := ReadInputs(p.Read, &p.cache)
out := LeaseAnswer{Lease: Lease(p.Read), Unkept: map[string][]string{}, Manifests: len(in.Manifests), Unread: in.Refused,
Beside: NotJudgedHere}
for _, g := range in.Gaps {
if g.Failed && strings.HasPrefix(g.Input, ReadModules) {
out.Unread = append(out.Unread, g)
}
}
out.Dependencies = Dependencies(in.Manifests)
if out.Dependencies == nil {
out.Dependencies = []Dependency{}
}
for _, d := range out.Dependencies {
if d.Kept == "" {
out.Unkept[d.Module] = append(out.Unkept[d.Module], d.Kind+" "+d.What)
}
}
var gaps []Gap
for _, raw := range each(p.Read, ReadAssignments, &gaps) {
var a assignment
if json.Unmarshal(raw, &a) == nil && a.stands() {
out.Assignments++
}
}
if used, idle, err := Duties(p.Call); err != nil {
out.StreamsError = err.Error()
} else {
out.Streams, out.Idle = used, idle
}
// Only the indexer's own modules count as read: a manifest the mesh provides itself must not stand in for one missing.
cov := Coverage{Indexed: -1, Unread: out.Unread}
if keys, err := p.Read.Keys(ReadModules); err == nil {
cov.Indexed = len(keys)
for _, k := range keys {
if _, ok := in.Manifests[k]; ok {
cov.Read++
}
}
}
out.Indexed = cov.Indexed
out.Check = Judge(out.Dependencies, PhaseWrite, cov, out.Lease.HandedAway, out.Lease.Says)
out.Before = Judge(out.Dependencies, PhaseBefore, cov, out.Lease.HandedAway, out.Lease.Says)
return out, nil
}