Review round 1: the indexer learns its build from the node-engine's seat; a held module's seats and hearings come from its running manifest; indexer_judge; one judge pass at a time (issue 774)

This commit is contained in:
2026-11-03 13:03:11 +01:00
parent 40da5e924e
commit 1fe1beee58
32 changed files with 274 additions and 79 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-assigner
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-builder
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -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.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-checker
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -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.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -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.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
@@ -1,6 +1,6 @@
// Command mesh-delivery-indexer-tools serves the indexer's doing tool (novox/hq issue 1945): the runtime launches it for a
// call. indexer_take_in writes one value of the indexer's takeins state and waits for the indexer's answer there. The
// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge_again asks the same way, and
// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge asks the same way, and
// indexer_verdicts reads the indexer's verdicts state. The read tools of the indexer's states are
// mesh-delivery-view's (indexer_module, indexer_modules, indexer_candidate, indexer_candidates).
package main
@@ -56,7 +56,7 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState) []stdio.Tool {
"unknown, and every reader holds the module as a refused one and keeps what runs; refused: the judge's problems), the " +
"judge that made it and when. A module's name narrows it to that module. A waiting manifest is judged again at each " +
"new indexer and refused only once the newest build of the indexer on the trunk still does not know the field; " +
"indexer_judge_again judges now.",
"indexer_judge judges now.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"module": map[string]any{"type": "string", "description": "only this module's verdicts"},
"all": map[string]any{"type": "boolean", "description": "list accepted verdicts too"}}},
@@ -151,7 +151,7 @@ func askTakeIn(s stdio.KeptState, bound time.Duration) asker {
}
func main() {
if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State("verdicts"))); err != nil {
if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState))); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err)
os.Exit(1)
}
@@ -300,7 +300,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf),
Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf,
Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call),
ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call)}
ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call),
OwnBuild: ownBuildVia(calls.Call, cred.Node)}
// The log's entries at the start are read before any fingerprint is computed or registered module indexed (below); each
// new one indexes the module it names when that module is registered and the bucket holds none (registered.go).
var started atomic.Bool
@@ -50,6 +50,19 @@ func TestTheTrunkIsTheDirectoriesTheForgeListsTrees(t *testing.T) { // 1900/AC3
if _, has := gotArgs.(map[string]any)["paths"]; has {
t.Error("the trunk asked for paths")
}
// The build the indexer runs is the engine's word (Runs.FP of its composition verb), asked of its own machine.
var asked string
own := func(key string, body any) (json.RawMessage, error) {
asked = key
return json.RawMessage(`{"runs":{"fp":"src2:abc"}}`), nil
}
if fp, err := ownBuildVia(own, "n1")(context.Background()); err != nil || fp != "src2:abc" || asked != "seat:node-engine.composition@n1" {
t.Errorf("own build = %q %v asked %q", fp, err, asked)
}
none := func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }
if _, err := ownBuildVia(none, "n1")(context.Background()); err == nil {
t.Error("an engine that says nothing runs gave a build")
}
}
func TestReadViaReadsManifestsAndLocks(t *testing.T) {
@@ -0,0 +1,37 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// compositionVerb is the node-engine seat's read verb that says what runs of a module on a machine (its Runs.FP), asked of
// that machine's own engine as `seat:node-engine.composition@<machine>`. The indexer learns the build it runs from it, never
// from the trunk's value of itself or the haves bucket.
const compositionVerb = "seat:node-engine.composition"
// ownBuildVia asks the engine of node which fingerprint of the indexer runs there.
func ownBuildVia(call toolCaller, node string) func(ctx context.Context) (string, error) {
return func(context.Context) (string, error) {
raw, err := call(compositionVerb+"@"+node, map[string]any{"module": delivery.IndexerModule})
if err != nil {
return "", fmt.Errorf("%s: %w", compositionVerb, err)
}
var a struct {
Runs *struct {
FP string `json:"fp"`
} `json:"runs"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return "", fmt.Errorf("the answer of %s is not a composition: %w", compositionVerb, err)
}
if a.Runs == nil || a.Runs.FP == "" {
return "", errors.New("the engine says no fingerprint of the indexer runs")
}
return a.Runs.FP, nil
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
@@ -92,24 +92,27 @@ type Indexer struct {
// closed message the indexer missed heals at the next one (issue 1126); nil asks none.
ReadOpen func(ctx context.Context) ([]delivery.OpenPull, error)
mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach
source sourceNow
previews map[string]delivery.Preview // by previews key (previews.go)
toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go)
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
recompute sync.Mutex // one Recompute at a time
untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go)
filling atomic.Bool // a FillTaken runs in the background (KickFill)
fillAgain atomic.Bool // a kick arrived while it ran
atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished)
ownMu sync.Mutex // guards ownFP
ownFP string // the indexer's own fingerprint its modules value named at the start pass (ownBuildIsNewest)
judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go)
mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach
source sourceNow
previews map[string]delivery.Preview // by previews key (previews.go)
toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go)
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
recompute sync.Mutex // one Recompute at a time
untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go)
filling atomic.Bool // a FillTaken runs in the background (KickFill)
fillAgain atomic.Bool // a kick arrived while it ran
atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished)
// OwnBuild answers the fingerprint of the build this program runs, from the node-engine's seat; nil knows none.
OwnBuild func(ctx context.Context) (string, error)
judgeMu sync.Mutex // one judge pass at a time (verdicts.go)
ownMu sync.Mutex // guards ownFP
ownFP string // the fingerprint of the build this program runs, asked at the start pass (learnOwnBuild)
judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go)
}
func (ix *Indexer) logf(format string, args ...any) {
@@ -24,9 +24,11 @@ import (
// changed (a new indexer) judges every manifest again, and after every merge it takes in, since a rule between manifests
// can turn on any of them. Nothing polls.
// VerdictsBucket is the indexer's state of verdicts.
// VerdictsBucket is the indexer's state of verdicts; VerdictsState is its local name, as the tools bundle reads it.
var VerdictsBucket = delivery.VerdictsBucket
const VerdictsState = "verdicts"
// Judged is what one pass of the judge did: the manifests judged, the verdicts written, and the modules refused.
type Judged struct {
Judged, Written int
@@ -47,6 +49,10 @@ 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) {
// 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()
defer ix.judgeMu.Unlock()
var out Judged
if ix.Verdicts == nil || ix.Modules == nil || ix.Manifests == nil {
return out, nil
@@ -146,11 +152,7 @@ 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).
func (ix *Indexer) JudgeAtStart(ctx context.Context) (Judged, error) {
if held, err := ix.heldModules(ctx); err == nil {
ix.ownMu.Lock()
ix.ownFP = held[delivery.IndexerModule].FP2 // this program's build, until the trunk names another
ix.ownMu.Unlock()
}
ix.learnOwnBuild(ctx)
j, err := ix.JudgeAll(ctx, nil)
ix.judgePending.Store(err != nil)
return j, err
@@ -203,11 +205,26 @@ func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.M
return wrote, err
}
// ownBuildIsNewest says whether the build of the indexer this program is, is the newest the trunk holds: nothing gives a
// module the fingerprint it was built from, so it is the one the indexer's own modules value named when this program
// judged at its start (ownFP), and the trunk holds no newer one while that value still names it. A newer build of the
// indexer on the trunk, as one that pins a changed mesh-sdk always is, ends it at the trunk's merge, before it runs; false
// when the value names none or this program judged no start pass.
// learnOwnBuild asks where the indexer's program is told which build it runs (OwnBuild: the node-engine's seat, never the
// trunk's value of the indexer, which names a build that may be merged and not delivered). Asked at the start pass; where
// it is not answered the build stays unknown and every unknown field waits, which is the safe side.
func (ix *Indexer) learnOwnBuild(ctx context.Context) {
if ix.OwnBuild == nil {
return
}
fp, err := ix.OwnBuild(ctx)
if err != nil || fp == "" {
ix.logf("judge: the build this program runs is not known (%v), so a manifest with a field it does not know waits and is not refused", err)
return
}
ix.ownMu.Lock()
ix.ownFP = fp
ix.ownMu.Unlock()
}
// ownBuildIsNewest says whether the build of the indexer this program runs (ownFP, from the node-engine) is the newest the
// trunk holds: the indexer's own modules value names it. A newer build merged and not yet delivered ends it at the trunk's
// merge, as one that pins a changed mesh-sdk always is; false when either is unknown.
func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool {
fp := held[delivery.IndexerModule].FP2
if fp == "" {
@@ -277,10 +294,10 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList
}
// The tools that read and steer the verdicts (the instruction "tools are how a module is spoken to"): indexer_verdicts
// lists the manifests that wait or are refused, and indexer_judge_again judges one module again now.
// lists the manifests that wait or are refused, and indexer_judge judges one module again now.
const (
VerdictsTool = "indexer_verdicts"
JudgeAgainTool = "indexer_judge_again"
JudgeAgainTool = "indexer_judge"
judgeAgainPrefix = "judge."
)
@@ -331,7 +348,7 @@ 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_again. A module the modules state does not hold, or whose manifest is not in the manifests state, is
// 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 {
out := TakenIn{Module: module}
@@ -367,7 +384,7 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn {
return out
}
// NewJudgeAgain is the value indexer_judge_again writes for a module: its key in the takeins state and the value.
// 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) {
key, data, err := NewTakeIn(module, why, by, at)
if err != nil {
@@ -2,6 +2,7 @@ package index
import (
"context"
"errors"
"strings"
"testing"
"time"
@@ -138,6 +139,19 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
!strings.Contains(m.Problems[0], "does not know yet") {
t.Fatalf("modules.later carries %+v", m)
}
// The build this program runs is asked of the engine, and the trunk moving before the start pass changes nothing: the
// trunk's value of the indexer names a newer build than the one the engine says runs, so the field still waits.
ix.ownFP = ""
ix.OwnBuild = func(context.Context) (string, error) { return ownFP, nil }
if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() {
t.Fatalf("the trunk moved before the start pass: %v %+v", err, verdict(later))
}
ix.OwnBuild = func(context.Context) (string, error) { return "", errors.New("the engine does not answer") }
ix.ownFP = ""
if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() {
t.Fatalf("an unknown build refused: %v %+v", err, verdict(later))
}
ix.ownFP = ownFP
// A renamed field is a known mistake: refused at once.
renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`)
if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !verdict(renamed).Refused() {
@@ -151,7 +165,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if m := module("later"); m.Verdict != delivery.VerdictRefused || m.VerdictOf("later").Waits() || len(m.Unknown) != 0 {
t.Fatalf("modules.later after the refusal carries %+v", m)
}
// The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge_again
// The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge
// judges now and says how the module's verdict stands.
keys, _ := ix.Verdicts.Keys(ctx)
get := func(key string) ([]byte, error) { v, _, _, err := ix.Verdicts.Get(ctx, key); return v, err }
@@ -163,13 +177,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
ix.ownFP = ownFP
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_again: %+v", got)
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") {
t.Fatalf("indexer_judge_again of nobody: %+v", got)
t.Fatalf("indexer_judge of nobody: %+v", got)
}
if key, value, err := NewJudgeAgain("later", "why", "me", time.Now()); err != nil || key != "judge.later" {
t.Fatalf("the ask: %q %v", key, err)
@@ -109,13 +109,13 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
// commit is in another (issue 1900).
if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls"}; !reflect.DeepEqual(m.Invokes, want) {
if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "seat:node-engine.composition"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
// reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state
// (indexer_judge_again; issue 774, clause 3).
if want := []string{"indexer_take_in", "indexer_verdicts", "indexer_judge_again"}; !reflect.DeepEqual(m.Tools, want) {
// (indexer_judge; issue 774, clause 3).
if want := []string{"indexer_take_in", "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.
+3 -2
View File
@@ -5,7 +5,7 @@
"tools": [
"indexer_take_in",
"indexer_verdicts",
"indexer_judge_again"
"indexer_judge"
],
"consumes": [
"mesh-build-go.toolchain.reported",
@@ -40,7 +40,8 @@
"gitea.gitea_commit_trees",
"gitea.gitea_contains",
"gitea.gitea_get_file",
"gitea.gitea_open_pulls"
"gitea.gitea_open_pulls",
"seat:node-engine.composition"
],
"own-secrets": {
"broker": "${dir:state}/broker"
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-teller
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-view
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-watchman
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-enrolment
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-grants
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/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=
+47
View File
@@ -739,6 +739,53 @@ func TestGrants(t *testing.T) {
}
})
// ADR 0601 clause 3: a held module's seats, hearings and reads are composed from the manifest its running build was judged
// on, so a principal that claims a seat it defines keeps its grants while its trunk manifest is refused or waits.
t.Run("a held module's seat is composed from the manifest its running build was judged on", func(t *testing.T) {
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
`"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}`
const telegram = `{"module":"telegram","version":"1","runs-as":"telegram","resources":[{"id":"account","type":"user","name":"telegram","shell":"/usr/bin/nologin","home":"/var/lib/telegram"}],"own-secrets":{"broker":"/var/lib/t/broker"},"secrets-owner":"telegram",` +
`"claims":[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]}`
setup := func(held, runs bool) *fakeBus {
b := fixture()
b.manifestOf("messenger", router)
b.manifestOf("telegram", telegram)
b.assign("a", "messenger")
b.assign("b", "telegram")
if held {
// The trunk's manifest waits for a field; the build that runs was judged on the manifest before.
b.put(grants.ReadModules, "messenger", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(`{"module":"messenger","version":"2"}`)), Repository: "novox/x", Commit: "c",
Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
if runs {
fp := "src2:" + strings.Repeat("cd", 32)
digest := delivery.ManifestDigest([]byte(router))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "messenger", JSON: router})
b.put(grants.ReadHave, "a.messenger", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp})
key, _ := delivery.BuilderBuildKey("messenger", fp)
b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest})
}
}
return b
}
grantsOf := func(b *fakeBus) string {
for _, u := range shadowOf(t, b) {
if u.User == "b.telegram" {
return strings.Join(u.Publish, ",") + "|" + strings.Join(u.Subscribe, ",")
}
}
return ""
}
accepted, held, unseen := grantsOf(setup(false, false)), grantsOf(setup(true, true)), grantsOf(setup(true, false))
if accepted == "" || held != accepted {
t.Errorf("a caller of a held module's seat lost grants:\n%s\nwas\n%s", held, accepted)
}
if unseen == accepted {
t.Errorf("the row proves nothing: the grants are the same without the held module's running manifest")
}
})
t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) {
const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` +
`"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` +
+62 -1
View File
@@ -131,7 +131,11 @@ type Inputs struct {
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
Refused []Gap
Gaps []Gap
// Held is, by module, the manifest a held module's running build was judged on (its have's fingerprint, the builder's
// build of it): what the seats it defines, the streams others hear from it and its reads are composed from while its
// trunk manifest is refused or waits, so no other principal loses a grant to it (ADR 0601 clause 3).
Held map[string]catalogue.Manifest
Gaps []Gap
}
// HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said by the part, and
@@ -440,6 +444,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
sort.Strings(fromTrunk)
in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")})
}
in.Held = heldManifests(r, in, cache)
in.Hashes = readHashes(r, &in.Gaps)
// The seat mesh-enrolment's keys are every token since the controller's end (issue 1126).
in.Enrolling, _ = readEnrolling(r, &in.Gaps)
@@ -573,3 +578,59 @@ func (in Inputs) ReadFailures() []string {
}
return out
}
// heldManifests are the manifests the running builds of the held modules (in.Refused) were judged on, by module: the have
// of the module on a node names the fingerprint that runs, and the builder's build of it the manifest. A module that runs
// nowhere, or whose build or manifest cannot be read, has none, and that is said as a gap.
func heldManifests(r Reader, in Inputs, cache *Manifests) map[string]catalogue.Manifest {
out := map[string]catalogue.Manifest{}
for _, g := range in.Refused {
module, ok := strings.CutPrefix(g.Input, "manifest of ")
if !ok {
continue
}
for _, node := range in.Nodes {
raw, found, err := r.Get(ReadHave, node+"."+module)
if err != nil || !found {
continue
}
h, err := delivery.DecodeHave(raw)
if err != nil || h.FP == "" {
continue
}
bk, err := delivery.BuilderBuildKey(module, h.FP)
if err != nil {
continue
}
braw, found, err := r.Get(ReadBuilds, bk)
if err != nil || !found {
continue
}
b, err := delivery.DecodeBuilderBuild(braw)
if err != nil || b.Manifest == "" {
continue
}
if m, err := cache.get(r, b.Manifest); err == nil {
out[module] = m
break
}
}
}
return out
}
// declared are the manifests the seats, hearings and reads are composed from: the trunk's, and for a held module the
// manifest its running build was judged on (Held).
func (in Inputs) declared() map[string]catalogue.Manifest {
if len(in.Held) == 0 {
return in.Manifests
}
out := make(map[string]catalogue.Manifest, len(in.Manifests)+len(in.Held))
for m, man := range in.Held {
out[m] = man
}
for m, man := range in.Manifests {
out[m] = man
}
return out
}
@@ -27,7 +27,8 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
var gaps []Gap
seats := map[string]catalogue.SeatDeclaration{}
declarers := map[string]string{}
for _, m := range in.Manifests {
declared := in.declared()
for _, m := range declared {
for _, s := range m.DefinesSeats {
seats[s.Name] = s
declarers[s.Name] = m.Module
@@ -79,7 +80,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
for _, sr := range catalogue.ReadsGranted(m, here, node) {
d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key})
}
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, in.Manifests, node, values, d.Consumes)
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes)
d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values)
return d
}
@@ -101,7 +102,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
}
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
// part; it adds no principal, since each node's engine speaks as the node's account.
if m, ok := in.Manifests[buscompose.EngineModule]; ok {
if m, ok := declared[buscompose.EngineModule]; ok {
out.Engine = &buscompose.Declared{Module: m.Module, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
}
return out, gaps