indexer: the seed's steps fetch carries the entry's packages; the time a seat is unanswered is kept in the own-build state

Builds: design 47 G §704, A-66

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:58:21 +01:00
parent c0e2621bba
commit 940889c2a2
2 changed files with 56 additions and 14 deletions
@@ -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)
}
}
}
@@ -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)
}