Ask a held first build again when a delivery of build-agent ends (issue 1173)

ADR 0482 section 5: the build waiting for a build-agent that reads its manifest is asked once the plan delivering it ends.
This commit is contained in:
2026-10-22 16:19:45 +02:00
parent 980d396d40
commit 4538d05732
4 changed files with 172 additions and 0 deletions
+55
View File
@@ -310,3 +310,58 @@ func probeFirstBuilds(ctx context.Context, d *doctor) ([]conditions.Observation,
}
return firstBuildObservations(waiting, time.Now().UTC()), nil
}
// askHeldFirstBuildsAgain asks again the first build of each first merge held for a build-agent that reads its manifest,
// when the plan that ended delivered build-agent (novox/hq issue 1173): once for each end, since the ask answers the merge
// and a refusal that comes again holds it anew. Answers how many merges it asked for.
func askHeldFirstBuildsAgain(ctx context.Context, open *stores, p inventory.Plan) int {
if !deliversBuildAgent(p) {
return 0
}
actingOnMerges.Lock()
defer actingOnMerges.Unlock()
inv := open.inventory
waiting, err := inv.WaitingMerges(ctx)
if err != nil {
fmt.Printf("%s delivered build-agent, and the first merges held for it could not be read: %v\n", p.ID, err)
return 0
}
asked := 0
for _, w := range waiting {
k := announcedOf(w)
if k.Held == nil {
continue
}
entries, err := inv.Catalogued(ctx)
if err != nil {
fmt.Printf("%s delivered build-agent, and the catalogue could not be read: %v\n", p.ID, err)
return asked
}
read, err := readForPlanning(ctx, inv)
if err != nil {
fmt.Printf("%s delivered build-agent, and what the builds read could not be read: %v\n", p.ID, err)
return asked
}
added, why, err := firstBuildsOf(ctx, inv, k.SourceMoved, entries, read)
if err != nil || len(added) == 0 {
fmt.Printf("%s delivered build-agent, and the merge %.8s of %s held for it asks no build now (%v): it stays "+
"waiting\n", p.ID, w.Commit, w.Repository, err)
continue
}
fmt.Printf("%s delivered build-agent: the first build of %s, held for the word %q, is asked again\n", p.ID,
k.Held.Module, k.Held.Word)
if err := askFirstBuilds(ctx, inv, k.SourceMoved, time.Now().UTC(), added, why); err != nil {
fmt.Printf(" and could not be asked: %v\n", err)
continue
}
asked++
}
return asked
}
// deliversBuildAgent says a delivery plan ended delivered with build-agent built in it: the reader a held first build
// waits for is the one now running.
func deliversBuildAgent(p inventory.Plan) bool {
m := p.Modules["build-agent"]
return p.WalkSaid == inventory.WalkEnded && p.Outcome() == inventory.WalkDelivered && m != nil && m.State == "built"
}
@@ -0,0 +1,91 @@
//go:build integration
package main
import (
"context"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// 1173/AC1, 1173/AC3 whole against the store: a first merge's build refused for a word the running build-agent does not
// read keeps the merge waiting with no failed build recorded; a delivery plan that does not deliver build-agent asks
// nothing; the one that ends delivering it asks the first build again once, and its pass registers the module.
func TestAHeldFirstMergeIsAskedAgainWhenABuildAgentIsDelivered(t *testing.T) {
const head, commit = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "0177ea7a00000000"
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
a := aDeliveryMergeOf(t, head, map[string]string{"novox/mesh-delivery@a1a1a1a1a1a1": head})
wasKept, wasAsk, realAsk := keptDeliveryMerge, routerAskOf, askABuild
keptDeliveryMerge = func(context.Context, string, string, int, string) (deliveryMerge, bool, error) {
return a.merge, true, nil
}
routerAskOf = func(context.Context, catalogueReader, string, string) (*asks.Ask, *asks.Warrant, error) {
return &a.ask, &a.warrant, nil
}
var asked []string
askABuild = func(_ context.Context, source buildSource, path, ref string) (string, error) {
id := "build-" + string(rune('1'+len(asked)))
asked = append(asked, id)
return id, nil
}
t.Cleanup(func() { keptDeliveryMerge, routerAskOf, askABuild = wasKept, wasAsk, realAsk })
m := link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main", HeadSHA: head, Commit: commit, Number: 7,
MergedAt: time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano), Paths: []string{"module.json", "main.go"},
ModuleDirs: []string{"."}, ModuleDirsSaid: true}
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
t.Fatal(err)
}
if len(asked) != 1 {
t.Fatalf("1173/AC3: the first merge asked %v, want the one first build", asked)
}
// The running build-agent refuses the manifest's word.
refused := onTrunk("build-1", "novox/mesh-delivery", "git", "", nil)
refused.Failed, refused.Manifest, refused.Commit = refusedKeyTTL, nil, commit
if err := (builds{inv: inv, open: open}).Built(ctx, refused); err != nil {
t.Fatal(err)
}
if _, recorded, _ := inv.BuildByID(ctx, "build-1"); recorded {
t.Fatal("1173/AC1: a refused first build was recorded as a failed build")
}
waiting, err := inv.WaitingMerges(ctx)
if err != nil || len(waiting) != 1 || announcedOf(waiting[0]).Held == nil {
t.Fatalf("1173/AC1: the merges waiting are %+v (%v), want the first merge, held", waiting, err)
}
if obs := firstBuildObservations(waiting, time.Now()); len(obs) != 1 || obs[0].Kind != kindFirstBuildHeld {
t.Fatalf("1173/AC2: the held merge is named by %+v", obs)
}
// A plan that ended without delivering build-agent asks nothing; one that delivered it asks once.
other := inventory.Plan{ID: "p1", State: inventory.PlanDone, WalkSaid: inventory.WalkEnded,
Modules: map[string]*inventory.PlanModule{"mesh-delivery": {State: "built"}}}
if n := askHeldFirstBuildsAgain(ctx, open, other); n != 0 || len(asked) != 1 {
t.Fatalf("1173/AC3: a plan that delivered no build-agent asked %d builds again (%v)", n, asked)
}
delivered := inventory.Plan{ID: "p2", State: inventory.PlanDone, WalkSaid: inventory.WalkEnded,
Modules: map[string]*inventory.PlanModule{"build-agent": {State: "built"}}}
if n := askHeldFirstBuildsAgain(ctx, open, delivered); n != 1 || len(asked) != 2 {
t.Fatalf("1173/AC3: the plan that delivered build-agent asked %d builds again (%v), want one", n, asked)
}
if n := askHeldFirstBuildsAgain(ctx, open, delivered); n != 0 || len(asked) != 2 {
t.Fatalf("1173/AC3: the same end asked again: %d (%v)", n, asked)
}
// Its pass registers the module.
pass := onTrunk("build-2", "novox/mesh-delivery", "git", "", map[string]any{"module": "mesh-delivery-watchman", "version": "1"})
pass.Commit = commit
if err := (builds{inv: inv, open: open}).Built(ctx, pass); err != nil {
t.Fatal(err)
}
if _, known := inventoryHolds(t, inv, "mesh-delivery-watchman"); !known {
t.Fatal("1173/AC3: the module is not registered after the first build passed")
}
}
@@ -83,3 +83,25 @@ func TestAFirstBuildFailingForAnyOtherReasonIsNotHeld(t *testing.T) {
}
}
}
// 1173/AC3 (the part that needs no store): only a plan that ended delivered with build-agent built in it is the end a held
// first build waits for.
func TestOnlyADeliveredPlanOfBuildAgentIsWhatAHeldFirstBuildWaitsFor(t *testing.T) {
plan := func(state, walk string, modules map[string]*inventory.PlanModule) inventory.Plan {
return inventory.Plan{ID: "p", State: state, WalkSaid: walk, Modules: modules}
}
built := map[string]*inventory.PlanModule{"build-agent": {State: "built"}}
if !deliversBuildAgent(plan(inventory.PlanDone, inventory.WalkEnded, built)) {
t.Fatal("1173/AC3: a plan that delivered build-agent is not the end a held first build waits for")
}
for what, p := range map[string]inventory.Plan{
"a plan still running": plan(inventory.PlanBuilding, "", built),
"a plan that failed": plan(inventory.PlanFailed, inventory.WalkEnded, built),
"a failed build-agent": plan(inventory.PlanDone, inventory.WalkEnded, map[string]*inventory.PlanModule{"build-agent": {State: "failed"}}),
"another module's plan": plan(inventory.PlanDone, inventory.WalkEnded, map[string]*inventory.PlanModule{"mailu": {State: "built"}}),
} {
if deliversBuildAgent(p) {
t.Fatalf("1173/AC3: %s is the end a held first build waits for", what)
}
}
}
+4
View File
@@ -209,6 +209,10 @@ func servingPlanSaved(ctx context.Context, bus link.Bus, open *stores) func(*inv
keepBuildingKey(ctx, p)
if p.WalkSaid == inventory.WalkEnded {
go forgetChecksOfWalk(context.WithoutCancel(ctx), open.inventory, p)
// A first build refused for a word only a newer build-agent reads is asked again (novox/hq issue 1173).
if deliversBuildAgent(p) {
go askHeldFirstBuildsAgain(context.WithoutCancel(ctx), open, p)
}
}
}
}