L3 final: verb, reader-capability and bus-state edges; one unreadable manifest holds only its logins; the indexer holds mesh-catalog (ADR 0614, ADR 0604, issue 774) #149
@@ -28,6 +28,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
)
|
||||
|
||||
@@ -62,6 +64,7 @@ var readBuckets = []string{
|
||||
provisions.ProvisionsBucket,
|
||||
delivery.SourceBucket,
|
||||
delivery.MergesBucket,
|
||||
graph.Bucket,
|
||||
}
|
||||
|
||||
// readFamilies are the states kept one bucket per node the assigner reads, each named in its manifest's reads: every
|
||||
@@ -148,6 +151,11 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
|
||||
logf("the bucket %s cannot be opened (%v): no release's Approve answers a record ask until it is bound", name, err)
|
||||
continue
|
||||
}
|
||||
if name == graph.Bucket && err != nil {
|
||||
// The indexer's module graph (ADR 0614 §2, §3): without it no want waits for a verb it invokes until it is bound.
|
||||
logf("the bucket %s cannot be opened (%v): no want waits for a verb it invokes until it is bound once the bus says it was made", name, err)
|
||||
continue
|
||||
}
|
||||
if name == delivery.PartGrantsBucket && err != nil {
|
||||
// The grants part's grants (ADR 0571 clause 9): without them no module counts as granted until it is bound.
|
||||
logf("the bucket %s cannot be opened (%v): no grant is read until it is bound once the bus says it was made", name, err)
|
||||
@@ -166,7 +174,7 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
|
||||
func startsWithout(name string) bool {
|
||||
switch name {
|
||||
case delivery.SeatHoldersBucket, provisions.ProvisionsBucket, assign.PoliciesBucket, assign.FindingsBucket,
|
||||
delivery.AssignerPutsBucket, delivery.SourceBucket, delivery.MergesBucket:
|
||||
delivery.AssignerPutsBucket, delivery.SourceBucket, delivery.MergesBucket, graph.Bucket:
|
||||
return true
|
||||
}
|
||||
return slices.Contains(seatBuckets, name) || name == delivery.PartGrantsBucket
|
||||
@@ -251,6 +259,7 @@ var watched = []string{
|
||||
delivery.SeatHoldersBucket,
|
||||
delivery.SourceBucket,
|
||||
delivery.MergesBucket,
|
||||
graph.Bucket,
|
||||
}
|
||||
|
||||
// follow watches every bucket a pass reads and makes a pass at start, once every watch handed what stands, and after
|
||||
|
||||
@@ -155,10 +155,14 @@ func (p *passer) wants(ctx context.Context) {
|
||||
defer func() { p.sayNeither(neither, stale, left) }()
|
||||
// Where grants are read from, once for the pass: the grants part's (issue 774, ADR 0571 clause 9).
|
||||
grants := p.w.grantsBucket()
|
||||
// The verb edges, once for the pass (ADR 0614 §2, §3).
|
||||
moduleGraph := p.moduleGraph()
|
||||
served := p.served(ctx, get, moduleGraph, nodes)
|
||||
offering := p.offering(ctx, get, manifests, nodes) // bus state (ADR 0614 §5)
|
||||
for name := range names {
|
||||
fp := trunk[name].FP2
|
||||
name := name
|
||||
m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Chosen: chosenOf(chosen, name), Provisions: provisionsOf(name), Answering: answering(manifests, nodes), Offered: offered(manifests), Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name],
|
||||
m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Needs: trunk[name].Needs, Chosen: chosenOf(chosen, name), Provisions: provisionsOf(name), Answering: answering(manifests, nodes), Offered: offered(manifests), Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name],
|
||||
Self: func(node string) bool { return placed[node] },
|
||||
Composed: func(node, fp string) (delivery.ComposedDigest, bool) {
|
||||
return p.w.composed(node, name, fp) // a placed node's own: the digest its own have names (ADR 0571 §6)
|
||||
@@ -192,6 +196,10 @@ func (p *passer) wants(ctx context.Context) {
|
||||
p.clearHeld(name, fp)
|
||||
}
|
||||
}
|
||||
if served != nil {
|
||||
m.Invokes, m.Served = keptInvokes(moduleGraph, name), served
|
||||
}
|
||||
m.Offering = offering
|
||||
last := map[string]string{}
|
||||
for _, node := range m.Nodes {
|
||||
last[node] = p.w.lastHealthy(node, name, fp)
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
)
|
||||
|
||||
@@ -148,6 +150,23 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) {
|
||||
t.Error("an answer to an ask taken away was taken")
|
||||
}
|
||||
t.Run("a put of the bus acts on the operator's warrant alone", putOfTheBus)
|
||||
// ADR 0614 §4: the rules read what each node's engine states it reads from its record in `<node>.node`; a node with no
|
||||
// record, or one that states nothing, is read as stating nothing (the rules then read the baseline).
|
||||
t.Run("the rules read what each node's engine reads", func(t *testing.T) {
|
||||
w := newWorld()
|
||||
for node, reads := range map[string][]string{"ace": {"word:module", "word:tool-runner"}, "g14": nil} {
|
||||
raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: node, ManifestReads: reads}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key, _ := delivery.HaveNodeKey(node)
|
||||
w.put(delivery.HaveBucket, key, raw)
|
||||
}
|
||||
n := w.nodes(nil, nil)
|
||||
if !slices.Equal(n.Reads["ace"], []string{"word:module", "word:tool-runner"}) || len(n.Reads["g14"]) != 0 || len(n.Reads["novox"]) != 0 {
|
||||
t.Errorf("the nodes' reads are %v", n.Reads)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// memEntry is a value a memKV holds, as a bucket's Get answers it.
|
||||
@@ -388,6 +407,8 @@ func putOfTheBus(t *testing.T) {
|
||||
func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) {
|
||||
// A pair only one source names is a condition of the same kind (issue 1126): folded here to hold the count.
|
||||
aPlacementAloneNamesWhereModulesRun(t)
|
||||
// The verb edges a want waits on come from the graph and what runs (ADR 0614 §2, §3): folded here to hold the count.
|
||||
aWantWaitsOnTheVerbEdgesTheGraphKeeps(t)
|
||||
t.Run("gathers", testThePassGathersTheModulesOnNeitherList)
|
||||
p := newPasser(buckets{}, time.Date(2026, 10, 25, 15, 0, 0, 0, time.UTC))
|
||||
var said []asks.Condition
|
||||
@@ -1004,3 +1025,61 @@ func TestAnApprovedPlacementCallsNoControllerVerb(t *testing.T) {
|
||||
t.Fatalf("the old conditions cleared = %+v", conds)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0614 §2, §3: of what a module invokes, the pass keeps as edges the verbs the graph does not leave out and another
|
||||
// module serves; a verb is served once a running healthy build's manifest (the builder's build, the indexer's manifest)
|
||||
// serves it, and a build whose manifest cannot be read counts what the graph says its module serves. No graph read: no wait.
|
||||
func aWantWaitsOnTheVerbEdgesTheGraphKeeps(t *testing.T) {
|
||||
p := newPasser(buckets{}, time.Now())
|
||||
fp := func(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) }
|
||||
if p.served(context.Background(), nil, graph.Graph{}, assign.Nodes{}) != nil {
|
||||
t.Error("with no graph read, a want waits on a verb")
|
||||
}
|
||||
for module, n := range map[string]graph.Node{
|
||||
"caller": {FP: fp("c"), Repository: "novox/caller", Invokes: []string{"seat:x.v", "loop.l", "none.t", "caller.own"}, LeftOut: []string{"loop.l"},
|
||||
Serves: []string{"caller.own"}},
|
||||
"server": {FP: fp("e"), Repository: "novox/server", Serves: []string{"seat:x.v", "seat:x.w"}},
|
||||
"loop": {FP: fp("1"), Repository: "novox/loop", Serves: []string{"loop.l"}},
|
||||
"late": {FP: fp("2"), Repository: "novox/late", Serves: []string{"late.t"}},
|
||||
} {
|
||||
raw, err := graph.Encode(n)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.w.put(graph.Bucket, module, raw)
|
||||
}
|
||||
g := p.moduleGraph()
|
||||
if got := keptInvokes(g, "caller"); !slices.Equal(got, []string{"seat:x.v"}) || keptInvokes(g, "unknown") != nil {
|
||||
t.Errorf("the kept verb edges of caller = %v", got)
|
||||
}
|
||||
digest := strings.Repeat("d", 64)
|
||||
man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "server", JSON: `{"module":"server","claims":[{"name":"x","scope":"mesh","serves":["w"]}]}`})
|
||||
get := func(_ context.Context, key string) ([]byte, error) {
|
||||
if key == digest {
|
||||
return man, nil
|
||||
}
|
||||
return nil, errNotThere
|
||||
}
|
||||
build, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest})
|
||||
key, _ := delivery.BuilderBuildKey("server", fp("f"))
|
||||
p.w.put(delivery.BuilderBuildsBucket, key, build)
|
||||
running := delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
||||
at := func(f string) delivery.Have { h := running; h.FP = f; return h }
|
||||
served := p.served(context.Background(), get, g, assign.Nodes{Haves: map[string]map[string]delivery.Have{
|
||||
"g14": {"server": at(fp("f")), "late": at(fp("2"))}}})
|
||||
for v, want := range map[string]bool{"seat:x.w": true, "seat:x.v": false, "late.t": true} {
|
||||
if served(v) != want {
|
||||
t.Errorf("served(%s) = %v, want %v: the running server's manifest serves w alone, late's is not read", v, served(v), want)
|
||||
}
|
||||
}
|
||||
// ADR 0614 §5: bus-state counts as offered only from a healthy build on a node present; the bus module's build on an
|
||||
// offline node offers nothing, and one whose manifest is not read counts its trunk manifest's offer.
|
||||
bus := map[string]catalogue.Manifest{"bus": {Module: "bus", Provides: []catalogue.Offer{{Name: graph.BusState, Scope: catalogue.ScopeMesh}}}}
|
||||
for _, online := range []bool{false, true} {
|
||||
offering := p.offering(context.Background(), get, bus, assign.Nodes{Online: map[string]bool{"n2": online},
|
||||
Haves: map[string]map[string]delivery.Have{"n2": {"bus": at(fp("2"))}}})
|
||||
if offering(graph.BusState) != online {
|
||||
t.Errorf("with the bus module's node online %v, bus-state offered %v", online, offering(graph.BusState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
)
|
||||
|
||||
// The verb edges a want waits on (novox/hq ADR 0614 §2, §3): the module graph the indexer writes names what each module
|
||||
// invokes and serves and which of its verb edges it leaves out on a cycle (one place decides the cycle); a want waits for
|
||||
// each kept edge until a build serving its verb runs healthy, read from the haves, the builder's builds and the indexer's
|
||||
// manifests, as a provider is counted (ADR 0603 §5).
|
||||
|
||||
// moduleGraph is the module graph as the indexer last wrote it; a key that does not decode is left out, so its module
|
||||
// waits on no verb.
|
||||
func (p *passer) moduleGraph() graph.Graph {
|
||||
g := graph.Graph{}
|
||||
for key, raw := range p.w.read(graph.Bucket) {
|
||||
module, err := graph.ParseKey(key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if n, err := graph.Decode(raw); err == nil {
|
||||
g[module] = n
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// keptInvokes are the verbs of module the graph keeps as edges: each it invokes and does not leave out, that a module
|
||||
// other than itself serves. A verb no module of the graph serves is no edge, so it holds nothing; nil when the graph holds
|
||||
// no node of module.
|
||||
func keptInvokes(g graph.Graph, module string) []string {
|
||||
n, ok := g[module]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, v := range n.Invokes {
|
||||
if slices.Contains(n.LeftOut, v) {
|
||||
continue
|
||||
}
|
||||
if slices.ContainsFunc(g.Servers(v), func(s string) bool { return s != module }) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// served is, for this pass, whether a build serving a verb runs healthy on some node (graph.Served): every running healthy
|
||||
// have's build (the builder's builds key of its module and fingerprint) names its manifest, and the verbs that manifest
|
||||
// serves are served. A healthy build whose manifest cannot be read counts what the graph says its module serves, said in
|
||||
// the log, so a build read late never holds its callers for good; nil when the graph holds nothing, and no want waits on
|
||||
// a verb.
|
||||
func (p *passer) served(ctx context.Context, get getter, g graph.Graph, n assign.Nodes) func(string) bool {
|
||||
if len(g) == 0 {
|
||||
return nil
|
||||
}
|
||||
served, unread := graph.Served(n.Haves, func(module, fp string) ([]string, bool) { return p.servesOf(ctx, get, module, fp) },
|
||||
func(module string) []string { return g[module].Serves })
|
||||
if len(unread) > 0 {
|
||||
p.logf("the manifest of the running build of %v cannot be read, so what the module graph says they serve is counted as served", unread)
|
||||
}
|
||||
return served
|
||||
}
|
||||
|
||||
// offering is, for this pass, whether a build whose manifest offers a provision runs healthy on a node present (ADR 0603
|
||||
// §5, ADR 0614 §5): every running healthy have on an online node, its build's manifest read as servesOf reads it. A
|
||||
// healthy build whose manifest cannot be read counts what its module's trunk manifest offers, so a build read late never
|
||||
// holds a consumer for good.
|
||||
func (p *passer) offering(ctx context.Context, get getter, trunk map[string]catalogue.Manifest, n assign.Nodes) func(string) bool {
|
||||
offered := map[string]bool{}
|
||||
for node, haves := range n.Haves {
|
||||
if !n.Online[node] {
|
||||
continue
|
||||
}
|
||||
for module, h := range haves {
|
||||
if h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthOK {
|
||||
continue
|
||||
}
|
||||
m, ok := p.manifestOf(ctx, get, module, h.FP)
|
||||
if !ok {
|
||||
m = trunk[module]
|
||||
}
|
||||
for _, o := range m.Offers() {
|
||||
offered[o] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return func(provision string) bool { return offered[provision] }
|
||||
}
|
||||
|
||||
// servesOf are the verbs the manifest of module's build at fp serves (graph.ServesOf), and whether it could be read.
|
||||
func (p *passer) servesOf(ctx context.Context, get getter, module, fp string) ([]string, bool) {
|
||||
m, ok := p.manifestOf(ctx, get, module, fp)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return graph.ServesOf(m), true
|
||||
}
|
||||
|
||||
// manifestOf is the manifest of module's build at fp (the builder's build names its digest in the indexer's manifests),
|
||||
// and whether it could be read.
|
||||
func (p *passer) manifestOf(ctx context.Context, get getter, module, fp string) (catalogue.Manifest, bool) {
|
||||
key, err := delivery.BuilderBuildKey(module, fp)
|
||||
if err != nil {
|
||||
return catalogue.Manifest{}, false
|
||||
}
|
||||
raw := p.w.value(delivery.BuilderBuildsBucket, key)
|
||||
if raw == nil {
|
||||
return catalogue.Manifest{}, false
|
||||
}
|
||||
b, err := delivery.DecodeBuilderBuild(raw)
|
||||
if err != nil || b.Manifest == "" {
|
||||
return catalogue.Manifest{}, false
|
||||
}
|
||||
got := p.w.manifest(ctx, get, b.Manifest)
|
||||
if got.err != "" {
|
||||
return catalogue.Manifest{}, false
|
||||
}
|
||||
return got.manifest, true
|
||||
}
|
||||
@@ -338,7 +338,8 @@ func (w *world) wantsOf() map[string]map[string]delivery.Want {
|
||||
|
||||
// nodes is what the six rules read of the nodes: presence, haves, and every provision's providers with their nodes.
|
||||
func (w *world) nodes(manifests map[string]catalogue.Manifest, assigned map[string][]string) assign.Nodes {
|
||||
n := assign.Nodes{Online: map[string]bool{}, Haves: map[string]map[string]delivery.Have{}, Providers: map[string]map[string][]string{}}
|
||||
n := assign.Nodes{Online: map[string]bool{}, Haves: map[string]map[string]delivery.Have{}, Providers: map[string]map[string][]string{},
|
||||
Reads: map[string][]string{}}
|
||||
for key, value := range w.read(delivery.PresenceBucket) {
|
||||
node, _, _, err := delivery.ParsePresenceKey(key)
|
||||
if err != nil || node == "" {
|
||||
@@ -350,7 +351,14 @@ func (w *world) nodes(manifests map[string]catalogue.Manifest, assigned map[stri
|
||||
}
|
||||
for key, value := range w.read(delivery.HaveBucket) {
|
||||
node, module, isNode, err := delivery.ParseHaveKey(key)
|
||||
if err != nil || isNode {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if isNode {
|
||||
// What the node's engine reads (ADR 0614 §4); a record that states none is read as the baseline.
|
||||
if h, err := delivery.DecodeHaveNode(value); err == nil && h.Record != nil {
|
||||
n.Reads[node] = h.Record.ManifestReads
|
||||
}
|
||||
continue
|
||||
}
|
||||
if h, err := delivery.DecodeHave(value); err == nil {
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-assigner
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
@@ -20,3 +20,7 @@ require (
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package assign
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
|
||||
// verbWaiting says why the module's want on node waits for a verb it invokes (novox/hq ADR 0614 §2): the first seat verb or
|
||||
// module tool of Invokes (the module graph's verb edges, those it leaves out on a cycle not among them, §3) that no
|
||||
// running healthy build serves. Nil Served (nothing read of what runs) waits for none, and a verb a running build serves
|
||||
// already adds no wait. It holds under the provisions' rule (RuleProvisions): no reader knows a rule number of its own
|
||||
// yet, so the words name the edge (graph.VerbWaitWords, which the delivery view reads).
|
||||
func (m Module) verbWaiting(node string) (string, bool) {
|
||||
if m.Served == nil {
|
||||
return "", false
|
||||
}
|
||||
for _, v := range slices.Sorted(slices.Values(m.Invokes)) {
|
||||
if !m.Served(v) {
|
||||
return graph.VerbWaitWords(v, m.Name, node), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// busStateWaiting says why the module's want on node waits for bus state (ADR 0614 §5): its manifest asks bus state (a
|
||||
// bus-state entry, as the module graph reads it) and no build whose manifest offers bus-state runs healthy on a node
|
||||
// present. The provision bus-state is answered with no credential of a consumer's own, so ADR 0603 §5 counts the
|
||||
// provider's own have and no provisioner key is waited for, handed or not. Nil Offering (nothing read of what runs) waits
|
||||
// for none, and neither does a module that offers bus-state itself. It holds under the provisions' rule, its words naming bus state (graph.BusStateWaitWords).
|
||||
func (m Module) busStateWaiting(node string) (string, bool) {
|
||||
if len(m.Manifest.BusState) == 0 || m.Offering == nil || m.Offering(graph.BusState) ||
|
||||
slices.Contains(m.Manifest.Offers(), graph.BusState) { // the bus module offering it waits on nobody for it
|
||||
|
||||
return "", false
|
||||
}
|
||||
return graph.BusStateWaitWords(m.Name, node), true
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
|
||||
// Module is what the six rules read of one module (ADR 0429 §6, ADR 0528 clause 3): its newest fingerprint F at the
|
||||
@@ -40,6 +42,17 @@ type Module struct {
|
||||
// manifest the trunk holds and the modules the mesh provides itself, placed or not. Nil: not known, and no requirement
|
||||
// is waited on for want of an offer.
|
||||
Offered func(provision string) bool
|
||||
// Invokes are the seat verbs and module tools the module graph says the module invokes and keeps as edges to the
|
||||
// modules serving them, those it leaves out on a cycle not among them (ADR 0614 §2, §3): the want waits until each is
|
||||
// served (Served). Nil: no graph node is read, and no want waits on a verb.
|
||||
Invokes []string
|
||||
// Served says whether a build serving a seat verb or module tool runs healthy, counted from its holder's have (ADR 0614
|
||||
// §2, as ADR 0603 §5 counts a provider's). Nil: nothing is read of what runs, and no want waits on a verb.
|
||||
Served func(invoke string) bool
|
||||
// Offering says whether a build whose manifest offers a provision runs healthy on a node present, counted from the
|
||||
// provider's own have (ADR 0603 §5): what a want asking bus state waits for (ADR 0614 §5). Nil: nothing is read of what
|
||||
// runs, and no want waits on bus state.
|
||||
Offering func(provision string) bool
|
||||
// Chosen is a person's choice of the module's upgrade policy (the assigner's `policies`, ADR 0565 §7); nil: none.
|
||||
Chosen *catalogue.PersonPolicy
|
||||
Nodes []string
|
||||
@@ -64,6 +77,9 @@ type Module struct {
|
||||
Now time.Time
|
||||
// Puts are the puts of the module, by node (ADR 0546): while one owns a node's want (PutOwns), the rules pass over it.
|
||||
Puts map[string]delivery.Put
|
||||
// Needs are the reader capabilities the manifest at FP needs (the indexer's modules value, catalogue.ManifestNeeds, ADR
|
||||
// 0614 §4): a node's want moves only while that node's engine states each of them. Nil: none.
|
||||
Needs []string
|
||||
// Self says whether node composes the module itself: the catalogue's placement names the node (ADR 0547 §2). It is the one
|
||||
// source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while
|
||||
// the node was placed does not outlive the placement (issue 1126). Nil: no node does.
|
||||
@@ -85,6 +101,9 @@ type Nodes struct {
|
||||
Haves map[string]map[string]delivery.Have
|
||||
// Providers is, by provision, the modules that provide it, with the nodes each is assigned to.
|
||||
Providers map[string]map[string][]string
|
||||
// Reads is, by node, the reader capabilities its node-engine states in its node record (delivery.NodeRecord.ManifestReads,
|
||||
// ADR 0614 §4). A node with none, or none read, is read as stating the baseline (catalogue.StatedManifestReads).
|
||||
Reads map[string][]string
|
||||
}
|
||||
|
||||
// healthy says whether a have runs fp healthy.
|
||||
@@ -138,15 +157,36 @@ func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) {
|
||||
return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s on %s for %s", m.Name, node,
|
||||
short(m.FP))
|
||||
}
|
||||
if words, lacking := m.readerLacking(node, n); lacking {
|
||||
return false, delivery.RuleComposed, words
|
||||
}
|
||||
if _, ok := m.composition(node, m.FP); !ok {
|
||||
return false, delivery.RuleComposed, fmt.Sprintf("%s has named no composition of its own for %s at %s yet", node, m.Name, short(m.FP))
|
||||
}
|
||||
if words, waiting := m.provisionWaiting(node); waiting {
|
||||
return false, delivery.RuleProvisions, words
|
||||
}
|
||||
if words, waiting := m.verbWaiting(node); waiting {
|
||||
return false, delivery.RuleProvisions, words
|
||||
}
|
||||
if words, waiting := m.busStateWaiting(node); waiting {
|
||||
return false, delivery.RuleProvisions, words
|
||||
}
|
||||
return true, 0, ""
|
||||
}
|
||||
|
||||
// readerLacking says why the module's want on node is held for the node's engine (ADR 0614 §4): a reader capability its
|
||||
// manifest needs that the engine does not state, read as the baseline when it states none. It is held under rule 7 (the
|
||||
// composition), since no rule of its own is numbered in this release, with words naming the capability; it moves once the
|
||||
// engine states it. The node-engine's own new build is held only by a word of its own manifest no engine states.
|
||||
func (m Module) readerLacking(node string, n Nodes) (string, bool) {
|
||||
missing := catalogue.MissingManifestReads(m.Needs, n.Reads[node])
|
||||
if len(missing) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return graph.ReaderWaitWords(node, missing, m.Name, short(m.FP)), true // the words the delivery view names the edge by
|
||||
}
|
||||
|
||||
// provisionWaiting says why the module's want on node waits for a provision (ADR 0570 §4): a provision it requires that has
|
||||
// no key yet, or the first of its keys that is not provided. Nil Provisions (the seat's state not on the bus) waits for none.
|
||||
func (m Module) provisionWaiting(node string) (string, bool) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
|
||||
func fpOf(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) }
|
||||
@@ -104,6 +106,90 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
||||
t.Errorf("provided provisions held g14's want: %+v (%s)", out.Wants["g14"], out.Rules["g14"].Words)
|
||||
}
|
||||
}
|
||||
// ADR 0614 §4: a want is held on a node whose engine does not state a reader capability the manifest needs, under rule 7
|
||||
// with the capability as its words, and moves once the engine states it; an engine stating nothing reads the baseline,
|
||||
// so a manifest of the baseline's words, the node-engine's own new build among them, is not held.
|
||||
hostNeeds, err := catalogue.ManifestNeeds([]byte(`{"module":"mesh-host","version":"1","slug":"host","build":{"artifacts":[` +
|
||||
`{"name":"host-arch","kind":"bundle","language":"go","system":"arch","from":"cmd/mesh-host"}]},"consumes":["restic.backed-up"],` +
|
||||
`"state":[{"name":"health"}],"config":{"x":{"type":"number","minimum":1,"default":50,"runtime":true}},` +
|
||||
`"resources":[{"id":"k","type":"file","path":"/var/lib/k","content":"${config:x}\n"}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
why string
|
||||
name string
|
||||
needs []string
|
||||
reads map[string][]string
|
||||
held bool
|
||||
}{
|
||||
{"a word the baseline lacks, on engines stating nothing", "mail", []string{"word:module", "word:tool-runner"}, nil, true},
|
||||
{"the same, once both engines state it", "mail", []string{"word:module", "word:tool-runner"},
|
||||
map[string][]string{"ace": catalogue.ManifestReads(), "g14": catalogue.ManifestReads()}, false},
|
||||
{"the baseline's words on engines stating nothing", "mail", []string{"word:claims.serves", "seat:mesh-catalog", "form:config"}, nil, false},
|
||||
{"the node-engine's new build, whose contract has more words than its manifest uses", "mesh-host", hostNeeds, nil, false},
|
||||
} {
|
||||
m, n := ready()
|
||||
m.Name, m.Needs, n.Reads = c.name, c.needs, c.reads
|
||||
n.Haves["ace"][c.name], n.Haves["g14"][c.name] = n.Haves["ace"]["mail"], n.Haves["g14"]["mail"]
|
||||
out := Wants(m, n, wantedOld(), nil)
|
||||
if moved := out.Wants["g14"].FP == fNew; moved == c.held {
|
||||
t.Errorf("%s: g14's want %+v, held by %+v", c.why, out.Wants["g14"], out.Rules["g14"])
|
||||
}
|
||||
if r := out.Rules["g14"]; c.held && (r.Rule != delivery.RuleComposed || !strings.Contains(r.Words, "word:tool-runner") ||
|
||||
!strings.Contains(r.Words, "node-engine of g14")) {
|
||||
t.Errorf("%s: held by %+v, want rule 7 naming the capability", c.why, r)
|
||||
}
|
||||
if w := out.Wants["g14"]; c.held && (w.Rule != delivery.RuleComposed || w.Newest != fNew) {
|
||||
t.Errorf("%s: g14's want %+v, want it kept at what it runs and naming rule 7", c.why, w)
|
||||
}
|
||||
}
|
||||
// ADR 0614 §2, §3: a want waits, under the provisions' rule, until each verb it invokes is served by a running build,
|
||||
// and moves once one is; a verb a running build serves already adds no wait, and an edge the graph leaves out on a cycle
|
||||
// is not among Invokes, so it holds nothing; nothing read of what runs waits for none.
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
invokes []string
|
||||
served []string
|
||||
nilRead bool
|
||||
holds string
|
||||
}{
|
||||
{"a verb no running build serves", []string{"store.store_put", "seat:mesh-catalog.modules"}, []string{"store.store_put"}, false, "seat:mesh-catalog.modules"},
|
||||
{"once a build serving it runs", []string{"seat:mesh-catalog.modules"}, []string{"seat:mesh-catalog.modules"}, false, ""},
|
||||
{"an edge left out on a cycle", nil, nil, false, ""},
|
||||
{"nothing read of what runs", []string{"seat:mesh-catalog.modules"}, nil, true, ""},
|
||||
// ADR 0614 §5: a module asking bus state is held while no build offering bus-state runs, and moves once one does.
|
||||
{"bus state no running build offers", nil, nil, false, graph.BusState},
|
||||
{"bus state a running build offers", nil, []string{graph.BusState}, false, ""},
|
||||
{"bus state its own module offers", nil, nil, false, ""},
|
||||
} {
|
||||
m, n := ready()
|
||||
m.Invokes = c.invokes
|
||||
if !c.nilRead {
|
||||
m.Served = func(v string) bool { return slices.Contains(c.served, v) }
|
||||
}
|
||||
if strings.HasPrefix(c.name, "bus state") {
|
||||
m.Manifest.BusState = []catalogue.BusStateEntry{{Name: "cache", Storage: "key-value"}}
|
||||
m.Offering = func(p string) bool { return slices.Contains(c.served, p) }
|
||||
if c.name == "bus state its own module offers" {
|
||||
m.Manifest.Provides = []catalogue.Offer{{Name: graph.BusState, Scope: catalogue.ScopeMesh}}
|
||||
}
|
||||
}
|
||||
words := graph.VerbWaitWords(c.holds, "mail", "g14")
|
||||
if c.holds == graph.BusState {
|
||||
words = graph.BusStateWaitWords("mail", "g14")
|
||||
}
|
||||
out := Wants(m, n, wantedOld(), nil)
|
||||
if c.holds == "" {
|
||||
if out.Wants["g14"].FP != fNew {
|
||||
t.Errorf("%s: held %+v", c.name, out.Rules["g14"])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if out.Wants["g14"].FP == fNew || out.Rules["g14"].Rule != delivery.RuleProvisions || out.Rules["g14"].Words != words {
|
||||
t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], out.Rules["g14"])
|
||||
}
|
||||
}
|
||||
// ADR 0603 §9: the newest source revision governs. The module now at fNew no longer requires the database an older revision
|
||||
// asked for and requires oidc instead; the seat's keys follow the newest manifest, so the older edge is no wait: a provided
|
||||
// oidc moves the want, an asked one holds it and names oidc and never database.
|
||||
|
||||
@@ -34,8 +34,9 @@ func TestManifest(t *testing.T) {
|
||||
reads := slices.Clone(m.Reads)
|
||||
slices.Sort(reads)
|
||||
// ADR 0593 §8: it composes nothing, so it reads no candidate, held or resolved value, compose step or build log; it reads
|
||||
// every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1).
|
||||
want := []string{"mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules",
|
||||
// every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1);
|
||||
// and the indexer's module graph, whose verb edges a want waits on (ADR 0614 §2, §3).
|
||||
want := []string{"mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", "mesh-delivery-indexer.graph", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed",
|
||||
"mesh-provisioner.provisions", "node-engine.have"}
|
||||
if !reflect.DeepEqual(reads, want) {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-gate.merges",
|
||||
"mesh-delivery-gate.source",
|
||||
"mesh-delivery-indexer.graph",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-teller.answers",
|
||||
|
||||
@@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-builder
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg=
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
"mesh-delivery-checker/internal/check"
|
||||
)
|
||||
|
||||
@@ -95,6 +97,58 @@ func (n nodeNames) Names(ctx context.Context) ([]string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// runningServes says what the running builds serve (novox/hq ADR 0614 §7), read when a check asks it: every module's have
|
||||
// in the node-engine's have that runs healthy, the builder's build of its fingerprint, and the verbs its manifest serves
|
||||
// (graph.Served). A build whose manifest cannot be read serves nothing here, said in the log.
|
||||
type runningServes struct {
|
||||
have *nodeNames
|
||||
builds check.BuildStates
|
||||
read check.ManifestReader
|
||||
logf func(format string, args ...any)
|
||||
}
|
||||
|
||||
func (r runningServes) Served(ctx context.Context) (func(string) bool, error) {
|
||||
if r.have == nil || r.have.kv == nil || r.builds == nil || r.read == nil {
|
||||
return nil, fmt.Errorf("the node-engine's have, the builder's builds or the indexer's manifests cannot be read")
|
||||
}
|
||||
lister, err := r.have.kv.ListKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
haves := map[string]map[string]delivery.Have{}
|
||||
for key := range lister.Keys() {
|
||||
node, module, isNode, err := delivery.ParseHaveKey(key)
|
||||
if err != nil || isNode {
|
||||
continue
|
||||
}
|
||||
e, err := r.have.kv.Get(ctx, key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if h, err := delivery.DecodeHave(e.Value()); err == nil {
|
||||
if haves[node] == nil {
|
||||
haves[node] = map[string]delivery.Have{}
|
||||
}
|
||||
haves[node][module] = h
|
||||
}
|
||||
}
|
||||
served, unread := graph.Served(haves, func(module, fp string) ([]string, bool) {
|
||||
b, found, err := r.builds.Build(ctx, module, fp)
|
||||
if err != nil || !found || b.Manifest == "" {
|
||||
return nil, false
|
||||
}
|
||||
m, err := r.read.Manifest(ctx, b.Manifest)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return graph.ServesOf(m), true
|
||||
}, nil)
|
||||
if len(unread) > 0 && r.logf != nil {
|
||||
r.logf("the manifest of the running build of %v cannot be read, so it serves nothing the check counts (ADR 0614 §7)", unread)
|
||||
}
|
||||
return served, nil
|
||||
}
|
||||
|
||||
func newJudge(d judgeDeps) check.Judge {
|
||||
var nodes interface {
|
||||
Names(ctx context.Context) ([]string, error)
|
||||
@@ -102,8 +156,12 @@ func newJudge(d judgeDeps) check.Judge {
|
||||
if d.nodes != nil {
|
||||
nodes = d.nodes
|
||||
}
|
||||
return &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
|
||||
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Logf: d.logf}
|
||||
if d.nodes != nil && d.builds != nil && d.repos != nil {
|
||||
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
|
||||
}
|
||||
return ex
|
||||
}
|
||||
|
||||
// manifestKV reads a manifest by its digest.
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg=
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -28,6 +28,9 @@ import (
|
||||
// trunk's base; the base's own build is waited on.
|
||||
//
|
||||
// A module the trunk holds no record of yet is not built by the builder until it is merged, so it is not waited on either.
|
||||
//
|
||||
// A changed build module whose manifest invokes a seat verb or module tool no running build serves is one the check cannot
|
||||
// run (ADR 0614 §7): the modules it builds are composed from their manifests, as above, and the words name the verb.
|
||||
|
||||
// buildsOfTheCheck are the builds the check waits on before it composes (module to fingerprint) and the words for the
|
||||
// changed modules it composes without waiting on their build, one line each, sorted. A manifest that cannot be read leaves
|
||||
@@ -88,6 +91,14 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str
|
||||
words = append(words, fmt.Sprintf("the change alters the build module %s: %s, which it builds, are built by the running %s, not the changed one, which the check cannot run, so the check does not wait on their builds; they are built again once the agreed toolchain moves (ADR 0603 §3, §10)",
|
||||
bm, strings.Join(built, ", "), bm))
|
||||
}
|
||||
if unserved := st.unservedVerbs(ctx, changed[bm]); len(unserved) > 0 {
|
||||
what := "nothing it builds is in the change"
|
||||
if len(built) > 0 {
|
||||
what = strings.Join(built, ", ") + ", which it builds, are composed from their manifests"
|
||||
}
|
||||
words = append(words, fmt.Sprintf("the changed build module %s invokes %s, which no running build serves, so the check cannot run it: %s (ADR 0614 §7)",
|
||||
bm, strings.Join(unserved, ", "), what))
|
||||
}
|
||||
}
|
||||
g := graph.OfManifests(changed)
|
||||
for _, base := range slices.Sorted(maps.Keys(changed)) {
|
||||
@@ -109,3 +120,23 @@ func buildsWith(m catalogue.Manifest, language string) bool {
|
||||
return a.Kind == catalogue.ArtifactBundle && a.Language == language
|
||||
})
|
||||
}
|
||||
|
||||
// unservedVerbs are the seat verbs and module tools m invokes (graph.InvokesOf) that no running build serves, sorted; none
|
||||
// when nothing reads what runs, or what runs cannot be read (said in the log: nothing is said that was not read).
|
||||
func (st *state) unservedVerbs(ctx context.Context, m catalogue.Manifest) []string {
|
||||
if st.j.Serving == nil {
|
||||
return nil
|
||||
}
|
||||
served, err := st.j.Serving.Served(ctx)
|
||||
if err != nil {
|
||||
st.j.logf("%s: what the running builds serve cannot be read, so no build module is named for a verb it invokes: %v", st.c.Key, err)
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, v := range graph.InvokesOf(m) {
|
||||
if !served(v) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -127,6 +127,13 @@ func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[strin
|
||||
|
||||
func (f *fakeEnds) Done(context.Context, map[string]string) bool { return false }
|
||||
|
||||
// fixedServing are the verbs the running builds serve.
|
||||
type fixedServing []string
|
||||
|
||||
func (f fixedServing) Served(context.Context) (func(string) bool, error) {
|
||||
return func(v string) bool { return slices.Contains(f, v) }, nil
|
||||
}
|
||||
|
||||
// knownRuns are the modules the trunk holds a record of (the indexer's modules): any commit.
|
||||
type knownRuns []string
|
||||
|
||||
@@ -154,10 +161,17 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
|
||||
builds map[string]delivery.BuilderBuild // by module, at fp; nil leaves the check without the builds' ends
|
||||
runs knownRuns // nil: the trunk holds every module
|
||||
want []string
|
||||
waited string // the modules waited on, sorted
|
||||
failed string // the failure's test, when the check fails on a build
|
||||
tests bool // the tests fail: the check fails before it would wait
|
||||
waited string // the modules waited on, sorted
|
||||
failed string // the failure's test, when the check fails on a build
|
||||
tests bool // the tests fail: the check fails before it would wait
|
||||
served []string // what the running builds serve (ADR 0614 §7); nil reads nothing of what runs
|
||||
}{
|
||||
{name: "the Go build module invokes a verb no running build serves",
|
||||
manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild,
|
||||
Invokes: []string{"seat:package-registry.publish", "seat:artifact-store.put"}}, "web": {Module: "web", Build: goBuild}},
|
||||
builds: map[string]delivery.BuilderBuild{"mesh-build-go": done}, served: []string{"seat:artifact-store.put"},
|
||||
want: []string{"the changed build module mesh-build-go invokes seat:package-registry.publish, which no running build serves, " +
|
||||
"so the check cannot run it: web, which it builds, are composed from their manifests (ADR 0614 §7)"}, waited: "mesh-build-go"},
|
||||
{name: "the Go build module changes",
|
||||
manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild}, "web": {Module: "web", Build: goBuild},
|
||||
"mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}},
|
||||
@@ -211,6 +225,9 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
|
||||
if row.runs != nil {
|
||||
b.ex.Runs = row.runs
|
||||
}
|
||||
if row.served != nil {
|
||||
b.ex.Serving = fixedServing(row.served)
|
||||
}
|
||||
read, fps := fixedManifests{}, map[string]delivery.CandidateModule{}
|
||||
for module, m := range row.manifest {
|
||||
d := dig(string(rune('a' + len(read))))
|
||||
|
||||
@@ -1029,32 +1029,53 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
// untouched is the judge's refusal of mail, which the change leaves as it is, and trunkMail its refusal on the trunk:
|
||||
// a problem the trunk had is a warning, one the change causes fails (ADR 0579 §2)
|
||||
untouched, trunkMail string
|
||||
// cycle reads the manifests, so the change's module graph is worked out (ADR 0603 §2): the change's web awaits
|
||||
// mail's site while mail awaits web's database; trunkCycle has the trunk's web await it too.
|
||||
cycle, trunkCycle bool
|
||||
// cycle reads the manifests, so the change's module graph is worked out (ADR 0603 §2): with "provisions" the change's
|
||||
// web awaits mail's site while mail awaits web's database, and trunkCycle has the trunk's web await it too; with "bases"
|
||||
// each is built on the other; with "verbs" each invokes the other's tool, and with "a verb and a provision" web invokes
|
||||
// mail's tool while mail awaits web's database: both left out, so neither is refused (ADR 0614 §3, §6).
|
||||
cycle string
|
||||
trunkCycle bool
|
||||
}{
|
||||
{name: "accepted"},
|
||||
{name: "an untouched manifest's problem the trunk had", untouched: "mail says no credential form", trunkMail: "mail says no credential form"},
|
||||
{name: "a problem the change causes in an untouched manifest", untouched: "mail reads a state web no longer keeps",
|
||||
fails: "refuses the manifest of mail: mail reads a state web no longer keeps"},
|
||||
{name: "a cycle the change makes", cycle: true, fails: "the change's module graph has the cycle mail -> web -> mail"},
|
||||
{name: "a cycle the trunk has", cycle: true, trunkCycle: true},
|
||||
{name: "a cycle the change makes", cycle: "provisions", fails: "the change's module graph has the cycle mail -> web -> mail"},
|
||||
{name: "a cycle the trunk has", cycle: "provisions", trunkCycle: true},
|
||||
{name: "a cycle of bases", cycle: "bases", fails: "the change's module graph has the cycle mail -> web -> mail"},
|
||||
{name: "a cycle of verbs", cycle: "verbs"},
|
||||
{name: "a cycle of a verb and a provision", cycle: "a verb and a provision"},
|
||||
{name: "refused", refused: "web requires \"amqp\"\nweb has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web requires "amqp"; web has no id`},
|
||||
} {
|
||||
b := newBench(t)
|
||||
b.ex.Manifests = fixedDigests{"mail": strings.Repeat("a", 64), "web": strings.Repeat("b", 64)}
|
||||
b.ex.Trunks = fixedTrunks{heads: map[string]string{DeliveryRepository: trunk}}
|
||||
b.ex.Predefined = keptList(`{"font-size": {"type": "number", "description": "the size"}}`)
|
||||
if row.cycle {
|
||||
if row.cycle != "" {
|
||||
offer := func(name string) []catalogue.Offer {
|
||||
return []catalogue.Offer{{Name: name, Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}}
|
||||
}
|
||||
on := func(module string) *catalogue.Build {
|
||||
return &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: module, Artifact: "img"}}}
|
||||
}
|
||||
trunkWeb := catalogue.Manifest{Module: "web", Provides: offer("database")}
|
||||
if row.trunkCycle {
|
||||
trunkWeb.Requires = []string{"site"}
|
||||
}
|
||||
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail", Provides: offer("site"), Requires: []string{"database"}},
|
||||
strings.Repeat("b", 64): trunkWeb, web: {Module: "web", Provides: offer("database"), Requires: []string{"site"}}}
|
||||
mail := catalogue.Manifest{Module: "mail", Provides: offer("site"), Requires: []string{"database"}}
|
||||
changed := catalogue.Manifest{Module: "web", Provides: offer("database"), Requires: []string{"site"}}
|
||||
switch row.cycle {
|
||||
case "bases":
|
||||
mail = catalogue.Manifest{Module: "mail", Build: on("web")}
|
||||
changed = catalogue.Manifest{Module: "web", Build: on("mail")}
|
||||
case "verbs":
|
||||
mail = catalogue.Manifest{Module: "mail", Tools: []string{"mail_send"}, Invokes: []string{"web.web_page"}}
|
||||
changed = catalogue.Manifest{Module: "web", Tools: []string{"web_page"}, Invokes: []string{"mail.mail_send"}}
|
||||
case "a verb and a provision":
|
||||
mail = catalogue.Manifest{Module: "mail", Tools: []string{"mail_send"}, Requires: []string{"database"}}
|
||||
changed = catalogue.Manifest{Module: "web", Provides: offer("database"), Invokes: []string{"mail.mail_send"}}
|
||||
}
|
||||
b.ex.Read = fixedManifests{strings.Repeat("a", 64): mail, strings.Repeat("b", 64): trunkWeb, web: changed}
|
||||
}
|
||||
var judged []delivery.ComposeJob
|
||||
answer := b.asks.Answer
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
)
|
||||
|
||||
// The check refuses a change whose module graph has a cycle and names it (novox/hq ADR 0603 §2): modules that would each
|
||||
// wait for the other for good, through their bases or the provisions they await. The graph is worked out from the
|
||||
// wait for the other for good, through their bases, the provisions and bus state they await, or the verb edges the graph
|
||||
// keeps (ADR 0614 §6). A verb edge on a cycle is left out of the graph, so it is never refused (§3), and a package or a
|
||||
// build module's edge is a trigger, never a wait, so a cycle through one is not refused either (§6). The graph is worked out from the
|
||||
// catalogue as the change leaves it (every trunk manifest, each one a pull request touches replaced by its candidate's),
|
||||
// with the contract's ManifestGraph, the way the indexer works out its graph's edges. A cycle the trunk already has among
|
||||
// the same modules is the trunk's: said in the verdict's notes, not the change's failure.
|
||||
@@ -70,5 +72,5 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
|
||||
}
|
||||
}
|
||||
return fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+
|
||||
"good, through their bases or the provisions they await (ADR 0603 §2)", words))
|
||||
"good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words))
|
||||
}
|
||||
|
||||
@@ -122,6 +122,12 @@ type Examiner struct {
|
||||
// Read reads a kept manifest by its digest: the change's module graph is worked out from them (graph_cycle.go, novox/hq
|
||||
// ADR 0603 §2); nil works out no graph.
|
||||
Read ManifestReader
|
||||
// Serving says, from what runs, whether a running healthy build serves a seat verb or module tool (novox/hq ADR 0614
|
||||
// §7): the node-engines' haves, the builder's build of each, its manifest (graph.Served). Nil reads nothing, and no
|
||||
// changed build module is named for a verb it invokes.
|
||||
Serving interface {
|
||||
Served(ctx context.Context) (func(invoke string) bool, error)
|
||||
}
|
||||
// Predefined reads the catalogue's predefined keys the indexer keeps (manifests_judge.go); nil reads none.
|
||||
Predefined PredefinedList
|
||||
// Compositions makes a check's composition (ADR 0593 §7): the engines' answers (EngineCompositions); nil leaves the
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
@@ -12,30 +11,6 @@ import (
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// CatalogTools are the seat mesh-catalog's four verbs as tools named <seat>.<verb>, with the descriptions and inputs the
|
||||
// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the
|
||||
// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not.
|
||||
func CatalogTools(c index.CatalogRead) []stdio.Tool {
|
||||
seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
|
||||
runs := map[string]func(map[string]any) (any, error){
|
||||
"modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() },
|
||||
"module": func(a map[string]any) (any, error) { return c.CatalogModuleAnswer(str(a, "module")) },
|
||||
"provides": func(a map[string]any) (any, error) { return c.CatalogProvidesAnswer(str(a, "provision")) },
|
||||
"dependents": func(a map[string]any) (any, error) { return c.CatalogDependentsAnswer(str(a, "module")) },
|
||||
}
|
||||
var out []stdio.Tool
|
||||
for _, v := range seat.Serves {
|
||||
if run, ok := runs[v.Name]; ok {
|
||||
out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs.
|
||||
func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
|
||||
return index.CatalogRead{
|
||||
@@ -97,7 +72,7 @@ func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
|
||||
}
|
||||
}
|
||||
|
||||
// catalogTools are the seat's verbs over the indexer's own states, as main serves them once the indexer claims the seat.
|
||||
// catalogTools are the seat's verbs over the indexer's own states, served beside the indexer's own tools, since the indexer's manifest claims the seat (ADR 0604 §2).
|
||||
func catalogTools() []stdio.Tool {
|
||||
return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State)))
|
||||
return index.CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State)))
|
||||
}
|
||||
|
||||
@@ -104,12 +104,14 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState, counts func()
|
||||
Description: "Judge the manifests again now, as the indexer does at its start and after each merge, and answer how the module's " +
|
||||
"verdict stands (waits, refused or accepted, and why). For a manifest that waits for a judge that knows its fields, or " +
|
||||
"after a manifest was mended where the indexer has not heard it. A waiting manifest becomes refused here only when the " +
|
||||
"running indexer is the newest build of itself the trunk holds. indexer_verdicts reads the result.",
|
||||
"running indexer is the newest build of itself the trunk holds. With a digest it judges that one manifest of the " +
|
||||
"module (a candidate's, or one an older judge refused) in the catalogue the trunk holds. indexer_verdicts reads the result.",
|
||||
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
|
||||
"module": map[string]any{"type": "string", "description": "the module's name, as the catalogue registers it"},
|
||||
"digest": map[string]any{"type": "string", "description": "one manifest digest of the module to judge (indexer_verdicts names it); none judges every manifest the trunk holds"},
|
||||
"why": map[string]any{"type": "string", "description": "why it is judged again now, in plain words"}}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
key, value, err := index.NewJudgeAgain(str(args, "module"), str(args, "why"), stdio.CallerOf(args), now())
|
||||
key, value, err := index.NewJudgeAgain(str(args, "module"), str(args, "digest"), str(args, "why"), stdio.CallerOf(args), now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -190,7 +192,7 @@ func heldCounts(s stdio.KeptState) func() (index.OldSectionsCount, error) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState), heldCounts(stdio.State(index.OldSectionsState)))); err != nil {
|
||||
if err := stdio.Serve("", append(Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState), heldCounts(stdio.State(index.OldSectionsState))), catalogTools()...)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
type takerIn interface {
|
||||
TakeIn(ctx context.Context, module string) index.TakenIn
|
||||
TakeInRepository(ctx context.Context, repository string) index.TakenIn
|
||||
JudgeAgain(ctx context.Context, module string) index.TakenIn
|
||||
JudgeAgain(ctx context.Context, module, digest string) index.TakenIn
|
||||
}
|
||||
|
||||
// takenIn answers one value of the takeins state (the tool indexer_take_in, issue 1945): a value not yet answered takes the
|
||||
@@ -38,7 +38,7 @@ func takenIn(ctx context.Context, ix takerIn, kv jetstream.KeyValue, e jetstream
|
||||
asked, r = "take-in of a repository", ix.TakeInRepository(ctx, a.Repository)
|
||||
a.Module = a.Repository
|
||||
} else if a.Judge {
|
||||
asked, r = "judging again", ix.JudgeAgain(ctx, a.Module)
|
||||
asked, r = "judging again", ix.JudgeAgain(ctx, a.Module, a.Digest)
|
||||
} else {
|
||||
r = ix.TakeIn(ctx, a.Module)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -186,6 +186,20 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied,
|
||||
if held, err = ix.heldModules(ctx); err != nil {
|
||||
return applied, err
|
||||
}
|
||||
// Each manifest the pull carries is judged by this judge, unless it holds a verdict already that this judge made or
|
||||
// that accepts it: a refusal an older judge made never stands for a candidate (or a digest back on the trunk).
|
||||
over := map[string]string{}
|
||||
for _, digest := range digests {
|
||||
if ix.Manifests == nil {
|
||||
break
|
||||
}
|
||||
if value, _, found, err := ix.Manifests.Get(ctx, digest); err == nil && found {
|
||||
if kept, err := delivery.DecodeManifest(value); err == nil {
|
||||
over[kept.Module] = digest
|
||||
}
|
||||
}
|
||||
}
|
||||
ix.judgeDigests(ctx, over, fmt.Sprintf("%s#%d", f.Repository, f.Number))
|
||||
if err := ix.configCandidateOfPull(ctx, f, held); err != nil {
|
||||
if !errors.Is(err, ErrUnreadable) {
|
||||
return applied, err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
@@ -147,3 +148,27 @@ func (c CatalogRead) graphAndModules() (graph.Graph, map[string]delivery.Module,
|
||||
}
|
||||
return g, mods, nil
|
||||
}
|
||||
|
||||
// CatalogTools are the seat mesh-catalog's four verbs as tools named <seat>.<verb>, with the descriptions and inputs the
|
||||
// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the
|
||||
// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not.
|
||||
func CatalogTools(c CatalogRead) []stdio.Tool {
|
||||
seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
|
||||
runs := map[string]func(map[string]any) (any, error){
|
||||
"modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() },
|
||||
"module": func(a map[string]any) (any, error) { return c.CatalogModuleAnswer(str(a, "module")) },
|
||||
"provides": func(a map[string]any) (any, error) { return c.CatalogProvidesAnswer(str(a, "provision")) },
|
||||
"dependents": func(a map[string]any) (any, error) { return c.CatalogDependentsAnswer(str(a, "module")) },
|
||||
}
|
||||
var out []stdio.Tool
|
||||
for _, v := range seat.Serves {
|
||||
if run, ok := runs[v.Name]; ok {
|
||||
out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -93,6 +93,19 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
|
||||
}
|
||||
return true
|
||||
}
|
||||
// The tools bundle serves the four verbs as mesh-catalog.<verb>, answering from the same states.
|
||||
var names []string
|
||||
for _, tool := range CatalogTools(c) {
|
||||
names = append(names, tool.Name)
|
||||
if tool.Name == "mesh-catalog.modules" {
|
||||
if got, err := tool.Run(nil); err != nil || !strings.Contains(string(answer(got, nil)), `"module":"avahi"`) {
|
||||
t.Errorf("mesh-catalog.modules answered %v, %v", got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !slices.Equal(names, []string{"mesh-catalog.modules", "mesh-catalog.module", "mesh-catalog.provides", "mesh-catalog.dependents"}) {
|
||||
t.Errorf("the tools bundle serves %v", names)
|
||||
}
|
||||
raw := answer(c.CatalogModulesAnswer())
|
||||
if got, want := keysAt(t, raw, "modules"), capturedKeys(t, "catalog_modules.json", "modules"); !contains(got, want) {
|
||||
t.Errorf("modules' fields %v, want the old catalog's %v", got, want)
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
// The module graph (novox/hq ADR 0603 §2): at every recompute the indexer writes, in its state graph
|
||||
// (graph.Bucket), one key per module it holds the inputs of, its build and provision edges as that recompute
|
||||
// computed them, and deletes the key of a module the modules state no longer holds. A key is written only when its value
|
||||
// (graph.Bucket), one key per module it holds the inputs of, its build, provision and verb edges as that recompute
|
||||
// computed them (ADR 0614 §1), the verb edges on a cycle left out over the whole graph (§3), and deletes the key of a module the modules state no longer holds. A key is written only when its value
|
||||
// changes, so its history of 5 is five changes of the module's edges. The delivery view, the checker and the seat
|
||||
// mesh-catalog's verbs read it; nothing stores an order (ADR 0603 §1).
|
||||
|
||||
@@ -89,13 +89,20 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges)
|
||||
}
|
||||
return out
|
||||
}
|
||||
// The verb edges on a cycle are left out over the whole graph (ADR 0614 §3), every module it knows, so a module past
|
||||
// GraphMost still closes the cycle it lies on.
|
||||
whole := graph.Graph{}
|
||||
for name, e := range edges {
|
||||
whole[name] = graphNode(e, providers)
|
||||
}
|
||||
whole.LeaveOut()
|
||||
for _, name := range names {
|
||||
key, err := graph.Key(name)
|
||||
if err != nil {
|
||||
ix.logf("graph.%s is not written: %v", name, err)
|
||||
continue
|
||||
}
|
||||
next := graphNode(edges[name], providers)
|
||||
next := whole[name]
|
||||
err = write(ctx, ix.Graph, key, func(value []byte, found bool) ([]byte, bool, error) {
|
||||
if found {
|
||||
if cur, err := graph.Decode(value); err == nil && graph.Same(cur, next) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-indexer/internal/fingerprint"
|
||||
@@ -95,6 +96,9 @@ func (ix *Indexer) applyMoving(ctx context.Context, f delivery.Forge, drop bool,
|
||||
}
|
||||
dirOf[name] = dir
|
||||
next := delivery.Module{Repository: f.Repository, Commit: f.Merge, Src: ix.srcOf(f.Repository, f.Merge), Manifest: digests[dir], Path: pathOf(dir)}
|
||||
// The reader capabilities a changed manifest needs are written with it, before the judge's pass names them with its
|
||||
// verdict, so no reader takes a new manifest as needing nothing (ADR 0614 §4).
|
||||
next.Needs, _ = catalogue.ManifestNeeds([]byte(raw))
|
||||
if next.Manifest == "" {
|
||||
next.None2 = "the manifest cannot be read: it cannot be kept as a value"
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
)
|
||||
@@ -413,6 +414,11 @@ func TestAMergeAddingAModuleDirectoryWritesItsValueWithNoBuildRecordAnywhere(t *
|
||||
if m.FP2 == "" && m.None2 == "" {
|
||||
t.Errorf("module = %+v, want a new-form fingerprint or its reason", m)
|
||||
}
|
||||
// ADR 0614 §4: the merge writes the reader capabilities its manifest needs, so no reader takes it as needing nothing
|
||||
// before the judge's pass.
|
||||
if want, _ := catalogue.ManifestNeeds([]byte(avahi)); len(m.Needs) == 0 || !slices.Equal(m.Needs, want) {
|
||||
t.Errorf("module needs %v, want %v", m.Needs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMergeWritesOnlyTheModulesItsManifestsName(t *testing.T) { // I1
|
||||
|
||||
@@ -31,7 +31,9 @@ type TakeInAsk struct {
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
// Judge asks the manifests to be judged again now (indexer_judge_again) instead of the module to be taken in.
|
||||
Judge bool `json:"judge,omitempty"`
|
||||
Judge bool `json:"judge,omitempty"`
|
||||
// Digest names, with Judge, the one manifest digest of the module to judge (indexer_judge with a digest).
|
||||
Digest string `json:"digest,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
Result *TakenIn `json:"result,omitempty"`
|
||||
|
||||
@@ -365,8 +365,9 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
|
||||
held := found && err == nil
|
||||
if held && cur.Manifest == next.Manifest {
|
||||
// The judge's verdict stands while the digest does (ADR 0578 §3, ADR 0579 §1): a recompute never writes a
|
||||
// refused or outgrown module as accepted; only the judge changes it.
|
||||
// refused or outgrown module as accepted; only the judge changes it. So do the reader capabilities it named.
|
||||
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
|
||||
next.Needs = cur.Needs
|
||||
}
|
||||
// The source revision at which FP2 last changed (ADR 0601 §1): this merge's when it moved it (0 until the
|
||||
// source revision naming the merge is heard, which stamps it), else the one held.
|
||||
@@ -401,6 +402,7 @@ func sameButWhere(cur, next delivery.Module) bool {
|
||||
next = withV(next)
|
||||
next.Commit, next.Src, next.FPSrc = cur.Commit, cur.Src, cur.FPSrc
|
||||
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
|
||||
next.Needs = cur.Needs
|
||||
return reflect.DeepEqual(cur, next)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -330,6 +331,24 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
|
||||
t.Errorf("%s moved with a build of build-agent: fp2 = %q, want %s", name, m.FP2, fp)
|
||||
}
|
||||
}
|
||||
// ADR 0614 §2, §3: the graph names what each module invokes and serves, and leaves out, over the whole graph, the
|
||||
// verb edges of two modules calling each other, from either side; a third calling one of them keeps its edge.
|
||||
invoking := func(name, invokes string) mod {
|
||||
raw := renamed(t, "avahi", name)
|
||||
return mod{name, strings.Replace(raw, `"version": "1",`, `"version": "1", "invokes": [`+invokes+`],`, 1)}
|
||||
}
|
||||
vx, _ := newFormIndexer(t, invoking("caller", `"callee.avahi_status"`), invoking("callee", `"caller.avahi_browse", "*"`),
|
||||
invoking("third", `"callee.avahi_status"`))
|
||||
for name, want := range map[string][2][]string{
|
||||
"caller": {{"callee.avahi_status"}, {"callee.avahi_status"}},
|
||||
"callee": {{"caller.avahi_browse"}, {"caller.avahi_browse"}},
|
||||
"third": {{"callee.avahi_status"}, nil},
|
||||
} {
|
||||
if g := graphOf(t, vx, name); !reflect.DeepEqual(g.Invokes, want[0]) || !reflect.DeepEqual(g.LeftOut, want[1]) ||
|
||||
!slices.Contains(g.Serves, name+".avahi_status") {
|
||||
t.Errorf("graph.%s = %+v, want it to invoke %v and leave out %v", name, g, want[0], want[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildHeardBeforeAnyToolchainIsReadDropsNoFingerprint(t *testing.T) { // 1727 (review: a restart flapped every FP2)
|
||||
|
||||
@@ -49,6 +49,14 @@ type Judged struct {
|
||||
// watchman raises as manifest-outgrown. A manifest in fresh (the digests a merge just took in) with no verdict yet, and
|
||||
// one already refused, is judged in full.
|
||||
func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, error) {
|
||||
return ix.judgeOver(ctx, fresh, nil)
|
||||
}
|
||||
|
||||
// judgeOver is JudgeAll; with over (module to manifest digest) it judges those digests alone, each in the catalogue the
|
||||
// modules state holds with its module's manifest replaced by it (a module the state does not hold added), and writes
|
||||
// their verdicts and nothing else: no event, no count of the old sections, and no module's value but one that names the
|
||||
// digest judged (carryVerdict). A digest of over with no verdict yet is judged in full, as a fresh one.
|
||||
func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, over map[string]string) (Judged, error) {
|
||||
// A merge, the start pass and indexer_judge may all judge at once: one pass at a time, so none writes a verdict from
|
||||
// modules another is rewriting.
|
||||
ix.judgeMu.Lock()
|
||||
@@ -61,6 +69,15 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if over != nil {
|
||||
fresh = map[string]bool{}
|
||||
for module, digest := range over {
|
||||
m := held[module]
|
||||
m.Manifest = digest
|
||||
held[module] = m
|
||||
fresh[digest] = true
|
||||
}
|
||||
}
|
||||
raws := map[string][]byte{}
|
||||
digests := map[string]string{}
|
||||
for _, name := range slices.Sorted(maps.Keys(held)) {
|
||||
@@ -99,12 +116,12 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
// release's Approve; ADR 0578 §1): it is refused only when this program is the newest build of the indexer the trunk
|
||||
// holds, so no judge that could know the field is still to come.
|
||||
newest := ix.ownBuildIsNewest(held)
|
||||
id := ix.JudgeID
|
||||
if id == "" {
|
||||
id = judge.Identity()
|
||||
}
|
||||
id := ix.judgeID()
|
||||
var accepted []catalogue.Manifest
|
||||
for _, name := range slices.Sorted(maps.Keys(digests)) {
|
||||
if _, judged := over[name]; over != nil && !judged {
|
||||
continue
|
||||
}
|
||||
out.Judged++
|
||||
v := delivery.ManifestVerdict{Module: name, State: delivery.VerdictAccepted, Judge: id, At: ix.now()}
|
||||
if fields := waits[name]; len(fields) > 0 {
|
||||
@@ -143,7 +160,10 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
ix.logf("judge: the verdict on %s (digest %s) is not written: %v", name, digests[name], err)
|
||||
continue
|
||||
}
|
||||
if wrote {
|
||||
if wrote && over != nil {
|
||||
out.Written++
|
||||
ix.logf("judge: %s judged again by %s: %s", digests[name], id, v.State)
|
||||
} else if wrote {
|
||||
out.Written++
|
||||
if v.Waits() {
|
||||
ix.logf("judge: %s", v.Why())
|
||||
@@ -160,10 +180,15 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ix.carryVerdict(ctx, name, digests[name], v); err != nil {
|
||||
// The reader capabilities the manifest's own words, forms and seat names need, named with the verdict (ADR 0614 §4).
|
||||
needs, _ := catalogue.ManifestNeeds(raws[name])
|
||||
if err := ix.carryVerdict(ctx, name, digests[name], v, needs); err != nil {
|
||||
ix.logf("judge: modules.%s does not carry its verdict: %v", name, err)
|
||||
}
|
||||
}
|
||||
if over != nil {
|
||||
return out, nil
|
||||
}
|
||||
if err := ix.keepOldSections(ctx, judge.CountOldSections(accepted)); err != nil {
|
||||
ix.logf("judge: the count of the old sections is not kept: %v", err)
|
||||
}
|
||||
@@ -171,14 +196,86 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
|
||||
}
|
||||
|
||||
// JudgeAtStart is JudgeAll of every manifest held, none fresh, as the indexer starts (a judge that changed judges every
|
||||
// one again); one that fails is judged again at the next message or source revision heard (kickJudge).
|
||||
// one again); one that fails is judged again at the next message or source revision heard (kickJudge). Then every other
|
||||
// digest of the verdicts state that another judge refused or held waiting (one of a candidate, or one back on the trunk
|
||||
// later) is judged again by this judge (judgeStale), so a refusal an older judge made never stands for good.
|
||||
func (ix *Indexer) JudgeAtStart(ctx context.Context) (Judged, error) {
|
||||
ix.learnOwnBuild(ctx)
|
||||
j, err := ix.JudgeAll(ctx, nil)
|
||||
ix.judgePending.Store(err != nil)
|
||||
if err == nil {
|
||||
ix.judgeStale(ctx)
|
||||
}
|
||||
return j, err
|
||||
}
|
||||
|
||||
// judgeID is the judge a verdict of this program names.
|
||||
func (ix *Indexer) judgeID() string {
|
||||
if ix.JudgeID != "" {
|
||||
return ix.JudgeID
|
||||
}
|
||||
return judge.Identity()
|
||||
}
|
||||
|
||||
// staleVerdict says whether the digest's verdict wants this judge's judgement: there is none, or another judge refused
|
||||
// it or held it waiting. An accepted verdict stays accepted under any later judge (ADR 0579 §1), so judging it again
|
||||
// changes nothing.
|
||||
func (ix *Indexer) staleVerdict(ctx context.Context, digest string) bool {
|
||||
value, _, found, err := ix.Verdicts.Get(ctx, digest)
|
||||
if err != nil {
|
||||
return false // unread: left, said by the next pass that reads it
|
||||
}
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
v, err := delivery.DecodeManifestVerdict(value)
|
||||
return err != nil || (v.Held() && v.Judge != ix.judgeID())
|
||||
}
|
||||
|
||||
// judgeDigests judges each digest of over (module to digest) whose verdict is stale (staleVerdict), one at a time in the
|
||||
// catalogue the modules state holds; a pass that fails is said and the others go on.
|
||||
func (ix *Indexer) judgeDigests(ctx context.Context, over map[string]string, why string) {
|
||||
if ix.Verdicts == nil || ix.Manifests == nil || ix.Modules == nil {
|
||||
return
|
||||
}
|
||||
for _, module := range slices.Sorted(maps.Keys(over)) {
|
||||
if !ix.staleVerdict(ctx, over[module]) {
|
||||
continue
|
||||
}
|
||||
if _, err := ix.judgeOver(ctx, nil, map[string]string{module: over[module]}); err != nil {
|
||||
ix.logf("judge: the manifest %s of %s (%s) is not judged: %v", over[module], module, why, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// judgeStale judges again every digest of the verdicts state another judge refused or held waiting, each as a manifest
|
||||
// of the module its verdict names (judgeDigests). Never on a timer: at the start pass.
|
||||
func (ix *Indexer) judgeStale(ctx context.Context) {
|
||||
if ix.Verdicts == nil {
|
||||
return
|
||||
}
|
||||
keys, err := ix.Verdicts.Keys(ctx)
|
||||
if err != nil {
|
||||
ix.logf("judge: the verdicts other judges made are not judged again: listing the verdicts: %v", err)
|
||||
return
|
||||
}
|
||||
byModule := map[string][]string{}
|
||||
for _, digest := range keys {
|
||||
value, _, found, err := ix.Verdicts.Get(ctx, digest)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
if v, err := delivery.DecodeManifestVerdict(value); err == nil && v.Held() && v.Judge != ix.judgeID() {
|
||||
byModule[v.Module] = append(byModule[v.Module], digest)
|
||||
}
|
||||
}
|
||||
for _, module := range slices.Sorted(maps.Keys(byModule)) {
|
||||
for _, digest := range byModule[module] {
|
||||
ix.judgeDigests(ctx, map[string]string{module: digest}, "made by another judge")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kickJudge judges again a start pass that failed, when the indexer hears something: never on a timer.
|
||||
func (ix *Indexer) kickJudge(ctx context.Context) {
|
||||
if !ix.judgePending.Load() {
|
||||
@@ -257,9 +354,10 @@ func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool {
|
||||
}
|
||||
|
||||
// carryVerdict puts a refusal into the module's value, and takes one out once the manifest is accepted, while the value
|
||||
// still names the digest judged; an accepted module's value carries nothing, so judging writes no value of a module the
|
||||
// still names the digest judged, and the reader capabilities the manifest needs beside it (needs, ADR 0614 §4); an
|
||||
// accepted module's value carries no verdict, so judging writes no value of a module the
|
||||
// judge accepts (a reader takes a value without a verdict as accepted, delivery.TakeManifestVerdict).
|
||||
func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v delivery.ManifestVerdict) error {
|
||||
func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v delivery.ManifestVerdict, needs []string) error {
|
||||
key, err := delivery.ModulesKey(name)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -280,10 +378,11 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli
|
||||
case v.Refused():
|
||||
state = delivery.VerdictRefused
|
||||
}
|
||||
if cur.Verdict == state && slices.Equal(cur.Problems, problems) && slices.Equal(cur.Unknown, v.Unknown) && slices.Equal(cur.Warnings, v.Warnings) {
|
||||
if cur.Verdict == state && slices.Equal(cur.Problems, problems) && slices.Equal(cur.Unknown, v.Unknown) && slices.Equal(cur.Warnings, v.Warnings) &&
|
||||
slices.Equal(cur.Needs, needs) {
|
||||
return nil, false, nil
|
||||
}
|
||||
cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings = state, problems, v.Unknown, v.Warnings
|
||||
cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings, cur.Needs = state, problems, v.Unknown, v.Warnings, needs
|
||||
if state == "" {
|
||||
cur.Problems = nil
|
||||
}
|
||||
@@ -370,26 +469,55 @@ func ListVerdicts(keys []string, get func(key string) ([]byte, error), module st
|
||||
|
||||
// JudgeAgain judges the manifests again now, as at the start, and answers how the module's stands: the tool
|
||||
// indexer_judge. A module the modules state does not hold, or whose manifest is not in the manifests state, is
|
||||
// answered so.
|
||||
func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn {
|
||||
// answered so. With a digest it judges that manifest alone, in the catalogue the trunk holds (judgeOver), whoever made
|
||||
// its verdict: a candidate's, or one the trunk names again, refused by an older judge.
|
||||
func (ix *Indexer) JudgeAgain(ctx context.Context, module, digest string) TakenIn {
|
||||
out := TakenIn{Module: module}
|
||||
held, err := ix.heldModules(ctx)
|
||||
if err != nil {
|
||||
out.Answer = "the modules state cannot be read: " + err.Error()
|
||||
return out
|
||||
}
|
||||
// The build this program runs is asked again, so a start pass that missed the engine's answer is recovered without a restart.
|
||||
ix.learnOwnBuild(ctx)
|
||||
if digest != "" {
|
||||
// One digest (a candidate's, or one the trunk no longer names), judged by this judge in the catalogue the trunk holds.
|
||||
value, _, found, err := ix.Manifests.Get(ctx, digest)
|
||||
kept, derr := delivery.DecodeManifest(value)
|
||||
switch {
|
||||
case err != nil || !found:
|
||||
out.Answer = "the indexer keeps no manifest of digest " + digest
|
||||
return out
|
||||
case derr != nil:
|
||||
out.Answer = "the manifest of digest " + digest + " cannot be read: " + derr.Error()
|
||||
return out
|
||||
case module != "" && kept.Module != module:
|
||||
out.Answer = "the manifest of digest " + digest + " is " + kept.Module + "'s, not " + module + "'s"
|
||||
return out
|
||||
}
|
||||
out.Module = kept.Module
|
||||
if _, err := ix.judgeOver(ctx, nil, map[string]string{kept.Module: digest}); err != nil {
|
||||
out.Answer = "the manifest could not be judged: " + err.Error()
|
||||
return out
|
||||
}
|
||||
return ix.answerVerdict(ctx, out, digest)
|
||||
}
|
||||
m, ok := held[module]
|
||||
if !ok || m.Manifest == "" {
|
||||
out.Answer = "the indexer holds no manifest of " + module
|
||||
return out
|
||||
}
|
||||
// The build this program runs is asked again, so a start pass that missed the engine's answer is recovered without a restart.
|
||||
ix.learnOwnBuild(ctx)
|
||||
if _, err := ix.JudgeAll(ctx, nil); err != nil {
|
||||
out.Answer = "the manifests could not be judged: " + err.Error()
|
||||
return out
|
||||
}
|
||||
value, _, found, err := ix.Verdicts.Get(ctx, m.Manifest)
|
||||
return ix.answerVerdict(ctx, out, m.Manifest)
|
||||
}
|
||||
|
||||
// answerVerdict is out answering how the verdict under digest stands.
|
||||
func (ix *Indexer) answerVerdict(ctx context.Context, out TakenIn, digest string) TakenIn {
|
||||
module := out.Module
|
||||
value, _, found, err := ix.Verdicts.Get(ctx, digest)
|
||||
if err != nil || !found {
|
||||
out.Answer = "judged, but the verdict on " + module + " cannot be read"
|
||||
return out
|
||||
@@ -407,8 +535,14 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn {
|
||||
return out
|
||||
}
|
||||
|
||||
// NewJudgeAgain is the value indexer_judge writes for a module: its key in the takeins state and the value.
|
||||
func NewJudgeAgain(module, why, by string, at time.Time) (string, []byte, error) {
|
||||
// NewJudgeAgain is the value indexer_judge writes for a module, and one digest of its manifest when given: its key in
|
||||
// the takeins state and the value.
|
||||
func NewJudgeAgain(module, digest, why, by string, at time.Time) (string, []byte, error) {
|
||||
if digest != "" {
|
||||
if _, err := delivery.ManifestsKey(digest); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
key, data, err := NewTakeIn(module, why, by, at)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
@@ -417,7 +551,7 @@ func NewJudgeAgain(module, why, by string, at time.Time) (string, []byte, error)
|
||||
if err := json.Unmarshal(data, &a); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
a.Judge = true
|
||||
a.Judge, a.Digest = true, digest
|
||||
data, err = json.Marshal(a)
|
||||
return judgeAgainPrefix + key, data, err
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -113,6 +115,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
// A merge's write of a module whose digest it leaves as it is carries the verdict over, and nothing is fresh; a changed
|
||||
// digest is fresh, judged in full (review R1, R2).
|
||||
w := &mergeWrite{}
|
||||
oldNeeds := module("old").Needs
|
||||
same := delivery.Module{FP2: "src2:" + strings.Repeat("c", 64), Manifest: changed, Repository: "novox/mesh-catalog", Commit: "c2"}
|
||||
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -120,6 +123,9 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
if m := module("old"); m.Verdict != delivery.VerdictRefused || len(m.Problems) == 0 || len(w.fresh) != 0 {
|
||||
t.Fatalf("a merge rewrote a refused module as %+v, fresh %v", m, w.fresh)
|
||||
}
|
||||
if got := module("old").Needs; len(oldNeeds) == 0 || !slices.Equal(got, oldNeeds) {
|
||||
t.Fatalf("a merge leaving the digest dropped the reader capabilities the judge named: %v, held %v", got, oldNeeds)
|
||||
}
|
||||
other := strings.Repeat("d", 64)
|
||||
same.Manifest = other
|
||||
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil || !w.fresh[other] {
|
||||
@@ -137,6 +143,15 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// ADR 0614 §4: the judge names, with the verdict, the reader capabilities the manifest's own words need: the bus-state
|
||||
// words, which an engine stating nothing (the baseline) lacks; a manifest of the baseline's words lacks none.
|
||||
if needs := module("moved").Needs; !slices.Contains(needs, "word:bus-state.storage") ||
|
||||
!slices.Contains(catalogue.MissingManifestReads(needs, nil), "word:bus-state") {
|
||||
t.Fatalf("modules.moved needs %v", needs)
|
||||
}
|
||||
if needs := module("good").Needs; !slices.Contains(needs, "word:module") || len(catalogue.MissingManifestReads(needs, nil)) != 0 {
|
||||
t.Fatalf("modules.good needs %v", needs)
|
||||
}
|
||||
if len(accepted) != 1 || accepted[0].Module != "moved" || accepted[0].Digest != moved || accepted[0].Judge != "judge-2" ||
|
||||
len(accepted[0].BusState) != 2 || accepted[0].BusState[0].Name != "cache" || accepted[0].BusState[1].Template != "log" ||
|
||||
string(accepted[0].BusState[0].Options["history"]) != "8" {
|
||||
@@ -261,25 +276,66 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
// the indexer is the newer one, so the field waits; asked and answered with that same build it is refused).
|
||||
ix.ownFP = ""
|
||||
ix.OwnBuild = func(context.Context) (string, error) { return "src2:" + strings.Repeat("a", 64), nil }
|
||||
if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "refused") || ix.ownFP == "" {
|
||||
if got := ix.JudgeAgain(ctx, "later", ""); !got.Stored || !strings.Contains(got.Answer, "refused") || ix.ownFP == "" {
|
||||
t.Fatalf("indexer_judge did not learn the running build: %+v", got)
|
||||
}
|
||||
ix.ownFP = ownFP
|
||||
ix.OwnBuild = nil
|
||||
if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") {
|
||||
if got := ix.JudgeAgain(ctx, "later", ""); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") {
|
||||
t.Fatalf("indexer_judge: %+v", got)
|
||||
}
|
||||
if rows, _ := ListVerdicts(keys, get, "later", false); len(rows) != 1 {
|
||||
t.Fatalf("indexer_verdicts of later after: %+v", rows)
|
||||
}
|
||||
if got := ix.JudgeAgain(ctx, "nobody"); got.Stored || !strings.Contains(got.Answer, "holds no manifest") {
|
||||
if got := ix.JudgeAgain(ctx, "nobody", ""); got.Stored || !strings.Contains(got.Answer, "holds no manifest") {
|
||||
t.Fatalf("indexer_judge of nobody: %+v", got)
|
||||
}
|
||||
if key, value, err := NewJudgeAgain("later", "why", "me", time.Now()); err != nil || key != "judge.later" {
|
||||
if key, value, err := NewJudgeAgain("later", "", "why", "me", time.Now()); err != nil || key != "judge.later" {
|
||||
t.Fatalf("the ask: %q %v", key, err)
|
||||
} else if a, err := DecodeTakeIn(value); err != nil || !a.Judge || a.Module != "later" {
|
||||
t.Fatalf("the ask's value: %+v %v", a, err)
|
||||
}
|
||||
// A refusal an older judge made never stands: a digest the trunk no longer names (a candidate's, or one back on the
|
||||
// trunk later) refused by judge-0 for words it did not know is judged again by this judge at the start, at a pull that
|
||||
// carries it, and by indexer_judge with its digest; one this judge refused itself is left as it is.
|
||||
stale := func(module, raw string) string {
|
||||
digest, err := ix.keepManifest(ctx, module, raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ix.writeVerdict(ctx, digest, delivery.ManifestVerdict{Module: module, State: delivery.VerdictRefused,
|
||||
Problems: []string{module + " says tool-runner, which this judge does not know"}, Judge: "judge-0", At: time.Now()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return digest
|
||||
}
|
||||
atStart := stale("tools", `{"module":"tools","version":"7"}`)
|
||||
ownRefusal := stale("client", `{"module":"client","version":"7"}`)
|
||||
if _, err := ix.writeVerdict(ctx, ownRefusal, delivery.ManifestVerdict{Module: "client", State: delivery.VerdictRefused,
|
||||
Problems: []string{"x"}, Judge: ix.judgeID(), At: time.Now()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ix.JudgeAtStart(ctx); err != nil || verdict(atStart).Refused() || verdict(atStart).Judge != ix.judgeID() || !verdict(ownRefusal).Refused() {
|
||||
t.Fatalf("the start pass: %+v, this judge's own refusal %+v (%v)", verdict(atStart), verdict(ownRefusal), err)
|
||||
}
|
||||
byPull := stale("tools", `{"module":"tools","version":"8"}`)
|
||||
pull := delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main",
|
||||
Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8"}`}}
|
||||
if _, err := ix.ApplyMessage(ctx, pull); err != nil || verdict(byPull).Refused() || verdict(byPull).Judge != ix.judgeID() {
|
||||
t.Fatalf("a pull carrying a digest an older judge refused: %+v %v", verdict(byPull), err)
|
||||
}
|
||||
byTool := stale("tools", `{"module":"tools","version":"9"}`)
|
||||
if got := ix.JudgeAgain(ctx, "client", byTool); got.Stored || !strings.Contains(got.Answer, "tools's, not client's") || !verdict(byTool).Refused() {
|
||||
t.Fatalf("indexer_judge with another module's digest: %+v", got)
|
||||
}
|
||||
if got := ix.JudgeAgain(ctx, "tools", byTool); !got.Stored || !strings.Contains(got.Answer, "accepted") || verdict(byTool).Refused() {
|
||||
t.Fatalf("indexer_judge with a digest: %+v %+v", got, verdict(byTool))
|
||||
}
|
||||
if key, value, err := NewJudgeAgain("tools", byTool, "why", "me", time.Now()); err != nil || key != "judge.tools" {
|
||||
t.Fatalf("the ask with a digest: %q %v", key, err)
|
||||
} else if a, err := DecodeTakeIn(value); err != nil || a.Digest != byTool {
|
||||
t.Fatalf("the ask's digest: %+v %v", a, err)
|
||||
}
|
||||
// Without the verdicts state the indexer judges nothing.
|
||||
ix.Verdicts = nil
|
||||
if j, err := ix.JudgeAll(ctx, nil); err != nil || j.Judged != 0 {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
json "encoding/json"
|
||||
errors "errors"
|
||||
fmt "fmt"
|
||||
toolrunner "git.novox.be/novox/mesh-delivery/sdk/toolrunner"
|
||||
catalogue "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
math "math"
|
||||
net "net"
|
||||
@@ -137,7 +136,7 @@ func spokenSystems() string {
|
||||
// and was simply absent — seven modules' tools went missing that way on 2026-10-03. Refused here,
|
||||
// naming the field that would deliver it.
|
||||
func undeliveredBundles(m catalogue.Manifest) []string {
|
||||
if m.Build == nil || toolrunner.Is(m) {
|
||||
if m.Build == nil || catalogue.IsToolRunner(m) {
|
||||
return nil
|
||||
}
|
||||
named := runByAResource(m)
|
||||
@@ -3307,7 +3306,7 @@ func waitProblems(m catalogue.Manifest) []string {
|
||||
if len(m.Waits) == 0 {
|
||||
return nil
|
||||
}
|
||||
if !toolrunner.Is(m) {
|
||||
if !catalogue.IsToolRunner(m) {
|
||||
return []string{fmt.Sprintf("%s says what a session waits on; only the tool runner's module (it says "+
|
||||
"\"tool-runner\": true) declares waits (novox/hq ADR 0367 §2)", m.Module)}
|
||||
}
|
||||
|
||||
@@ -163,18 +163,11 @@ func TestABundleNothingDeliversIsRefused(t *testing.T) {
|
||||
t.Errorf("a bundle delivered by %s was refused: %v", name, p)
|
||||
}
|
||||
}
|
||||
// The tool runner is what says it is one (issue 774), and, while node-tools' trunk manifest leaves `tool-runner` out
|
||||
// for the running indexer, mesh-sdk's deprecated catalogue.RuntimeModule too (issue 1585 note 42): any other name alone
|
||||
// exempts nothing.
|
||||
kept := base()
|
||||
kept.Module = catalogue.RuntimeModule
|
||||
if p := undeliveredBundles(kept); len(p) != 0 {
|
||||
t.Errorf("a bundle of the kept runtime module, which does not say it is the tool runner, was refused: %v", p)
|
||||
}
|
||||
// The tool runner is what says it is one (issue 774): its name alone exempts nothing.
|
||||
named := base()
|
||||
named.Module = "agent-tools"
|
||||
named.Module = "node-tools"
|
||||
if p := undeliveredBundles(named); len(p) != 1 {
|
||||
t.Errorf("a bundle of a module that does not say it is the tool runner: %v", p)
|
||||
t.Errorf("a bundle of a module named as the tool runner, which does not say it is one: %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3122,7 +3115,7 @@ func TestAVerbWithAnUnknownKeyIsRefused(t *testing.T) {
|
||||
func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) {
|
||||
for manifest, want := range map[string]string{
|
||||
`{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module",
|
||||
`{"module":"agent-tools","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774)
|
||||
`{"module":"node-tools","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774)
|
||||
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.*"]}`: "never a wildcard",
|
||||
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["**"]}`: "never a wildcard",
|
||||
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.>"]}`: "never a wildcard",
|
||||
@@ -3141,11 +3134,6 @@ func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) {
|
||||
t.Errorf("%s refused for the wrong reason (want %q): %v", manifest, want, err)
|
||||
}
|
||||
}
|
||||
// mesh-sdk's deprecated catalogue.RuntimeModule waits without saying `tool-runner`, while node-tools' trunk manifest
|
||||
// leaves the word out for the running indexer (issue 774, issue 1585 note 42).
|
||||
if _, err := ParseManifest([]byte(`{"module":"` + catalogue.RuntimeModule + `","version":"1","waits":["mesh-knowledge.reviewed"]}`)); err != nil {
|
||||
t.Errorf("the kept runtime module's waits were refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1178/AC2: a work-queue stream is read by exactly one process of its module and by no other module's consumes: one
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
@@ -38,9 +39,14 @@ type artifact struct {
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Slug string `json:"slug"`
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Slug string `json:"slug"`
|
||||
Claims []struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
Serves []string `json:"serves"`
|
||||
} `json:"claims"`
|
||||
Tools []string `json:"tools"`
|
||||
Emits []string `json:"emits"`
|
||||
Consumes []string `json:"consumes"`
|
||||
@@ -122,8 +128,14 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
if want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// No streams, no bus, no seat (ADR 0437 §4); it says no event.
|
||||
for _, k := range []string{"streams", "logs", "claims", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} {
|
||||
// ADR 0604 §2: it holds the seat mesh-catalog, serving its four verbs from its tools bundle (which stays without a
|
||||
// request subject).
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != catalogue.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
|
||||
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents"}) {
|
||||
t.Errorf("claims = %+v, want mesh-catalog with its four verbs", m.Claims)
|
||||
}
|
||||
// No streams, no bus, no other seat (ADR 0437 §4); it says no event.
|
||||
for _, k := range []string{"streams", "logs", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} {
|
||||
if _, ok := top[k]; ok {
|
||||
t.Errorf("the manifest declares %q: the indexer has none", k)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
"module": "mesh-delivery-indexer",
|
||||
"version": "1",
|
||||
"slug": "indexer",
|
||||
"claims": [
|
||||
{
|
||||
"name": "mesh-catalog",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"modules",
|
||||
"module",
|
||||
"provides",
|
||||
"dependents"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
"indexer_take_in",
|
||||
"indexer_old_sections",
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-teller
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -48,9 +48,11 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
|
||||
}},
|
||||
{Name: view.TiersTool,
|
||||
Description: "A source revision's tiers, worked out now and never stored (novox/hq ADR 0603 §6): the modules the revision moves " +
|
||||
"(each whose fingerprint last changed at it), ordered by the module graph (a module after the bases and the providers " +
|
||||
"of the provisions it awaits among them), and for each module what it waits on now and why: its build key, on each node " +
|
||||
"that wants it its want (the assigner's rule and words when held back), its have, and each provision it awaits. Nodes " +
|
||||
"(each whose fingerprint last changed at it), ordered by the module graph (a module after the bases, the providers " +
|
||||
"of the provisions and bus state it awaits and the modules serving the verbs it invokes, among them; novox/hq ADR 0614), " +
|
||||
"and for each module what it waits on now and why, named by its edge: its build key, on each node that wants it its want " +
|
||||
"(the assigner's rule and words when held back: a verb no running build serves, a reader capability the node-engine " +
|
||||
"does not state), its have, each provision and its bus state; and the verb edges the graph leaves out on a cycle. Nodes " +
|
||||
"the watchman says are offline are named away and left out. At most 500 modules, said when cut short; said when the " +
|
||||
"module graph cannot be read. Give revision for an older one, none for the gate's newest. Read-only.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
|
||||
@@ -26,6 +26,9 @@ func encoded(t *testing.T, v []byte, err error) string {
|
||||
return string(v)
|
||||
}
|
||||
|
||||
// short is a fingerprint as the assigner's words shorten it.
|
||||
func short(fp string) string { return fp[:len("src2:")+12] }
|
||||
|
||||
// tiersToolSaysEachModulesWait: view_tiers orders a source revision's modules by the module graph and names each one's
|
||||
// wait (ADR 0603 §6): a consumer after its provider, held by the assigner's provision rule and its provision not provided;
|
||||
// a module whose build waits for a package; a provider with no wait; a node offline named away; older modules left out.
|
||||
@@ -104,6 +107,32 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
|
||||
if out, err := run(t, r, view.TiersTool, map[string]any{}); err != nil || out.(view.Tiers).Revision != 9 || out.(view.Tiers).Modules != 3 {
|
||||
t.Errorf("view_tiers of the gate's newest = %+v, %v", out, err)
|
||||
}
|
||||
// ADR 0614 §2-§5: a want held on a verb, a reader capability or bus state is named by its edge; bus state is no
|
||||
// provisioner key (none is ever made for it), so a module asking it shows no provision wait of it; and each verb edge
|
||||
// the graph leaves out on a cycle is named.
|
||||
r.GraphState = fake{
|
||||
"db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}),
|
||||
"web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database", graph.BusState}, Awaits: []string{"database", graph.BusState},
|
||||
Invokes: []string{"seat:x.v", "db.loop"}, LeftOut: []string{"db.loop"}}),
|
||||
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
|
||||
}.reader().State
|
||||
for words, edge := range map[string]string{
|
||||
graph.VerbWaitWords("seat:x.v", "web", "ace"): "verb seat:x.v",
|
||||
graph.ReaderWaitWords("ace", []string{"tool-runner"}, "web", short(fp("b"))): "reader capability tool-runner",
|
||||
"the provision ace.web.database of web on ace is asked, not provided": "want",
|
||||
} {
|
||||
r.WantsState = fake{key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: words})}.reader().State
|
||||
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
|
||||
got, _ := out.(view.Tiers)
|
||||
if err != nil || len(got.Tiers) != 2 {
|
||||
t.Fatalf("view_tiers 9 = %+v, %v", out, err)
|
||||
}
|
||||
web := got.Tiers[1][0]
|
||||
if len(web.Waits) != 2 || web.Waits[0].Edge != edge || web.Waits[1].Edge != "provision database" ||
|
||||
!reflect.DeepEqual(web.LeftOut, []string{"db.loop"}) {
|
||||
t.Errorf("web held by %q = %+v, want its want named %s, no wait for a bus-state key and db.loop left out", words, web, edge)
|
||||
}
|
||||
}
|
||||
// The graph not readable: every module in the first tier, said; past 500 modules the answer says it is cut short.
|
||||
r.GraphState = view.StateRead{Keys: func() ([]string, error) { return nil, errors.New("no bucket") }, Get: r.GraphState.Get}
|
||||
if out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || len(out.(view.Tiers).Tiers) != 1 ||
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-view
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -16,9 +16,11 @@ import (
|
||||
)
|
||||
|
||||
// The tiers of a source revision are a view, worked out at each call and never stored (novox/hq ADR 0603 §1, §6): the
|
||||
// modules the revision moves, ordered by the module graph's ordering edges among them (a base, a provision awaited from
|
||||
// another of them), and for each module what it waits on now: its build key, each node's want and have, and each
|
||||
// provision it awaits on each node, with the rule and the words of the part that holds it. Nothing in the mesh waits on
|
||||
// modules the revision moves, ordered by the module graph's ordering edges among them (a base, a provision or bus state
|
||||
// awaited from another of them, a verb another of them serves whose edge the graph keeps, ADR 0614 §1), and for each module
|
||||
// what it waits on now: its build key, each node's want (named by its edge: a verb, a reader capability) and have, and
|
||||
// each provision it awaits on each node, with the rule and the words of the part that holds it, and the verb edges the
|
||||
// graph leaves out. Nothing in the mesh waits on
|
||||
// a tier; the tiers say where the revision stands. A node the watchman says is offline is named away and left out of
|
||||
// what the revision waits on (ADR 0603 §7).
|
||||
|
||||
@@ -43,18 +45,22 @@ type Tiers struct {
|
||||
Away []string `json:"away,omitempty"`
|
||||
}
|
||||
|
||||
// TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), and each
|
||||
// wait of its own now; no wait is a module that reached every node present.
|
||||
// TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), each
|
||||
// wait of its own now (no wait is a module that reached every node present), and the verbs it invokes whose edge the
|
||||
// module graph leaves out on a cycle, so nothing waits on them (LeftOut, ADR 0614 §3).
|
||||
type TierModule struct {
|
||||
Module string `json:"module"`
|
||||
FP string `json:"fp,omitempty"`
|
||||
None string `json:"none,omitempty"`
|
||||
After []string `json:"after,omitempty"`
|
||||
Waits []TierWait `json:"waits,omitempty"`
|
||||
Module string `json:"module"`
|
||||
FP string `json:"fp,omitempty"`
|
||||
None string `json:"none,omitempty"`
|
||||
After []string `json:"after,omitempty"`
|
||||
Waits []TierWait `json:"waits,omitempty"`
|
||||
LeftOut []string `json:"left-out,omitempty"`
|
||||
}
|
||||
|
||||
// TierWait is one thing a module waits on: the edge (build, want, have, or provision <name>), the node it waits on (none
|
||||
// for its build), the assigner's rule for a want held back, the part's words and since when, where the part says it.
|
||||
// TierWait is one thing a module waits on: the edge (build, have, provision <name>, or for a want held back want, verb
|
||||
// <verb>, reader capability <capabilities> or bus state as the assigner's words name it, ADR 0614 §2, §4, §5), the node
|
||||
// it waits on (none for its build), the assigner's rule for a want held back, the part's words and since when, where the
|
||||
// part says it.
|
||||
type TierWait struct {
|
||||
Edge string `json:"edge"`
|
||||
Node string `json:"node,omitempty"`
|
||||
@@ -169,7 +175,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
|
||||
out.Tiers = append(out.Tiers, nil)
|
||||
}
|
||||
m := moved[n]
|
||||
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]}
|
||||
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n], LeftOut: gr[n].LeftOut}
|
||||
tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away)
|
||||
out.Tiers[t] = append(out.Tiers[t], tm)
|
||||
}
|
||||
@@ -234,11 +240,20 @@ func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, p
|
||||
if words == "" {
|
||||
words = "the want names " + short(w.FP) + ", not " + short(m.FP2)
|
||||
}
|
||||
out = append(out, TierWait{Edge: "want", Node: node, Rule: w.Rule, Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))})
|
||||
edge := graph.EdgeOfWords(w.Words)
|
||||
if edge == "" {
|
||||
edge = "want"
|
||||
}
|
||||
out = append(out, TierWait{Edge: edge, Node: node, Rule: w.Rule, Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))})
|
||||
} else if hw, ok := r.haveWait(node, module, m.FP2); ok {
|
||||
out = append(out, hw)
|
||||
}
|
||||
for _, a := range g.Awaits {
|
||||
if a == graph.BusState {
|
||||
// Bus state is offered with no credential of a consumer's own, so the provisioner keys nothing for it: its wait is
|
||||
// the assigner's held want alone, named bus state from its words (ADR 0614 §5).
|
||||
continue
|
||||
}
|
||||
out = append(out, provisionWaits(node, module, a, provided, perr)...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-watchman
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-enrolment
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -93,6 +93,8 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw})
|
||||
put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest,
|
||||
Repository: "novox/x", Commit: "c"})
|
||||
verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: module, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()})
|
||||
put(grants.ReadVerdicts, digest, json.RawMessage(verdict))
|
||||
}
|
||||
manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"}}`)
|
||||
manifest("mod-c", `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["nats.done"]}`)
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-grants
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
@@ -20,7 +20,3 @@ require (
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -154,6 +154,12 @@ func (b *fakeBus) put(state, key string, v any) {
|
||||
b.states[state] = map[string][]byte{}
|
||||
}
|
||||
b.states[state][key] = raw
|
||||
// A modules value that carries no verdict is accepted by the indexer's verdict on its digest, as the indexer writes it;
|
||||
// a row that wants the window before the judge deletes that key.
|
||||
if m, ok := v.(delivery.Module); ok && state == grants.ReadModules && m.Verdict == "" && m.Manifest != "" {
|
||||
verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: key, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()})
|
||||
b.put(grants.ReadVerdicts, m.Manifest, json.RawMessage(verdict))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *fakeBus) Keys(state string) ([]string, error) {
|
||||
@@ -2822,7 +2828,8 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("%s: the kept user was not said: %+v", held.name, last)
|
||||
}
|
||||
}
|
||||
// A held module that runs no build the part can read declares nothing to anyone: the list in force is kept, said.
|
||||
// A held module that runs no build the part can read declares nothing to anyone: only its own users stay as the list in
|
||||
// force holds them, said unreadable; the others move (ADR 0614 §8).
|
||||
delete(b.states[grants.ReadHave], "b.mod-r")
|
||||
b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
|
||||
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
|
||||
@@ -2831,8 +2838,28 @@ func TestGrants(t *testing.T) {
|
||||
wasList, _, _ := p.Sealed()
|
||||
res := p.Compose()
|
||||
nowList, _, _ := p.Sealed()
|
||||
if nowList.Digest != wasList.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "running manifest of mod-r") {
|
||||
t.Errorf("a held module with no running manifest did not keep the list in force: %+v", res.Sealed)
|
||||
if nowList.Digest == wasList.Digest || slices.ContainsFunc(res.Errors, func(e string) bool { return strings.Contains(e, "composed whole") }) {
|
||||
t.Errorf("a module with no readable manifest held the whole list: %+v", res.Sealed)
|
||||
}
|
||||
if kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(nowList.Digest, "b.mod-r.mod-r-tools")); !ok || string(kept) != string(before) {
|
||||
t.Errorf("b.mod-r.mod-r-tools did not keep its grants of the list in force: %s", kept)
|
||||
}
|
||||
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "the manifest of mod-r is unreadable") {
|
||||
t.Errorf("the unreadable manifest was not said: %+v", last)
|
||||
}
|
||||
// The window before the judge: the indexer wrote a changed manifest's value and no verdict on its digest yet. Only that
|
||||
// module's users stay (named unreadable); the others move.
|
||||
b.manifestOf("mod-r", ctl)
|
||||
delete(b.states[grants.ReadVerdicts], delivery.ManifestDigest([]byte(ctl)))
|
||||
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen11", "generation": 11, "fingerprint": "sha256:11"})
|
||||
wasList, _, _ = p.Sealed()
|
||||
res = p.Compose()
|
||||
nowList, _, _ = p.Sealed()
|
||||
if nowList.Digest == wasList.Digest || strings.Contains(fmt.Sprint(res.Sealed), "kept while a read") {
|
||||
t.Errorf("a manifest with no verdict yet held the whole list: %+v", res.Sealed)
|
||||
}
|
||||
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "no verdict on this manifest yet") {
|
||||
t.Errorf("a manifest with no verdict yet was not said: %+v", last)
|
||||
}
|
||||
// Accepted again: the users compose from the manifest and the condition clears.
|
||||
b.manifestOf("mod-r", ctl)
|
||||
@@ -2842,38 +2869,27 @@ func TestGrants(t *testing.T) {
|
||||
// ADR 0601 §4: a tool runner whose manifest does not say so yet (an older build's) keeps its user as the list in force
|
||||
// holds it, known by what it runs (it says the runner is alive), and the held condition says so; never composed as a
|
||||
// plain module.
|
||||
const runner = `{"module":"agent-runtime","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}`
|
||||
b.manifestOf("agent-runtime", runner)
|
||||
b.assign("a", "agent-runtime")
|
||||
b.put(grants.ReadAccounts, "a.agent-runtime", map[string]any{"user": "a.agent-runtime", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
|
||||
const runner = `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}`
|
||||
b.manifestOf("node-tools", runner)
|
||||
b.assign("a", "node-tools")
|
||||
b.put(grants.ReadAccounts, "a.node-tools", map[string]any{"user": "a.node-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
|
||||
p.Compose()
|
||||
inForce, _, _ = p.Sealed()
|
||||
ran, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "a.agent-runtime"))
|
||||
ran, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "a.node-tools"))
|
||||
if !ok || !strings.Contains(string(ran), "mesh.control.a.tools-alive") {
|
||||
t.Fatalf("the list in force holds no tool runner on a: %s", ran)
|
||||
}
|
||||
b.manifestOf("agent-runtime", strings.Replace(runner, `"tool-runner":true,`, "", 1))
|
||||
b.manifestOf("node-tools", strings.Replace(runner, `"tool-runner":true,`, "", 1))
|
||||
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen10", "generation": 10, "fingerprint": "sha256:10"})
|
||||
p.Compose()
|
||||
now, _, _ := p.Sealed()
|
||||
kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.agent-runtime"))
|
||||
kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.node-tools"))
|
||||
if !ok || string(kept) != string(ran) {
|
||||
t.Errorf("the tool runner of a was composed without what it runs: %s, was %s", kept, ran)
|
||||
}
|
||||
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "a.agent-runtime (stays in the list") {
|
||||
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "a.node-tools (stays in the list") {
|
||||
t.Errorf("the kept tool runner was not said: %+v", last)
|
||||
}
|
||||
// While node-tools' trunk manifest leaves `tool-runner` out for the running indexer, mesh-sdk's deprecated
|
||||
// catalogue.RuntimeModule is the tool runner without saying so (issue 774, issue 1585 note 42): composed as one.
|
||||
b.manifestOf(catalogue.RuntimeModule, strings.Replace(strings.Replace(runner, `"tool-runner":true,`, "", 1), "agent-runtime", catalogue.RuntimeModule, 1))
|
||||
b.assign("c", catalogue.RuntimeModule)
|
||||
b.put(grants.ReadAccounts, "c."+catalogue.RuntimeModule, map[string]any{"user": "c." + catalogue.RuntimeModule, "hash": "$2a$10$c", "generation": 1, "fingerprint": "sha256:c"})
|
||||
p.Compose()
|
||||
now, _, _ = p.Sealed()
|
||||
if composed, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "c."+catalogue.RuntimeModule)); !ok ||
|
||||
!strings.Contains(string(composed), "mesh.control.c.tools-alive") {
|
||||
t.Errorf("the kept runtime module on c was not composed as the tool runner: %s", composed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ type Watcher interface {
|
||||
//
|
||||
// And the seat mesh-enrolment's keys (ADR 0587 §4, §5): a key at live composes enrol.<node>, and its end, by the holder or by
|
||||
// the bus's expiry, is a change of the key that takes the user out; no event is needed beside the watch.
|
||||
var Followed = []string{ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments,
|
||||
var Followed = []string{ReadModules, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments,
|
||||
ReadNodeLogins}
|
||||
|
||||
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
|
||||
|
||||
@@ -47,9 +47,15 @@ func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
||||
continue
|
||||
}
|
||||
prefix := node + "." + module
|
||||
// No manifest of the module can be read at all (the trunk's is refused and no running build's is found): named
|
||||
// unreadable, in the condition and in grants_user_list (ADR 0614 §8).
|
||||
state := "held"
|
||||
if _, readable := in.Held[module]; !readable {
|
||||
state = "unreadable"
|
||||
}
|
||||
for _, user := range candidates {
|
||||
if user == prefix || strings.HasPrefix(user, prefix+".") {
|
||||
out[user] = fmt.Sprintf("the manifest of %s is held: %s", module, why)
|
||||
out[user] = fmt.Sprintf("the manifest of %s is %s: %s", module, state, why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,13 @@ import (
|
||||
const (
|
||||
ReadModules = "mesh-delivery-indexer.modules"
|
||||
ReadManifests = "mesh-delivery-indexer.manifests"
|
||||
ReadHave = "node-engine.have"
|
||||
ReadResolved = "mesh-config.resolved"
|
||||
ReadWants = "mesh-delivery-assigner.wants"
|
||||
ReadBuilds = "mesh-delivery-builder.builds"
|
||||
// ReadVerdicts is the indexer's verdicts, one key per manifest digest: a manifest it has not judged yet has none, and is
|
||||
// held like a refused one (ADR 0614 §8).
|
||||
ReadVerdicts = "mesh-delivery-indexer.verdicts"
|
||||
ReadHave = "node-engine.have"
|
||||
ReadResolved = "mesh-config.resolved"
|
||||
ReadWants = "mesh-delivery-assigner.wants"
|
||||
ReadBuilds = "mesh-delivery-builder.builds"
|
||||
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
|
||||
ReadAccounts = "mesh-vault.bus-accounts"
|
||||
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
|
||||
@@ -44,7 +47,7 @@ const (
|
||||
|
||||
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
|
||||
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
|
||||
var Reads = []string{ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||
var Reads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||
ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins}
|
||||
|
||||
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
||||
@@ -336,7 +339,10 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
for _, module := range indexed {
|
||||
raw, found, err := r.Get(ReadModules, module)
|
||||
if err != nil {
|
||||
in.Gaps = append(in.Gaps, Gap{Input: ReadModules + " " + module, Why: err.Error(), Failed: true})
|
||||
// One module's value that does not read holds that module alone, as a refused manifest does (ADR 0614 §8).
|
||||
g := Gap{Input: "manifest of " + module, Why: "its value in " + ReadModules + " does not read: " + err.Error()}
|
||||
in.Refused = append(in.Refused, g)
|
||||
in.Gaps = append(in.Gaps, g)
|
||||
continue
|
||||
}
|
||||
if !found {
|
||||
@@ -360,6 +366,10 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
// The indexer's refusal, or its wait for a judge that knows a field, carried in its value (ADR 0578 §3): held and named as a refused manifest.
|
||||
if verdict := v.VerdictOf(module); verdict != nil && verdict.Held() {
|
||||
err = errors.New(verdict.Why())
|
||||
} else if verdict == nil {
|
||||
// The indexer writes a changed manifest's value before it judges it, and a new digest has no verdict: until
|
||||
// the verdict is written the manifest is held (ADR 0614 §8).
|
||||
err = noVerdictYet(r, v.Manifest)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
@@ -446,8 +456,9 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
}
|
||||
in.Held = heldManifests(r, in, cache)
|
||||
// A held module placed on a node whose running manifest is not found declares no seat, stream or object bucket to
|
||||
// anyone: composing past it would drop the grants of those who use them, so the list in force is kept (ADR 0601
|
||||
// clause 3), and the gap says which module and why.
|
||||
// anyone: composing past it would drop the grants of those who use them, so the users of that module alone stay as the
|
||||
// list in force holds them (held.go, named unreadable; ADR 0614 §8, ADR 0601 clause 3). The gap says which module and why,
|
||||
// and holds nothing else: it is not a failed read.
|
||||
for _, g := range in.Refused {
|
||||
module, ok := strings.CutPrefix(g.Input, "manifest of ")
|
||||
if _, found := in.Held[module]; !ok || found {
|
||||
@@ -455,8 +466,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
}
|
||||
for _, assigned := range in.Assigned {
|
||||
if slices.Contains(assigned, module) {
|
||||
in.Gaps = append(in.Gaps, Gap{Input: "running manifest of " + module, Failed: true,
|
||||
Why: "its manifest is held and no running build's manifest is found, so the seats and streams it declares cannot be composed: the list in force is kept"})
|
||||
in.Gaps = append(in.Gaps, Gap{Input: "running manifest of " + module,
|
||||
Why: "its manifest is unreadable (held, and no running build's manifest is found), so the seats and streams it declares cannot be composed: its users stay as the list in force holds them"})
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -650,3 +661,27 @@ func (in Inputs) declared() map[string]catalogue.Manifest {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// noVerdictYet is nil when the indexer's verdicts state holds an accepted verdict on the manifest, and the reason it is held
|
||||
// when it holds none yet, cannot be read, or refuses it.
|
||||
func noVerdictYet(r Reader, digest string) error {
|
||||
key, err := delivery.VerdictsKey(digest)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
raw, found, err := r.Get(ReadVerdicts, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("its verdict cannot be read: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return errors.New("the indexer has written no verdict on this manifest yet")
|
||||
}
|
||||
v, err := delivery.DecodeManifestVerdict(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("its verdict does not read: %w", err)
|
||||
}
|
||||
if v.Held() {
|
||||
return errors.New(v.Why())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/toolrunner"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"mesh-grants/internal/buscompose"
|
||||
@@ -133,7 +132,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
fromModules = append(fromModules, c)
|
||||
}
|
||||
d := buscompose.Declared{
|
||||
Module: m.Module, ToolRunner: toolrunner.Is(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
|
||||
Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
|
||||
Invokes: m.Invokes, Waits: waitsOf(m, seats), State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m),
|
||||
Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), Checks: catalogue.HealthChecks(m),
|
||||
RunsAs: m.RunsAs, Issued: issuedTools(m),
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.verdicts",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"node-engine.have",
|
||||
"mesh-vault.bus-accounts",
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ module git.novox.be/novox/mesh-delivery/sdk
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
+222
-13
@@ -91,6 +91,10 @@ func ParseKey(key string) (string, error) {
|
||||
// build module and the toolchain it was computed with. Provides are the provisions its manifest offers (its own name is
|
||||
// provided too and is not listed), Requires what it requires and does not require optionally, and Awaits those of
|
||||
// Requires a want of it waits for (catalogue.Awaited, ADR 0570 §4): the provision edges that order a source revision.
|
||||
// A bus-state entry of its manifest is a requirement of the provision BusState (ADR 0614 §5). Invokes are the seat verbs
|
||||
// (`seat:<seat>.<verb>`) and module tools (`<module>.<tool>`) its manifest and its own bus accounts invoke, Serves those
|
||||
// its claims serve and its own tools answer, in the same two forms (ADR 0614 §2), and LeftOut those of Invokes whose
|
||||
// verb edge lies on a cycle and is left out of the graph, so no want waits on it (§3; LeaveOut works them out).
|
||||
// Every list is sorted and each entry once; an encode sorts them.
|
||||
type Node struct {
|
||||
V int `json:"v"`
|
||||
@@ -104,8 +108,15 @@ type Node struct {
|
||||
Provides []string `json:"provides,omitempty"`
|
||||
Requires []string `json:"requires,omitempty"`
|
||||
Awaits []string `json:"awaits,omitempty"`
|
||||
Invokes []string `json:"invokes,omitempty"`
|
||||
Serves []string `json:"serves,omitempty"`
|
||||
LeftOut []string `json:"left-out,omitempty"`
|
||||
}
|
||||
|
||||
// BusState is the provision a bus-state entry of a manifest requires (ADR 0614 §5): the bus module's build that offers it
|
||||
// goes before the module that asks bus state.
|
||||
const BusState = "bus-state"
|
||||
|
||||
// Toolchain is a build edge of one language: the build module that builds it (Module) and the toolchain version
|
||||
// and recipe version its fingerprint names.
|
||||
type Toolchain struct {
|
||||
@@ -152,9 +163,30 @@ func (g Node) check() error {
|
||||
return fmt.Errorf("graph: the graph node awaits %q, which it does not require", a)
|
||||
}
|
||||
}
|
||||
for _, list := range [][]string{g.Invokes, g.Serves} {
|
||||
for _, v := range list {
|
||||
if !isVerb(v) {
|
||||
return fmt.Errorf("graph: the graph node names %q, which is neither seat:<seat>.<verb> nor <module>.<tool>", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, l := range g.LeftOut {
|
||||
if !slices.Contains(g.Invokes, l) {
|
||||
return fmt.Errorf("graph: the graph node leaves out %q, which it does not invoke", l)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isVerb says whether v is a seat verb `seat:<seat>.<verb>` or a module tool `<module>.<tool>`.
|
||||
func isVerb(v string) bool {
|
||||
if _, _, ok := catalogue.SeatInvoke(v); ok {
|
||||
return true
|
||||
}
|
||||
module, tool, ok := strings.Cut(v, ".")
|
||||
return ok && module != "" && tool != "" && !strings.HasPrefix(v, "seat:")
|
||||
}
|
||||
|
||||
func sortedOnce(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
@@ -167,6 +199,7 @@ func sortedOnce(in []string) []string {
|
||||
func (g Node) normal() Node {
|
||||
g.Bases, g.Packages = sortedOnce(g.Bases), sortedOnce(g.Packages)
|
||||
g.Provides, g.Requires, g.Awaits = sortedOnce(g.Provides), sortedOnce(g.Requires), sortedOnce(g.Awaits)
|
||||
g.Invokes, g.Serves, g.LeftOut = sortedOnce(g.Invokes), sortedOnce(g.Serves), sortedOnce(g.LeftOut)
|
||||
if len(g.Toolchains) == 0 {
|
||||
g.Toolchains = nil
|
||||
}
|
||||
@@ -304,9 +337,22 @@ func (g Graph) Dependents(module string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// Before are the modules name waits for, by the graph's ordering edges, sorted: its bases and, for each provision it
|
||||
// awaits, every other module that provides it (ADR 0603 §1). A toolchain or package edge orders nothing (§3).
|
||||
func (g Graph) Before(name string) []string {
|
||||
// Servers are the modules whose Serves names invoke (a seat verb or a module tool), sorted.
|
||||
func (g Graph) Servers(invoke string) []string {
|
||||
var out []string
|
||||
for name, n := range g {
|
||||
if slices.Contains(n.Serves, invoke) {
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// waits are the modules name waits for, sorted: its bases, for each provision it awaits every other module that provides
|
||||
// it (ADR 0603 §1), and with verbs for each invoke the module serving it (ADR 0614 §2), those it leaves out (§3) left
|
||||
// out unless all is set.
|
||||
func (g Graph) waits(name string, verbs, all bool) []string {
|
||||
n := g[name]
|
||||
out := slices.Clone(n.Bases)
|
||||
for _, a := range n.Awaits {
|
||||
@@ -316,15 +362,101 @@ func (g Graph) Before(name string) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
if verbs {
|
||||
for _, v := range n.Invokes {
|
||||
if !all && slices.Contains(n.LeftOut, v) {
|
||||
continue
|
||||
}
|
||||
for _, s := range g.Servers(v) {
|
||||
if s != name || all {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedOnce(out)
|
||||
}
|
||||
|
||||
// Cycle is the first cycle of the graph's ordering edges, as the modules it passes through with the first named again
|
||||
// at its end (a -> b -> a), or nil for none (ADR 0603 §2: the check refuses it and names it). The ordering edges are a
|
||||
// module's bases and, for each provision it awaits, every other module that provides it: modules ordered on each other
|
||||
// would each wait for the other for good. A toolchain edge is no ordering edge (ADR 0603 §3: nothing waits on a build
|
||||
// module), and neither is a package edge. The modules are visited in sorted order, so the same graph names the same
|
||||
// cycle.
|
||||
// Before are the modules name waits for, by the graph's ordering edges, sorted: its bases, for each provision it awaits
|
||||
// (bus state among them, ADR 0614 §5) every other module that provides it (ADR 0603 §1), and for each verb it invokes
|
||||
// and the graph keeps, every other module that serves it (ADR 0614 §2, §3). A toolchain or package edge orders nothing:
|
||||
// it is a trigger, never a wait (ADR 0614 §6).
|
||||
func (g Graph) Before(name string) []string { return g.waits(name, true, false) }
|
||||
|
||||
// LeaveOut sets each node's LeftOut (ADR 0614 §3): every invoke whose verb edge lies on a cycle of the edges a part waits
|
||||
// on (bases, awaited provisions, every verb edge), whichever module the edge starts from, a module invoking what it
|
||||
// serves itself included. An edge lies on a cycle when the module it reaches reaches the module it starts from: both are
|
||||
// in one strongly connected part of the graph. What is left of the graph then has no cycle through a verb edge, so the
|
||||
// check's refusal (Cycle) never meets one.
|
||||
func (g Graph) LeaveOut() {
|
||||
part := g.parts(func(name string) []string { return g.waits(name, true, true) })
|
||||
for _, name := range slices.Sorted(maps.Keys(g)) {
|
||||
n := g[name]
|
||||
n.LeftOut = nil
|
||||
for _, v := range n.Invokes {
|
||||
for _, s := range g.Servers(v) {
|
||||
if part[s] == part[name] {
|
||||
n.LeftOut = append(n.LeftOut, v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
g[name] = n.normal()
|
||||
}
|
||||
}
|
||||
|
||||
// parts numbers each module by the strongly connected part of edges it is in (Tarjan's), so two modules have one number
|
||||
// when each reaches the other.
|
||||
func (g Graph) parts(edges func(string) []string) map[string]int {
|
||||
index, low, part := map[string]int{}, map[string]int{}, map[string]int{}
|
||||
onStack := map[string]bool{}
|
||||
var stack []string
|
||||
next, parts := 0, 0
|
||||
var visit func(string)
|
||||
visit = func(name string) {
|
||||
index[name], low[name] = next, next
|
||||
next++
|
||||
stack = append(stack, name)
|
||||
onStack[name] = true
|
||||
for _, to := range edges(name) {
|
||||
if _, ours := g[to]; !ours {
|
||||
continue
|
||||
}
|
||||
if _, seen := index[to]; !seen {
|
||||
visit(to)
|
||||
low[name] = min(low[name], low[to])
|
||||
} else if onStack[to] {
|
||||
low[name] = min(low[name], index[to])
|
||||
}
|
||||
}
|
||||
if low[name] == index[name] {
|
||||
for {
|
||||
top := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
onStack[top] = false
|
||||
part[top] = parts
|
||||
if top == name {
|
||||
break
|
||||
}
|
||||
}
|
||||
parts++
|
||||
}
|
||||
}
|
||||
for _, name := range slices.Sorted(maps.Keys(g)) {
|
||||
if _, seen := index[name]; !seen {
|
||||
visit(name)
|
||||
}
|
||||
}
|
||||
return part
|
||||
}
|
||||
|
||||
// Cycle is the first cycle of the graph's ordering edges (Before), as the modules it passes through with the first named
|
||||
// again at its end (a -> b -> a), or nil for none (ADR 0603 §2: the check refuses it and names it). The ordering edges
|
||||
// are what a part waits on, provisions, bus state and the verb edges the graph keeps (ADR 0614 §6), and bases, since a
|
||||
// module built on another's artifact can never build in a cycle of bases: modules ordered on each other would each wait
|
||||
// for the other for good. A verb edge the graph leaves out (LeaveOut) is none, and neither is a toolchain edge (ADR 0603
|
||||
// §3: nothing waits on a build module) or a package edge (ADR 0614 §6: a trigger). The modules are visited in sorted
|
||||
// order, so the same graph names the same cycle.
|
||||
func (g Graph) Cycle() []string {
|
||||
edges := g.Before
|
||||
const (
|
||||
@@ -367,9 +499,11 @@ func (g Graph) Cycle() []string {
|
||||
}
|
||||
|
||||
// EdgesOf is the graph node of manifest's own edges: its bases (each module its build stands on), what it provides,
|
||||
// what it requires and does not require optionally, and of those what a want of it waits for (catalogue.Awaited, as the
|
||||
// assigner judges it), providers answering the manifests that provide a provision. The fingerprint, place, packages and
|
||||
// toolchains are the indexer's to add; this is the part a reader of manifests alone (the checker) works out the same way.
|
||||
// what it requires and does not require optionally, a bus-state entry as a requirement of BusState (ADR 0614 §5), and of
|
||||
// those what a want of it waits for (catalogue.Awaited, as the assigner judges it; BusState whenever a provider offers
|
||||
// it), providers answering the manifests that provide a provision; and the verbs and tools it invokes and serves
|
||||
// (InvokesOf, ServesOf). The fingerprint, place, packages, toolchains and the verb edges left out (LeaveOut, over the
|
||||
// whole graph) are the indexer's to add; this is the part a reader of manifests alone (the checker) works out the same way.
|
||||
func EdgesOf(manifest catalogue.Manifest, providers func(provision string) []catalogue.Manifest) Node {
|
||||
var n Node
|
||||
if manifest.Build != nil {
|
||||
@@ -388,11 +522,56 @@ func EdgesOf(manifest catalogue.Manifest, providers func(provision string) []cat
|
||||
}
|
||||
}
|
||||
n.Awaits = catalogue.Awaited(manifest, providers)
|
||||
if len(manifest.BusState) > 0 {
|
||||
n.Requires = append(n.Requires, BusState)
|
||||
if len(providers(BusState)) > 0 {
|
||||
n.Awaits = append(n.Awaits, BusState)
|
||||
}
|
||||
}
|
||||
n.Invokes, n.Serves = InvokesOf(manifest), ServesOf(manifest)
|
||||
return n.normal()
|
||||
}
|
||||
|
||||
// InvokesOf are the seat verbs and module tools manifest invokes, its own and its processes' and tools bundles' bus
|
||||
// accounts' (catalogue.Manifest.BusAccounts), sorted, each once; `*`, every tool, names no edge and is left out.
|
||||
func InvokesOf(manifest catalogue.Manifest) []string {
|
||||
all := slices.Clone(manifest.Invokes)
|
||||
for _, a := range manifest.BusAccounts() {
|
||||
all = append(all, a.Bus.Invokes...)
|
||||
}
|
||||
var out []string
|
||||
for _, v := range all {
|
||||
if isVerb(v) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return sortedOnce(out)
|
||||
}
|
||||
|
||||
// ServesOf are the seat verbs manifest's claims serve (`seat:<seat>.<verb>`, catalogue.Claim.ServesFor) and its own
|
||||
// tools (`<module>.<tool>`), sorted, each once.
|
||||
func ServesOf(manifest catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for _, c := range manifest.Claims {
|
||||
for _, v := range c.ServesFor(manifest) {
|
||||
out = append(out, "seat:"+c.Name+"."+v)
|
||||
}
|
||||
}
|
||||
for _, t := range manifest.Tools {
|
||||
out = append(out, manifest.Module+"."+t)
|
||||
}
|
||||
var verbs []string
|
||||
for _, v := range out {
|
||||
if isVerb(v) {
|
||||
verbs = append(verbs, v)
|
||||
}
|
||||
}
|
||||
return sortedOnce(verbs)
|
||||
}
|
||||
|
||||
// OfManifests is the graph of these manifests' own edges (EdgesOf), by module, each provision's providers being
|
||||
// the manifests among them that offer it: what the check works out for a change to find a cycle (ADR 0603 §2).
|
||||
// the manifests among them that offer it, with the verb edges on a cycle left out (LeaveOut): what the check works out
|
||||
// for a change to find a cycle (ADR 0603 §2, ADR 0614 §3).
|
||||
func OfManifests(manifests map[string]catalogue.Manifest) Graph {
|
||||
providers := func(provision string) []catalogue.Manifest {
|
||||
var out []catalogue.Manifest
|
||||
@@ -407,5 +586,35 @@ func OfManifests(manifests map[string]catalogue.Manifest) Graph {
|
||||
for name, m := range manifests {
|
||||
g[name] = EdgesOf(m, providers)
|
||||
}
|
||||
g.LeaveOut()
|
||||
return g
|
||||
}
|
||||
|
||||
// Served is whether a running build serves a seat verb or module tool (ADR 0614 §2, §7), counted from the holders' own
|
||||
// haves as a provider is (ADR 0603 §5): every have running healthy, by node and module, whose build's manifest serves it
|
||||
// (servesOf answers the verbs of a module's build at a fingerprint, ServesOf of its manifest, and whether it was read).
|
||||
// A build whose manifest cannot be read counts what fallback answers of its module (the graph's Serves, so a build read
|
||||
// late never holds its callers for good, or nothing), and is named in unread, sorted, so its reader says so.
|
||||
func Served(haves map[string]map[string]delivery.Have, servesOf func(module, fp string) ([]string, bool),
|
||||
fallback func(module string) []string) (served func(invoke string) bool, unread []string) {
|
||||
set := map[string]bool{}
|
||||
for _, node := range slices.Sorted(maps.Keys(haves)) {
|
||||
for _, module := range slices.Sorted(maps.Keys(haves[node])) {
|
||||
h := haves[node][module]
|
||||
if h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthOK {
|
||||
continue
|
||||
}
|
||||
serves, ok := servesOf(module, h.FP)
|
||||
if !ok {
|
||||
unread = append(unread, module)
|
||||
if fallback != nil {
|
||||
serves = fallback(module)
|
||||
}
|
||||
}
|
||||
for _, v := range serves {
|
||||
set[v] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return func(v string) bool { return set[v] }, slices.Compact(slices.Sorted(slices.Values(unread)))
|
||||
}
|
||||
|
||||
+76
-10
@@ -2,6 +2,7 @@ package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -97,6 +98,51 @@ func TestGraphEdges(t *testing.T) {
|
||||
if got := mg.Cycle(); !reflect.DeepEqual(got, []string{"db", "web", "db"}) {
|
||||
t.Errorf("the cycle of manifests = %v", got)
|
||||
}
|
||||
// The verb and bus-state edges (ADR 0614 §2, §3, §5, §6), from manifests alone: a caller waits for the module serving
|
||||
// what it invokes, a bus-state entry requires bus-state of the bus module offering it, the builder's remove and the
|
||||
// holder's builder_versions close a cycle and both are left out, read from either side, a package edge gives no order,
|
||||
// and a module invoking its own tool is no wait.
|
||||
builder := catalogue.Manifest{Module: "builder", Tools: []string{"builder_versions"}, Invokes: []string{"seat:artifact-store.remove"}}
|
||||
store := catalogue.Manifest{Module: "store", Claims: []catalogue.Claim{{Name: "artifact-store", Scope: catalogue.ScopeMesh,
|
||||
Serves: []string{"remove"}}}, Invokes: []string{"builder.builder_versions", "*"}}
|
||||
verbs := map[string]catalogue.Manifest{
|
||||
"builder": builder, "store": store,
|
||||
"bus": {Module: "bus", Provides: []catalogue.Offer{{Name: BusState, Scope: catalogue.ScopeMesh}}},
|
||||
"keeper": {Module: "keeper", BusState: []catalogue.BusStateEntry{{Name: "cache", Storage: "key-value"}}},
|
||||
"caller": {Module: "caller", Invokes: []string{"seat:artifact-store.remove", "caller.own", "nobody.serves"}, Tools: []string{"own"}},
|
||||
}
|
||||
vg := OfManifests(verbs)
|
||||
for name, want := range map[string][]string{"caller": {"store"}, "keeper": {"bus"}, "builder": nil, "store": nil} {
|
||||
if got := vg.Before(name); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s waits for %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
if b, s := vg["builder"], vg["store"]; !reflect.DeepEqual(b.LeftOut, []string{"seat:artifact-store.remove"}) ||
|
||||
!reflect.DeepEqual(s.LeftOut, []string{"builder.builder_versions"}) ||
|
||||
!reflect.DeepEqual(s.Serves, []string{"seat:artifact-store.remove"}) || !reflect.DeepEqual(s.Invokes, []string{"builder.builder_versions"}) {
|
||||
t.Errorf("the builder's cycle is not left out from either side: %+v %+v", b, s)
|
||||
}
|
||||
if k := vg["keeper"]; !reflect.DeepEqual(k.Requires, []string{BusState}) || !reflect.DeepEqual(k.Awaits, []string{BusState}) {
|
||||
t.Errorf("a bus-state entry is no requirement of bus-state: %+v", k)
|
||||
}
|
||||
if c := vg["caller"]; !reflect.DeepEqual(c.LeftOut, []string{"caller.own"}) || vg.Cycle() != nil {
|
||||
t.Errorf("a module's own tool is a wait, or a left-out verb edge a cycle: %+v %v", c, vg.Cycle())
|
||||
}
|
||||
if got := OfManifests(map[string]catalogue.Manifest{"keeper": verbs["keeper"]})["keeper"]; got.Awaits != nil {
|
||||
t.Errorf("bus-state with no module offering it is awaited: %+v", got)
|
||||
}
|
||||
// The words of a want held on a verb or a reader capability name their edge, and no other words do.
|
||||
for words, want := range map[string]string{
|
||||
VerbWaitWords("seat:mesh-catalog.modules", "artifact-store-tools", "g14"): "verb seat:mesh-catalog.modules",
|
||||
ReaderWaitWords("g14", []string{"tool-runner", "bus-state"}, "node-tools", "src2:abcdef"): "reader capability tool-runner, bus-state",
|
||||
BusStateWaitWords("keeper", "g14"): "bus state",
|
||||
"the verb is served": "",
|
||||
"the node-engine of g14 has not started": "",
|
||||
} {
|
||||
if got := EdgeOfWords(words); got != want {
|
||||
t.Errorf("the edge of %q = %q, want %q", words, got, want)
|
||||
}
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
edit func(Graph)
|
||||
want []string
|
||||
@@ -115,6 +161,19 @@ func TestGraphEdges(t *testing.T) {
|
||||
w.Provides = []string{"site"}
|
||||
g["web"] = w
|
||||
}, []string{"db", "web", "db"}},
|
||||
"a package cycle orders nothing": {func(g Graph) {
|
||||
l := g["lib"]
|
||||
l.Packages = []string{"git.novox.be/novox/mesh-catalog/modules/app"}
|
||||
g["lib"] = l
|
||||
}, nil},
|
||||
"a verb edge with a provision, before LeaveOut leaves it out": {func(g Graph) {
|
||||
d := g["db"]
|
||||
d.Invokes = []string{"web.page"}
|
||||
g["db"] = d
|
||||
w := g["web"]
|
||||
w.Serves = []string{"web.page"}
|
||||
g["web"] = w
|
||||
}, []string{"db", "web", "db"}},
|
||||
"a requirement not awaited is no edge": {func(g Graph) {
|
||||
d := g["db"]
|
||||
d.Requires = []string{"site"}
|
||||
@@ -132,6 +191,9 @@ func TestGraphEdges(t *testing.T) {
|
||||
if got := h.Cycle(); !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("%s: cycle = %v, want %v", name, got, c.want)
|
||||
}
|
||||
if h.LeaveOut(); h.Cycle() != nil && slices.ContainsFunc(slices.Collect(maps.Values(h)), func(n Node) bool { return len(n.Invokes) > 0 }) {
|
||||
t.Errorf("%s: a cycle through a verb edge is left once its verb edges are left out: %v", name, h.Cycle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +202,9 @@ func sampleNode() Node {
|
||||
return Node{FP: "src2:" + strings.Repeat("a", 64), Repository: "novox/mesh-catalog", Path: "modules/gitea",
|
||||
Bases: []string{"z-base", "a-base"}, Packages: []string{"git.novox.be/novox/mesh-sdk/go"},
|
||||
Toolchains: map[string]Toolchain{"go": {Module: "build-go", Version: "go1.27.1", Recipe: "3"}},
|
||||
Provides: []string{"forge"}, Requires: []string{"postgres-database", "dns"}, Awaits: []string{"postgres-database"}}
|
||||
Provides: []string{"forge"}, Requires: []string{"postgres-database", "dns"}, Awaits: []string{"postgres-database"},
|
||||
Invokes: []string{"seat:mesh-catalog.modules", "builder.builder_versions"}, Serves: []string{"seat:forge.pulls"},
|
||||
LeftOut: []string{"builder.builder_versions"}}
|
||||
}
|
||||
|
||||
// nodeValues: a node round-trips with its lists sorted, an unknown version is refused, a node that does not hold is
|
||||
@@ -169,15 +233,17 @@ func nodeValues(t *testing.T) {
|
||||
change func(*Node)
|
||||
word string
|
||||
}{
|
||||
"both": {func(g *Node) { g.None = "why" }, "both"},
|
||||
"neither": {func(g *Node) { g.FP = "" }, "neither"},
|
||||
"an old form": {func(g *Node) { g.FP = "src1:x" }, "src2:"},
|
||||
"a dotted base": {func(g *Node) { g.Bases = []string{"a.b"} }, "a.b"},
|
||||
"a foreign package": {func(g *Node) { g.Packages = []string{"example.com/x"} }, "example.com/x"},
|
||||
"a toolchain of no module": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Version: "go1", Recipe: "1"}} }, "build module"},
|
||||
"a toolchain of no recipe": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Module: "b", Version: "go1"}} }, "recipe"},
|
||||
"awaiting what it does not require": {func(g *Node) { g.Awaits = []string{"other"} }, "other"},
|
||||
"no repository": {func(g *Node) { g.Repository = "" }, "repositor"},
|
||||
"both": {func(g *Node) { g.None = "why" }, "both"},
|
||||
"neither": {func(g *Node) { g.FP = "" }, "neither"},
|
||||
"an old form": {func(g *Node) { g.FP = "src1:x" }, "src2:"},
|
||||
"a dotted base": {func(g *Node) { g.Bases = []string{"a.b"} }, "a.b"},
|
||||
"a foreign package": {func(g *Node) { g.Packages = []string{"example.com/x"} }, "example.com/x"},
|
||||
"a toolchain of no module": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Version: "go1", Recipe: "1"}} }, "build module"},
|
||||
"a toolchain of no recipe": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Module: "b", Version: "go1"}} }, "recipe"},
|
||||
"awaiting what it does not require": {func(g *Node) { g.Awaits = []string{"other"} }, "other"},
|
||||
"no repository": {func(g *Node) { g.Repository = "" }, "repositor"},
|
||||
"leaving out what it does not invoke": {func(g *Node) { g.LeftOut = []string{"x.y"} }, "x.y"},
|
||||
"an invoke of no verb": {func(g *Node) { g.Invokes = []string{"seat:nothing"} }, "seat:nothing"},
|
||||
} {
|
||||
g := sampleNode()
|
||||
c.change(&g)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The words of a want held on an edge of ADR 0614 (novox/hq ADR 0614 §2, §4, §5): no reader knows a rule number of its own for
|
||||
// either yet, so the assigner holds them under the provisions' and the composition's rules, and these words are the one
|
||||
// place they are written and read. The assigner writes them (VerbWaitWords, ReaderWaitWords, BusStateWaitWords); the delivery view names
|
||||
// the edge a want waits on from them (EdgeOfWords).
|
||||
|
||||
const (
|
||||
verbOpen = "the verb "
|
||||
verbClose = " invokes is served by no running build"
|
||||
readerOpen = "the node-engine of "
|
||||
readerMid = " does not read "
|
||||
readerTail = ", which the manifest of "
|
||||
busOpen = "bus state: "
|
||||
)
|
||||
|
||||
// BusStateWaitWords are the words of a want of module on node held until a build of a module offering bus-state runs
|
||||
// healthy on a node present (ADR 0614 §5, through ADR 0603 §5's count of the provider's own have).
|
||||
func BusStateWaitWords(module, node string) string {
|
||||
return fmt.Sprintf("%s%s asks bus state, and no build of a module offering %s runs healthy on a node present: its want on %s waits for one (ADR 0614 §5)",
|
||||
busOpen, module, BusState, node)
|
||||
}
|
||||
|
||||
// VerbWaitWords are the words of a want of module on node held until a build serving verb runs healthy (ADR 0614 §2).
|
||||
func VerbWaitWords(verb, module, node string) string {
|
||||
return fmt.Sprintf("%s%s %s%s: its want on %s waits until a build serving it runs healthy (ADR 0614 §2)", verbOpen, verb,
|
||||
module, verbClose, node)
|
||||
}
|
||||
|
||||
// ReaderWaitWords are the words of a want of module at fp held on node until its node-engine states the reader
|
||||
// capabilities missing (ADR 0614 §4).
|
||||
func ReaderWaitWords(node string, missing []string, module, fp string) string {
|
||||
return fmt.Sprintf("%s%s%s%s%s%s at %s needs: the want moves once that engine states it", readerOpen, node, readerMid,
|
||||
strings.Join(missing, ", "), readerTail, module, fp)
|
||||
}
|
||||
|
||||
// EdgeOfWords is the edge a held want's words name: `verb <verb>` for a verb wait, `reader capability <capabilities>` for
|
||||
// a reader capability's hold, `bus state` for a bus-state wait, or "" for words of none of them.
|
||||
func EdgeOfWords(words string) string {
|
||||
if strings.HasPrefix(words, busOpen) {
|
||||
return "bus state"
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(words, verbOpen); ok {
|
||||
if verb, _, ok := strings.Cut(rest, " "); ok && strings.Contains(rest, verbClose) {
|
||||
return "verb " + verb
|
||||
}
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(words, readerOpen); ok {
|
||||
if _, rest, ok := strings.Cut(rest, readerMid); ok {
|
||||
if missing, _, ok := strings.Cut(rest, readerTail); ok {
|
||||
return "reader capability " + missing
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Package toolrunner says whether a manifest is the node's tool runner, for the parts of mesh-delivery that judge or
|
||||
// compose one (the indexer's judge, mesh-grants).
|
||||
package toolrunner
|
||||
|
||||
import "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
|
||||
// Is says whether a manifest is the node's tool runner: it says so (`tool-runner`, catalogue.IsToolRunner), or its module
|
||||
// is mesh-sdk's deprecated catalogue.RuntimeModule.
|
||||
//
|
||||
// The second clause is kept only while node-tools' manifest on mesh-tools' trunk does not say `tool-runner`: the running
|
||||
// indexer refuses that word, so node-tools' trunk manifest leaves it out until the indexer that knows it is delivered
|
||||
// (issue 774, issue 1585 note 42). It goes, with this helper's use of catalogue.RuntimeModule, once `tool-runner` is
|
||||
// back on node-tools' trunk manifest; then every caller asks catalogue.IsToolRunner alone.
|
||||
func Is(m catalogue.Manifest) bool {
|
||||
return catalogue.IsToolRunner(m) || m.Module == catalogue.RuntimeModule
|
||||
}
|
||||
Reference in New Issue
Block a user