Compare commits

..
3 Commits
Author SHA1 Message Date
jschoubben 83f7a7612d Review round 2: a held module's object reads come from its running manifest; one with none keeps the list in force, said; indexer_judge asks the engine for the running build again (issue 774)
mesh/delivery ready: it is merged on the Approve
mesh/delivery-group group feat/l3a-waits rejected: composed together: the checker's verdict at the members' heads (de2cdacb9cde): shanks
mesh/check failed (3): shanks: novox/mesh-sdk#163 does not merge into the trunk: files in conflict: go/deli…; and 2 more | 83f7a7612d19 on c4470f42dba9
2026-11-03 13:07:11 +01:00
jschoubben 1fe1beee58 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) 2026-11-03 13:03:11 +01:00
jschoubben 40da5e924e A manifest with a field the judge does not know waits instead of being refused; a held module keeps its bus users; tools list and redo the verdicts (ADR 0578 amended, issue 774) 2026-11-03 12:53:07 +01:00
58 changed files with 957 additions and 1230 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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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=
@@ -128,7 +128,9 @@ func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) {
delete(m.by, module)
return
}
m.by[module] = *v
// The sandbox's composer may be the node-engine of an older delivery, which does not know the verdict waits: it is handed a
// wait as the refusal it is held as.
m.by[module] = v.AsRefused()
}
func (m *manifestVerdicts) all() map[string]delivery.ManifestVerdict {
@@ -170,4 +170,10 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) {
if _, err := parseManifest(manifest); err != nil {
t.Errorf("manifestReader: %v", err)
}
// The sandbox's composer may be an older node-engine, which knows no verdict waits: it is handed a wait as a refusal.
var held manifestVerdicts
held.put("nats", &delivery.ManifestVerdict{Module: "nats", State: delivery.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"})
if v := held.all()["nats"]; v.State != delivery.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 {
t.Errorf("a waiting verdict is handed to the sandbox as %+v", v)
}
}
+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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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,7 @@
// 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). The read tools of the indexer's states are
// 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
@@ -21,23 +22,10 @@ import (
// asker writes a takeins value and waits for the indexer's answer to it.
type asker func(key string, value []byte) (index.TakeInAsk, error)
// Tools are the indexer's tools.
func Tools(ask asker, now func() time.Time, counts func() (index.OldSectionsCount, error)) []stdio.Tool {
// Tools are the indexer's tools. read is the indexer's state of verdicts, which indexer_verdicts reads.
func Tools(ask asker, now func() time.Time, read stdio.KeptState) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: index.OldSectionsTool,
Description: "How many accepted modules use each old section of a manifest (state, logs, streams, objects), the foundation " +
"(nats, mesh-grants, the indexer) left out: the count the judge refuses a use of a section at 0 by, as modules move " +
"their bus state into `requires` by template. Read from the indexer's oldsections state, kept as the indexer judges " +
"the manifests; an empty answer means the indexer has not judged since it started.",
Input: map[string]any{"type": "object", "properties": map[string]any{}},
Run: func(map[string]any) (any, error) {
c, err := counts()
if err != nil {
return nil, err
}
return c.Counts, nil
}},
{Name: index.TakeInTool,
Description: "Take a registered module in now: the indexer reads its manifest from the repository and commit the controller's " +
"build records register it from (at the head the gate's source names, when it names one), whatever it tried before, and " +
@@ -62,6 +50,50 @@ func Tools(ask asker, now func() time.Time, counts func() (index.OldSectionsCoun
}
return a.Result, nil
}},
{Name: index.VerdictsTool,
Description: "The indexer's verdicts on manifests that wait or are refused (all: every verdict), each as its verdicts state holds it: " +
"the module, the digest, the state (waits: the manifest carries a field the indexer's judge does not know yet, named in " +
"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 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"}}},
Run: func(args map[string]any) (any, error) {
keys, err := read.Keys()
if err != nil {
return nil, fmt.Errorf("the verdicts state cannot be listed: %w", err)
}
all, _ := args["all"].(bool)
rows, unread := index.ListVerdicts(keys, func(key string) ([]byte, error) {
e, err := read.Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}, str(args, "module"), all)
return map[string]any{"verdicts": rows, "unread": unread}, nil
}},
{Name: index.JudgeAgainTool,
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.",
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"},
"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())
if err != nil {
return nil, err
}
a, err := ask(key, value)
if err != nil {
return nil, err
}
return a.Result, nil
}},
}
}
@@ -118,22 +150,8 @@ func askTakeIn(s stdio.KeptState, bound time.Duration) asker {
}
}
// heldCounts reads the count of the old sections the indexer keeps.
func heldCounts(s stdio.KeptState) func() (index.OldSectionsCount, error) {
return func() (index.OldSectionsCount, error) {
e, err := s.Get(index.OldSectionsKey)
if err != nil {
return index.OldSectionsCount{}, fmt.Errorf("reading %s.%s: %w", index.OldSectionsBucket, index.OldSectionsKey, err)
}
if e == nil {
return index.OldSectionsCount{}, fmt.Errorf("the indexer has not counted the old sections yet: %s holds no %s", index.OldSectionsBucket, index.OldSectionsKey)
}
return index.DecodeOldSections(e.Value)
}
}
func main() {
if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, heldCounts(stdio.State(index.OldSectionsState)))); 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)
}
@@ -296,14 +296,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The published packages (ADR 0593 §2): a state this delivery brings, bound when it appears; until then every
// fingerprint takes the version line.
packages := process.AwaitBucket(ctx, js, conn, delivery.PackagesBucket, "no package is published, so every fingerprint takes the version line", logger.Printf)
// How many modules use each old section (issue 1966 D4): a state this delivery brings, bound when it appears; until then
// the judge refuses no use of an old section.
oldSections := process.AwaitBucket(ctx, js, conn, index.OldSectionsBucket, "the count of the old sections is not kept, so the judge refuses no use of one", logger.Printf)
ix := &index.Indexer{OldSections: awaitedStore{oldSections}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known,
Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), SayAccepted: sayAccepted(js, cred, logger.Printf),
ix := &index.Indexer{Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known,
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
@@ -424,8 +422,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
logger.Printf("judging the manifests at start: %v; judged again at the next message heard", err)
return
}
logger.Printf("judged %d manifests at start: %d verdicts written, %d with problems, of which outgrown and kept accepted: %v",
j.Judged, j.Written, len(j.Refused), j.Outgrown)
logger.Printf("judged %d manifests at start: %d verdicts written, %d with problems, of which outgrown and kept accepted: %v, waiting for a judge that knows their fields: %v",
j.Judged, j.Written, len(j.Refused), j.Outgrown, j.Waiting)
})
// A build module says each time it wrote its toolchains key: its stream, and the indexer's durable consumer on it, are
// made by the controller as the forge's are. Where the stream is not there yet the indexer says so and is started again
@@ -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
}
}
@@ -14,6 +14,7 @@ import (
// takerIn is the part of the indexer a takeins value asks through.
type takerIn interface {
TakeIn(ctx context.Context, module string) index.TakenIn
JudgeAgain(ctx context.Context, module 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
@@ -31,9 +32,14 @@ func takenIn(ctx context.Context, ix takerIn, kv jetstream.KeyValue, e jetstream
if !a.Answered.IsZero() {
return
}
r := ix.TakeIn(ctx, a.Module)
asked, r := "take-in", index.TakenIn{}
if a.Judge {
asked, r = "judging again", ix.JudgeAgain(ctx, a.Module)
} else {
r = ix.TakeIn(ctx, a.Module)
}
a.Result, a.Answered = &r, time.Now().UTC()
logf("%s: a take-in asked by %s (%s): %s", a.Module, a.By, a.Why, r.Answer)
logf("%s: a %s asked by %s (%s): %s", a.Module, asked, a.By, a.Why, r.Answer)
data, err := json.Marshal(a)
if err != nil {
return
@@ -36,29 +36,3 @@ func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string,
}
}
}
// sayAccepted says the indexer's event manifest-accepted (issue 1966 D3, novox/hq ADR 0595 §4):
// mesh.mod.<module>.event.manifest-accepted, carrying the module's bus-state entries whole, with the digest and judge as
// the bus's dedup id (delivery.ManifestAccepted.EventID), so one acceptance is said once.
func sayAccepted(js jetstream.JetStream, cred process.Account, logf func(string, ...any)) func(context.Context, delivery.ManifestAccepted) {
return func(ctx context.Context, a delivery.ManifestAccepted) {
data, err := json.Marshal(a)
if err != nil {
logf("judge: %s for %s is not said: %v", delivery.ManifestAcceptedEvent, a.Module, err)
return
}
msg := nats.NewMsg(cred.EventSubject(delivery.ManifestAcceptedEvent))
id := a.EventID()
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", cred.Module)
msg.Header.Set("x-node", cred.Node)
msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json")
msg.Data = data
pub, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
if _, err := js.PublishMsg(pub, msg, jetstream.WithMsgID(id)); err != nil {
logf("judge: %s", fmt.Errorf("saying %s for %s: %w", delivery.ManifestAcceptedEvent, a.Module, err))
}
}
}
+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.20261103142843-f8bd12e4b7b1
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 -4
View File
@@ -1,7 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 h1:T66tg9T/1hHg6KN/bDScHPT41IHkxLAGGO8VJiAOlb8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1/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=
@@ -54,16 +54,10 @@ type Indexer struct {
Taken Store
// Verdicts is the verdicts state (verdicts.go, novox/hq ADR 0578 §1); nil judges nothing.
Verdicts Store
// OldSections is the state of how many accepted modules use each old section (oldsections.go, issue 1966 D4); nil keeps no
// count and the judge refuses no use of an old section.
OldSections Store
// JudgeID names the judge a verdict says made it; empty is judge.Identity, the running program's.
JudgeID string
// SayRefused says the event manifest-refused for a refusal newly written; nil says nothing.
SayRefused func(ctx context.Context, r delivery.ManifestRefused)
// SayAccepted says the event manifest-accepted for an accepted verdict newly written whose manifest asks for bus state,
// carrying the entries whole (verdicts.go; issue 1966 D3); nil says nothing.
SayAccepted func(ctx context.Context, a delivery.ManifestAccepted)
// Packages is the packages state (delivery.PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none,
// and every fingerprint takes the version line.
Packages Store
@@ -98,22 +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)
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) {
@@ -1,76 +0,0 @@
package index
import (
"context"
"encoding/json"
"fmt"
"maps"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/judge"
)
// How many accepted modules use each old section (issue 1966 D4; novox/hq ADR 0595 §11): the indexer counts them, the
// foundation left out, as it judges, keeps the count in its state oldsections under one key, and the tool
// indexer_old_sections reads it. The judge refuses a manifest of another module that uses a section whose count is 0.
const (
// OldSectionsTool is the tool that reads the count.
OldSectionsTool = "indexer_old_sections"
// OldSectionsState is the indexer's state of the count: the bucket mesh-delivery-indexer_oldsections.
OldSectionsState = "oldsections"
OldSectionsBucket = delivery.IndexerModule + "_" + OldSectionsState
// OldSectionsKey is the one key it holds.
OldSectionsKey = "counts"
)
// OldSectionsCount is the value of the oldsections state: per old section, the accepted modules outside the foundation using it.
type OldSectionsCount struct {
V int `json:"v"`
Counts map[string]int `json:"counts"`
}
// DecodeOldSections reads the oldsections value.
func DecodeOldSections(value []byte) (OldSectionsCount, error) {
var c OldSectionsCount
if err := json.Unmarshal(value, &c); err != nil {
return c, fmt.Errorf("the count of the old sections cannot be read: %w", err)
}
if c.V != 1 {
return c, fmt.Errorf("a count of the old sections of version %d, not 1", c.V)
}
return c, nil
}
// heldOldSections is the count the last pass wrote (the accepted set it held): a module that keeps using a section was
// accepted with it, so it is counted, and a fresh manifest changes nothing in it. Nil when none is held yet or it cannot be
// read; a failed read is said, never taken as 0 (issue 1966 D4).
func (ix *Indexer) heldOldSections(ctx context.Context) judge.OldSectionCounts {
value, _, found, err := ix.OldSections.Get(ctx, OldSectionsKey)
if err == nil && found {
var c OldSectionsCount
if c, err = DecodeOldSections(value); err == nil {
return c.Counts
}
}
if err != nil {
ix.logf("judge: the count of the old sections cannot be read (%v), so the zero rule is not applied in this pass", err)
}
return nil
}
// keepOldSections writes the counts under OldSectionsKey when they differ from the ones held.
func (ix *Indexer) keepOldSections(ctx context.Context, counts judge.OldSectionCounts) error {
if ix.OldSections == nil {
return nil
}
return write(ctx, ix.OldSections, OldSectionsKey, func(value []byte, found bool) ([]byte, bool, error) {
if found {
if cur, err := DecodeOldSections(value); err == nil && maps.Equal(cur.Counts, counts) {
return nil, false, nil
}
}
next, err := json.Marshal(OldSectionsCount{V: 1, Counts: counts})
return next, false, err
})
}
@@ -25,9 +25,11 @@ const (
// TakeInAsk is a value of the takeins state: which module a person asked to be taken in, why, who and when, and, once the
// indexer read it, its answer.
type TakeInAsk struct {
V int `json:"v"`
Module string `json:"module,omitempty"`
Why string `json:"why,omitempty"`
V int `json:"v"`
Module string `json:"module,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"`
By string `json:"by,omitempty"`
At time.Time `json:"at"`
Result *TakenIn `json:"result,omitempty"`
@@ -353,7 +353,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
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.
next.Verdict, next.Problems, next.Warnings = cur.Verdict, cur.Problems, cur.Warnings
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
}
if held && sameButWhere(cur, next) {
return nil, false, nil
@@ -380,7 +380,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
func sameButWhere(cur, next delivery.Module) bool {
next = withV(next)
next.Commit, next.Src = cur.Commit, cur.Src
next.Verdict, next.Problems, next.Warnings = cur.Verdict, cur.Problems, cur.Warnings
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
return reflect.DeepEqual(cur, next)
}
@@ -7,6 +7,8 @@ import (
"fmt"
"maps"
"slices"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -18,19 +20,23 @@ import (
// The indexer judges every manifest it holds, once, with its own judge (novox/hq ADR 0578 §1): it writes each verdict in
// its state verdicts (delivery.VerdictsBucket), one key per manifest digest, only when the verdict differs from the one
// held; a module's value in the modules state carries a refusal (Verdict, Problems), so a reader of modules takes it
// without a second read; a new refusal is said as the event manifest-refused. It judges at its start, so a judge that
// without a second read; a new refusal is said as the event manifest-refused; a manifest with a field the judge does not know waits instead (ownBuildIsNewest). It judges at its start, so a judge that
// 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
Refused map[string][]string
// Outgrown are the modules accepted before whose problems are now warnings (ADR 0579 §1).
Outgrown []string
// Waiting are the modules whose manifest carries a field this judge does not know and waits.
Waiting []string
}
// JudgeAll judges every manifest the modules state names, as one catalogue with the predefined list the indexer keeps,
@@ -43,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
@@ -74,26 +84,37 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
raws[name] = []byte(kept.JSON)
digests[name] = digest
}
predefined := ix.predefinedList(ctx)
// The old sections end when no module is left in them (issue 1966 D4): the count is the accepted set the last pass held.
var counts judge.OldSectionCounts
if ix.OldSections != nil {
counts = ix.heldOldSections(ctx)
}
refused, unnamed := judge.ShelfCounting(raws, predefined, 0, counts)
refused, waits, unnamed := judge.ShelfWaiting(raws, ix.predefinedList(ctx), 0)
for _, p := range unnamed {
ix.logf("judge: a problem between the manifests that no one module's removal ends, so it refuses none: %s", p)
}
out.Refused = refused
// A manifest with a field this judge does not know waits for a judge that may know it (clause 3 of the record on a
// 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()
}
var accepted []catalogue.Manifest
for _, name := range slices.Sorted(maps.Keys(digests)) {
out.Judged++
v := delivery.ManifestVerdict{Module: name, State: delivery.VerdictAccepted, Judge: id, At: ix.now()}
if problems := refused[name]; len(problems) > 0 {
if fields := waits[name]; len(fields) > 0 {
accepted := ix.acceptedBefore(ctx, digests[name], fresh)
if !newest && !accepted {
v.State, v.Unknown = delivery.VerdictManifestWaits, fields
out.Waiting = append(out.Waiting, name)
} else {
_, problems := judge.Manifest(raws[name], ix.predefinedList(ctx))
if accepted {
v.Warnings = problems
out.Outgrown = append(out.Outgrown, name)
} else {
v.State, v.Problems = delivery.VerdictRefused, problems
}
}
} else if problems := refused[name]; len(problems) > 0 {
if ix.acceptedBefore(ctx, digests[name], fresh) {
v.Warnings = problems
out.Outgrown = append(out.Outgrown, name)
@@ -101,12 +122,6 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
v.State, v.Problems = delivery.VerdictRefused, problems
}
}
if !v.Refused() {
if m, err := catalogue.Decode(raws[name]); err == nil {
m.Module = name
accepted = append(accepted, m)
}
}
wrote, err := ix.writeVerdict(ctx, digests[name], v)
if errors.Is(err, process.ErrNotYet) {
return out, fmt.Errorf("the state %s is not on the bus yet: %w", VerdictsBucket, err)
@@ -117,31 +132,27 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
}
if wrote {
out.Written++
if v.Waits() {
ix.logf("judge: %s", v.Why())
}
if v.Refused() {
ix.logf("judge: %s", v.Why())
if ix.SayRefused != nil {
ix.SayRefused(ctx, delivery.RefusedOf(digests[name], v))
}
} else if ix.SayAccepted != nil {
// A manifest asking for bus state, newly accepted, is said whole to the bus module (issue 1966 D3, ADR 0595 §4).
if m, problems := judge.Manifest(raws[name], predefined); len(problems) == 0 && len(m.BusState) > 0 {
ix.SayAccepted(ctx, delivery.AcceptedOf(digests[name], v, m))
}
}
}
if err := ix.carryVerdict(ctx, name, digests[name], v); err != nil {
ix.logf("judge: modules.%s does not carry its verdict: %v", name, err)
}
}
if err := ix.keepOldSections(ctx, judge.CountOldSections(accepted)); err != nil {
ix.logf("judge: the count of the old sections is not kept: %v", err)
}
return out, nil
}
// 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) {
ix.learnOwnBuild(ctx)
j, err := ix.JudgeAll(ctx, nil)
ix.judgePending.Store(err != nil)
return j, err
@@ -170,7 +181,7 @@ func (ix *Indexer) acceptedBefore(ctx context.Context, digest string, fresh map[
return !fresh[digest]
}
prev, err := delivery.DecodeManifestVerdict(value)
return err == nil && !prev.Refused()
return err == nil && prev.State == delivery.VerdictAccepted
}
// writeVerdict writes v under the digest's key unless the verdict held there says the same (ManifestVerdict.Same); wrote
@@ -194,6 +205,36 @@ func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.M
return wrote, err
}
// 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 == "" {
return false
}
ix.ownMu.Lock()
defer ix.ownMu.Unlock()
return ix.ownFP != "" && ix.ownFP == fp
}
// 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
// judge accepts (a reader takes a value without a verdict as accepted, delivery.TakeManifestVerdict).
@@ -210,14 +251,18 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli
if err != nil || cur.Manifest != digest {
return nil, false, nil // gone, unreadable, or moved to another manifest since: the next pass judges it
}
state := ""
if v.Refused() {
// A wait is written as a refusal naming the fields, so a reader pinned before the verdict waits holds it as refused.
state, problems := "", v.Problems
switch {
case v.Waits():
state, problems = delivery.VerdictRefused, []string{v.WaitsOn()}
case v.Refused():
state = delivery.VerdictRefused
}
if cur.Verdict == state && slices.Equal(cur.Problems, v.Problems) && 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) {
return nil, false, nil
}
cur.Verdict, cur.Problems, cur.Warnings = state, v.Problems, v.Warnings
cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings = state, problems, v.Unknown, v.Warnings
if state == "" {
cur.Problems = nil
}
@@ -247,3 +292,111 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList
}
return list
}
// 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 judges one module again now.
const (
VerdictsTool = "indexer_verdicts"
JudgeAgainTool = "indexer_judge"
judgeAgainPrefix = "judge."
)
// VerdictRow is one verdict as indexer_verdicts lists it: the manifest's digest, the module, its state, the fields it waits
// on or the problems that refuse it, and the judge that made it.
type VerdictRow struct {
Module string `json:"module"`
Digest string `json:"digest"`
State string `json:"state"`
Words string `json:"words,omitempty"`
Unknown []string `json:"unknown,omitempty"`
Problems []string `json:"problems,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Judge string `json:"judge"`
At time.Time `json:"at"`
}
// ListVerdicts reads the verdicts under keys with get and lists those of module (every module when empty) that wait or are
// refused, or every one with all, sorted by module and digest. A value that does not read is said in unread by its key.
func ListVerdicts(keys []string, get func(key string) ([]byte, error), module string, all bool) (rows []VerdictRow, unread map[string]string) {
unread = map[string]string{}
for _, key := range keys {
value, err := get(key)
if err != nil || value == nil {
if err != nil {
unread[key] = err.Error()
}
continue
}
v, err := delivery.DecodeManifestVerdict(value)
if err != nil {
unread[key] = err.Error()
continue
}
if (module != "" && v.Module != module) || (!all && !v.Held()) {
continue
}
rows = append(rows, VerdictRow{Module: v.Module, Digest: key, State: v.State, Words: v.Why(), Unknown: v.Unknown,
Problems: v.Problems, Warnings: v.Warnings, Judge: v.Judge, At: v.At})
}
slices.SortFunc(rows, func(a, b VerdictRow) int {
if c := strings.Compare(a.Module, b.Module); c != 0 {
return c
}
return strings.Compare(a.Digest, b.Digest)
})
return rows, unread
}
// 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 {
out := TakenIn{Module: module}
held, err := ix.heldModules(ctx)
if err != nil {
out.Answer = "the modules state cannot be read: " + err.Error()
return out
}
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)
if err != nil || !found {
out.Answer = "judged, but the verdict on " + module + " cannot be read"
return out
}
v, err := delivery.DecodeManifestVerdict(value)
if err != nil {
out.Answer = "judged, but the verdict on " + module + " cannot be read: " + err.Error()
return out
}
out.Stored = true
out.Answer = "judged again by " + v.Judge + ": " + v.State
if v.Held() {
out.Answer += " (" + v.Why() + ")"
}
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) {
key, data, err := NewTakeIn(module, why, by, at)
if err != nil {
return "", nil, err
}
var a TakeInAsk
if err := json.Unmarshal(data, &a); err != nil {
return "", nil, err
}
a.Judge = true
data, err = json.Marshal(a)
return judgeAgainPrefix + key, data, err
}
@@ -3,7 +3,6 @@ package index
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
@@ -120,92 +119,88 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil || !w.fresh[other] {
t.Fatalf("a changed digest is not fresh: %v %v", w.fresh, err)
}
// 1966/AC10: a newly accepted manifest asking for bus state is said once as manifest-accepted, carrying both entries
// whole; a verdict already held, and a manifest asking for none, say nothing.
var accepted []delivery.ManifestAccepted
ix.SayAccepted = func(_ context.Context, a delivery.ManifestAccepted) { accepted = append(accepted, a) }
moved := hold("moved", `{"module":"moved","version":"1","requires":[`+
`{"name":"cache","storage":"key-value","template":"state","options":{"history":8}},`+
`{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64}}]}`)
for pass := 0; pass < 2; pass++ {
if _, err := ix.JudgeAll(ctx, map[string]bool{moved: true}); err != nil {
t.Fatal(err)
}
// Clause 3 of the record on a release's Approve: a manifest with a field this judge does not know waits (no refusal, no
// event) while a newer build of the indexer is on the trunk, and is judged again at the next judge; once this program's
// build is the trunk's newest it is refused; a wait is written into the module's value as a refusal naming the fields.
ownFP := "src2:" + strings.Repeat("e", 64)
hold(delivery.IndexerModule, `{"module":"mesh-delivery-indexer","version":"1"}`)
ix.ownFP = ownFP // this program started when the trunk held another build of the indexer than the one it holds now
later := hold("later", `{"module":"later","version":"1","a-field-from-later":1,"another":{"x":1}}`)
said = nil
before := len(said)
j, err = ix.JudgeAll(ctx, map[string]bool{later: true})
if err != nil || len(j.Waiting) != 1 || j.Waiting[0] != "later" || len(said) != before {
t.Fatalf("a manifest with fields from later: %+v %v, said %d", j, err, len(said))
}
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" {
t.Fatalf("said accepted %+v", accepted)
if v := verdict(later); !v.Waits() || v.Refused() || strings.Join(v.Unknown, ",") != "a-field-from-later,another" {
t.Fatalf("later: %+v", v)
}
// 1966/AC11: modules using state and none using logs but the foundation: logs counts 0, so a manifest of another
// module using logs is refused and the foundation's is accepted.
ix.OldSections = newMem()
hold("user", `{"module":"user","version":"1","state":["x"]}`)
hold("mesh-grants", `{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)
if _, err := ix.JudgeAll(ctx, nil); err != nil {
t.Fatal(err)
if m := module("later"); m.Verdict != delivery.VerdictRefused || len(m.Unknown) != 2 || !m.VerdictOf("later").Waits() ||
!strings.Contains(m.Problems[0], "does not know yet") {
t.Fatalf("modules.later carries %+v", m)
}
counts := func() map[string]int {
value, _, found, err := ix.OldSections.Get(ctx, OldSectionsKey)
if err != nil || !found {
t.Fatalf("no count of the old sections: %v", err)
}
c, err := DecodeOldSections(value)
if err != nil {
t.Fatal(err)
}
return c.Counts
// 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))
}
if c := counts(); c["state"] != 1 || c["logs"] != 0 || c["streams"] != 0 || c["objects"] != 0 {
t.Fatalf("the old sections count %v, want state 1 and the rest 0", c)
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))
}
late := hold("late", `{"module":"late","version":"1","logs":[{"name":"l"}]}`)
grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2","logs":[{"name":"l"},{"name":"m"}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{late: true, grants: true}); err != nil {
t.Fatal(err)
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() {
t.Fatalf("a renamed field: %+v %v", j, err)
}
if v := verdict(late); !v.Refused() || !strings.Contains(v.Problems[0], "late uses the old section logs, which no other module uses any more") {
t.Fatalf("late: %+v", v)
// The trunk's newest build of the indexer is this program's: the field is one no judge to come knows.
ix.ownFP = "src2:" + strings.Repeat("a", 64)
if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Waiting) != 0 || !verdict(later).Refused() || !strings.Contains(verdict(later).Problems[0], "a-field-from-later") {
t.Fatalf("the newest build refuses: %+v %v %+v", j, err, verdict(later))
}
if v := verdict(grants); v.Refused() {
t.Fatalf("the foundation's manifest was refused: %+v", v)
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)
}
// 1966/AC11: the sole user of an old section changes its manifest and keeps the section: accepted, since it was accepted
// with it (the count is the accepted set held from the last pass, not recounted from fresh digests).
hold("solo", `{"module":"solo","version":"1","objects":[{"name":"o","max-mib":1}]}`)
if _, err := ix.JudgeAll(ctx, nil); err != nil {
t.Fatal(err)
// 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 }
if rows, unread := ListVerdicts(keys, get, "later", false); len(rows) != 1 || rows[0].State != delivery.VerdictRefused || len(unread) != 0 {
t.Fatalf("indexer_verdicts of later: %+v %v", rows, unread)
}
solo2 := hold("solo", `{"module":"solo","version":"2","objects":[{"name":"o","max-mib":2}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{solo2: true}); err != nil {
t.Fatal(err)
if rows, _ := ListVerdicts(keys, get, "", true); len(rows) <= len(keys)-3 || rows[0].Module > rows[len(rows)-1].Module {
t.Fatalf("indexer_verdicts, all: %d rows of %d verdicts", len(rows), len(keys))
}
if v := verdict(solo2); v.Refused() {
t.Fatalf("the sole user of objects, changing its manifest and keeping objects, was refused: %+v", v)
// indexer_judge asks the engine for the running build again: a start pass that missed it is recovered (the trunk's value of
// 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 == "" {
t.Fatalf("indexer_judge did not learn the running build: %+v", got)
}
// A count that cannot be read is never taken as 0: the zero rule is not applied in that pass and the log says so.
var logged []string
ix.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
held := ix.OldSections
ix.OldSections = unreadable{held}
retry := hold("late", `{"module":"late","version":"2","logs":[{"name":"l"}]}`)
if _, err := ix.JudgeAll(ctx, map[string]bool{retry: true}); err != nil {
t.Fatal(err)
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") {
t.Fatalf("indexer_judge: %+v", got)
}
if v := verdict(retry); v.Refused() || !strings.Contains(strings.Join(logged, "\n"), "the zero rule is not applied") {
t.Fatalf("a count that could not be read: %+v, logged %q", v, logged)
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 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)
} else if a, err := DecodeTakeIn(value); err != nil || !a.Judge || a.Module != "later" {
t.Fatalf("the ask's value: %+v %v", a, err)
}
ix.OldSections = held
// Without the verdicts state the indexer judges nothing.
ix.Verdicts = nil
if j, err := ix.JudgeAll(ctx, nil); err != nil || j.Judged != 0 {
t.Fatalf("without the state: %+v %v", j, err)
}
}
// unreadable is a store whose reads fail.
type unreadable struct{ Store }
func (unreadable) Get(context.Context, string) ([]byte, uint64, bool, error) {
return nil, 0, false, errors.New("the bus did not answer")
}
@@ -1,420 +0,0 @@
package judge
import (
"bytes"
"encoding/json"
"fmt"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The rules for bus state asked for in `requires` (issue 1966 D1; novox/hq ADR 0595 §1 to §3, ADR 0596 §2, ADR 0597 §2,
// design 25 § 12.1). mesh-sdk keeps only the entry's shape (ADR 0578 §7); every bound below is this judge's. An entry is
// judged once, here, and a refusal names the module, the entry and the field.
// optionKind is what an option's value is.
type optionKind int
const (
optionInt optionKind = iota
optionFlag
optionText
optionEvents
)
// optionRule is one option a template takes: its kind, its bound where it is a number, and whether it must be given.
type optionRule struct {
kind optionKind
least, most int
required bool
}
// busTemplate is a template: the storage type it is on, the options it takes, and what it fixes in plain words.
type busTemplate struct {
storage string
options map[string]optionRule
fixed string
}
// busTemplates are the six templates of design 25 § 12.1 and the bounds of its table.
var busTemplates = map[string]busTemplate{
catalogue.TemplateState: {
storage: catalogue.StorageKeyValue,
fixed: "what happens when full, the size of a value and of the bucket, and the copies",
options: map[string]optionRule{
catalogue.OptionPastValues: {kind: optionInt, least: 1, most: catalogue.StateMostHistory},
catalogue.OptionTimeToLive: {kind: optionInt, least: 1, most: 315360000},
catalogue.OptionLifetimePerKey: {kind: optionFlag},
catalogue.OptionPerMachine: {kind: optionFlag},
},
},
catalogue.TemplateEvents: {
storage: catalogue.StorageStream,
fixed: "what happens when full (the oldest is dropped) and the module's own subjects",
options: map[string]optionRule{
"events": {kind: optionEvents, required: true},
catalogue.OptionAgeDays: {kind: optionInt, least: catalogue.StreamLeastDays, most: catalogue.StreamMostDays},
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
catalogue.OptionGrant: {kind: optionText},
},
},
catalogue.TemplateWorkQueue: {
storage: catalogue.StorageStream,
fixed: "what happens when full (a new message is refused), one taker, and no grant",
options: map[string]optionRule{
"events": {kind: optionEvents, required: true},
catalogue.OptionDeliveries: {kind: optionInt, least: catalogue.StreamLeastDeliveries, most: catalogue.StreamMostDeliveries},
catalogue.OptionDuplicates: {kind: optionInt, least: 1, most: catalogue.StreamMostDays * 86400},
catalogue.OptionAgeDays: {kind: optionInt, least: catalogue.StreamLeastDays, most: catalogue.StreamMostDays},
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
},
},
catalogue.TemplateLog: {
storage: catalogue.StorageStream,
fixed: "what happens when full (a new entry is refused), that an entry is never rewritten, and its size",
options: map[string]optionRule{
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650},
},
},
catalogue.TemplateObjects: {
storage: catalogue.StorageObjectStore,
fixed: "what happens when full (a new object is refused) and that objects are kept for good",
options: map[string]optionRule{
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.ObjectsLeastMiB, most: catalogue.ObjectsMostMiB, required: true},
catalogue.OptionGrant: {kind: optionText},
},
},
catalogue.TemplateLedger: {
storage: catalogue.StorageLedger,
fixed: "what happens when full (a new entry is refused) and its state and index buckets",
options: map[string]optionRule{
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB},
},
},
}
// bareStream is a stream of a module's own configuration, where no template fits: the three settings the judge
// refuses one for missing (design 25 § 12.1), and those it may also give.
var bareStream = map[string]optionRule{
catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.StreamLeastMiB, most: catalogue.StreamMostMiB, required: true},
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650, required: true},
catalogue.OptionGrant: {kind: optionText},
"events": {kind: optionEvents},
}
// ledgerConfiguration is what a ledger's own configuration may give: how long its history is kept (design 25 § 12.1).
var ledgerConfiguration = map[string]optionRule{
catalogue.OptionAgeDays: {kind: optionInt, least: 1, most: 3650},
}
// noAge is how a bare stream says it keeps its messages for good.
const noAge = "none"
// ledgerEffects are the four effects an operation may have (ADR 0597 §2).
var ledgerEffects = []string{catalogue.EffectSetFields, catalogue.EffectSetEntry, catalogue.EffectAppend, catalogue.EffectRemove}
// BusStateProblems is what is wrong with one manifest's bus state, judged on the manifest alone (issue 1966 D1).
func BusStateProblems(m catalogue.Manifest) []string {
if len(m.BusState) == 0 {
return nil
}
var problems []string
if !stateName.MatchString(m.Module) {
problems = append(problems, fmt.Sprintf(
"%s keeps bus state, and a module's name is part of its names on the bus, which take one plain name — "+
"no dot (novox/hq ADR 0201, ADR 0595 §1)", m.Module))
}
storageOf := map[string]string{}
oldSection := map[string]string{} // a local name an old section keeps, to that section (issue 1966 D2)
for _, st := range m.State {
oldSection[st.Name] = "state"
}
for _, l := range m.Logs {
oldSection[l.Name] = "logs"
}
for _, st := range m.Streams {
oldSection[st.Name] = "streams"
}
for _, o := range m.Objects {
oldSection[o.Name] = "objects"
}
terminal := catalogue.TerminalKeys(m)
for _, e := range m.BusState {
at := fmt.Sprintf("%s's bus state %q", m.Module, e.Name)
switch {
case strings.Contains(e.Name, "_"):
problems = append(problems, fmt.Sprintf(
"%s: its name holds an underscore, which separates the module from the local name on the bus; use a "+
"hyphen (novox/hq design 25 § 12.1)", at))
case !stateName.MatchString(e.Name):
problems = append(problems, fmt.Sprintf(
"%s: its name is a local name — lower-case letters, digits and hyphens, no dot and no underscore; the "+
"mesh derives the name on the bus (novox/hq ADR 0201)", at))
}
if first, taken := storageOf[e.Name]; taken {
problems = append(problems, fmt.Sprintf(
"%s: the name is used twice (as %s and as %s); each name is one, whatever its storage type (novox/hq "+
"ADR 0595 §1)", at, first, e.Storage))
}
storageOf[e.Name] = e.Storage
if section, kept := oldSection[e.Name]; kept {
problems = append(problems, fmt.Sprintf(
"%s: the name is also kept in the old section %s; one local name is one form, so a module that moves an "+
"entry removes it from the old section (novox/hq ADR 0595 §11)", at, section))
}
problems = append(problems, busEntryProblems(at, e, terminal)...)
}
return problems
}
// busEntryProblems judges one entry's storage type, template, options and configuration.
func busEntryProblems(at string, e catalogue.BusStateEntry, terminal []string) []string {
var problems []string
switch e.Storage {
case catalogue.StorageStream, catalogue.StorageKeyValue, catalogue.StorageObjectStore, catalogue.StorageLedger:
default:
return []string{fmt.Sprintf("%s: the storage type %q is unknown; it is %s", at, e.Storage,
strings.Join([]string{catalogue.StorageStream, catalogue.StorageKeyValue, catalogue.StorageObjectStore, catalogue.StorageLedger}, ", "))}
}
if e.Storage != catalogue.StorageLedger && len(e.Types) > 0 {
problems = append(problems, fmt.Sprintf("%s: types are a ledger's; storage type %s has none", at, e.Storage))
}
if e.Template == "" {
if e.Storage != catalogue.StorageStream {
return append(problems, fmt.Sprintf(
"%s: the storage type %s is asked for by its template; only a stream is asked for bare, with a "+
"configuration of its own (novox/hq design 25 § 12.1)", at, e.Storage))
}
if len(e.Options) > 0 {
problems = append(problems, fmt.Sprintf(
"%s: a bare stream gives its settings in its configuration, not in options", at))
}
return append(problems, settingsProblems(at, "configuration", e.Configuration, bareStream, "a bare stream", terminal, true)...)
}
tpl, known := busTemplates[e.Template]
switch {
case !known:
names := make([]string, 0, len(busTemplates))
for n := range busTemplates {
names = append(names, n)
}
slices.Sort(names)
return append(problems, fmt.Sprintf("%s: the template %q is unknown; it is %s", at, e.Template, strings.Join(names, ", ")))
case tpl.storage != e.Storage:
return append(problems, fmt.Sprintf(
"%s: the template %s is on the storage type %s, not %s", at, e.Template, tpl.storage, e.Storage))
}
problems = append(problems, settingsProblems(at, "options", e.Options, tpl.options, "the template "+e.Template+" (it fixes "+tpl.fixed+")", terminal, false)...)
if e.Template == catalogue.TemplateLedger {
problems = append(problems, settingsProblems(at, "configuration", e.Configuration, ledgerConfiguration, "a ledger's own configuration", terminal, false)...)
problems = append(problems, ledgerTypeProblems(at, e)...)
} else if len(e.Configuration) > 0 {
problems = append(problems, fmt.Sprintf(
"%s: the template %s is given a configuration; a configuration of its own is for a bare stream or a "+
"ledger whose history expires, and a template's settings are its options (novox/hq ADR 0595 §2)", at, e.Template))
}
if e.Template == catalogue.TemplateWorkQueue {
problems = append(problems, workQueueProblems(at, e)...)
}
return problems
}
// settingsProblems judges the given settings against the rules: an unknown one is refused (a fixed setting among them),
// a number outside its bound, a kind mismatch, and a required one missing. A bare stream's when-full and age are read
// here too.
func settingsProblems(at, field string, given map[string]json.RawMessage, rules map[string]optionRule, of string, terminal []string, bare bool) []string {
var problems []string
for _, k := range sortedKeys(given) {
raw := given[k]
if bare && k == catalogue.ConfigWhenFull {
var v string
if json.Unmarshal(raw, &v) != nil || (v != catalogue.WhenFullRefuse && v != catalogue.WhenFullDropOldest) {
problems = append(problems, fmt.Sprintf(
"%s: %s %q is %s; a bare stream says %q or %q", at, field, k, string(raw), catalogue.WhenFullRefuse, catalogue.WhenFullDropOldest))
}
continue
}
rule, ok := rules[k]
if !ok {
if k == catalogue.ConfigWhenFull {
problems = append(problems, fmt.Sprintf(
"%s: %s %q is given to %s; what happens when full is fixed and is never an option (novox/hq ADR 0595 §3)", at, field, k, of))
continue
}
problems = append(problems, fmt.Sprintf(
"%s: %s %q is not a setting of %s (novox/hq ADR 0595 §3)", at, field, k, of))
continue
}
problems = append(problems, valueProblems(at, field, k, raw, rule, terminal, bare)...)
}
for _, k := range sortedKeys(rules) {
if _, given := given[k]; !given && rules[k].required {
problems = append(problems, fmt.Sprintf("%s: %s %q is missing; %s gives it", at, field, k, of))
}
}
if bare {
if _, given := given[catalogue.ConfigWhenFull]; !given {
problems = append(problems, fmt.Sprintf(
"%s: %s %q is missing; a bare stream says %q or %q", at, field, catalogue.ConfigWhenFull, catalogue.WhenFullRefuse, catalogue.WhenFullDropOldest))
}
}
return problems
}
// valueProblems judges one setting's value against its rule.
func valueProblems(at, field, key string, raw json.RawMessage, rule optionRule, terminal []string, bare bool) []string {
switch rule.kind {
case optionInt:
if bare && key == catalogue.OptionAgeDays {
var word string
if json.Unmarshal(raw, &word) == nil && word == noAge {
return nil
}
}
n, ok := wholeNumber(raw)
if !ok {
return []string{fmt.Sprintf("%s: %s %q is %s, which is not a whole number", at, field, key, string(raw))}
}
if n < rule.least || n > rule.most {
extra := ""
if key == catalogue.OptionPastValues && n == 0 {
extra = "; 0 is refused here"
}
return []string{fmt.Sprintf("%s: %s %q is %d, outside its bound of %d to %d%s", at, field, key, n, rule.least, rule.most, extra)}
}
case optionFlag:
var b bool
if json.Unmarshal(raw, &b) != nil {
return []string{fmt.Sprintf("%s: %s %q is %s, which is not true or false", at, field, key, string(raw))}
}
case optionText:
var config string
if json.Unmarshal(raw, &config) != nil || !slices.Contains(terminal, config) {
return []string{fmt.Sprintf(
"%s: %s %q names %s, which is not a trusted config value of the module, set only at the controller's "+
"terminal or on the operator's Approve (novox/hq ADR 0348 §8)", at, field, key, string(raw))}
}
case optionEvents:
var events []string
if json.Unmarshal(raw, &events) != nil || len(events) == 0 {
return []string{fmt.Sprintf("%s: %s %q is %s; it is a list of at least one event", at, field, key, string(raw))}
}
var out []string
for _, ev := range events {
if err := eventPattern(ev, false); err != nil {
out = append(out, fmt.Sprintf("%s: %s %q carries %q: %v", at, field, key, ev, err))
}
}
return out
}
return nil
}
// wholeNumber reads a JSON number that is a whole number.
func wholeNumber(raw json.RawMessage) (int, bool) {
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
var n json.Number
if dec.Decode(&n) != nil {
return 0, false
}
i, err := n.Int64()
return int(i), err == nil
}
// workQueueProblems holds a work queue's duplicate window to its age (design 25 § 12.1: up to its age).
func workQueueProblems(at string, e catalogue.BusStateEntry) []string {
raw, given := e.Options[catalogue.OptionDuplicates]
if !given {
return nil
}
window, ok := wholeNumber(raw)
if !ok {
return nil // valueProblems said so
}
days := catalogue.StreamDefaultDays
if age, ok := wholeNumber(e.Options[catalogue.OptionAgeDays]); ok && age > 0 {
days = age
}
if window > days*86400 {
return []string{fmt.Sprintf(
"%s: options %q is %d seconds, longer than the queue's age of %d days (novox/hq design 25 § 12.1)",
at, catalogue.OptionDuplicates, window, days)}
}
return nil
}
// ledgerTypeProblems judges a ledger's types: named locally and once, and every operation's effect one of the four.
func ledgerTypeProblems(at string, e catalogue.BusStateEntry) []string {
var problems []string
if len(e.Types) == 0 {
problems = append(problems, fmt.Sprintf("%s: a ledger names at least one type (novox/hq ADR 0597 §1)", at))
}
seen := map[string]bool{}
for _, t := range e.Types {
where := fmt.Sprintf("%s: type %q", at, t.Name)
switch {
case !stateName.MatchString(t.Name):
problems = append(problems, fmt.Sprintf("%s: its name is lower-case letters, digits and hyphens", where))
case seen[t.Name]:
problems = append(problems, fmt.Sprintf("%s: the type is named twice", where))
}
seen[t.Name] = true
operations := map[string]bool{}
for _, op := range t.Operations {
if operations[op.Name] {
problems = append(problems, fmt.Sprintf("%s: the operation %q is named twice", where, op.Name))
}
operations[op.Name] = true
if !slices.Contains(ledgerEffects, op.Effect) {
problems = append(problems, fmt.Sprintf(
"%s: the operation %q has the effect %q, outside the four (%s) (novox/hq ADR 0597 §2)",
where, op.Name, op.Effect, strings.Join(ledgerEffects, ", ")))
}
}
}
return problems
}
// BusStateCatalogueProblems are the rules of bus state no single manifest can be judged against: two modules' names on
// the bus alike, and a `reads` naming bus state in the new form, which has no readers (novox/hq ADR 0596 §2).
func BusStateCatalogueProblems(shelf catalogue.Shelf) []string {
var problems []string
owner := map[string]string{} // a name on the bus, upper-cased, to the module asking for it
for _, module := range shelfOrder(shelf) {
for _, o := range shelf[module].Objects {
owner[strings.ToUpper(o.Bucket(module))] = module
}
}
for _, module := range shelfOrder(shelf) {
for _, e := range shelf[module].BusState {
name := catalogue.ObjectsBucket(module, e.Name)
if first, taken := owner[strings.ToUpper(name)]; taken && first != module {
problems = append(problems, fmt.Sprintf(
"%s's bus state %q has the name %s on the bus, which %s's already has or is alike; a name on the bus "+
"has one owner (novox/hq ADR 0201 §4)", module, e.Name, name, first))
continue
}
owner[strings.ToUpper(name)] = module
}
}
for _, module := range shelfOrder(shelf) {
for _, r := range shelf[module].Reads {
from, local, err := catalogue.ReadState(r)
if err != nil || from == module {
continue // StateProblems says why
}
for _, e := range shelf[from].BusState {
if e.Name == local {
problems = append(problems, fmt.Sprintf(
"%s reads %q, which %s has moved to bus state in `requires`; the new form has no readers: ask %s's "+
"tools, or hear its events (novox/hq ADR 0596 §2)", module, r, from, from))
}
}
}
}
return problems
}
@@ -1,95 +0,0 @@
package judge
import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// A manifest's bus state, asked for in `requires` by template, is judged once and each refusal names the module, the entry
// and the field (issue 1966 D1, D2; novox/hq design 25 § 12.1, ADR 0595, ADR 0596 §2, ADR 0597 §2).
func TestBusStateIsJudgedOnce(t *testing.T) {
const ledger = `{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":512},"types":[` +
`{"name":"decision","schema":{"title":{"type":"text"}},"operations":[{"name":"propose","effect":"set-fields","fields":["title"]}]}]}`
manifest := func(entries string) string {
return `{"module":"a","version":"1","requires":[` + entries + `]}`
}
for _, c := range []struct {
name string
raw string
refused []string // words the first problem holds, in order: absent is accepted
}{
// 1966/AC8: a state, a log, objects and a ledger within bounds
{"within bounds", manifest(`{"name":"cache","storage":"key-value","template":"state","options":{"history":8,"key-ttl":true}},` +
`{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64,"max-age-days":365}},` +
`{"name":"files","storage":"object-store","template":"objects","options":{"max-mib":2048}},` + ledger), nil},
// 1966/AC7: each refused case, naming the module, the entry and the field
{"unknown storage type", manifest(`{"name":"x","storage":"tape"}`), []string{"a's bus state \"x\"", "storage type \"tape\""}},
{"unknown template", manifest(`{"name":"x","storage":"stream","template":"pile"}`), []string{"\"x\"", "template \"pile\""}},
{"template on another storage type", manifest(`{"name":"x","storage":"stream","template":"state"}`), []string{"\"x\"", "template state is on the storage type key-value"}},
{"past values 0", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":0}}`), []string{"\"x\"", "options \"history\" is 0", "0 is refused"}},
{"past values 65", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":65}}`), []string{"\"x\"", "\"history\" is 65", "1 to 64"}},
{"a lifetime that is no number", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"ttl-seconds":"soon"}}`), []string{"\"x\"", "\"ttl-seconds\""}},
{"events age over 30 days", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-age-days":31}}`), []string{"\"x\"", "\"max-age-days\" is 31", "1 to 30"}},
{"events cap over 8192", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-mib":8193}}`), []string{"\"x\"", "\"max-mib\" is 8193"}},
{"events naming none", manifest(`{"name":"x","storage":"stream","template":"events","options":{}}`), []string{"\"x\"", "\"events\" is missing"}},
{"a grant that is no trusted value", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"grant":"nobody"}}`), []string{"\"x\"", "\"grant\"", "trusted config value"}},
{"deliveries 11", manifest(`{"name":"x","storage":"stream","template":"work-queue","options":{"events":["job"],"max-deliveries":11}}`), []string{"\"x\"", "\"max-deliveries\" is 11", "1 to 10"}},
{"duplicates past the age", manifest(`{"name":"x","storage":"stream","template":"work-queue","options":{"events":["job"],"max-age-days":1,"duplicates":86401}}`), []string{"\"x\"", "\"duplicates\"", "longer than the queue's age of 1 days"}},
{"log age 3651", manifest(`{"name":"x","storage":"stream","template":"log","options":{"max-age-days":3651}}`), []string{"\"x\"", "\"max-age-days\" is 3651"}},
{"objects without a cap", manifest(`{"name":"x","storage":"object-store","template":"objects"}`), []string{"\"x\"", "\"max-mib\" is missing"}},
{"when full given as an option", manifest(`{"name":"x","storage":"stream","template":"log","options":{"when-full":"drop-oldest"}}`), []string{"\"x\"", "\"when-full\"", "fixed"}},
{"another fixed setting given", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"max-value-kib":1024}}`), []string{"\"x\"", "\"max-value-kib\" is not a setting of the template state"}},
{"a configuration beside a template", manifest(`{"name":"x","storage":"key-value","template":"state","configuration":{"max-mib":1}}`), []string{"\"x\"", "configuration"}},
{"a name used twice across storage types", manifest(
`{"name":"x","storage":"key-value","template":"state"},{"name":"x","storage":"stream","template":"log"}`), []string{"\"x\"", "used twice (as key-value and as stream)"}},
{"a local name holding an underscore", manifest(`{"name":"my_x","storage":"key-value","template":"state"}`), []string{"\"my_x\"", "underscore"}},
{"a bare stream without when full", manifest(`{"name":"x","storage":"stream","configuration":{"max-mib":10,"max-age-days":"none"}}`), []string{"\"x\"", "\"when-full\" is missing"}},
{"a bare stream without a cap", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"refuse","max-age-days":3}}`), []string{"\"x\"", "\"max-mib\" is missing"}},
{"a bare stream without an age", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"refuse","max-mib":10}}`), []string{"\"x\"", "\"max-age-days\" is missing"}},
{"a bare stream, complete", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"drop-oldest","max-mib":10,"max-age-days":"none"}}`), nil},
{"a bare stream with a wrong when full", manifest(`{"name":"x","storage":"stream","configuration":{"when-full":"crash","max-mib":10,"max-age-days":9}}`), []string{"\"x\"", "\"when-full\"", "\"refuse\" or \"drop-oldest\""}},
{"a ledger effect outside the four", manifest(strings.Replace(ledger, `"set-fields"`, `"upsert"`, 1)), []string{"\"records\"", "effect \"upsert\"", "outside the four"}},
{"a ledger whose history expires", manifest(strings.Replace(ledger, `"options"`, `"configuration":{"max-age-days":30},"options"`, 1)), nil},
} {
_, problems := Manifest([]byte(c.raw), nil)
switch {
case c.refused == nil && len(problems) > 0:
t.Errorf("%s: refused: %v", c.name, problems)
case c.refused != nil && len(problems) == 0:
t.Errorf("%s: accepted", c.name)
case c.refused != nil:
text := strings.Join(problems, "\n")
at := 0
for _, word := range c.refused {
i := strings.Index(text[at:], word)
if i < 0 {
t.Errorf("%s: problems %q lack %q", c.name, text, word)
break
}
at += i
}
}
}
// Between the manifests: two modules' names on the bus alike, and a read of bus state that moved (ADR 0596 §2).
entry := func(name string) []catalogue.BusStateEntry {
return []catalogue.BusStateEntry{{Name: name, Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState}}
}
for _, c := range []struct {
name string
shelf catalogue.Shelf
want string
}{
{"names alike", catalogue.Shelf{"a_b": {Module: "a_b", BusState: entry("c")}, "a": {Module: "a", BusState: entry("b_c")}}, "has one owner"},
{"a read of moved state", catalogue.Shelf{"owner": {Module: "owner", BusState: entry("cache")},
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, "reader reads \"owner.cache\", which owner has moved"},
{"a read of state still kept", catalogue.Shelf{"owner": {Module: "owner", State: []catalogue.StateDeclaration{{Name: "cache"}}},
"reader": {Module: "reader", Reads: []string{"owner.cache"}}}, ""},
} {
got := strings.Join(BusStateCatalogueProblems(c.shelf), "\n")
if (c.want == "") != (got == "") || !strings.Contains(got, c.want) {
t.Errorf("%s: between the manifests: %q, want %q", c.name, got, c.want)
}
}
}
+19 -45
View File
@@ -69,15 +69,27 @@ func problemsOf(err error) []string {
// whose removal from the catalogue ends it (a rule two modules break together refuses both); one no removal ends is in
// unnamed, for the caller to say. A module not in the answer is accepted.
func Shelf(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int) (refused map[string][]string, unnamed []string) {
return ShelfCounting(raws, predefined, longestMachine, nil)
refused, waits, unnamed := ShelfWaiting(raws, predefined, longestMachine)
for name := range waits {
// Strictly, as the checker judges a change: the judge that runs there is the one built at the change's head, which
// knows the fields the change brings, so a field it does not know is a mistake.
_, refused[name] = Manifest(raws[name], predefined)
}
return refused, unnamed
}
// ShelfCounting is Shelf also refusing a manifest of a module outside the foundation that uses an old section whose count
// is 0 (OldSectionProblems; issue 1966 D4); nil counts refuse none.
func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts) (refused map[string][]string, unnamed []string) {
refused = map[string][]string{}
// ShelfWaiting is Shelf where a manifest that carries a field this judge does not know (UnknownFields) is not refused: it is
// in waits, by module, naming each field, and judged no further, because a newer judge may know the field (novox/hq ADR
// 0578 §1, amended). The indexer judges so; the checker, which runs the judge built at the head of the change, judges with
// Shelf.
func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int) (refused, waits map[string][]string, unnamed []string) {
refused, waits = map[string][]string{}, map[string][]string{}
shelf := catalogue.Shelf{}
for name, raw := range raws {
if unknown := UnknownFields(raw); len(unknown) > 0 {
waits[name] = unknown
continue
}
m, problems := Manifest(raw, predefined)
if len(problems) > 0 {
refused[name] = problems
@@ -87,10 +99,6 @@ func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList,
refused[name] = []string{fmt.Sprintf("the manifest is of module %q, not %q", m.Module, name)}
continue
}
if problems := OldSectionProblems(m, counts); len(problems) > 0 {
refused[name] = problems
continue
}
shelf[name] = m
}
between := Between(shelf, longestMachine)
@@ -132,7 +140,7 @@ func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList,
for name := range refused {
sort.Strings(refused[name])
}
return refused, unnamed
return refused, waits, unnamed
}
// Between is every problem between the manifests of shelf, sorted: catalogue.CatalogueProblems, IdentityProblems over
@@ -189,7 +197,6 @@ const Unnamed = "(between manifests)"
func Request(r composer.Request) (composer.Verdict, error) {
v := composer.Verdict{V: composer.Version, SDK: Identity()}
seen := map[string]bool{}
counts := trunkOldSections(r)
for _, c := range r.Cases {
if seen[c.Name] {
return composer.Verdict{}, fmt.Errorf("the case %q is named twice", c.Name)
@@ -202,7 +209,7 @@ func Request(r composer.Request) (composer.Verdict, error) {
}
raws[name] = m.Raw
}
refused, unnamed := ShelfCounting(raws, r.Predefined, 0, counts)
refused, unnamed := Shelf(raws, r.Predefined, 0)
cv := composer.CaseVerdict{Name: c.Name, Manifests: map[string]composer.ManifestRead{}}
for name := range raws {
if problems := refused[name]; len(problems) > 0 {
@@ -219,39 +226,6 @@ func Request(r composer.Request) (composer.Verdict, error) {
return v, nil
}
// TrunkCase is the case of a request holding the catalogue as the trunk holds it (the checker's JudgeTrunk).
const TrunkCase = "trunk"
// trunkOldSections counts the old sections the modules accepted at the trunk use, as the indexer counts them from its
// verdicts (issue 1966 D4, ADR 0595 §11): a change is refused at the merge check exactly as at take-in (judged once, ADR
// 0578). Nil where the request holds no trunk case, which refuses no use of a section.
func trunkOldSections(r composer.Request) OldSectionCounts {
for _, c := range r.Cases {
if c.Name != TrunkCase {
continue
}
raws := map[string][]byte{}
for name, m := range c.Manifests {
if len(m.Raw) > 0 {
raws[name] = m.Raw
}
}
refused, _ := Shelf(raws, r.Predefined, 0)
var accepted []catalogue.Manifest
for name, raw := range raws {
if len(refused[name]) > 0 {
continue
}
if m, err := catalogue.Decode(raw); err == nil {
m.Module = name
accepted = append(accepted, m)
}
}
return CountOldSections(accepted)
}
return nil
}
var (
identityOnce sync.Once
identity string
@@ -54,6 +54,21 @@ func TestTheJudge(t *testing.T) {
}
}
}
// A field the judge does not know waits in the indexer's judge and stays a refusal in the checker's (clause 3 of the
// record on a release's Approve); a renamed field and any other problem are refused by both.
waitRaws := map[string][]byte{
"later": []byte(`{"module":"later","version":"1","core-credentials":true,"other":1}`),
"renamed": []byte(`{"module":"renamed","version":"1","needs":{"x":"y"}}`),
"bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`),
"ok": []byte(`{"module":"ok","version":"1"}`),
}
refused, waits, _ := ShelfWaiting(waitRaws, nil, 0)
if !reflect.DeepEqual(waits, map[string][]string{"later": {"core-credentials", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil {
t.Errorf("ShelfWaiting: waits %v, refused %v", waits, refused)
}
if strict, _ := Shelf(waitRaws, nil, 0); len(strict) != 3 || !strings.Contains(strings.Join(strict["later"], ""), "core-credentials") {
t.Errorf("Shelf refuses as before: %v", strict)
}
// The private network's manifest, which the mesh provides itself, is accepted (moved from mesh-sdk overlay's test).
if raw, err := json.Marshal(overlay.Manifest()); err != nil {
t.Fatal(err)
@@ -86,27 +101,6 @@ func TestTheJudge(t *testing.T) {
v.Cases[0].Manifests["b"].Accepted || !strings.Contains(v.Cases[0].Manifests["b"].Refused, `requires "amqp"`) {
t.Fatalf("request: %+v %v", v, err)
}
// 1966/AC11: the checker's judge counts the old sections from the trunk case as the indexer does from its verdicts (the
// foundation left out): a change adding logs where no trunk module uses them is refused, the foundation's accepted, and
// state, which a trunk module uses, is accepted.
old := composer.Request{V: composer.Version, Cases: []composer.Case{
{Name: "change", Manifests: map[string]composer.Manifest{
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)},
"late": {Raw: json.RawMessage(`{"module":"late","version":"1","logs":[{"name":"l"}]}`)},
"newer": {Raw: json.RawMessage(`{"module":"newer","version":"1","state":["y"]}`)},
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}},
{Name: "trunk", Manifests: map[string]composer.Manifest{
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)},
"mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}}}
ov, err := Request(old)
if err != nil {
t.Fatal(err)
}
got := ov.Cases[0].Manifests
if got["late"].Accepted || !strings.Contains(got["late"].Refused, "late uses the old section logs") ||
!got["newer"].Accepted || !got["user"].Accepted || !got["mesh-grants"].Accepted {
t.Errorf("the change's old sections: %+v", got)
}
r.Cases[0].Manifests["c"] = composer.Manifest{Digest: strings.Repeat("a", 64)}
if _, err := Request(r); err == nil {
t.Fatal("a manifest left as a digest was judged")
@@ -1,69 +0,0 @@
package judge
import (
"fmt"
"slices"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The old sections end when no module is left in them (issue 1966 D4; novox/hq ADR 0595 §11): the indexer counts, per old
// section, the accepted modules using it, the foundation left out, and the judge refuses a manifest of another module
// that uses a section whose count is 0. The foundation (the bus module, the grants part and the indexer itself) is
// created before the bus module can create anything, so it keeps the old sections for good and is always accepted.
// OldSections are the sections of a manifest that keep bus state in the old forms.
var OldSections = []string{"state", "logs", "streams", "objects"}
// Foundation are the modules that keep using the old sections for good.
var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer"}
// OldSectionCounts is, per old section, how many accepted modules outside the foundation use it. Nil is no count: the
// judge then refuses no use of a section.
type OldSectionCounts map[string]int
// SectionsUsed are the old sections a manifest uses, in OldSections' order.
func SectionsUsed(m catalogue.Manifest) []string {
var used []string
for _, s := range OldSections {
if (s == "state" && len(m.State) > 0) || (s == "logs" && len(m.Logs) > 0) ||
(s == "streams" && len(m.Streams) > 0) || (s == "objects" && len(m.Objects) > 0) {
used = append(used, s)
}
}
return used
}
// CountOldSections counts, per old section, the manifests using it, the foundation's left out; every section is in the
// answer, with 0 where none uses it.
func CountOldSections(manifests []catalogue.Manifest) OldSectionCounts {
counts := OldSectionCounts{}
for _, s := range OldSections {
counts[s] = 0
}
for _, m := range manifests {
if slices.Contains(Foundation, m.Module) {
continue
}
for _, s := range SectionsUsed(m) {
counts[s]++
}
}
return counts
}
// OldSectionProblems are the old sections a manifest uses that no accepted module outside the foundation uses any more.
func OldSectionProblems(m catalogue.Manifest, counts OldSectionCounts) []string {
if counts == nil || slices.Contains(Foundation, m.Module) {
return nil
}
var problems []string
for _, s := range SectionsUsed(m) {
if counts[s] == 0 {
problems = append(problems, fmt.Sprintf(
"%s uses the old section %s, which no other module uses any more (0 modules count); ask for it in "+
"`requires` by template (novox/hq ADR 0595 §11)", m.Module, s))
}
}
return problems
}
+39 -5
View File
@@ -2388,6 +2388,44 @@ func ownSecretPromptProblems(m catalogue.Manifest, name, prompt string, family b
return problems
}
// unknownFieldText is how the lenient decoder words the first field its shape does not have, and readWithout the list of
// every field it read the manifest without (catalogue.Manifest.SetAside).
var (
unknownFieldText = regexp.MustCompile(`json: unknown field "([^"]+)"`)
readWithout = regexp.MustCompile(`\(read without ([^)]*)\)`)
)
// UnknownFields are the fields of a manifest the judge does not know, as the lenient decoder sets them aside (catalogue.Decode,
// each once, the path it names them with), or none: a manifest this judge reads whole has none, and a field that used to
// mean something (renamed) is a known mistake, not a field a later judge may know, so a manifest with one has none
// either.
func UnknownFields(raw []byte) []string {
m, err := catalogue.Decode(raw)
aside := m.SetAside()
if err != nil || aside == "" {
return nil
}
first := unknownFieldText.FindStringSubmatch(aside)
if first == nil {
return nil // set aside for another reason: the judge's refusal as it is
}
fields := []string{first[1]}
if list := readWithout.FindStringSubmatch(aside); list != nil {
fields = fields[:0]
for _, f := range strings.Split(list[1], ", ") {
if f = strings.TrimSpace(f); f != "" && !slices.Contains(fields, f) {
fields = append(fields, f)
}
}
}
for _, f := range fields {
if _, was := renamed[f[strings.LastIndex(f, ".")+1:]]; was {
return nil
}
}
return fields
}
func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.Manifest, error) {
var m catalogue.Manifest
// Strictly. **An unknown key is refused**, which is the discipline the host's declaration
@@ -2626,8 +2664,6 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
// And the object buckets it owns (novox/hq ADR 0592 §5, §9).
problems = append(problems, ObjectsProblems(m)...)
problems = append(problems, WorkQueueStreamProblems(m)...)
// And the bus state it asks for in `requires`, by template (busstate.go, novox/hq ADR 0595; issue 1966 D1).
problems = append(problems, BusStateProblems(m)...)
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262), or, written in the
// config form, the whole declaration of each value (config.go, novox/hq ADR 0348).
if m.DeclaresConfig() {
@@ -4556,8 +4592,6 @@ func CatalogueProblems(shelf catalogue.Shelf) []string {
problems = append(problems, StreamNameProblems(shelf)...)
// An object bucket's name has one owner, and another module reads it only through its owner's grant (ADR 0592 §5, §9).
problems = append(problems, ObjectsCatalogueProblems(shelf)...)
// Bus state in `requires`: a name on the bus has one owner, and the new form has no readers (ADR 0595, ADR 0596 §2).
problems = append(problems, BusStateCatalogueProblems(shelf)...)
// A module that hears a restricted stream runs as an account of its own (novox/hq ADR 0376 §2).
problems = append(problems, RestrictedHearingProblems(shelf)...)
// A work-queue stream is never asked for by another module's consumes (novox/hq ADR 0436 §6).
@@ -5362,7 +5396,7 @@ func StateProblems(m catalogue.Manifest) []string {
seen[s.Name] = true
if s.History < 0 || s.History > catalogue.StateMostHistory {
problems = append(problems, fmt.Sprintf(
"%s keeps %d values of %q; a key keeps 0 or 1 to %d (0 is taken as 1)", m.Module, s.History, s.Name, catalogue.StateMostHistory))
"%s keeps %d values of %q; a key keeps between 1 and %d", m.Module, s.History, s.Name, catalogue.StateMostHistory))
}
if s.TTLSeconds < 0 {
problems = append(problems, fmt.Sprintf("%s gives %q a negative lifetime", m.Module, s.Name))
@@ -54,6 +54,12 @@ func TestABinaryMustSayWhichSystemItIsFor(t *testing.T) {
}
}
func TestABinaryThatNamesASystemIsAccepted(t *testing.T) {
if got := buildProblemsOf(t, bundleFor("go", "arch")); got != "" {
t.Fatalf("a compiled bundle naming a system was refused:\n%s", got)
}
}
func TestABlockOutsideTheShapeIsRefused(t *testing.T) {
cases := map[string]string{
`{"seat":"node-bar","kind":"block","content":"[[block]]"}`: "has no data",
@@ -907,12 +913,6 @@ func TestALogIsNamedLocallyAndCapped(t *testing.T) {
{`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`},
} {
_, err := ParseManifest([]byte(c.manifest))
if c.says == "" {
if err != nil {
t.Errorf("%s was refused: %v", c.manifest, err)
}
continue
}
if err == nil {
t.Errorf("%s was accepted", c.manifest)
continue
@@ -2356,19 +2356,18 @@ func TestAPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
}
}
// **A language that runs anywhere must not name a system.** A system that decides nothing reads as
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045); a compiled
// bundle names one, and a portable bundle naming none is an ordinary one.
func TestABundleNamesASystemOnlyWhereItCompiles(t *testing.T) {
for _, c := range []struct{ language, system, refused string }{
{"go", "arch", ""},
{"typescript", "", ""},
{"typescript", "arch", "runs anywhere"},
} {
got := buildProblemsOf(t, bundleFor(c.language, c.system))
if (c.refused == "") != (got == "") || !strings.Contains(got, c.refused) {
t.Errorf("a %s bundle naming %q: %q, want %q", c.language, c.system, got, c.refused)
}
// **And a language that runs anywhere must not name one.** A system that decides nothing reads as
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045).
func TestAPortableBundleMayNotNameASystem(t *testing.T) {
got := buildProblemsOf(t, bundleFor("typescript", "arch"))
if !strings.Contains(got, "runs anywhere") {
t.Fatalf("a portable bundle was allowed to name a system:\n%s", got)
}
}
func TestAPortableBundleNamingNoSystemIsAccepted(t *testing.T) {
if got := buildProblemsOf(t, bundleFor("typescript", "")); got != "" {
t.Fatalf("an ordinary bundle was refused:\n%s", got)
}
}
@@ -2883,23 +2882,13 @@ func TestAStateNameIsLocalAndOneToken(t *testing.T) {
{`{"module":"a","version":"1","state":["mesh.servers"]}`, `keeps state "mesh.servers": a state is named locally`},
{`{"module":"a","version":"1","state":["my_servers"]}`, `keeps state "my_servers"`},
{`{"module":"a","version":"1","state":["s","s"]}`, `keeps state "s" twice`},
{`{"module":"a","version":"1","state":[{"name":"s","history":65}]}`, `a key keeps 0 or 1 to 64 (0 is taken as 1)`},
{`{"module":"a","version":"1","state":[{"name":"s","history":0}]}`, ``}, // 0 passes, as before (ADR 0579 §1)
// 1966/AC9: one name, one form
{`{"module":"a","version":"1","state":["x"],"requires":[{"name":"x","storage":"key-value","template":"state"}]}`,
`a's bus state "x": the name is also kept in the old section state`},
{`{"module":"a","version":"1","state":[{"name":"s","history":65}]}`, `a key keeps between 1 and 64`},
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl}`},
} {
_, err := ParseManifest([]byte(c.manifest))
if c.says == "" {
if err != nil {
t.Errorf("%s was refused: %v", c.manifest, err)
}
continue
}
if err == nil {
t.Errorf("%s was accepted", c.manifest)
continue
@@ -84,13 +84,12 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
t.Errorf("module = %q", m.Module)
}
// 1126: placement is the seat mesh-placement's (ADR 0567): the indexer holds no placement state. ADR 0578 §1: it says
// a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as
// manifest-accepted.
if !reflect.DeepEqual(m.Emits, []string{"manifest-refused", "manifest-accepted"}) {
t.Errorf("emits = %v, want manifest-refused and manifest-accepted", m.Emits)
// a refused manifest as manifest-refused, its one event.
if !reflect.DeepEqual(m.Emits, []string{"manifest-refused"}) {
t.Errorf("emits = %v, want manifest-refused alone", m.Emits)
}
// 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard.
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}}; !reflect.DeepEqual(m.State, want) {
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-assigner.placement", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
@@ -110,12 +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. 1966/AC11:
// indexer_old_sections reads the count of the old sections (state oldsections).
if want := []string{"indexer_take_in", "indexer_old_sections"}; !reflect.DeepEqual(m.Tools, 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; 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.
+5 -5
View File
@@ -4,7 +4,8 @@
"slug": "indexer",
"tools": [
"indexer_take_in",
"indexer_old_sections"
"indexer_verdicts",
"indexer_judge"
],
"consumes": [
"mesh-build-go.toolchain.reported",
@@ -22,12 +23,10 @@
{ "name": "taken", "history": 1 },
{ "name": "takeins", "history": 1 },
{ "name": "verdicts", "history": 5 },
{ "name": "oldsections", "history": 1 },
{ "name": "packages", "history": 5 }
],
"emits": [
"manifest-refused",
"manifest-accepted"
"manifest-refused"
],
"reads": [
"mesh-build-go.toolchains",
@@ -41,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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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=
@@ -59,6 +59,18 @@ func TestTheWatchmansFindings(t *testing.T) {
t.Fatalf("findings = %v", o.findings)
}
w.ModuleVerdict("node-tools", value(""), Put)
// Clause 3 of the record on a release's Approve: a manifest that waits for a judge that knows its fields is its own
// warning (a refusal in the value, naming the fields), not manifest-refused, and goes when it is accepted.
waiting, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64),
Repository: "novox/mesh-catalog", Commit: "c", Verdict: delivery.VerdictRefused, Problems: []string{"waits"}, Unknown: []string{"core-credentials"}})
w.ModuleVerdict("nats", waiting, Put)
if f := o.findings[FindingKey(ScopeModule, "nats", KindManifestWaits)]; f.Severity != Warning || !strings.Contains(f.Summary, "core-credentials") {
t.Fatalf("findings = %v", o.findings)
}
if _, refused := o.findings[FindingKey(ScopeModule, "nats", KindManifestRefused)]; refused {
t.Fatalf("a waiting manifest was found refused: %v", o.findings)
}
w.ModuleVerdict("nats", value(""), Put)
}, map[string]string{}},
{"WM4: a stuck key not moved is its finding, and its move deletes it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.Step("go", "n1.ask1", runningStep("ask1", t0), Put)
@@ -14,6 +14,11 @@ const KindManifestRefused = "manifest-refused"
// (novox/hq ADR 0579 §1): it stays accepted and runs; the warning stands until the manifest is mended.
const KindManifestOutgrown = delivery.ManifestOutgrown
// KindManifestWaits is a module whose manifest carries a field the indexer's judge does not know yet (novox/hq ADR 0578
// §1, amended): every reader holds it as a refused one and keeps what runs, and the warning stands until a judge knows the
// field or the manifest is mended.
const KindManifestWaits = delivery.ManifestWaits
// ModuleVerdict is a change of the indexer's value of a module (delivery.ModulesBucket): a value carrying a refusal is a
// finding, one that does not (accepted, or not judged yet) or a deleted key ends it.
func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
@@ -21,9 +26,11 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
defer w.mu.Unlock()
key := FindingKey(ScopeModule, module, KindManifestRefused)
outgrown := FindingKey(ScopeModule, module, KindManifestOutgrown)
waits := FindingKey(ScopeModule, module, KindManifestWaits)
if op != Put {
w.unfind(key)
w.unfind(outgrown)
w.unfind(waits)
return
}
m, err := delivery.DecodeModule(value)
@@ -32,6 +39,20 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) {
return
}
v := m.VerdictOf(module)
if v != nil && v.Waits() {
w.unfind(key)
w.unfind(outgrown)
w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestWaits, Severity: Warning, Source: delivery.ModulesBucket,
Summary: v.Why(),
Said: strings.Join(v.Unknown, "; "),
Headline: "The manifest of " + module + " waits for a judge that knows its fields",
Explanation: "The manifest carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet, so every " +
"reader holds " + module + " as a refused one and keeps what runs; an indexer that knows the fields judges it again " +
"at its start, and the newest build of the indexer refuses a field it does not know.",
Needs: "nothing while a newer indexer is on its way; else a pull request that mends the manifest of " + module})
return
}
w.unfind(waits)
if refused, _ := delivery.TakeManifestVerdict(v); !refused {
w.unfind(key)
if v == nil || len(v.Warnings) == 0 {
+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.20261102191058-f02f142af128
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.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/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.20261103142843-f8bd12e4b7b1
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 -4
View File
@@ -1,7 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 h1:T66tg9T/1hHg6KN/bDScHPT41IHkxLAGGO8VJiAOlb8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1/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=
+150 -24
View File
@@ -680,6 +680,22 @@ func TestGrants(t *testing.T) {
if slices.Contains(web.Publish, "$JS.API.DIRECT.GET.KV_mesh-knowledge_sections.>") {
t.Error("the object bucket's read is granted as a state's")
}
// ADR 0601 clause 3: the owner's trunk manifest now waits for a field; its running build's manifest still declares the
// bucket, so web keeps its object grant.
fp := "src2:" + strings.Repeat("cd", 32)
b.put(grants.ReadModules, "mesh-knowledge", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(`{"module":"mesh-knowledge","version":"2"}`)), Repository: "novox/x", Commit: "c",
Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
b.put(grants.ReadHave, "a.mesh-knowledge", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp})
heldKey, _ := delivery.BuilderBuildKey("mesh-knowledge", fp)
b.put(grants.ReadBuilds, heldKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(knowledge))})
if heldWeb := (func() grants.User {
us := shadowOf(t, b)
return us[slices.IndexFunc(us, func(u grants.User) bool { return u.User == "a.web" })]
})(); !slices.Contains(heldWeb.Publish, read) {
t.Errorf("a reader of a held module's object store lost its grant: %v", heldWeb.Publish)
}
b.manifestOf("mesh-knowledge", knowledge) // accepted again for the rest of the row
// ADR 0592 §6, §25: the bucket past 75 percent of its cap is said, in one warning, and cleared once below; the history
// stream at 10 percent is not.
var said []asks.Condition
@@ -739,6 +755,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"]}],` +
@@ -1487,7 +1550,7 @@ func TestGrants(t *testing.T) {
fp := partOn(b)
fp.Objects = p.Objects
fp.Say = func(c asks.Condition) error {
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows
if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition && c.Key != grants.HeldModuleCondition { // their own rows
fresh = append(fresh, c)
}
return nil
@@ -2202,28 +2265,6 @@ func TestGrants(t *testing.T) {
if res := bare.Compose(); res.Accounts != nil || !strings.Contains(strings.Join(res.Errors, " "), "no bus address") {
t.Errorf("a part with no bus address wrote %+v: %v", res.Accounts, res.Errors)
}
// 1966/AC20 (issue 1966 D5, D5b; novox/hq ADR 0597 §9): a module asking for a ledger in `requires` is wanted a ledger
// account on each machine it is assigned to, minted by the vault like every kind and held in the tool runner's
// `ledgers`; the account is retired once the module leaves that machine.
l := fixture()
l.manifestOf("mod-x", strings.Replace(xManifest, `"state"`, `"requires":[{"name":"records","storage":"ledger","template":"ledger",`+
`"types":[{"name":"decision","schema":{},"operations":[{"name":"propose","effect":"set-fields"}]}]}],"state"`, 1))
l.assign("b", "mod-x")
l.loaded = shadowOf(t, l)
lp := partOn(l)
lp.Bus = grants.BusAddress{URL: "nats://a.mesh:4222"}
lp.Compose()
for _, user := range []string{"ledger.a.mod-x", "ledger.b.mod-x"} {
w, err := buscompose.ReadWantedAccount(user, l.own[grants.StateAccounts][user])
if err != nil || w.Kind != buscompose.KindLedger || w.OwnSecret() != "ledgers" || w.Module != "mod-x" {
t.Errorf("%s is wanted as %+v (%v)", user, w, err)
}
}
l.unassign("b", "mod-x")
if res := lp.Compose(); res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-x", "ledger.b.mod-x"}) ||
l.own[grants.StateAccounts]["ledger.a.mod-x"] == nil {
t.Errorf("after mod-x left b: %+v", res.Accounts)
}
})
// 774/AC5 (novox/hq ADR 0561 §1): the vault's account-issued wakes a pass, so the next list carries the new user's hash
@@ -2391,7 +2432,7 @@ func TestGrants(t *testing.T) {
var said, behind []asks.Condition
p.Say = func(c asks.Condition) error {
switch c.Key {
case grants.GrantMissingCondition, grants.RefusedCondition, grants.FullCondition: // their own rows
case grants.GrantMissingCondition, grants.RefusedCondition, grants.HeldModuleCondition, grants.FullCondition: // their own rows
case grants.ConsumerBehindCondition:
behind = append(behind, c)
default:
@@ -2690,6 +2731,91 @@ func TestGrants(t *testing.T) {
t.Errorf("the refusal was not cleared once it composes: %v, said %+v", res.PrincipalsRefused, said)
}
})
// Clause 3 of the record on a release's Approve (issue 774): a module whose manifest waits for a judge that knows a field
// of it, or is refused, and whose want names no build, keeps its users of the list in force and is said in a condition;
// the rest of the list is written as ever, and the users go the day the manifest is accepted and the module unassigned.
t.Run("a module whose manifest waits or is refused keeps its users of the list in force", func(t *testing.T) {
b := fixture()
const ctl = `{"module":"mod-r","version":"1","own-secrets":{"broker":"/var/lib/r/broker","broker-mod-r-tools":"${dir:tools-secret}/broker-mod-r-tools"},` +
`"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/mod-r-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools",` +
`"kind":"bundle","language":"go","system":"arch","from":"cmd/mod-r-tools","binary":"mod-r-tools","loads":["mod-r-tools"],"bus":{}}]}}`
b.manifestOf("mod-r", ctl)
b.assign("b", "mod-r")
for _, u := range shadowOf(t, b) {
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
}
b.put(grants.ReadAccounts, "b.mod-r.mod-r-tools", map[string]any{"user": "b.mod-r.mod-r-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"})
busKey, err := seal.GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}})
// mod-r runs a build whose manifest is the accepted one: the held module's seats and streams are composed from it.
runFP := "src2:" + strings.Repeat("cd", 32)
b.put(grants.ReadHave, "b.mod-r", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: runFP})
runKey, _ := delivery.BuilderBuildKey("mod-r", runFP)
b.put(grants.ReadBuilds, runKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(ctl))})
p := partOn(b)
p.Objects = &fakeObjects{}
var said []asks.Condition
p.Say = func(c asks.Condition) error {
if c.Key == grants.HeldModuleCondition {
said = append(said, c)
}
return nil
}
p.Compose()
inForce, _, _ := p.Sealed()
before, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools"))
if !ok || len(said) != 1 || said[0].State != asks.StateCleared {
t.Fatalf("the list in force keeps no grants of b.mod-r.mod-r-tools (%v), or a held module was said before: %+v", ok, said)
}
for gen, held := range []struct{ name, unknown string }{{"waits", "core-credentials"}, {"refused", ""}} {
// Another user's hash changes in the same pass, so a list that is written shows the held module did not hold it.
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": fmt.Sprintf("$2a$10$gen%d", gen), "generation": gen + 2, "fingerprint": fmt.Sprintf("sha256:%d", gen+2)})
was, _, _ := p.Sealed()
var unknown []string
if held.unknown != "" {
unknown = []string{held.unknown}
}
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,
Problems: []string{"it is held"}, Unknown: unknown})
b.put(grants.ReadModules, "mod-q", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c"})
res := p.Compose()
now, _, _ := p.Sealed()
after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mod-r.mod-r-tools"))
if now.Digest == was.Digest || slices.ContainsFunc(res.Errors, func(e string) bool { return strings.Contains(e, "composed whole") }) {
t.Errorf("%s: a held module held the whole list: sealed %+v", held.name, res.Sealed)
}
if !ok || string(after) != string(before) || len(res.PrincipalsRefused) != 0 {
t.Errorf("%s: b.mod-r.mod-r-tools did not keep its grants of the list in force: %s, was %s", held.name, after, before)
}
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "b.mod-r.mod-r-tools (stays in the list") {
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.
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,
Problems: []string{"it is held"}, Unknown: []string{"core-credentials"}})
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen9", "generation": 9, "fingerprint": "sha256:9"})
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)
}
// Accepted again: the users compose from the manifest and the condition clears.
b.manifestOf("mod-r", ctl)
if p.Compose(); said[len(said)-1].State != asks.StateCleared {
t.Errorf("the condition was not cleared once the manifest is accepted: %+v", said[len(said)-1])
}
})
}
// fakeObjects is the part's object bucket of sealed lists.
@@ -50,8 +50,7 @@ type ClaimedSeat struct {
// WantedAccount is one key of the grants part's state `accounts`: an account the mesh needs, without its password.
type WantedAccount struct {
User string `json:"user"`
// Kind is KindModule (KindNodeTools for the runtime, named as a module is), KindProcess, KindProvisioner or
// KindLedger.
// Kind is KindModule (KindNodeTools for the runtime, named as a module is), KindProcess or KindProvisioner.
Kind Kind `json:"kind"`
Node string `json:"node"`
Module string `json:"module"`
@@ -66,12 +65,8 @@ type WantedAccount struct {
At time.Time `json:"at"`
}
// LedgersSecret is the tool runner's own secret holding its machine's ledger accounts, a map of module to credential
// (ADR 0597 §9).
const LedgersSecret = "ledgers"
// mintedKinds are the kinds of account the vault mints (ADR 0561 §1).
var mintedKinds = []Kind{KindModule, KindNodeTools, KindProcess, KindProvisioner, KindLedger}
var mintedKinds = []Kind{KindModule, KindNodeTools, KindProcess, KindProvisioner}
// principal is the account as the composer names it.
func (w WantedAccount) principal() Principal {
@@ -117,12 +112,9 @@ func (w WantedAccount) Same(o WantedAccount) bool {
}
// OwnSecret is the module's own secret the account is held in: `broker`, `broker-<process id>` or `provisioner-broker`
// (the controller's names, ADR 0436 §2, ADR 0349 §11); a ledger account in the tool runner's `ledgers`, which keeps every
// ledger account of its machine, by module (ADR 0597 §9).
// (the controller's names, ADR 0436 §2, ADR 0349 §11).
func (w WantedAccount) OwnSecret() string {
switch w.Kind {
case KindLedger:
return LedgersSecret
case KindProcess:
return "broker-" + w.Process
case KindProvisioner:
@@ -49,7 +49,6 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Run("only declared streams are created", theGrantsPartCreatesOnlyDeclaredStreams)
t.Run("a declared stream is created as the controller asserted it", declaredStreamsAreCreatedAsTheControllerAssertsThem)
t.Run("every node's compositions are read by their named readers alone and written by their node", everyNodesCompositions)
t.Run("a module's ledger is written by its ledger account alone", aLedgerIsWrittenByItsLedgerAccountAlone)
// ADR 0567 §3, §9: a node reads every key of the seat's placement and its candidates (it composes with the whole
// proposal laid over), and none of its open proposals; it writes neither.
reads := strings.Join(NodeComposeReads("laptop"), " ")
@@ -943,75 +942,3 @@ func everyNodesCompositions(t *testing.T) {
t.Errorf("the view's membership issues %+v", m.State)
}
}
// 1966/AC20, AC21 (issue 1966 D5, ADR 0597 §9): a module asking for a ledger, assigned on two machines, gets one ledger account
// on each, publishing only on that module's ledger histories and snapshots; the account is gone from a machine the module
// leaves; and the module's own account, its process and the machine's tool runner hold no publish on its ledger.
func aLedgerIsWrittenByItsLedgerAccountAlone(t *testing.T) {
assigned := func(nodes ...string) []Principal {
r := Records{Nodes: nodes, Assigned: map[string][]Declared{}}
for _, n := range nodes {
r.Assigned[n] = []Declared{
{Module: RuntimeModule},
{Module: "m", Ledgers: []string{"records", "audit"}, Processes: []ProcessPrincipal{{Process: "w"}}},
{Module: "plain"},
}
}
users, err := Users(r)
if err != nil {
t.Fatal(err)
}
return users
}
ledgers := func(users []Principal) []string {
var out []string
for _, p := range users {
if p.Kind == KindLedger {
out = append(out, p.Username())
}
}
return out
}
both := assigned("a", "b")
if got := ledgers(both); !slices.Equal(got, []string{"ledger.a.m", "ledger.b.m"}) {
t.Fatalf("ledger accounts %v, want ledger.a.m and ledger.b.m", got)
}
if got := ledgers(assigned("a")); !slices.Equal(got, []string{"ledger.a.m"}) {
t.Fatalf("after m left b: ledger accounts %v, want ledger.a.m alone", got)
}
want := []string{"mesh.ledger.m.audit.history.>", "mesh.ledger.m.audit.snapshot.>",
"mesh.ledger.m.records.history.>", "mesh.ledger.m.records.snapshot.>"}
raw := []string{"mesh.ledger.m.records.history.decision/0290", "mesh.ledger.m.records.snapshot.decision/0290"}
for _, p := range both {
perms, err := PermissionsFor(p)
if err != nil {
t.Fatalf("%s: %v", p.Username(), err)
}
if p.Kind == KindLedger {
if !slices.Equal(perms.Publish, want) || len(perms.PublishDeny) != 0 {
t.Errorf("%s may publish %v, want %v alone", p.Username(), perms.Publish, want)
}
continue
}
for _, pattern := range perms.Publish {
for _, subject := range raw {
if SubjectsOverlap(pattern, subject) {
t.Errorf("%s may publish %s, which reaches the ledger subject %s", p.Username(), pattern, subject)
}
}
}
if p.Kind == KindModule || p.Kind == KindNodeTools || p.Kind == KindProcess {
if err := CheckWriters(p, want); err == nil {
t.Errorf("%s was granted a raw publish on a ledger's history", p.Username())
}
}
}
if err := CheckWriters(Principal{Kind: KindLedger, Node: "a", Module: "other", Ledgers: []string{"records"}}, want); err == nil {
t.Error("another module's ledger account was granted m's ledger histories")
}
for _, bad := range []Principal{{Kind: KindLedger, Node: "a", Module: "m"}, {Kind: KindLedger, Node: "a", Module: "m", Ledgers: []string{"a.b"}}} {
if _, err := PermissionsFor(bad); err == nil {
t.Errorf("%+v was composed", bad)
}
}
}
@@ -1,57 +0,0 @@
package buscompose
import (
"fmt"
"slices"
"sort"
"strings"
sdk "git.novox.be/novox/mesh-sdk/go/buscompose"
)
// A module's ledger and the one account that writes it (issue 1966 D5; novox/hq ADR 0597 §9, design 25 § 12.4).
//
// **Only the tool runner's ledger credential publishes on a ledger's history** (ADR 0597 §9): one per module assignment,
// named ledger.<machine>.<module> (KindLedger), scoped to the subjects of that module's ledger histories and snapshots, and
// gone when the module is unassigned from the machine. The module's own account, its processes and the machine's tool
// runner hold no publish there, so a raw publish on a history is refused by the bus. The subjects are named in one place, mesh-sdk's
// buscompose (LedgerHistorySubject, LedgerSnapshotSubject), `mesh.ledger.<module>.<local name>.history|snapshot` and
// one subject per instance below it (design 25 § 12.4).
// LedgerPublishGrants are the publish patterns a module's ledger account is granted: the histories and the snapshots of the
// ledgers named, every instance of each, and nothing else. Sorted.
func LedgerPublishGrants(module string, locals []string) []string {
var out []string
for _, local := range locals {
out = append(out, sdk.LedgerHistorySubject(module, local)+".>", sdk.LedgerSnapshotSubject(module, local)+".>")
}
sort.Strings(out)
return out
}
// writesLedger is a module's ledger account publishing on that module's own ledgers' histories and snapshots, and no other
// principal (ADR 0597 §9).
func writesLedger(p Principal, pattern string) bool {
return p.Kind == KindLedger && slices.Contains(LedgerPublishGrants(p.Module, p.Ledgers), pattern)
}
// ledgerSubjects are the subjects no principal but a ledger account may publish on, for the writers table.
var ledgerSubjects = []string{"mesh.ledger.*.*.history.>", "mesh.ledger.*.*.snapshot.>"}
// ledgerPermissions are a ledger account's whole authority: publishing on its module's ledgers, and its own reply space for
// the stream's acknowledgements. Refused when it names no ledger or a name that is no subject token.
func ledgerPermissions(p Principal) (Permissions, error) {
pub := LedgerPublishGrants(p.Module, p.Ledgers)
if len(p.Ledgers) == 0 {
return Permissions{}, fmt.Errorf("%s asks for no ledger, so its account would publish nowhere", p.Username())
}
for _, l := range p.Ledgers {
if !safeSubject.MatchString(l) || strings.Contains(l, "_") {
return Permissions{}, fmt.Errorf("%s: %q cannot be part of a subject: a permission is a subject pattern, and this would widen it", p.Username(), l)
}
}
if err := CheckWriters(p, pub); err != nil {
return Permissions{}, err
}
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
}
@@ -52,10 +52,6 @@ const (
// exactly what its bus says — never root, never the operator account, and never able to answer a tool
// call or a seat's verb (§3). One per process and machine.
KindProcess Kind = "process"
// KindLedger is one module assignment's ledger credential (novox/hq ADR 0597 §9, design 25 § 12.4): held by that
// machine's tool runner, never by the module's own code, and the only account that publishes on that module's ledger
// histories and snapshots. One per module assignment, whose module asks for a ledger in `requires`.
KindLedger Kind = "ledger"
)
// RuntimeModule is the module that IS the node's tool runtime (novox/hq ADR 0175). Where it is
@@ -106,9 +102,6 @@ type Principal struct {
Kind Kind
Node string
Module string
// Ledgers are the local names of the ledgers its module asks for in `requires`, for a KindLedger principal alone
// (issue 1966 D5): its whole authority is publishing on those ledgers' histories and snapshots (LedgerPublishGrants).
Ledgers []string
// Process is the process resource's id, for a KindProcess principal alone (novox/hq ADR 0436 §2):
// Username and inbox are <node>.<module>.<process id>.
Process string
@@ -432,10 +425,6 @@ func (p Principal) Username() string {
return p.Login
}
return "node." + p.Node
case KindLedger:
// `ledger.<machine>.<module>`: never `<node>.<module>`, which is the module's own account or the tool runner's,
// so the module's code never holds it.
return "ledger." + p.Node + "." + p.Module
case KindProvisioner:
// Named for the machine, as a node is: one client per machine, whichever module runs it, and never
// `<node>.<module>`, which is the module's own account or the tool runner's.
@@ -578,11 +567,6 @@ func PermissionsFor(p Principal) (Permissions, error) {
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
}
// A ledger account publishes on its module's ledger histories and snapshots and nothing else (issue 1966 D5).
if p.Kind == KindLedger {
return ledgerPermissions(p)
}
if p.Kind == KindProcess && len(p.Removes) > 0 {
pub, err := BusRemovalGrants(p.Removes, p.FilesFor)
if err != nil {
@@ -103,9 +103,6 @@ type Declared struct {
// composed whether or not the module itself has an account (NoAccount), because a process account is
// delivered in its own secret, broker-<process id>, never the module's `broker`.
Processes []ProcessPrincipal
// Ledgers are the local names of the ledgers it asks for in `requires` (catalogue.StorageLedger); each assignment of a
// module with any gets a ledger principal of its own (issue 1966 D5, ADR 0597 §9).
Ledgers []string
}
// ProcessPrincipal is one process of a module that declares bus (novox/hq ADR 0436 §1, §2), as the broker
@@ -232,11 +229,6 @@ func Users(r Records) ([]Principal, error) {
out[len(out)-1].FilesFor = sortedCopy(r.Nodes)
}
}
// **A ledger account per assignment of a module that asks for a ledger** (issue 1966 D5, ADR 0597 §9): held by the
// machine's tool runner, so it is composed whether or not the module has an account of its own.
if len(d.Ledgers) > 0 {
out = append(out, Principal{Kind: KindLedger, Node: node, Module: d.Module, Ledgers: sortedCopy(d.Ledgers)})
}
if runtimeHere && d.Module == RuntimeModule {
continue
}
@@ -236,12 +236,6 @@ var WritersTable = []WriterRow{
ProvisionerAppliedBucket,
Others: "the watchman reads it; a failure is that machine's failed apply",
Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner},
// A ledger's history and its snapshots (novox/hq ADR 0597 §9, issue 1966 D5): the tool runner's ledger credential of
// the owning module alone, so the module's own code, its processes and the runner's own account cannot write one raw.
{State: "a ledger's history and snapshots", Writer: "the tool runner's ledger credential of the owning module",
KeptIn: "the bus, the ledger's history stream and its snapshot subject",
Others: "the owner reads them through the tool runner's calls; nobody else reads them (ADR 0596 §2)",
Subjects: ledgerSubjects, Writes: writesLedger},
}
// CheckWriters refuses a grant that lets a principal publish on a subject the writers table gives
@@ -46,9 +46,8 @@ type BusAddress struct {
Fingerprint string
}
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address: a
// module's, a process's, a machine's provisioner client's and a module assignment's ledger account (issue 1966 D5b, ADR
// 0597 §9). A node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address. A
// node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[string]buscompose.WantedAccount, error) {
principals, err := buscompose.Users(recs)
if err != nil {
@@ -63,7 +62,7 @@ func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[str
if m, ok := manifestOn(in, p.Node, p.Module); ok {
w.Claims = ClaimsOf(m)
}
case buscompose.KindProcess, buscompose.KindProvisioner, buscompose.KindLedger:
case buscompose.KindProcess, buscompose.KindProvisioner:
default:
continue
}
+120
View File
@@ -0,0 +1,120 @@
package grants
import (
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// **A module whose manifest waits or is refused keeps its bus users** (clause 3 of the record on a release's Approve; issue
// 774). The indexer holds a manifest it refuses, or that waits for a judge that knows a field of it, out of the manifests
// the part composes from (inputs.go, Refused), so a module with no want naming a build had no manifest to derive its users
// from and its users left the list: the bus module's accounts among them, which locked its tools out. Now such a module's
// users, the ones the list in force carries, stay in the list as it holds them, the same way a refused principal's do
// (refused.go), and the part says so as the warning mesh-grants.module-held until the manifest is accepted. One module's
// manifest never holds the list: every other principal is composed as ever. A module whose want names a build is composed
// from the manifest of that build (Inputs.Running), not kept.
// HeldModuleCondition is the key of the warning raised while a module's manifest is held and its users are kept.
const HeldModuleCondition = "mesh-grants.module-held"
// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that
// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of
// a module on a node is `<node>.<module>` or `<node>.<module>.<process>` (buscompose.Principal.Username). Pure.
func heldModuleUsers(in Inputs, candidates []string) map[string]string {
held := map[string]string{}
for _, g := range in.Refused {
if module, ok := strings.CutPrefix(g.Input, "manifest of "); ok {
held[module] = g.Why
}
}
out := map[string]string{}
if len(held) == 0 {
return out
}
for node, modules := range in.Assigned {
for _, module := range modules {
why, ok := held[module]
if !ok {
continue
}
if _, runs := in.Running[node+"."+module]; runs {
continue
}
prefix := node + "." + module
for _, user := range candidates {
if user == prefix || strings.HasPrefix(user, prefix+".") {
out[user] = fmt.Sprintf("the manifest of %s is held: %s", module, why)
}
}
}
}
return out
}
// inForceGrants are the grants of each user of the list in force as the part sealed them, or nil where it kept none.
func (p *Part) inForceGrants() map[string]User {
if g, _, had := p.Sealed(); had && g.Digest != "" && p.Store != nil {
inForce, _ := p.sealedGrantsOf(g.Digest)
return inForce
}
return nil
}
// mergedRefusals is refused and held in one map, for the parts that keep a user's grants of the list in force.
func mergedRefusals(refused, held map[string]string) map[string]string {
if len(held) == 0 {
return refused
}
out := make(map[string]string, len(refused)+len(held))
for u, why := range held {
out[u] = why
}
for u, why := range refused {
out[u] = why
}
return out
}
// sayHeldModules raises HeldModuleCondition while a held module's users are kept, naming each user with what became of it,
// and clears it once none is; said once per set, again when it changes, and at the first pass after a start.
func (p *Part) sayHeldModules(held map[string]string, kept map[string]User) {
if p.Say == nil {
return
}
var lines []string
users := make([]string, 0, len(held))
for u := range held {
users = append(users, u)
}
sort.Strings(users)
for _, user := range users {
became := "not in the list in force and left out"
if _, ok := kept[user]; ok {
became = "stays in the list as the list in force holds it"
}
lines = append(lines, user+" ("+became+"): "+held[user])
}
why := strings.Join(lines, "; ")
p.holdMu.Lock()
defer p.holdMu.Unlock()
if p.heldModulesKnown && why == p.heldModulesSaid {
return
}
c := asks.Condition{Key: HeldModuleCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
if why == "" {
c.State = asks.StateCleared
} else {
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
c.Summary = fmt.Sprintf("%d bus user(s) of a module whose manifest is held are kept in the list: %s", len(users), why)
c.Headline, c.Needs = "A module's manifest is held, its bus users are kept", "a manifest the indexer accepts, or a judge that knows its fields"
c.Explanation = asks.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one judged by an indexer that knows its fields). " +
"The rest of the user list is written as ever; a held module's users stay as the list in force holds them: " + why
c.Resolved = "every module's manifest is accepted"
}
if p.Say(c) == nil {
p.heldModulesSaid, p.heldModulesKnown = why, true
}
}
+91 -5
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
@@ -353,8 +357,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
}
var m catalogue.Manifest
if err == nil {
// The indexer's refusal, carried in its value (ADR 0578 §3): held and named as a refused manifest.
if verdict := v.VerdictOf(module); verdict != nil && verdict.Refused() {
// 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())
}
}
@@ -440,6 +444,23 @@ 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)
// 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.
for _, g := range in.Refused {
module, ok := strings.CutPrefix(g.Input, "manifest of ")
if _, found := in.Held[module]; !ok || found {
continue
}
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"})
break
}
}
}
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)
@@ -551,16 +572,81 @@ func sortGaps(g []Gap) {
sort.SliceStable(g, func(i, j int) bool { return strings.Compare(g[i].Input, g[j].Input) < 0 })
}
// ReadFailures names every read this pass could not make and every manifest refused: what a list composed now may lack.
func (in Inputs) ReadFailures() []string {
// ReadFailed names every read this pass could not make: what a list composed now may lack. A manifest the indexer holds
// (refused, or waiting for a judge that knows a field of it) is not one: it holds its module's users in the list as the
// list in force has them (held.go), and never holds the list, so one manifest never freezes the bus's users (issue 774).
func (in Inputs) ReadFailed() []string {
var out []string
for _, g := range in.Gaps {
if g.Failed {
out = append(out, g.Input+": "+g.Why)
}
}
return out
}
// ReadFailures is ReadFailed and every manifest refused or waiting: what the part retires nothing on (the accounts it
// keeps, the hearings it withholds).
func (in Inputs) ReadFailures() []string {
out := in.ReadFailed()
for _, g := range in.Refused {
out = append(out, g.Input+": "+g.Why)
}
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
}
+16 -3
View File
@@ -189,6 +189,9 @@ type Part struct {
// refusedSaid is the refused principals last said as a condition, once refusedKnown (refused.go).
refusedSaid string
refusedKnown bool
// heldModulesSaid is the held modules' kept users last said as a condition, once heldModulesKnown (held.go).
heldModulesSaid string
heldModulesKnown bool
// missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go).
missingSaid string
missingKnown bool
@@ -259,14 +262,24 @@ func (p *Part) Compose() Result {
res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error())
}
// A refused principal keeps its grants of the list in force, said once per change (refused.go).
kept, _ := p.keptOf(refused)
// And a module whose manifest is held (refused, or waiting for a judge that knows a field of it) and whose want names no
// build keeps its users of the list in force (held.go).
loaded, loadErr := LoadedUsers(p.Call)
inForce := p.inForceGrants()
candidates := names(loaded)
for user := range inForce {
candidates = append(candidates, user)
}
heldUsers := heldModuleUsers(in, candidates)
allRefused := mergedRefusals(refused, heldUsers)
kept, _ := p.keptOf(allRefused)
for _, user := range sortedKeys(refused) {
res.PrincipalsRefused = append(res.PrincipalsRefused, user+": "+refused[user])
}
p.sayPrincipalsRefused(refused, kept)
p.sayHeldModules(heldUsers, kept)
users = WithHashes(withKept(users, kept), in.Hashes)
st.Digest, st.Users = Digest(users), len(users)
loaded, loadErr := LoadedUsers(p.Call)
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
if loadErr != nil {
st.Compare.Unread = loadErr.Error()
@@ -298,7 +311,7 @@ func (p *Part) Compose() Result {
if p.Store != nil {
p.writeWithheld(&res, in, recs)
}
list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, refused, kept)
list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, allRefused, kept)
// The grants the assigner reads (ADR 0547 §4, §5): one that adds a permission only once the bus loaded a list that
// carries it (issue 774).
if p.Store != nil {
+5 -17
View File
@@ -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,8 +80,8 @@ 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.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values)
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, declared, values)
return d
}
d := declare(m)
@@ -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
@@ -139,8 +140,6 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
Served: catalogue.ServedSettings(m),
// Its own object buckets (ADR 0592 §5).
Objects: objectsOf(m),
// The ledgers it asks for in `requires` (issue 1966 D5): each assignment gets a ledger account.
Ledgers: ledgersOf(m),
}
d.Loads = loadsABundle(m)
if _, reads := m.OwnSecrets["broker"]; !reads {
@@ -284,17 +283,6 @@ func readsOfObjects(module, node string, reads, refused []string, manifests map[
return states, objects, refusedOut
}
// ledgersOf are the local names of the ledgers a manifest asks for in `requires`.
func ledgersOf(m catalogue.Manifest) []string {
var out []string
for _, e := range m.BusState {
if e.Storage == catalogue.StorageLedger {
out = append(out, e.Name)
}
}
return out
}
func logsOf(m catalogue.Manifest) []buscompose.Log {
var out []buscompose.Log
for _, l := range m.Logs {
@@ -299,7 +299,7 @@ func listUsers(list []byte) []string {
// they could not be read). Empty when it may.
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost []string, digest string) string {
p.hashHold = nil
if f := in.ReadFailures(); len(f) > 0 {
if f := in.ReadFailed(); len(f) > 0 {
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
}
if bus == nil {