Register a new module from the builder's first done key and ask no first build; its condition reads the key and names a manifest word this controller does not read (issue 1851, A5a, A5b)

This commit is contained in:
2026-10-22 19:05:04 +02:00
parent d984f0a55d
commit ea3c8ad7f6
19 changed files with 608 additions and 1011 deletions
+46 -194
View File
@@ -47,7 +47,7 @@ func (f following) firstBuilds(ctx context.Context, m link.SourceMoved, entries
case len(added) == 0:
return false, nil
}
if err := askFirstBuilds(ctx, inv, m, now, added, why); err != nil {
if err := askFirstBuilds(ctx, f.open, m, now, added, why); err != nil {
return true, notNow(err)
}
return true, nil
@@ -68,27 +68,35 @@ func keepFirstMerge(ctx context.Context, inv *inventory.Inventory, m link.Source
return err
}
// askFirstBuilds keeps the merge, asks the builds, and marks the merge as no longer waiting for them.
func askFirstBuilds(ctx context.Context, inv *inventory.Inventory, m link.SourceMoved, now time.Time, added []string,
// askFirstBuilds keeps the merge as answered and registers its modules from the builder's done keys (novox/hq ADR 0501
// §3, §6): nothing asks their first builds. The merge kept is the registration's warrant (approvedMove reads it by the
// commit the indexer's trunk value names); a module whose key is not done yet is registered when the key's write wakes
// the take-in (new_modules.go).
func askFirstBuilds(ctx context.Context, open *stores, m link.SourceMoved, now time.Time, added []string,
why string) error {
inv := open.inventory
repository := m.Owner + "/" + m.Repo
if err := keepFirstMerge(ctx, inv, m, now, false); err != nil {
return err
}
fmt.Printf("%s merged into %s (%.8s) on the operator's Approve (%s): the mesh builds nothing from it yet, so the "+
"first build of %s is asked\n", repository, m.Base, m.Commit, why, strings.Join(added, ", "))
// The ask carries the warrant just checked, for this repository and these module paths alone: the guard of a build
// through a verb (verbMayAsk) lets exactly that through.
paths := make([]string, 0, len(added))
for _, dir := range added {
if dir == "." {
dir = ""
}
paths = append(paths, dir)
fmt.Printf("%s merged into %s (%.8s) on the operator's Approve (%s): the mesh builds nothing from it yet, so %s "+
"are registered from the builder's done keys, asking no build\n", repository, m.Base, m.Commit, why,
strings.Join(added, ", "))
// Answered: it no longer waits, so the word's arrival again does nothing twice.
if err := inv.AnswerMerge(ctx, repository, m.Commit, inventory.WaitingRecord, false); err != nil {
return err
}
askBuilds(withFirstBuildWarrant(ctx, repository, paths, why), inv, m, buildSource{Repository: repository, Seat: gitSeat}, added)
// Answered: it no longer waits, so the word's arrival again asks nothing twice.
return inv.AnswerMerge(ctx, repository, m.Commit, inventory.WaitingRecord, false)
modules, err := newModulesOf(ctx, repository)
switch {
case err != nil:
// Said, not failed: the merge is answered, and the look at every module when a controller starts, or the next
// write of the builder or the indexer for one of them, registers them.
fmt.Printf(" the indexer's modules of %s cannot be read now (%v): they are registered when the builder's next "+
"write of one wakes the take-in\n", repository, err)
case len(modules) > 0:
go takeNewModules(context.WithoutCancel(ctx), open, modules...)
}
return nil
}
// resumeFirstBuilds runs firstBuilds for the first merges waiting for the word mesh-delivery just announced for a pull
@@ -126,7 +134,7 @@ func (f following) resumeFirstBuilds(ctx context.Context, d deliveryMerge) error
case err != nil:
return notNow(err)
case len(added) > 0:
if err := askFirstBuilds(ctx, inv, k.SourceMoved, time.Now().UTC(), added, why); err != nil {
if err := askFirstBuilds(ctx, f.open, k.SourceMoved, time.Now().UTC(), added, why); err != nil {
return notNow(err)
}
}
@@ -174,7 +182,9 @@ func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved,
}
// heldFirstBuild says a first build was refused because its manifest uses a word the running build-agent does not read,
// and its merge is kept waiting for a build-agent that reads it (novox/hq ADR 0482 §5, issue 1173).
// and its merge is kept waiting for a build-agent that reads it (novox/hq ADR 0482 §5, issue 1173). Read from a merge kept
// before R4e, never written since: nothing asks a first build (ADR 0501 §6), and a controller that starts answers such a
// merge and registers its modules from their keys (answerHeldMerges).
type heldFirstBuild struct {
Module string `json:"module"`
Path string `json:"path"`
@@ -182,52 +192,6 @@ type heldFirstBuild struct {
At time.Time `json:"at"`
}
// judgeRefusedFirstBuild is the hold a first build's outcome puts its merge under: every module the merge holds already,
// and this one, unless it is among them; nil unless the build failed on a word of its manifest the running build-agent
// does not know, and was asked for a first merge, answered by the record of merges no plan answers or put back to waiting
// by an earlier hold (a second refusal in one merge is held too, review of issue 1173).
func judgeRefusedFirstBuild(result link.BuildResult, req inventory.BuildRequest, merge inventory.BatchedMerge) []heldFirstBuild {
word := refusedWord(result.Failed)
if word == "" || req.For != "merge" {
return nil
}
var held []heldFirstBuild
switch k := announcedOf(merge); {
case merge.Plan == inventory.WaitingRecord:
// Answered when its builds were asked: a hold from before (a build asked again) is over, and starts anew.
case merge.Plan == "" && len(k.Held) > 0:
held = k.Held
default:
return nil
}
this := heldFirstBuild{Module: req.Name(), Path: strings.Trim(req.Path, "/"), Word: word, At: time.Now().UTC()}
for i, h := range held {
if strings.Trim(h.Path, "/") == this.Path {
out := append([]heldFirstBuild(nil), held...)
out[i] = this
return out
}
}
return append(append([]heldFirstBuild(nil), held...), this)
}
// heldDirsOf is the directories a held merge asks again: its held modules' own, each once, "." for the repository's root.
func heldDirsOf(k keptMerge) []string {
var dirs []string
seen := map[string]bool{}
for _, h := range k.Held {
dir := strings.Trim(h.Path, "/")
if dir == "" {
dir = "."
}
if !seen[dir] {
seen[dir] = true
dirs = append(dirs, dir)
}
}
return dirs
}
// unknownWord is how the strict reading of a manifest says a word is not one it knows: encoding/json's own words, which
// the build-agent's answer carries whole. The build's answer has no typed field for it (Failed is the builder's words).
var unknownWord = regexp.MustCompile(`json: unknown field "([^"]+)"`)
@@ -240,55 +204,12 @@ func refusedWord(failed string) string {
return ""
}
// holdRefusedFirstBuild keeps a first build's merge waiting when the build was refused for a word the running build-agent
// does not read (novox/hq ADR 0482 §5, issue 1173): no failed build is recorded, nothing expires the merge, and it is
// named by a condition until a delivery of build-agent asks the build again. Answers whether the outcome was dealt with.
func holdRefusedFirstBuild(ctx context.Context, inv *inventory.Inventory, result link.BuildResult) (bool, error) {
if result.Failed == "" || refusedWord(result.Failed) == "" {
return false, nil
}
req, found, err := inv.BuildRequestByID(ctx, result.ID)
if err != nil || !found {
return false, err
}
merge, found, err := inv.MergeOf(ctx, req.Repository, req.Commit)
if err != nil || !found {
return false, err
}
held := judgeRefusedFirstBuild(result, req, merge)
if len(held) == 0 {
return false, nil
}
k := announcedOf(merge)
k.Held = held
event, err := json.Marshal(k)
if err != nil {
return false, err
}
if err := inv.HoldMerge(ctx, merge.Repository, merge.Commit, event); err != nil {
return false, err
}
this := held[len(held)-1]
for _, h := range held {
if h.Path == strings.Trim(req.Path, "/") {
this = h
}
}
fmt.Printf("%s: the first build of %s was refused for the word %q, which the running build-agent does not read; the "+
"merge %.8s of %s is kept waiting for a build-agent that reads it (holding %s), and the build is asked again when "+
"one is delivered\n", result.ID, this.Module, this.Word, merge.Commit, merge.Repository, heldNames(held))
return true, nil
}
// probeFirstBuildsID is the probe that reads the first merges waiting for mesh-delivery's word.
const probeFirstBuildsID = "D18"
// kindFirstBuildUnasked is what a first merge waiting too long for mesh-delivery's word raises.
const kindFirstBuildUnasked = "first-build-unasked"
// kindFirstBuildHeld is what a first merge held for a build-agent that reads its manifest raises.
const kindFirstBuildHeld = "first-build-held"
// firstBuildWaitBound is how long a first merge waits for mesh-delivery's announcement of the operator's Approve before
// it is said: a bound in the sense of ADR 0426 §3, not a delay. Nothing waits on it: the word's arrival asks the builds
// at once, and the merge is said, never dropped, once it is past this (issue 1146).
@@ -300,10 +221,6 @@ func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []c
var out []conditions.Observation
for _, w := range waiting {
k := announcedOf(w)
if len(k.Held) > 0 {
out = append(out, heldFirstBuildObservation(k))
continue
}
if !k.FirstBuild || now.Sub(w.Heard) <= firstBuildWaitBound {
continue
}
@@ -318,108 +235,43 @@ func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []c
Headline: fmt.Sprintf("The first build of %s is not asked: no Approve was announced", repository),
Explanation: fmt.Sprintf("The merge %.8s into %s adds modules the mesh builds nothing from yet. It registers them "+
"only when mesh-delivery announces that it merged the pull request on your Approve of that head, and no such "+
"announcement has come in %s. The merge is kept, and the builds are asked the moment the announcement is kept.",
"announcement has come in %s. The merge is kept, and the modules are registered from the builder's keys the moment "+
"the announcement is kept.",
k.Commit, repository, for_),
Resolved: "mesh-delivery announces the merge, or a hand `build` registers the modules",
Resolved: "mesh-delivery announces the merge, or the operator registers the modules at the controller's terminal",
})
}
return out
}
// heldNames is the modules a merge holds, each with the word it waits for: "watchman (key-ttl), view (per-machine)".
func heldNames(held []heldFirstBuild) string {
names := make([]string, 0, len(held))
for _, h := range held {
names = append(names, fmt.Sprintf("%s (%q)", h.Module, h.Word))
}
return strings.Join(names, ", ")
}
// heldFirstBuildObservation names a first merge held for a build-agent that reads its manifests: at once, since nothing
// is lost and nothing times out, but the operator can see what the merge waits for. One condition names every module held.
func heldFirstBuildObservation(k keptMerge) conditions.Observation {
repository := k.Owner + "/" + k.Repo
modules := make([]string, 0, len(k.Held))
for _, h := range k.Held {
modules = append(modules, h.Module)
}
held := heldNames(k.Held)
return conditions.Observation{Scope: conditions.ScopeMerge, ID: fmt.Sprintf("%s_%.8s", repository, k.Commit),
Kind: kindFirstBuildHeld, Severity: conditions.Warning,
Summary: fmt.Sprintf("the merge %.8s of %s adds modules whose manifests use words the running build-agent does not "+
"read: %s; the merge waits for a build-agent that reads them", k.Commit, repository, held),
Said: fmt.Sprintf("%s at %s waits for a build-agent that reads the words of %s", repository, k.Commit, held),
Headline: fmt.Sprintf("The first build of %s waits for a newer build-agent", strings.Join(modules, ", ")),
Explanation: fmt.Sprintf("The merge %.8s into %s adds modules whose manifests use words the build-agent now "+
"running does not know: %s. The merge is kept, not failed: their first builds are asked again when a delivery "+
"of build-agent ends.", k.Commit, repository, held),
Resolved: "a delivery of build-agent ends and the first builds register the modules, or a hand `build` does",
}
}
// probeFirstBuilds is D18: no first merge waits past its bound for mesh-delivery's word unsaid.
func probeFirstBuilds(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
waiting, err := d.open.inventory.WaitingMerges(ctx)
if err != nil {
return nil, err
}
return firstBuildObservations(waiting, time.Now().UTC()), nil
now := time.Now().UTC()
return append(firstBuildObservations(waiting, now), newModuleObservations(now)...), 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
// answerHeldMerges answers every merge a controller of before held for a build-agent that reads its new modules'
// manifests (issue 1173): nothing asks their first builds any more, and the look at every module when this controller
// starts registers them from their keys (ADR 0501 §6, §7).
func answerHeldMerges(ctx context.Context, inv *inventory.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
fmt.Printf("the merges held for a newer build-agent could not be read: %v\n", err)
return
}
asked := 0
for _, w := range waiting {
k := announcedOf(w)
if len(k.Held) == 0 {
continue
}
// Its own held modules, not the rule of a first merge: a module of the merge that passed is registered by now,
// and the repository no longer counts as one the mesh builds nothing from (review of issue 1173).
why, err := approvedAnnounced(ctx, inv, k.Owner, k.Repo, keptMerge{SourceMoved: k.SourceMoved})
if err != nil {
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
}
dirs := heldDirsOf(k)
fmt.Printf("%s delivered build-agent: the first builds of %s are asked again\n", p.ID, heldNames(k.Held))
paths := make([]string, 0, len(dirs))
for _, dir := range dirs {
if dir == "." {
dir = ""
if k := announcedOf(w); len(k.Held) > 0 {
if err := inv.AnswerMerge(ctx, w.Repository, w.Commit, inventory.WaitingRecord, false); err != nil {
fmt.Printf("the merge %.8s of %s held for a newer build-agent could not be answered: %v\n", w.Commit,
w.Repository, err)
continue
}
paths = append(paths, dir)
fmt.Printf("the merge %.8s of %s was held for a newer build-agent; its modules are registered from the "+
"builder's keys instead\n", w.Commit, w.Repository)
}
repository := k.Owner + "/" + k.Repo
askBuilds(withFirstBuildWarrant(ctx, repository, paths, why), inv, k.SourceMoved,
buildSource{Repository: repository, Seat: gitSeat}, dirs)
// Answered: it no longer waits, so this end asks nothing twice; a refusal that comes again holds it anew.
if err := inv.AnswerMerge(ctx, w.Repository, w.Commit, inventory.WaitingRecord, false); err != nil {
fmt.Printf(" and the merge could not be marked answered: %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"
}