DLV-O1, DLV-O3, DLV-O4: the indexer answers its first views and says its views family
The indexer's tools bundle runs in a runtime of its own on its own account (runs-as) and serves the first-view tools manifests, modules and verdicts, each answering only the callers its manifest writes (ADR 0621 §1-§2, ADR 0622 §1), and the nine read tools of design 25 § 12.2. The process says each change of its ten read states on the family indexer-views in its old streams section (7 days, 256 MiB), with a mark per state the first view answers its revision from, so a reader that takes the answer misses no event after it (ADR 0615 §4-§5). The shared shape lives in sdk/views: event, answer, mark, the emitter, the tools and the reader's follower. Issue 1966. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -192,7 +192,7 @@ func heldCounts(s stdio.KeptState) func() (index.OldSectionsCount, error) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := stdio.Serve("", append(Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState), heldCounts(stdio.State(index.OldSectionsState))), catalogTools()...)); err != nil {
|
||||
if err := stdio.Serve("", append(Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState), heldCounts(stdio.State(index.OldSectionsState))), append(catalogTools(), viewTools(os.Getenv, keptSource, time.Now)...)...)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// viewTools are the indexer's first-view tools (ADR 0621 §1, ADR 0622 §1), each answering only the callers its manifest
|
||||
// writes into its env value (views.CallersEnv), and its ordinary read tools (design 25 § 12.2). Every one reads only the
|
||||
// indexer's own states, so it answers as soon as the runtime of its own is connected (ADR 0621 §3).
|
||||
func viewTools(getenv func(string) string, state func(string) views.Source, now func() time.Time) []stdio.Tool {
|
||||
marks := state(views.MarksState)
|
||||
var out []stdio.Tool
|
||||
for _, t := range index.FirstViewTools {
|
||||
out = append(out, views.FirstViewTool(delivery.IndexerModule, t.Tool, t.State, t.Words,
|
||||
views.ParseCallers(getenv(views.CallersEnv(t.Tool))), state(t.State), marks, now))
|
||||
}
|
||||
for _, t := range index.ReadTools {
|
||||
out = append(out, views.ReadTool(delivery.IndexerModule, t.Tool, t.State, t.Words, state(t.State), marks, now))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// keptSource is one of the indexer's states as its runtime reads it.
|
||||
func keptSource(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }
|
||||
@@ -5,11 +5,13 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -20,6 +22,10 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the
|
||||
@@ -83,8 +89,115 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10
|
||||
t.Errorf("%s#%d modules = %v, want [%s]", p.Repository, p.Number, p.Modules, want)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0615 §5 (DLV-O4): each change of the candidates is said on the indexer's views family, its value whole at its
|
||||
// revision; a first view taken at the mark, followed by the events after it, holds the candidate as the bucket does;
|
||||
// and a waiter on manifest-accepted never wakes on it.
|
||||
views0 := waitViews(t, ctx, js, "candidates", "feat/x")
|
||||
cur, err := candidates.Get(ctx, "feat/x")
|
||||
if err != nil || views0.Revision > cur.Revision() || views0.Deleted || len(views0.Value) == 0 || !views0.Created.Equal(bucketCreated(t, ctx, candidates)) {
|
||||
t.Errorf("views event = %+v, bucket at %d (%v)", views0, cur.Revision(), err)
|
||||
}
|
||||
marks, _ := js.KeyValue(ctx, index.MarksBucket)
|
||||
f := &views.Follower{Owner: delivery.IndexerModule, Tool: "indexer_read_candidates", State: "candidates",
|
||||
Ask: func(context.Context, string) (views.Answer, error) {
|
||||
return views.FirstView(delivery.IndexerModule, "candidates", jsSource{ctx, candidates}, jsSource{ctx, marks}, "", time.Now())
|
||||
}}
|
||||
if err := f.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range viewsEvents(t, ctx, js, "candidates") {
|
||||
if gap, why := f.Apply(e); gap {
|
||||
t.Errorf("a gap after the first view: %s", why)
|
||||
}
|
||||
}
|
||||
if v, ok := f.Get("feat/x"); !ok || string(v) != string(cur.Value()) {
|
||||
t.Errorf("the follower holds %s, the bucket %s", v, cur.Value())
|
||||
}
|
||||
if n := acceptedHeard.Load(); n != 0 {
|
||||
t.Errorf("a waiter on manifest-accepted woke %d times on the views family", n)
|
||||
}
|
||||
}
|
||||
|
||||
// jsSource is a bucket as a views.Source.
|
||||
type jsSource struct {
|
||||
ctx context.Context
|
||||
kv jetstream.KeyValue
|
||||
}
|
||||
|
||||
func (s jsSource) Keys() ([]string, error) {
|
||||
k, err := s.kv.Keys(s.ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return k, err
|
||||
}
|
||||
|
||||
func (s jsSource) Get(key string) (*views.Entry, bool, error) {
|
||||
e, err := s.kv.Get(s.ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &views.Entry{Value: e.Value(), Revision: e.Revision()}, true, nil
|
||||
}
|
||||
|
||||
func bucketCreated(t *testing.T, ctx context.Context, kv jetstream.KeyValue) time.Time {
|
||||
t.Helper()
|
||||
st, err := kv.Status(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return st.(*jetstream.KeyValueBucketStatus).StreamInfo().Created.UTC()
|
||||
}
|
||||
|
||||
// viewsEvents are every event the views family holds for state, in order, read message by message.
|
||||
func viewsEvents(t *testing.T, ctx context.Context, js jetstream.JetStream, state string) []views.Event {
|
||||
t.Helper()
|
||||
st, err := js.Stream(ctx, views0Stream)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err := st.Info(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out []views.Event
|
||||
for seq := info.State.FirstSeq; seq > 0 && seq <= info.State.LastSeq; seq++ {
|
||||
m, err := st.GetMsg(ctx, seq)
|
||||
if err != nil || m.Subject != "mesh.stream.mesh-delivery-indexer.event."+views.EventName(state) {
|
||||
continue
|
||||
}
|
||||
e, err := views.DecodeEvent(m.Data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// waitViews waits for the views family's event of state's key.
|
||||
func waitViews(t *testing.T, ctx context.Context, js jetstream.JetStream, state, key string) views.Event {
|
||||
t.Helper()
|
||||
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
|
||||
for _, e := range viewsEvents(t, ctx, js, state) {
|
||||
if e.Key == key {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("no views event of %s.%s within 20 s", state, key)
|
||||
return views.Event{}
|
||||
}
|
||||
|
||||
const views0Stream = "INDEXER_VIEWS"
|
||||
|
||||
// acceptedHeard counts what a waiter on manifest-accepted heard.
|
||||
var acceptedHeard atomic.Int64
|
||||
|
||||
// startIndexer starts the bus, the stream FORGE with the indexer's durable consumer, the buckets, lets put fill
|
||||
// mesh-controller_builds, and runs the program. mesh-delivery-gate_source is never created.
|
||||
func startIndexer(t *testing.T, put func(context.Context, buildLog), before ...func(context.Context, jetstream.JetStream)) (jetstream.JetStream, context.Context, jetstream.KeyValue, jetstream.KeyValue) {
|
||||
@@ -155,6 +268,20 @@ func startIndexer(t *testing.T, put func(context.Context, buildLog), before ...f
|
||||
Subjects: []string{buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog) + ".>"}, AllowDirect: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The indexer's views family and its marks (ADR 0615 §5), as the grants part makes them from the old sections, and a
|
||||
// waiter on manifest-accepted, which the views family never wakes.
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: views0Stream, Subjects: []string{"mesh.stream.mesh-delivery-indexer.event.views.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.MarksBucket, History: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
acceptedHeard.Store(0)
|
||||
waiter, err := ctl.Subscribe("mesh.mod.mesh-delivery-indexer.event.manifest-accepted", func(*nats.Msg) { acceptedHeard.Add(1) })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = waiter.Unsubscribe() })
|
||||
put(ctx, buildLog{ctx: ctx, js: js})
|
||||
for _, b := range before {
|
||||
b(ctx, js)
|
||||
|
||||
@@ -469,6 +469,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// What the indexer heard of FORGE (issue 1126 note 76) is bound when its state appears, as every state a delivery adds
|
||||
// (issue 1790): until then no message is written there, and the rest of the indexer runs.
|
||||
heard := awaitedStore{process.AwaitBucket(ctx, js, conn, index.HeardBucket, "no FORGE message the indexer handles is written for indexer_heard", logger.Printf)}
|
||||
// Each change of the states its readers read is said on its views family (ADR 0615 §5), from the mark each state was
|
||||
// said up to; a first view (the tools bundle) answers at that mark.
|
||||
startViews(ctx, js, cred, process.AwaitBucket(ctx, js, conn, index.MarksBucket, "no change is said on the views family, so every reader waits on its first view", logger.Printf),
|
||||
process.AwaitBucket(ctx, js, conn, findingsBucket, "no finding of the indexer is written", logger.Printf),
|
||||
map[string]readyOf{"manifests": boundNow(manifests), "modules": boundNow(modules), "candidates": boundNow(candidates),
|
||||
index.VerdictsState: awaited(verdicts), delivery.IndexerPredefinedState: awaited(predefined),
|
||||
delivery.IndexerConfigCandidatesState: awaited(configCandidates), delivery.PackagesState: awaited(packages),
|
||||
graphState: awaited(graph), index.HeardState: awaited(heard.b), "taken": awaited(taken)}, errs, logger.Printf)
|
||||
cc, err := process.HearEvents(cons, func(m jetstream.Msg) { handle(ctx, m, ix, heard, logger); ix.KickFill(ctx) }, logger.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
)
|
||||
|
||||
// findingsBucket is the indexer's own findings, which the condition keeper raises and clears (ADR 0564 §2): a value too
|
||||
// large for its views event, and its first view of the assigner's placement unanswered past its bound.
|
||||
var findingsBucket = health.FindingsBucket(delivery.IndexerModule)
|
||||
|
||||
// readyOf binds a state's bucket: now for one bound at the start, once it appears for one awaited.
|
||||
type readyOf func(func(jetstream.KeyValue))
|
||||
|
||||
func boundNow(kv jetstream.KeyValue) readyOf { return func(f func(jetstream.KeyValue)) { f(kv) } }
|
||||
|
||||
func awaited(b *process.AwaitedBucket) readyOf { return b.Ready }
|
||||
|
||||
// startViews says each change of the indexer's viewed states on its views family (ADR 0615 §5), once its marks state
|
||||
// is bound: each state from where its mark says, as soon as its own bucket is bound. A watch that ends is sent on errs.
|
||||
func startViews(ctx context.Context, js jetstream.JetStream, cred process.Account, marks, findings *process.AwaitedBucket, states map[string]readyOf, errs chan<- error, logf func(string, ...any)) {
|
||||
tooLarge := func(state, key string, size int) {
|
||||
kv := findings.KV()
|
||||
if kv == nil {
|
||||
return
|
||||
}
|
||||
k, v, err := views.TooLargeFinding(delivery.IndexerModule, state, key, size, time.Now())
|
||||
if err == nil {
|
||||
_, err = kv.Put(ctx, k, v)
|
||||
}
|
||||
if err != nil {
|
||||
logf("views: the finding of %s.%s too large is not written: %v", state, key, err)
|
||||
}
|
||||
}
|
||||
marks.Ready(func(mk jetstream.KeyValue) {
|
||||
em := views.Emitter{Owner: delivery.IndexerModule, Subject: cred.StreamSubject, Publish: views.JetStreamPublish(js),
|
||||
Marks: mk, TooLarge: tooLarge, Logf: logf}
|
||||
for _, state := range sortedStates(states) {
|
||||
states[state](func(kv jetstream.KeyValue) {
|
||||
if err := em.Start(ctx, state, kv, errs); err != nil {
|
||||
logf("%v: its readers ask its first view at each change until the indexer starts again", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func sortedStates(states map[string]readyOf) []string {
|
||||
out := make([]string, 0, len(states))
|
||||
for s := range states {
|
||||
out = append(out, s)
|
||||
}
|
||||
for i := range out {
|
||||
for j := i + 1; j < len(out); j++ {
|
||||
if out[j] < out[i] {
|
||||
out[i], out[j] = out[j], out[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// graphState is the module graph's state, named apart from run's bucket of the same name.
|
||||
const graphState = graph.State
|
||||
@@ -0,0 +1,55 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
)
|
||||
|
||||
// ViewsStream is the indexer's views family on its old streams section (ADR 0615 §5): one event per changed key of
|
||||
// each state in ViewedStates.
|
||||
const ViewsStream = views.IndexerStream
|
||||
|
||||
// MarksBucket is the indexer's state of what its views family said each state up to.
|
||||
const MarksBucket = delivery.IndexerModule + "_" + views.MarksState
|
||||
|
||||
// FirstViewTools are the indexer's first-view tools (ADR 0621 §1, ADR 0622 §1): the tool, the state it answers, and
|
||||
// the words of what that state holds. Each answers only its callers list (CallersEnv), which the manifest writes.
|
||||
var FirstViewTools = []ViewedTool{
|
||||
{Tool: "manifests", State: "manifests", Words: "every manifest the indexer keeps, by its digest."},
|
||||
{Tool: "modules", State: "modules", Words: "every module the indexer holds, its fingerprint, inputs and manifest digest."},
|
||||
{Tool: "verdicts", State: VerdictsState, Words: "the judge's verdict on each manifest: accepted, waiting or refused."},
|
||||
}
|
||||
|
||||
// ReadTools are the indexer's ordinary read tools, one per state its readers read (design 25 § 12.2).
|
||||
var ReadTools = []ViewedTool{
|
||||
{Tool: "indexer_read_manifests", State: "manifests", Words: "every manifest the indexer keeps, by its digest."},
|
||||
{Tool: "indexer_read_modules", State: "modules", Words: "every module the indexer holds, its fingerprint, inputs and manifest digest."},
|
||||
{Tool: "indexer_read_predefined", State: delivery.IndexerPredefinedState, Words: "the predefined config values of each module on the trunk."},
|
||||
{Tool: "indexer_read_config_candidates", State: delivery.IndexerConfigCandidatesState, Words: "the config declarations of each open pull request."},
|
||||
{Tool: "indexer_read_candidates", State: "candidates", Words: "the candidate groups: the pull requests sharing a branch, their heads and the modules each touches."},
|
||||
{Tool: "indexer_read_packages", State: delivery.PackagesState, Words: "the published packages per series."},
|
||||
{Tool: "indexer_read_graph", State: graph.State, Words: "the module graph: each module's build and provision edges."},
|
||||
{Tool: "indexer_read_heard", State: HeardState, Words: "the forge messages the indexer heard and what it did with each."},
|
||||
{Tool: "indexer_read_taken", State: "taken", Words: "each repository's merge the indexer took in last."},
|
||||
}
|
||||
|
||||
// ViewedTool is one tool over one state of the indexer's.
|
||||
type ViewedTool struct {
|
||||
Tool, State, Words string
|
||||
}
|
||||
|
||||
// ViewedStates are the states the indexer's views family says, each by its bucket: the ten its readers read.
|
||||
var ViewedStates = map[string]string{
|
||||
"manifests": delivery.ManifestsBucket,
|
||||
"modules": delivery.ModulesBucket,
|
||||
VerdictsState: VerdictsBucket,
|
||||
delivery.IndexerPredefinedState: PredefinedBucket,
|
||||
delivery.IndexerConfigCandidatesState: ConfigCandidatesBucket,
|
||||
"candidates": delivery.CandidatesBucket,
|
||||
delivery.PackagesState: delivery.PackagesBucket,
|
||||
graph.State: graph.Bucket,
|
||||
HeardState: HeardBucket,
|
||||
"taken": delivery.TakenBucket,
|
||||
}
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521,
|
||||
@@ -29,19 +33,29 @@ type state struct {
|
||||
}
|
||||
|
||||
type artifact struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Language string `json:"language"`
|
||||
System string `json:"system"`
|
||||
From string `json:"from"`
|
||||
Binary string `json:"binary"`
|
||||
Loads []string `json:"loads"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Language string `json:"language"`
|
||||
System string `json:"system"`
|
||||
From string `json:"from"`
|
||||
Binary string `json:"binary"`
|
||||
Loads []string `json:"loads"`
|
||||
Env map[string]string `json:"env"`
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
Name string `json:"name"`
|
||||
Events []string `json:"events"`
|
||||
MaxAgeDays int `json:"max-age-days"`
|
||||
MaxMiB int `json:"max-mib"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Slug string `json:"slug"`
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Slug string `json:"slug"`
|
||||
RunsAs string `json:"runs-as"`
|
||||
Streams []stream `json:"streams"`
|
||||
Claims []struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
@@ -99,7 +113,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
}
|
||||
// 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},
|
||||
{Name: graph.State, History: graph.History}}; !reflect.DeepEqual(m.State, want) {
|
||||
{Name: graph.State, History: graph.History}, {Name: views.MarksState, History: 1}, {Name: health.FindingsState, History: 5}}; !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) {
|
||||
@@ -125,17 +139,51 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
// 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). 1966/AC11: indexer_old_sections reads the count of the old sections (state oldsections).
|
||||
if want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
// ADR 0621 §1, ADR 0622 §1: the first-view tools manifests, modules and verdicts, and design 25 § 12.2's nine read
|
||||
// tools, all of the tools bundle, which runs in a runtime of its own on the indexer's account (runs-as).
|
||||
want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge"}
|
||||
for _, v := range append(append([]index.ViewedTool{}, index.FirstViewTools...), index.ReadTools...) {
|
||||
want = append(want, v.Tool)
|
||||
if _, ok := index.ViewedStates[v.State]; !ok {
|
||||
t.Errorf("%s answers the state %s, which the views family does not say", v.Tool, v.State)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(m.Tools, want) || len(want) != 16 {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
if m.RunsAs != m.Module || m.SecretsOwner != m.RunsAs {
|
||||
t.Errorf("runs-as = %q, secrets-owner %q: the tools bundle runs on the indexer's own account (ADR 0621 §1)", m.RunsAs, m.SecretsOwner)
|
||||
}
|
||||
// ADR 0621 §2, ADR 0622 §1: each first-view tool answers exactly its readers; the indexer is on none.
|
||||
callers := map[string]string{"manifests": "mesh-grants,node-tools,mesh-delivery-assigner", "modules": "mesh-grants,node-tools,mesh-delivery-assigner", "verdicts": "mesh-grants"}
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a.Name != "tools" {
|
||||
continue
|
||||
}
|
||||
if len(a.Env) != len(callers) {
|
||||
t.Errorf("tools env = %v, want only the three callers lists", a.Env)
|
||||
}
|
||||
for tool, list := range callers {
|
||||
if got := a.Env[views.CallersEnv(tool)]; got != list {
|
||||
t.Errorf("%s callers = %q, want %q", tool, got, list)
|
||||
}
|
||||
if views.ParseCallers(a.Env[views.CallersEnv(tool)]).Allows("n1." + m.Module) {
|
||||
t.Errorf("%s answers the indexer itself", tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ADR 0615 §5, ADR 0619 §2: its views family on its old streams section, 7 days and 256 MiB, dropping its oldest.
|
||||
if want := []stream{{Name: index.ViewsStream, Events: []string{views.EventPattern}, MaxAgeDays: views.StreamDays, MaxMiB: views.StreamMiB}}; !reflect.DeepEqual(m.Streams, want) {
|
||||
t.Errorf("streams = %+v, want %+v", m.Streams, want)
|
||||
}
|
||||
// ADR 0604 §2: it holds the seat mesh-catalog, serving its four verbs from its tools bundle (which stays without a
|
||||
// request subject).
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != catalogue.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
|
||||
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents"}) {
|
||||
t.Errorf("claims = %+v, want mesh-catalog with its four verbs", m.Claims)
|
||||
}
|
||||
// No streams, no bus, no other seat (ADR 0437 §4); it says no event.
|
||||
for _, k := range []string{"streams", "logs", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} {
|
||||
// No bus, no other seat (ADR 0437 §4).
|
||||
for _, k := range []string{"logs", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} {
|
||||
if _, ok := top[k]; ok {
|
||||
t.Errorf("the manifest declares %q: the indexer has none", k)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"module": "mesh-delivery-indexer",
|
||||
"version": "1",
|
||||
"slug": "indexer",
|
||||
"runs-as": "mesh-delivery-indexer",
|
||||
"claims": [
|
||||
{
|
||||
"name": "mesh-catalog",
|
||||
@@ -18,7 +19,19 @@
|
||||
"indexer_take_in",
|
||||
"indexer_old_sections",
|
||||
"indexer_verdicts",
|
||||
"indexer_judge"
|
||||
"indexer_judge",
|
||||
"manifests",
|
||||
"modules",
|
||||
"verdicts",
|
||||
"indexer_read_manifests",
|
||||
"indexer_read_modules",
|
||||
"indexer_read_predefined",
|
||||
"indexer_read_config_candidates",
|
||||
"indexer_read_candidates",
|
||||
"indexer_read_packages",
|
||||
"indexer_read_graph",
|
||||
"indexer_read_heard",
|
||||
"indexer_read_taken"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-build-go.toolchain.reported",
|
||||
@@ -38,7 +51,12 @@
|
||||
{ "name": "verdicts", "history": 5 },
|
||||
{ "name": "oldsections", "history": 1 },
|
||||
{ "name": "packages", "history": 5 },
|
||||
{ "name": "graph", "history": 5 }
|
||||
{ "name": "graph", "history": 5 },
|
||||
{ "name": "view-marks", "history": 1 },
|
||||
{ "name": "findings", "history": 5 }
|
||||
],
|
||||
"streams": [
|
||||
{ "name": "indexer-views", "events": ["views.**"], "max-age-days": 7, "max-mib": 256 }
|
||||
],
|
||||
"emits": [
|
||||
"manifest-refused",
|
||||
@@ -115,7 +133,12 @@
|
||||
"binary": "mesh-delivery-indexer-tools",
|
||||
"loads": [
|
||||
"mesh-delivery-indexer-tools"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"MESH_VIEW_CALLERS_MANIFESTS": "mesh-grants,node-tools,mesh-delivery-assigner",
|
||||
"MESH_VIEW_CALLERS_MODULES": "mesh-grants,node-tools,mesh-delivery-assigner",
|
||||
"MESH_VIEW_CALLERS_VERDICTS": "mesh-grants"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user