Kept calls in the assigner, teller, builder and indexer; take-in judges what it writes; hand tools for each recovery (ADR 0668; issue 1966; group fix/1966-call-made-once) #199
@@ -87,7 +87,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
|
||||
// The states and the stream the controller makes from the indexer's manifest (old sections, ADR 0595 §11).
|
||||
for bucket, history := range map[string]uint8{index.PendingBucket: 1, index.VerdictsBucket: index.VerdictsHistory,
|
||||
index.GraphBucket: 1, index.TakenBucket: 1, index.PackagesBucket: 1,
|
||||
index.SeatDefinitionsBucket: 1, index.RequirementsBucket: 1, findingsBucket: 1, index.HoldsBucket: 1} {
|
||||
index.SeatDefinitionsBucket: 1, index.RequirementsBucket: 1, findingsBucket: 1, index.HoldsBucket: 1, index.UnjudgedBucket: 1} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucket, History: history}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -415,6 +415,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// The module graph (ADR 0603 §2): a state this delivery brings, bound when it appears; until then no graph is written.
|
||||
graph := process.AwaitBucket(ctx, js, conn, index.GraphBucket, "no module graph is written", logger.Printf)
|
||||
ownBuildState := process.AwaitBucket(ctx, js, conn, index.OwnBuildBucket, "which build this indexer runs is not kept", logger.Printf)
|
||||
// The manifests a take-in wrote and no judge pass judged yet (issue 1966): a state this delivery brings, bound when it
|
||||
// appears; until then a take-in's new manifests are kept for their judging in its memory alone.
|
||||
unjudged := process.AwaitBucket(ctx, js, conn, index.UnjudgedBucket, "a take-in's new manifests are kept for their judging in its memory alone", logger.Printf)
|
||||
tryRecovery := process.AwaitBucket(ctx, js, conn, index.TryRecoveryBucket, "how the recovery of the open tries went is not kept", logger.Printf)
|
||||
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "no finding of the indexer is written", logger.Printf)
|
||||
// The seat definitions (design 26, DLV-I17): a state this delivery brings, bound when it appears; until then none is kept.
|
||||
@@ -423,7 +426,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
seatBus := &seatBus{defs: awaitedSource{ctx, seatDefs}, marks: awaitedSource{ctx, marks}}
|
||||
seatReader, saySeats := followSeatList(seatBus, saySeatDefinition(js, cred), logger.Printf)
|
||||
readWhenBound(seatReader, seatDefs, marks)
|
||||
ix := &index.Indexer{Findings: awaitedStore{b: findings}, OldSections: awaitedStore{b: oldSections}, Requirements: awaitedStore{b: requirements}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{b: taken}, Builds: known,
|
||||
ix := &index.Indexer{Findings: awaitedStore{b: findings}, OldSections: awaitedStore{b: oldSections}, Unjudged: awaitedStore{b: unjudged}, Requirements: awaitedStore{b: requirements}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{b: taken}, Builds: known,
|
||||
Verdicts: awaitedStore{b: verdicts}, SayRefused: sayRefused(conn, cred, logger.Printf), SayAccepted: sayAccepted(conn, cred),
|
||||
Predefined: awaitedStore{b: predefined}, ConfigCandidates: awaitedStore{b: configCandidates}, Logf: logger.Printf,
|
||||
Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call),
|
||||
|
||||
@@ -68,6 +68,9 @@ type Indexer struct {
|
||||
// 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
|
||||
// Unjudged is the state of the manifest digests a take-in wrote and no judge pass judged yet (unjudged.go, issue 1966);
|
||||
// nil keeps them only in the take-in's memory.
|
||||
Unjudged Store
|
||||
// Requirements is the requirements state (requirements.go, DLV-I7): what each consumer's go.sum requires of each mesh
|
||||
// Go module, kept at each merge; nil keeps none.
|
||||
Requirements Store
|
||||
|
||||
@@ -877,6 +877,8 @@ func (ix *Indexer) dry() *dryRun {
|
||||
x.Predefined, d.predefined = over(ix.Predefined)
|
||||
x.ConfigCandidates, d.configs = over(ix.ConfigCandidates)
|
||||
x.OldSections, d.oldSections = over(ix.OldSections)
|
||||
// What the trunk holds unjudged is read through, as fresh; what the dry run would keep stays in its overlay (issue 1966).
|
||||
x.Unjudged, _ = over(ix.Unjudged)
|
||||
ix.mu.Lock()
|
||||
x.source = sourceNow{rev: ix.source.rev, heads: maps.Clone(ix.source.heads)}
|
||||
x.previews = maps.Clone(ix.previews)
|
||||
|
||||
@@ -901,7 +901,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
|
||||
t.Run("the dry run names every field of the indexer", func(t *testing.T) {
|
||||
// dry (pending.go) builds an indexer field by field: a field added to Indexer is named there, or this fails.
|
||||
named := map[string]bool{}
|
||||
for _, f := range []string{"Store", "Modules", "Manifests", "Predefined", "ConfigCandidates", "Taken", "Verdicts", "OldSections",
|
||||
for _, f := range []string{"Store", "Modules", "Manifests", "Predefined", "ConfigCandidates", "Taken", "Verdicts", "OldSections", "Unjudged",
|
||||
"JudgeID", "SayRefused", "SayAccepted", "Packages", "Graph", "Files", "GoSum", "GoMod", "Now", "Builds", "Named", "Trunk",
|
||||
"Read", "Contains", "Logf", "ReadOpen", "OwnBuild", "Message", "Pending", "SayTakeIn",
|
||||
"Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinition", "Holds", "Counted", "AskEngines", "HoldBounds", "OpenTries", "TryRecovery", "OwnBuildState"} {
|
||||
|
||||
@@ -415,6 +415,10 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
|
||||
}
|
||||
if next.Manifest != "" && (!held || cur.Manifest != next.Manifest) {
|
||||
w.taken(next.Manifest) // a new module, or a changed manifest: fresh, judged in full (ADR 0579 §1)
|
||||
// Kept on the bus before the module names it, so a judge pass that fails does not lose it (issue 1966).
|
||||
if err := ix.keepUnjudged(ctx, next.Manifest, key, w); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
data, err := meshcatalogclient.EncodeModule(next)
|
||||
wrote = err == nil && !unchanged(data, false, value, found)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
)
|
||||
|
||||
// What a take-in newly wrote is kept until judged (issue 1966, ADR 0668 §4; ADR 0579 §1). A manifest a take-in writes new
|
||||
// is fresh: judged in full, never taken as accepted before. A judge pass that fails (the verdicts state not on the bus, a
|
||||
// restart between the write and the pass) used to lose that set, which lived only in the take-in's memory: the next pass
|
||||
// found no verdict, took the manifest as held before any verdict, and wrote a refusal as a warning. So the take-in writes
|
||||
// each digest into the indexer's own state unjudged before it writes the module naming it, every judge pass judges those
|
||||
// digests as fresh, and a pass deletes a digest's key once its verdict is written or no module holds it any more. The
|
||||
// tool indexer_read_unjudged reads the state; indexer_judge judges again by hand, which ends what it holds.
|
||||
const (
|
||||
// UnjudgedState is the indexer's state of the digests a take-in wrote and no judge pass judged yet: the bucket
|
||||
// mesh-delivery-indexer_unjudged, one key per manifest digest.
|
||||
UnjudgedState = "unjudged"
|
||||
UnjudgedBucket = Module + "_" + UnjudgedState
|
||||
)
|
||||
|
||||
// UnjudgedValue is one key of the unjudged state: the module whose new manifest the digest is, and the merge that wrote it.
|
||||
type UnjudgedValue struct {
|
||||
V int `json:"v"`
|
||||
Module string `json:"module"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Merge string `json:"merge,omitempty"`
|
||||
}
|
||||
|
||||
// keepUnjudged writes the digest into the unjudged state before the module naming it is written. A state not on the bus
|
||||
// yet (an indexer whose delivery brings it) is said and the take-in goes on, judged from its memory alone as before;
|
||||
// any other failure fails the write, so the module is not written with its digest unkept.
|
||||
func (ix *Indexer) keepUnjudged(ctx context.Context, digest, module string, w *mergeWrite) error {
|
||||
if ix.Unjudged == nil {
|
||||
return nil
|
||||
}
|
||||
v := UnjudgedValue{V: 1, Module: module}
|
||||
if w != nil {
|
||||
v.Repository, v.Merge = w.repository, w.merge
|
||||
}
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = write(ctx, ix.Unjudged, digest, func(value []byte, found bool) ([]byte, bool, error) {
|
||||
if found {
|
||||
return nil, false, nil // kept already: the first take-in that wrote it stands
|
||||
}
|
||||
return data, false, nil
|
||||
})
|
||||
if errors.Is(err, process.ErrNotYet) {
|
||||
ix.logf("judge: the state %s is not on the bus yet, so the new manifest %s of %s is kept for its judging only in this take-in", UnjudgedBucket, digest, module)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s.%s: %w", UnjudgedState, digest, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// withUnjudged is fresh with every digest the unjudged state holds; a state that cannot be listed is said and fresh
|
||||
// alone is judged.
|
||||
func (ix *Indexer) withUnjudged(ctx context.Context, fresh map[string]bool) map[string]bool {
|
||||
if ix.Unjudged == nil {
|
||||
return fresh
|
||||
}
|
||||
keys, err := ix.Unjudged.Keys(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, process.ErrNotYet) {
|
||||
ix.logf("judge: the state %s cannot be listed (%v), so only this take-in's new manifests are judged as new", UnjudgedBucket, err)
|
||||
}
|
||||
return fresh
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return fresh
|
||||
}
|
||||
out := maps.Clone(fresh)
|
||||
if out == nil {
|
||||
out = map[string]bool{}
|
||||
}
|
||||
for _, k := range keys {
|
||||
out[k] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// endUnjudged deletes each key of the unjudged state whose digest the pass judged with its verdict written, or which no
|
||||
// module holds any more (a later merge replaced it): what is left waits for the next pass.
|
||||
func (ix *Indexer) endUnjudged(ctx context.Context, judged, held map[string]bool) {
|
||||
if ix.Unjudged == nil {
|
||||
return
|
||||
}
|
||||
keys, err := ix.Unjudged.Keys(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, k := range slices.Sorted(slices.Values(keys)) {
|
||||
if held[k] && !judged[k] {
|
||||
continue
|
||||
}
|
||||
if err := write(ctx, ix.Unjudged, k, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }); err != nil {
|
||||
ix.logf("judge: %s.%s is not ended: %v; judged again as new by the next pass", UnjudgedState, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,11 +84,17 @@ func (ix *Indexer) judgeOverHeld(ctx context.Context, fresh map[string]bool, ove
|
||||
}
|
||||
if over == nil {
|
||||
ix.SayUnsaid(ctx) // a manifest-accepted a pass before failed to publish is said again first (DLV-I9b)
|
||||
// What a take-in wrote and no pass judged yet is fresh too, whichever pass judges it (unjudged.go, issue 1966).
|
||||
fresh = ix.withUnjudged(ctx, fresh)
|
||||
}
|
||||
held, err := ix.heldModules(ctx)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
heldDigests, judgedDigests := map[string]bool{}, map[string]bool{}
|
||||
for _, m := range held {
|
||||
heldDigests[m.Manifest] = m.Manifest != ""
|
||||
}
|
||||
trunkOf := map[string]string{} // a judged change's module to the trunk's manifest digest it changes (DLV-I15)
|
||||
// The known packages a manifest's `packages` may name (DLV-I14). The digest judged the trunk does not hold (a pull
|
||||
// request's candidate, one the trunk no longer names) is not judged on them: the checker judges a change's names,
|
||||
@@ -212,6 +218,7 @@ func (ix *Indexer) judgeOverHeld(ctx context.Context, fresh map[string]bool, ove
|
||||
ix.logf("judge: the verdict on %s (digest %s) is not written: %v", name, digests[name], err)
|
||||
continue
|
||||
}
|
||||
judgedDigests[digests[name]] = true
|
||||
if wrote && over != nil {
|
||||
out.Written++
|
||||
ix.logf("judge: %s judged again by %s: %s", digests[name], id, v.State)
|
||||
@@ -243,6 +250,7 @@ func (ix *Indexer) judgeOverHeld(ctx context.Context, fresh map[string]bool, ove
|
||||
if over != nil {
|
||||
return out, nil
|
||||
}
|
||||
ix.endUnjudged(ctx, judgedDigests, heldDigests)
|
||||
if err := ix.keepOldSections(ctx, judge.CountOldSections(accepted), judge.CountOwnerReads(accepted)); err != nil {
|
||||
ix.logf("judge: the count of the old sections is not kept: %v", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"mesh-delivery-indexer/judge"
|
||||
)
|
||||
@@ -137,6 +138,37 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
if err := ix.writeOverlay(ctx, map[string]meshcatalog.ModulesByModuleValue{"old": same}, nil, w); err != nil || !w.fresh[other] {
|
||||
t.Fatalf("a changed digest is not fresh: %v %v", w.fresh, err)
|
||||
}
|
||||
// 1966 (ADR 0668 §4, ADR 0579 §1): what a take-in newly wrote is kept on the bus until judged. A take-in writes a
|
||||
// manifest a judge refuses in full, its judge pass fails on the verdicts state, and the next pass, which knows nothing
|
||||
// of that take-in (a start, a pass kicked by a message), still refuses it in full, never as a warning, and ends the key.
|
||||
ix.Unjudged = newMem()
|
||||
keptDigest, err := ix.keepManifest(ctx, "kept", `{"module":"kept","version":"1","requires":["amqp"]}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kw := &mergeWrite{repository: "novox/mesh-catalog", merge: "m-kept"}
|
||||
keptValue := meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("e", 64), Manifest: keptDigest, Repository: "novox/mesh-catalog", Commit: "c3"}
|
||||
if err := ix.writeOverlay(ctx, map[string]meshcatalog.ModulesByModuleValue{"kept": keptValue}, nil, kw); err != nil || !kw.fresh[keptDigest] {
|
||||
t.Fatalf("the take-in's write: %v, fresh %v", err, kw.fresh)
|
||||
}
|
||||
if value, _, found, _ := ix.Unjudged.Get(ctx, keptDigest); !found || !strings.Contains(string(value), `"module":"kept"`) || !strings.Contains(string(value), "m-kept") {
|
||||
t.Fatalf("the new manifest is not kept as unjudged: %s", value)
|
||||
}
|
||||
ix.Verdicts.(*memStore).failWrite = func(string) error { return process.ErrNotYet }
|
||||
if _, err := ix.JudgeAll(ctx, kw.fresh); err == nil {
|
||||
t.Fatal("a pass whose verdicts state is not on the bus did not fail")
|
||||
}
|
||||
ix.Verdicts.(*memStore).failWrite = nil
|
||||
if j, err := ix.JudgeAll(ctx, nil); err != nil || slices.Contains(j.Outgrown, "kept") {
|
||||
t.Fatalf("the next pass: %+v %v", j, err)
|
||||
}
|
||||
if v := verdict(keptDigest); !meshcatalogclient.VerdictRefuses(v) || len(v.Warnings) != 0 {
|
||||
t.Fatalf("a refusal of a manifest whose judge pass failed became %+v", v)
|
||||
}
|
||||
if keys, err := ix.Unjudged.Keys(ctx); err != nil || len(keys) != 0 {
|
||||
t.Fatalf("the unjudged state still holds %v (%v) after its verdict is written", keys, err)
|
||||
}
|
||||
ix.Unjudged = nil
|
||||
// 1966/AC10: a newly accepted manifest asking for bus state is said once as manifest-accepted, carrying both entries
|
||||
// whole; a verdict already held says nothing.
|
||||
var accepted []meshcatalog.ManifestAcceptedEvent
|
||||
|
||||
@@ -32,6 +32,7 @@ var ReadTools = []ViewedTool{
|
||||
{Tool: "indexer_read_taken", State: "taken", Words: "each repository's merge the indexer took in last."},
|
||||
{Tool: "indexer_read_findings", State: mcc.FindingsState, Words: "the indexer's findings, which the condition keeper raises and clears."},
|
||||
{Tool: "indexer_read_seat_definitions", State: SeatDefinitionsState, Words: "the seat definitions the trunk's and each open pull request's contracts modules hold, resolved, or why one does not parse."},
|
||||
{Tool: "indexer_read_unjudged", State: UnjudgedState, Words: "the manifests a take-in newly wrote that no judge pass judged yet, by digest, each with its module and merge; indexer_judge judges them."},
|
||||
}
|
||||
|
||||
// ViewedTool is one tool over one state of the indexer's.
|
||||
@@ -53,4 +54,5 @@ var ViewedStates = map[string]string{
|
||||
"taken": TakenBucket,
|
||||
mcc.FindingsState: mcc.FindingsBucket(Module),
|
||||
SeatDefinitionsState: SeatDefinitionsBucket,
|
||||
UnjudgedState: UnjudgedBucket,
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
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: "try-recovery", History: 1}, {Name: "own-build", History: 1}, {Name: index.PackagesState, History: index.PackagesHistory},
|
||||
{Name: graph.State, History: graph.History}, {Name: views.MarksState, History: 1}, {Name: mcc.FindingsState, History: 5}, {Name: views.StatusState, History: 1},
|
||||
{Name: index.RequirementsState, History: 5}, {Name: index.SeatDefinitionsState, History: 5},
|
||||
{Name: index.HoldsState, History: 5}, {Name: index.EngineReadsState, History: 1}, {Name: index.HoldBoundsState, History: 1, KeyTTL: true}, {Name: index.PendingState, History: 1}}; !reflect.DeepEqual(m.State, want) { // DLV-I7, DLV-I17, ADR 0632 §2
|
||||
{Name: index.HoldsState, History: 5}, {Name: index.EngineReadsState, History: 1}, {Name: index.HoldBoundsState, History: 1, KeyTTL: true}, {Name: index.PendingState, History: 1}, {Name: index.UnjudgedState, History: 1}}; !reflect.DeepEqual(m.State, want) { // DLV-I7, DLV-I17, ADR 0632 §2
|
||||
t.Errorf("state = %+v, want %+v", m.State, want)
|
||||
}
|
||||
// D-11 (ADR 0634 §1): the holders and buckets the indexer names (internal/index/names.go) are its manifest's: its own
|
||||
@@ -176,7 +176,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
index.SeatDefinitionTool, index.SeatDefinitionsAgainTool, index.OutOfStepTool, // DLV-I19
|
||||
index.HoldsTool, index.EngineReadsTool, // ADR 0632 §2: the mesh-wide holds and the engines' reads they weigh
|
||||
index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TryCandidatesTool, index.DropTryCandidateTool, index.RecoverTriesTool, index.TakeInMergeTool, index.FingerprintsAgainTool) // issue 1966; 1585/T12, T13 (ADR 0633 §5)
|
||||
if !reflect.DeepEqual(m.Tools, want) || len(want) != 39 {
|
||||
if !reflect.DeepEqual(m.Tools, want) || len(want) != 40 {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// Every read tool of the indexer a reader of this repository invokes is one it serves (the checker's seventh first
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"indexer_read_taken",
|
||||
"indexer_read_findings",
|
||||
"indexer_read_seat_definitions",
|
||||
"indexer_read_unjudged",
|
||||
"indexer_first_views",
|
||||
"indexer_first_view_again",
|
||||
"indexer_config_readers",
|
||||
@@ -94,7 +95,8 @@
|
||||
{ "name": "holds", "history": 5 },
|
||||
{ "name": "engine-reads", "history": 1 },
|
||||
{ "name": "hold-bounds", "history": 1, "key-ttl": true },
|
||||
{ "name": "pending", "history": 1 }
|
||||
{ "name": "pending", "history": 1 },
|
||||
{ "name": "unjudged", "history": 1 }
|
||||
],
|
||||
"emits": [
|
||||
"take-in.pending",
|
||||
|
||||
Reference in New Issue
Block a user