Assemble merges in a rolling window and walk each batch once (hq ADR 0276, issue 362)
Every merge opened a walk and the next merge of the branch superseded it: two catalogue merges 18 s apart left a walk no delivery held, and the operator started it by hand 58 minutes later. A merge now joins the open batch, kept in the store (migration 0089), which is cut into one walk when no merge came for merge-window (90 s) or at merge-window-at-most (10 min): one commit per repository, the latest of its branch, with every file the batch's merges changed. One walk at a time; a started walk is never superseded, a waiting one is folded into the next. The walk names every merge it answers on the wire (delivery.merges, taken_over_by, batch). A failed walk walks its earlier merges alone, newest first, until one is delivered. A delivery group's order becomes tier edges inside the walk. plans shows the batch assembling; S18 and S19 bound its waits; S16 names the merges a waiting walk answers.
This commit is contained in:
@@ -141,7 +141,8 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string {
|
||||
// nothing it builds, and a new controller changes nothing about the holder it orders. A
|
||||
// packages edge, read from a record made before novox/hq ADR 0267, widens nothing either:
|
||||
// a shared file moves each module whose build source holds it, directly.
|
||||
if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf || e.Kind == inventory.EdgePackages {
|
||||
if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf || e.Kind == inventory.EdgePackages ||
|
||||
e.Kind == edgeGroupOrder {
|
||||
continue
|
||||
}
|
||||
if in[e.To] && !in[e.From] {
|
||||
@@ -180,115 +181,33 @@ func hasCycle(tiers [][]string, edges []inventory.Edge) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// planFor is the plan a merge produces: the moved modules and everything reachable from them,
|
||||
// planOfMerge is the plan one merge produces: the moved modules and everything reachable from them,
|
||||
// tiered, with the merge it answers.
|
||||
func planOfMerge(m link.SourceMoved, moved []string, edges []inventory.Edge) inventory.Plan {
|
||||
p := planOfMoves(moved, edges)
|
||||
merged, _ := time.Parse(time.RFC3339Nano, m.MergedAt)
|
||||
p.Repository, p.Branch, p.Commit, p.Merged = m.Owner+"/"+m.Repo, m.Base, m.Commit, merged.UTC()
|
||||
return p
|
||||
}
|
||||
|
||||
// planOfMoves is the walk of a set of moved modules (novox/hq ADR 0162, 0276): they and everything reachable
|
||||
// from them, tiered along the graph, with no merge named yet.
|
||||
func planOfMoves(moved []string, edges []inventory.Edge) inventory.Plan {
|
||||
set := reachableFrom(moved, edges)
|
||||
tiers := tiersOf(set, edges)
|
||||
modules := map[string]*inventory.PlanModule{}
|
||||
for _, name := range set {
|
||||
modules[name] = &inventory.PlanModule{}
|
||||
}
|
||||
merged, _ := time.Parse(time.RFC3339Nano, m.MergedAt)
|
||||
return inventory.Plan{
|
||||
ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()),
|
||||
Repository: m.Owner + "/" + m.Repo,
|
||||
Branch: m.Base,
|
||||
Commit: m.Commit,
|
||||
Merged: merged.UTC(),
|
||||
Created: time.Now().UTC(),
|
||||
State: inventory.PlanBuilding,
|
||||
Tiers: tiers,
|
||||
Modules: modules,
|
||||
ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()),
|
||||
Created: time.Now().UTC(),
|
||||
State: inventory.PlanBuilding,
|
||||
Tiers: tiers,
|
||||
Modules: modules,
|
||||
}
|
||||
}
|
||||
|
||||
// supersededBy is what a newer plan takes over from the open plans it supersedes (novox/hq issue
|
||||
// 254, ADR 0218): the modules they had not finished, and those plans closed as superseded.
|
||||
//
|
||||
// **A merge looked at no plan but its own.** Two merges of one repository a few minutes apart were
|
||||
// two open plans asking for the same modules, each sending machines what it built; and a plan that
|
||||
// would never move again — waiting on a report that could not come, at 97b1b2b — stayed open for
|
||||
// ever beside the newer ones, read as work in progress by everyone who looked. The newer merge is the
|
||||
// newer intent for that repository and branch, so its plan takes over: every open plan of the same
|
||||
// repository and branch **created before it** — by the time the plans were made, never by comparing
|
||||
// commits, which have no order of their own — gives up the modules it had not built, and those are
|
||||
// planned again in the newer plan beside what the newer merge moved.
|
||||
//
|
||||
// "Not built" is a module not yet asked, or asked and not answered; **and a module built and not
|
||||
// yet sent to its machines**, where its policy rolls it out: closed, the older plan would never send
|
||||
// it, and the catalogue announces no move for a rebuild (issue 189), so the newer plan builds and
|
||||
// sends it. A build the older plan asked still finishes and registers as any build does — ordered by
|
||||
// when it was asked (issue 219), so the newer plan's ask, made later, is the one that stands.
|
||||
//
|
||||
// A plan with no branch recorded is from before branches were kept, and is superseded by the next
|
||||
// plan of its repository: what it had not built is folded in, so nothing is lost by it.
|
||||
//
|
||||
// **Newer is the branch's order, not the plans'** (novox/hq issue 349). A merge the bus did not hand
|
||||
// over is acted on late, by the catch-up, so its plan is made after the plan of a merge that came after
|
||||
// it — and that later-made plan of the earlier commit superseded the later merge's, and built what it
|
||||
// folded in from the commit before the later merge: twice on 2026-10-09, once a security fix. So where
|
||||
// both plans know when their merge was made, that decides; only where one does not do the plans' own
|
||||
// times. A merge older than the newest planned merge of its branch never reaches here at its own commit:
|
||||
// it is planned at that merge's commit, which contains it (laterOnTheBranch).
|
||||
func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(string) bool) ([]string, []inventory.Plan) {
|
||||
folded := map[string]bool{}
|
||||
var closed []inventory.Plan
|
||||
for _, old := range open {
|
||||
if old.ID == newer.ID || !old.Open() || !strings.EqualFold(old.Repository, newer.Repository) ||
|
||||
(old.Branch != "" && old.Branch != newer.Branch) || !earlierOnTheBranch(old, newer) {
|
||||
continue
|
||||
}
|
||||
var took []string
|
||||
for name, s := range old.Modules {
|
||||
if s != nil && s.State == planDeleted {
|
||||
continue // deleted at its source: nothing to plan again
|
||||
}
|
||||
if s == nil || s.State != "built" || (s.SentAt == nil && rollsOut(name)) {
|
||||
folded[name] = true
|
||||
took = append(took, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(took)
|
||||
old.State = inventory.PlanSuperseded
|
||||
old.Note = fmt.Sprintf("superseded at tier %d by %s (%s at %s)", old.Tier, newer.ID, newer.Repository, short(newer.Commit))
|
||||
if len(took) > 0 {
|
||||
old.Note += "; " + strings.Join(took, ", ") + " planned there again"
|
||||
}
|
||||
closed = append(closed, old)
|
||||
}
|
||||
out := make([]string, 0, len(folded))
|
||||
for name := range folded {
|
||||
out = append(out, name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, closed
|
||||
}
|
||||
|
||||
// earlierOnTheBranch says plan a answers a merge made before b's: by when the forge made each merge where
|
||||
// both are known, else by when each plan was made. A merge's own plan made again at the same commit
|
||||
// (the same merge time) is ordered by when it was made. Pure.
|
||||
func earlierOnTheBranch(a, b inventory.Plan) bool {
|
||||
if !a.Merged.IsZero() && !b.Merged.IsZero() && !a.Merged.Equal(b.Merged) {
|
||||
return a.Merged.Before(b.Merged)
|
||||
}
|
||||
return a.Created.Before(b.Created)
|
||||
}
|
||||
|
||||
// laterOnTheBranch says the plan newest answers a merge into m's branch made after m: then newest's commit
|
||||
// contains m's change, and m is planned there, so the newest commit of the branch is what is built (novox/hq
|
||||
// issue 349). newest is the newest merge's plan of the branch in any state: a later plan already done
|
||||
// built what it moved at its commit, and m planned at its own would build m's dependents back at the older
|
||||
// one. Pure.
|
||||
func laterOnTheBranch(m link.SourceMoved, newest inventory.Plan) bool {
|
||||
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
|
||||
if err != nil || newest.Release != nil || newest.Commit == m.Commit {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(newest.Repository, m.Owner+"/"+m.Repo) && newest.Branch == m.Base &&
|
||||
newest.Merged.After(merged)
|
||||
}
|
||||
|
||||
// gates is what the next tier needs running from this one: a module of the tier that a later
|
||||
// tier is built by — the runtime dependency — and whose policy rolls it out, must be applied by
|
||||
// the machines running it before the next tier is asked. A base an image stands on need only be
|
||||
@@ -316,8 +235,11 @@ func gates(p inventory.Plan, edges []inventory.Edge, rollsOut func(string) bool)
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, e := range edges {
|
||||
if later[e.From] && inTier[e.To] && e.Kind == inventory.EdgeBuiltBy && !seen[e.To] && rollsOut(e.To) &&
|
||||
!isBaseOf(e.From, e.To, edges, all) {
|
||||
// A delivery group's order inside a walk (novox/hq ADR 0276 decision 5) gates as built-by does: the
|
||||
// member before is running on its machines before the member after is asked.
|
||||
ordered := e.Kind == edgeGroupOrder ||
|
||||
e.Kind == inventory.EdgeBuiltBy && !isBaseOf(e.From, e.To, edges, all)
|
||||
if later[e.From] && inTier[e.To] && ordered && !seen[e.To] && rollsOut(e.To) {
|
||||
seen[e.To] = true
|
||||
out = append(out, e.To)
|
||||
}
|
||||
@@ -417,8 +339,14 @@ func askModule(ctx context.Context, p *inventory.Plan, name string, byName map[s
|
||||
p.Note = fmt.Sprintf("%s could not be asked for: %v", name, err)
|
||||
return
|
||||
}
|
||||
// The commit of the module's own repository the walk carries (novox/hq ADR 0276): a batch's walk carries
|
||||
// one per repository.
|
||||
commit := p.CommitOf(e.Source.Repository)
|
||||
if commit == "" {
|
||||
commit = p.Commit
|
||||
}
|
||||
recordAsked(ctx, inventory.BuildRequest{ID: id, Repository: e.Source.Repository, Seat: e.Source.Seat,
|
||||
Path: e.Source.Path, Ref: followedBranch(e.Source.Ref), Commit: p.Commit, For: "plan"})
|
||||
Path: e.Source.Path, Ref: followedBranch(e.Source.Ref), Commit: commit, For: "plan"})
|
||||
state.State = "asked"
|
||||
state.AskedAt = &now
|
||||
state.Build = id
|
||||
@@ -537,6 +465,12 @@ func advancePlans(ctx context.Context, open *stores) {
|
||||
// advanceHeld is advancePlans for a caller already holding the plans.
|
||||
func advanceHeld(ctx context.Context, open *stores) {
|
||||
inv := open.inventory
|
||||
// A walk that ended lets the next batch be cut at once, not at the cutter's next look (novox/hq ADR 0276).
|
||||
defer func() {
|
||||
if err := cutBatchesHeld(ctx, open, time.Now().UTC()); err != nil {
|
||||
fmt.Printf("batches: %v\n", err)
|
||||
}
|
||||
}()
|
||||
plans, err := inv.OpenPlans(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("plans: cannot read them: %v\n", err)
|
||||
@@ -1244,20 +1178,23 @@ func inTierSince(p inventory.Plan) time.Time {
|
||||
func planLineWith(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration) string {
|
||||
where := fmt.Sprintf("tier %d of %d", min(p.Tier+1, len(p.Tiers)), len(p.Tiers))
|
||||
switch p.State {
|
||||
case inventory.PlanAssembling, inventory.PlanQueued:
|
||||
// A batch not yet a walk (novox/hq ADR 0276): what it holds and how long is left.
|
||||
return batchWords(p, now)
|
||||
case inventory.PlanDone:
|
||||
return fmt.Sprintf("%s %s done, %d tier(s)", p.Repository, short(p.Commit), len(p.Tiers))
|
||||
return fmt.Sprintf("%s done, %d tier(s)", p.Named(), len(p.Tiers))
|
||||
case inventory.PlanFailed:
|
||||
return fmt.Sprintf("%s %s FAILED at %s: %s", p.Repository, short(p.Commit), where, p.Note)
|
||||
return fmt.Sprintf("%s FAILED at %s: %s", p.Named(), where, p.Note)
|
||||
case inventory.PlanSuperseded:
|
||||
return fmt.Sprintf("%s %s %s", p.Repository, short(p.Commit), p.Note)
|
||||
return fmt.Sprintf("%s %s", p.Named(), p.Note)
|
||||
}
|
||||
since := now.Sub(inTierSince(p)).Round(time.Second)
|
||||
if p.Waiting() {
|
||||
// Waiting for its delivery's word is no lateness of the walk's (novox/hq ADR 0239).
|
||||
return fmt.Sprintf("%s %s %s, %s, for %s", p.Repository, short(p.Commit), where, waitingNote(p), since)
|
||||
return fmt.Sprintf("%s %s, %s, for %s", p.Named(), where, waitingNote(p), since)
|
||||
}
|
||||
if waiting, paused := pausedWaiting(p, pause, now); paused {
|
||||
return fmt.Sprintf("%s %s %s, %s", p.Repository, short(p.Commit), where, waiting)
|
||||
return fmt.Sprintf("%s %s, %s", p.Named(), where, waiting)
|
||||
}
|
||||
late := ""
|
||||
if since > bound {
|
||||
@@ -1267,7 +1204,7 @@ func planLineWith(p inventory.Plan, now time.Time, pause pauseView, bound time.D
|
||||
if p.State == inventory.PlanRolling {
|
||||
what = p.Note
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s, %s for %s%s", p.Repository, short(p.Commit), where, what, since, late)
|
||||
return fmt.Sprintf("%s %s, %s for %s%s", p.Named(), where, what, since, late)
|
||||
}
|
||||
|
||||
// planFailedBuild marks the module a failed build was for when the result names no module: by the
|
||||
@@ -1534,13 +1471,25 @@ func plansCommand(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(plans) == 0 {
|
||||
// **The batch being assembled first** (novox/hq ADR 0276 decision 7): what it holds, how long is left,
|
||||
// and that its plan is not yet calculated.
|
||||
batches, err := inv.Batches(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(plans) == 0 && len(batches) == 0 {
|
||||
fmt.Println("no merge has produced a plan yet")
|
||||
return nil
|
||||
}
|
||||
for _, b := range batches {
|
||||
fmt.Printf("%-28s %s\n", b.ID, batchWords(b, now))
|
||||
}
|
||||
pause := buildSeatPause(ctx, inv, plans)
|
||||
bounds := readTierBounds(ctx, inv, now)
|
||||
for _, p := range plans {
|
||||
if p.Batch() {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%-28s %s\n", p.ID, planLineWith(p, now, pause, bounds.of(p.Repository)))
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user