diff --git a/modules/mesh-delivery-indexer/internal/index/seed_steps.go b/modules/mesh-delivery-indexer/internal/index/seed_steps.go index ff6994e0..587261ae 100644 --- a/modules/mesh-delivery-indexer/internal/index/seed_steps.go +++ b/modules/mesh-delivery-indexer/internal/index/seed_steps.go @@ -87,7 +87,7 @@ func (ix *Indexer) SeedModulesFrom(ctx context.Context, defs map[string]seatdef. name := artifactName(a.Ref) sa := SeedArtifact{Name: name, Seat: seat} in := nodebuildgo.StepsInput{Module: e.Module, Artifact: nodebuildgo.DetectedStepsByModuleArtifactKey(e.Module, name), Fingerprint: e.Fp, - Fetch: &nodebuildgo.Fetch{Repository: e.Repository, Commit: e.Commit, Dir: e.Path, Paths: namedPaths(e, name)}} + Fetch: &nodebuildgo.Fetch{Repository: e.Repository, Commit: e.Commit, Dir: e.Path, Paths: namedPaths(e, name), Packages: maps.Clone(e.Packages)}} ans, err := steps(ctx, seat, in) switch { case err != nil: @@ -124,27 +124,20 @@ func (ix *Indexer) stepsFindings(ctx context.Context, answered map[string]bool, if ix.Findings == nil { return } - ix.mu.Lock() - if ix.stepsSince == nil { - ix.stepsSince = map[string]time.Time{} - } now := ix.now() var raise []string + since := map[string]time.Time{} for seat := range unanswered { - if _, ok := ix.stepsSince[seat]; !ok { - ix.stepsSince[seat] = now - } - if now.Sub(ix.stepsSince[seat]) >= mcc.CallUnansweredAfter { + since[seat] = ix.stepsUnansweredSince(ctx, seat, now) + if now.Sub(since[seat]) >= mcc.CallUnansweredAfter { raise = append(raise, seat) } } for seat := range answered { if _, bad := unanswered[seat]; !bad { - delete(ix.stepsSince, seat) + ix.stepsAnswered(ctx, seat) } } - since := maps.Clone(ix.stepsSince) - ix.mu.Unlock() for _, seat := range slices.Sorted(slices.Values(raise)) { id := seat + "." + StepsVerb f := ix.observed(mcc.ScopeCall, id, mcc.KindCallUnanswered, @@ -176,3 +169,50 @@ func namedPaths(e meshcatalog.CatalogEntry, name string) []string { } return e.Inputs.Paths[ArtifactRefOf(e.Module, name)] } + +// StepsSincePrefix opens the keys of the own-build state (OwnBuildState) that keep, by build seat, since when its steps +// has not answered (design 47 G A-66): a restart keeps the time, and the finding is written at the next seed attempt past +// five minutes. +const StepsSincePrefix = "steps-since." + +// stepsUnansweredSince is the time the seat first failed to answer, kept in the indexer's own state; the first failure +// writes now. Where no state is kept it is held in memory. +func (ix *Indexer) stepsUnansweredSince(ctx context.Context, seat string, now time.Time) time.Time { + if ix.OwnBuildState == nil { + ix.mu.Lock() + defer ix.mu.Unlock() + if ix.stepsSince == nil { + ix.stepsSince = map[string]time.Time{} + } + if _, ok := ix.stepsSince[seat]; !ok { + ix.stepsSince[seat] = now + } + return ix.stepsSince[seat] + } + key := StepsSincePrefix + seat + if v, _, found, err := ix.OwnBuildState.Get(ctx, key); err == nil && found { + if t, err := time.Parse(time.RFC3339Nano, string(v)); err == nil { + return t + } + } + if err := ix.OwnBuildState.Create(ctx, key, []byte(now.UTC().Format(time.RFC3339Nano))); err != nil { + ix.logf("seed: the time since %s.%s is unanswered is not kept: %v", seat, StepsVerb, err) + } + return now +} + +// stepsAnswered forgets the time a seat that answers was unanswered since. +func (ix *Indexer) stepsAnswered(ctx context.Context, seat string) { + if ix.OwnBuildState == nil { + ix.mu.Lock() + delete(ix.stepsSince, seat) + ix.mu.Unlock() + return + } + key := StepsSincePrefix + seat + if _, rev, found, err := ix.OwnBuildState.Get(ctx, key); err == nil && found { + if err := ix.OwnBuildState.Delete(ctx, key, rev); err != nil { + ix.logf("seed: the time since %s.%s is unanswered is not forgotten: %v", seat, StepsVerb, err) + } + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/seed_steps_test.go b/modules/mesh-delivery-indexer/internal/index/seed_steps_test.go index 849994c5..f6c14b0e 100644 --- a/modules/mesh-delivery-indexer/internal/index/seed_steps_test.go +++ b/modules/mesh-delivery-indexer/internal/index/seed_steps_test.go @@ -20,10 +20,11 @@ func TestTheSeedAsksStepsOfTheBuildSeatsOfTheTrunkEntries(t *testing.T) { ctx := context.Background() now := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC) w := newFindings() - ix := &Indexer{Findings: w, Revisions: newMemObjects(1 << 20), Now: func() time.Time { return now }} + ix := &Indexer{Findings: w, OwnBuildState: newMem(), Revisions: newMemObjects(1 << 20), Now: func() time.Time { return now }} fp := "src2:" + strings.Repeat("a", 64) entries := map[string]meshcatalog.CatalogEntry{"avahi": {Module: "avahi", Repository: "novox/r", Commit: "c1", Path: "mods/avahi", Fp: fp, Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "avahi:program", Language: "go"}, {Ref: "avahi:image", Language: "go", Kind: "image"}}, + Packages: map[string]string{"git.novox.be/novox/mesh-sdk": "c9"}, Inputs: &meshcatalog.EntryInputs{Paths: map[string][]string{"program": {"cmd"}}}}} rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, entries) if err != nil { @@ -70,13 +71,14 @@ func TestTheSeedAsksStepsOfTheBuildSeatsOfTheTrunkEntries(t *testing.T) { t.Errorf("the image artifact is %+v", a) } in := got[0] - if in.Module != "avahi" || in.Artifact != "avahi.program" || in.Fingerprint != fp || in.Fetch == nil || in.Fetch.Repository != "novox/r" || in.Fetch.Commit != "c1" || in.Fetch.Dir != "mods/avahi" || len(in.Fetch.Paths) != 1 { + if in.Module != "avahi" || in.Artifact != "avahi.program" || in.Fingerprint != fp || in.Fetch == nil || in.Fetch.Repository != "novox/r" || in.Fetch.Commit != "c1" || in.Fetch.Dir != "mods/avahi" || len(in.Fetch.Paths) != 1 || in.Fetch.Packages["git.novox.be/novox/mesh-sdk"] != "c9" { t.Errorf("the steps input is %+v", in) } if len(w.Held()) != 0 { t.Fatal("a call unanswered for no time wrote its finding") } now = now.Add(5 * time.Minute) + ix = &Indexer{Findings: w, OwnBuildState: ix.OwnBuildState, Revisions: ix.Revisions, Now: func() time.Time { return now }} // a restart keeps the time if _, err := ix.SeedModulesFrom(ctx, defs, call); err != nil { t.Fatal(err) }