Merge pull request 'Answer the controller's lease, what still depends on it and the lease write's check in one call (issue 774)' (#120) from feat/lease-tool into main
This commit was merged in pull request #120.
This commit is contained in:
@@ -159,6 +159,18 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
|||||||
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
||||||
Input: schema(nil),
|
Input: schema(nil),
|
||||||
Run: func(map[string]any) (any, error) { return p.TakeOverState() }},
|
Run: func(map[string]any) (any, error) { return p.TakeOverState() }},
|
||||||
|
{Name: grants.ToolLeaseCheck, Description: "The controller's lease and the controller's hand-over, in one call from bus state (issue 774, " +
|
||||||
|
"design 47 L3; novox/hq ADR 0547 §11, ADR 0561 §3, ADR 0571 §13): who holds the controller lease now, since when, and what " +
|
||||||
|
"the holder and the parts watching the lease do because of it; what still depends on the controller: every module whose " +
|
||||||
|
"manifest at the indexer's trunk reads a mesh-controller state, invokes a mesh-controller verb or tool, consumes or waits on " +
|
||||||
|
"its events, with which one and the record that keeps it until the lease write (the named keep list), the unkept ones by module; " +
|
||||||
|
"the controller's remaining live duties as the bus shows them: each of its streams and buckets someone consumes (with the " +
|
||||||
|
"durable consumers by name), and its standing assignments; and a dry run of the lease write's check (the take-over check that " +
|
||||||
|
"hands the lease to a holder that is no controller): whether it would pass now, at the write with only the three kept to the " +
|
||||||
|
"write and before the write's delivery with the whole list, each with its exact reasons, and the write's other conditions " +
|
||||||
|
"with the tool that answers each. Asks the controller nothing; writes nothing. Read-only.",
|
||||||
|
Input: schema(nil),
|
||||||
|
Run: func(map[string]any) (any, error) { return p.LeaseCheck() }},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,479 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ const (
|
|||||||
|
|
||||||
// Tools is every tool, in the manifest's order.
|
// Tools is every tool, in the manifest's order.
|
||||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
||||||
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes}
|
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes, ToolLeaseCheck}
|
||||||
|
|
||||||
// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the
|
// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the
|
||||||
// controller's lease is handed away (delivery.GrantSource).
|
// controller's lease is handed away (delivery.GrantSource).
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
|
||||||
|
"mesh-grants/internal/grants"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Issue 774, design 47 L3 (ADR 0547 §11, ADR 0561 §3, ADR 0571 §13): grants_lease_check answers from bus state who holds
|
||||||
|
// the controller's lease, every manifest's dependency on the controller with what keeps it, the controller's streams in
|
||||||
|
// use, and the lease write's check run dry, at the write (the three kept to it alone) and before its delivery.
|
||||||
|
func TestTheLeaseCheckJudgesEveryDependencyOnTheController(t *testing.T) {
|
||||||
|
const mManifest = `{"module":"mod-m","version":"1","reads":["mesh-controller.declared-packages"]}`
|
||||||
|
controllerHolds := map[string]any{"instance": "controller@novox-be pid 1 since 2026-10-30T14:52:10Z", "epoch": 60,
|
||||||
|
"taken": "2026-10-30T14:52:10Z", "renewed": "2026-10-30T22:00:00Z"}
|
||||||
|
handedAway := map[string]any{"handed-to": "the operator's hand-over", "handed-by": "operator"}
|
||||||
|
type want struct {
|
||||||
|
deps []string // "<module> <kind> <what> <kept?>" with kept as "kept" or "unkept"
|
||||||
|
writePasses bool
|
||||||
|
beforePasses bool
|
||||||
|
reason string // a reason the check at the write must name, or ""
|
||||||
|
bothPhases bool // the check before the delivery must name the reason too
|
||||||
|
handedAway bool
|
||||||
|
durable []string // durable consumers of KV_mesh-controller_assignments
|
||||||
|
}
|
||||||
|
for _, row := range []struct {
|
||||||
|
name string
|
||||||
|
manifests map[string]string
|
||||||
|
lease map[string]any
|
||||||
|
want want
|
||||||
|
setup func(b *fakeBus)
|
||||||
|
}{
|
||||||
|
{"a read of the lease alone is no dependency, and the controller's own manifest is not judged",
|
||||||
|
map[string]string{"mod-l": `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`,
|
||||||
|
"mesh-controller": `{"module":"mesh-controller","version":"1","invokes":["seat:mesh-controller.status"]}`},
|
||||||
|
controllerHolds, want{writePasses: true, beforePasses: true}, nil},
|
||||||
|
{"the builder's consume of artifacts-gone is kept to the write itself",
|
||||||
|
map[string]string{"mesh-delivery-builder": `{"module":"mesh-delivery-builder","version":"1","consumes":["mesh-controller.artifacts-gone"]}`},
|
||||||
|
controllerHolds, want{deps: []string{"mesh-delivery-builder consume mesh-controller.artifacts-gone kept"}, writePasses: true, beforePasses: true}, nil},
|
||||||
|
{"the assigner's read of composed is kept until the write's delivery, which drops it",
|
||||||
|
map[string]string{"mesh-delivery-assigner": `{"module":"mesh-delivery-assigner","version":"1","reads":["mesh-controller.composed","mesh-controller.lease"]}`},
|
||||||
|
controllerHolds, want{deps: []string{"mesh-delivery-assigner read mesh-controller.composed kept"}, beforePasses: true,
|
||||||
|
reason: "mesh-delivery-assigner declares read mesh-controller.composed, kept by ADR 0571 §8 only until the lease write's own delivery"}, nil},
|
||||||
|
{"a read no record keeps fails both phases, named",
|
||||||
|
map[string]string{"pacman": `{"module":"pacman","version":"1","reads":["mesh-controller.declared-packages"]}`},
|
||||||
|
controllerHolds, want{deps: []string{"pacman read mesh-controller.declared-packages unkept"},
|
||||||
|
reason: "pacman declares read mesh-controller.declared-packages, which no record keeps until the lease write"}, nil},
|
||||||
|
{"a seat verb and a module tool of the controller's, a consume and a wait are each one; every tool (*) is none",
|
||||||
|
map[string]string{"mod-i": `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` +
|
||||||
|
`"consumes":["mesh-controller.checked","mod-x.thing"],"uses":["mesh-controller"]}`,
|
||||||
|
"node-tools": `{"module":"node-tools","version":"1","waits":["mesh-controller.checked"]}`},
|
||||||
|
controllerHolds, want{deps: []string{"mod-i consume mesh-controller.checked unkept", "mod-i invoke mesh-controller.status unkept",
|
||||||
|
"mod-i invoke seat:mesh-controller.root-free unkept", "mod-i uses mesh-controller unkept", "node-tools waits mesh-controller.checked unkept"},
|
||||||
|
reason: "mod-i declares invoke seat:mesh-controller.root-free, which no record keeps"}, nil},
|
||||||
|
{"a process's own invoke of the controller is its module's dependency, named by the process",
|
||||||
|
map[string]string{"mod-p": `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},{"id":"worker","type":"process","run":["./w"],"user":"mod-p",` +
|
||||||
|
`"bus":{"invokes":["seat:mesh-controller.held"]}}]}`},
|
||||||
|
controllerHolds, want{deps: []string{"mod-p invoke seat:mesh-controller.held unkept"},
|
||||||
|
reason: "mod-p (process worker) declares invoke seat:mesh-controller.held"}, nil},
|
||||||
|
{"no manifest read from the indexer fails both phases, though nothing read depends on the controller",
|
||||||
|
map[string]string{},
|
||||||
|
controllerHolds, want{reason: "no manifest was read from the indexer (it holds 0 modules)", bothPhases: true}, nil},
|
||||||
|
{"a module whose manifest digest is missing fails both phases, named",
|
||||||
|
map[string]string{"mod-l": `{"module":"mod-l","version":"1"}`, "mod-m": mManifest},
|
||||||
|
controllerHolds, want{reason: "manifest of mod-m could not be read", bothPhases: true}, func(b *fakeBus) {
|
||||||
|
delete(b.states[grants.ReadManifests], delivery.ManifestDigest([]byte(mManifest)))
|
||||||
|
}},
|
||||||
|
{"a manifest mesh-sdk refuses fails both phases, named",
|
||||||
|
map[string]string{"mod-l": `{"module":"mod-l","version":"1"}`, "mod-r": `{"module":"mod-r","version":"1","waits":["mesh-controller.checked"]}`},
|
||||||
|
controllerHolds, want{reason: "manifest of mod-r could not be read", bothPhases: true}, nil},
|
||||||
|
{"a lease already handed away leaves nothing to write",
|
||||||
|
map[string]string{},
|
||||||
|
handedAway, want{handedAway: true, reason: "there is nothing to write: the lease is handed to the operator's hand-over"}, nil},
|
||||||
|
} {
|
||||||
|
t.Run(row.name, func(t *testing.T) {
|
||||||
|
b := newFakeBus()
|
||||||
|
for m, raw := range row.manifests {
|
||||||
|
b.manifestOf(m, raw)
|
||||||
|
}
|
||||||
|
b.put(grants.ReadLease, grants.LeaseKey, row.lease)
|
||||||
|
if row.setup != nil {
|
||||||
|
row.setup(b)
|
||||||
|
}
|
||||||
|
b.streams = []map[string]any{
|
||||||
|
{"name": "KV_mesh-controller_assignments", "consumers": 3, "messages": 2991, "subjects_held": 40, "last": "2026-10-30T12:55:13Z"},
|
||||||
|
{"name": "KV_mesh-controller_rollouts", "consumers": 0, "messages": 6652},
|
||||||
|
{"name": "KV_mesh-grants_memberships", "consumers": 243, "messages": 1573},
|
||||||
|
}
|
||||||
|
b.consumers = map[string][]map[string]any{"KV_mesh-controller_assignments": {{"name": "0AYfjxta"}, {"name": "1g9XJFUX"}, {"name": "grants-assignments"}}}
|
||||||
|
raw, err := partOn(b).LeaseCheck()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got grants.LeaseAnswer
|
||||||
|
data, _ := json.Marshal(raw)
|
||||||
|
if err := json.Unmarshal(data, &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var deps []string
|
||||||
|
for _, d := range got.Dependencies {
|
||||||
|
kept := "unkept"
|
||||||
|
if d.Kept != "" {
|
||||||
|
kept = "kept"
|
||||||
|
}
|
||||||
|
deps = append(deps, d.Module+" "+d.Kind+" "+d.What+" "+kept)
|
||||||
|
}
|
||||||
|
if !slices.Equal(deps, row.want.deps) {
|
||||||
|
t.Errorf("dependencies %q, want %q", deps, row.want.deps)
|
||||||
|
}
|
||||||
|
if got.Check.Phase != grants.PhaseWrite || got.Check.Passes != row.want.writePasses {
|
||||||
|
t.Errorf("check at the write passes=%v (%v), want %v", got.Check.Passes, got.Check.Reasons, row.want.writePasses)
|
||||||
|
}
|
||||||
|
if got.Before.Phase != grants.PhaseBefore || got.Before.Passes != row.want.beforePasses {
|
||||||
|
t.Errorf("check before the delivery passes=%v (%v), want %v", got.Before.Passes, got.Before.Reasons, row.want.beforePasses)
|
||||||
|
}
|
||||||
|
if row.want.reason != "" && !slices.ContainsFunc(got.Check.Reasons, func(r string) bool { return strings.Contains(r, row.want.reason) }) {
|
||||||
|
t.Errorf("reasons %q name nothing like %q", got.Check.Reasons, row.want.reason)
|
||||||
|
}
|
||||||
|
if row.want.bothPhases && !slices.ContainsFunc(got.Before.Reasons, func(r string) bool { return strings.Contains(r, row.want.reason) }) {
|
||||||
|
t.Errorf("reasons before the delivery %q name nothing like %q", got.Before.Reasons, row.want.reason)
|
||||||
|
}
|
||||||
|
if got.Lease.HandedAway != row.want.handedAway {
|
||||||
|
t.Errorf("lease handed away %v (%s), want %v", got.Lease.HandedAway, got.Lease.Says, row.want.handedAway)
|
||||||
|
}
|
||||||
|
if !row.want.handedAway && (got.Lease.Since == nil || got.Lease.Since.Format("15:04") != "14:52" ||
|
||||||
|
!strings.Contains(got.Lease.Says, "the controller holds the lease") || !slices.Equal(got.Lease.Does, grants.DoesWhileController)) {
|
||||||
|
t.Errorf("lease %+v: the controller's instance since 14:52, and what it does while it holds it", got.Lease)
|
||||||
|
}
|
||||||
|
if len(got.Streams) != 1 || got.Streams[0].Stream != "KV_mesh-controller_assignments" || got.Streams[0].Consumers != 3 ||
|
||||||
|
!slices.Equal(got.Streams[0].Durable, []string{"grants-assignments"}) || !slices.Equal(got.Idle, []string{"KV_mesh-controller_rollouts"}) {
|
||||||
|
t.Errorf("streams %+v idle %v: the controller's assignments bucket with its one durable consumer, the rollouts idle, no other module's", got.Streams, got.Idle)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every entry of the keep list names the record that keeps it, and exactly three are kept to the write itself (ADR 0571 §13).
|
||||||
|
func TestTheKeepListNamesItsRecordsAndThreeToTheWrite(t *testing.T) {
|
||||||
|
toTheWrite := 0
|
||||||
|
for _, k := range grants.KeepList {
|
||||||
|
if !strings.HasPrefix(k.Record, "ADR ") || k.Module == "" || k.What == "" {
|
||||||
|
t.Errorf("entry %+v names no record, module or declaration", k)
|
||||||
|
}
|
||||||
|
if k.ToTheWrite {
|
||||||
|
toTheWrite++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if toTheWrite != 3 {
|
||||||
|
t.Errorf("%d entries kept to the write itself, ADR 0571 §13 names three", toTheWrite)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,8 @@
|
|||||||
"grants_hand_back",
|
"grants_hand_back",
|
||||||
"grants_accounts",
|
"grants_accounts",
|
||||||
"grants_consumer_match",
|
"grants_consumer_match",
|
||||||
"grants_accept_hashes"
|
"grants_accept_hashes",
|
||||||
|
"grants_lease_check"
|
||||||
],
|
],
|
||||||
"state": [
|
"state": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user