Take no rebuild into a waiting walk, and ask what a tier left unasked (hq issue 754)
mesh/merge-gate head 9f072c110c14 merged into trunk 60cc9ec363df — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 9f072c110c14 merged into trunk 60cc9ec363df — pass: its merge-check.sh passed
mesh/delivery delivered

A rebuild that landed while a walk waited for mesh-delivery's word was taken as
the walk's build of that module; once let go, the walk asked a tier only when
every module of it was unasked, so it never asked the rest and stood still.

A walk now takes only the outcomes of builds it asked, and none while it waits;
a rebuild does not join a waiting walk and says it runs beside it; and a tier
asked in part is asked the rest, including a module held built that the walk
never asked for, which is somebody else's build and not sent as the walk's.
This commit is contained in:
2026-10-15 00:02:28 +02:00
parent 60cc9ec363
commit 9f072c110c
6 changed files with 206 additions and 28 deletions
+65 -24
View File
@@ -259,10 +259,11 @@ func applied(module string, builtAt time.Time, running []string, reports []inven
return len(waiting) == 0, waiting
}
// askTier asks the build machine for every module of the tier, and marks each asked. A module
// askTier asks the build machine for the named modules of the plan's tier — all of it, or what of it is still
// unasked (novox/hq issue 754) — and marks each asked. A module
// the catalogue no longer holds, or whose ask could not be made, is a failure of the plan: a tier
// half asked is a tier that will never complete.
func askTier(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan) error {
func askTier(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan, names []string) error {
entries, err := inv.Catalogued(ctx)
if err != nil {
return err
@@ -271,12 +272,62 @@ func askTier(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan) e
for _, e := range entries {
byName[e.Manifest.Module] = e
}
for _, name := range p.Tiers[p.Tier] {
for _, name := range names {
askModule(ctx, p, name, byName)
}
return nil
}
// unaskedOf are the modules of a plan's current tier it has still to ask: those it never asked, and one it holds
// built that it never asked for (novox/hq issue 754) — a build taken in by a controller from before a walk took
// only its own asks, somebody else's, which the walk would otherwise send as its own (ADR 0219). A tier partly asked is asked the rest, not
// waited on for ever: a walk that waits for every build of a tier it never asked whole never moves again.
func unaskedOf(p inventory.Plan) []string {
if p.Tier >= len(p.Tiers) {
return nil
}
var out []string
for _, m := range p.Tiers[p.Tier] {
s := p.Modules[m]
switch {
case s == nil || s.State == "":
out = append(out, m)
case s.State == "built" && s.Build == "" && s.AskedAt == nil:
out = append(out, m)
}
}
return out
}
// askedInTier says the plan asked a module of its current tier itself: under an id, or at a time it kept.
func askedInTier(p inventory.Plan) bool {
if p.Tier >= len(p.Tiers) {
return false
}
for _, m := range p.Tiers[p.Tier] {
if s := p.Modules[m]; s != nil && (s.Build != "" || s.AskedAt != nil) {
return true
}
}
return false
}
// takesOutcome says whether a build's outcome is the plan's for a module of its current tier (novox/hq ADR 0219,
// issues 219 and 754). **A walk that waits for its delivery's word asks nothing, and so is answered by nothing**
// (ADR 0239): a rebuild that lands in the wait is somebody else's build. **Nor is a module the plan never asked
// answered**: taken, the plan held a module built that it never asked, and waited for the rest of the tier for
// ever. Asked under an id, the plan takes that id's outcome and no other; asked before ids were kept, a build
// asked at or after the plan's ask.
func takesOutcome(p inventory.Plan, state *inventory.PlanModule, asked time.Time, id string) bool {
if p.Waiting() || state == nil || (state.Build == "" && state.AskedAt == nil) {
return false
}
if state.Build != "" {
return state.Build == id
}
return !askedBefore(asked, state.AskedAt)
}
// askABuild is how a plan asks for one build, not waited for, and learns the id it asked under. A
// variable so a test of what a plan does around an ask needs no build machine.
//
@@ -526,22 +577,15 @@ func planBuiltSaying(ctx context.Context, open *stores, module, commit, failed s
if !inTier {
continue
}
state := p.Modules[module]
if state == nil {
state = &inventory.PlanModule{}
p.Modules[module] = state
}
// **The plan's own ask is its outcome, by id** (novox/hq ADR 0219); another build of the module
// is, as before, when it was asked at or after the plan's ask (issue 219).
// **A module asked under an id is answered by that id's outcome and no other** (novox/hq ADR
// 0219): a replay, a rebuild beside the plan, or an older ask finishing late is somebody else's
// build, made from other source, and settling the plan with it would send that. A plan from
// before ids were kept is matched as it was: by when the build was asked (issue 219).
if state.Build != "" {
if state.Build != id {
continue
}
} else if askedBefore(asked, state.AskedAt) {
// before ids were kept is matched as it was: by when the build was asked (issue 219). A walk
// waiting for its word, or a module it never asked, takes no outcome at all (issue 754).
state := p.Modules[module]
if !takesOutcome(*p, state, asked, id) {
continue
}
if failed != "" && deletedAtSource(failed) {
@@ -747,17 +791,11 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
return true, nil
}
tier := p.Tiers[p.Tier]
// Not yet asked: ask.
unasked := 0
for _, m := range tier {
if s := p.Modules[m]; s == nil || s.State == "" {
unasked++
}
}
if unasked == len(tier) {
// Not yet asked: ask — the whole tier, or what of it is still unasked (novox/hq issue 754).
if unasked := unaskedOf(*p); len(unasked) > 0 {
// **One walk at a time** (novox/hq ADR 0276): a walk about to ask its first tier while another started
// walk is open waits for that one to end, let go or not, and says so.
if p.Tier == 0 {
if p.Tier == 0 && !askedInTier(*p) {
if behind, err := startedBeside(ctx, inv, p.ID, p.Created); err != nil {
return false, err
} else if behind != "" {
@@ -767,7 +805,10 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
return changed, nil
}
}
if err := askTier(ctx, inv, p); err != nil {
if len(unasked) < len(tier) {
fmt.Printf("%s: tier %d was asked in part; asking the rest: %s\n", p.ID, p.Tier, strings.Join(unasked, ", "))
}
if err := askTier(ctx, inv, p, unasked); err != nil {
return false, err
}
return true, nil