Let delivery-stop leave merges out and walk the others again, so a group stop ends only its own change (hq issue 459, ADR 0299)
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/459-a-stop-re-walks-the-others ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/459-a-stop-re-walks-the-others ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
One walk carries every merge of its batch, so ending it for one group member ended every other delivery on it. delivery-stop now takes without: the walk ends, every other merge it answered is batched again, and each later merge of a left-out merge's repository is left out too, since it contains it.
This commit is contained in:
@@ -117,28 +117,180 @@ func letGo(ctx context.Context, inv *inventory.Inventory, id, by, why string) (i
|
||||
// stopWalk ends a walk on its delivery's word: failed, said as stopped by whom, why. What it asked still
|
||||
// builds and registers; nothing further is asked or sent.
|
||||
func stopWalk(ctx context.Context, inv *inventory.Inventory, id, by, why string) (inventory.Plan, error) {
|
||||
p, _, err := stopWalkWithout(ctx, inv, id, by, why, nil, time.Now().UTC())
|
||||
return p, err
|
||||
}
|
||||
|
||||
// stopAnswer is what a stop that leaves merges out says, as data (novox/hq ADR 0299): the walk stopped, the
|
||||
// batch that walks its other merges again, those merges as `<repository>@<commit>`, and every merge left out.
|
||||
type stopAnswer struct {
|
||||
Stopped string `json:"stopped"`
|
||||
WalkedAgainBy string `json:"walked_again_by,omitempty"`
|
||||
WalkedAgain []string `json:"walked_again,omitempty"`
|
||||
LeftOut []inventory.PlanLeftOut `json:"left_out,omitempty"`
|
||||
}
|
||||
|
||||
// stopWalkWithout ends a walk on its delivery's word and, given merges to leave out, walks every other merge it
|
||||
// answered again in a fresh batch without them (novox/hq issue 459, ADR 0299): one walk carries every merge of its
|
||||
// batch, so ending it for one group member ended every other delivery on it. Left out are the merges named and
|
||||
// each later merge of their repository's branch, which contains one of them and cannot be walked without
|
||||
// delivering it. The fresh batch's window is closed — its merges were heard before — and it is cut by the usual
|
||||
// rules, as a failed walk's merges are walked again (ADR 0276 decision 3). With nothing left to walk, or no merge
|
||||
// named, the walk is only ended. A merge named that the walk does not answer refuses the stop whole.
|
||||
func stopWalkWithout(ctx context.Context, inv *inventory.Inventory, id, by, why string, without []string,
|
||||
now time.Time) (inventory.Plan, stopAnswer, error) {
|
||||
said := stopAnswer{Stopped: id}
|
||||
release, err := inv.HoldPlans(ctx, true)
|
||||
if err != nil {
|
||||
return inventory.Plan{}, err
|
||||
return inventory.Plan{}, said, err
|
||||
}
|
||||
defer release()
|
||||
p, err := inv.PlanByID(ctx, id)
|
||||
if err != nil {
|
||||
return inventory.Plan{}, err
|
||||
return inventory.Plan{}, said, err
|
||||
}
|
||||
if !p.Open() {
|
||||
return p, fmt.Errorf("%s is already %s", p.ID, p.State)
|
||||
return p, said, fmt.Errorf("%s is already %s", p.ID, p.State)
|
||||
}
|
||||
var again []inventory.BatchedMerge
|
||||
if len(without) > 0 {
|
||||
merges, err := inv.MergesOf(ctx, p.ID)
|
||||
if err != nil {
|
||||
return p, said, err
|
||||
}
|
||||
if again, said.LeftOut, err = leaveOut(merges, without); err != nil {
|
||||
return p, said, fmt.Errorf("%s is not stopped: %w", p.ID, err)
|
||||
}
|
||||
}
|
||||
if p.Delivery == nil {
|
||||
p.Delivery = &inventory.PlanDelivery{}
|
||||
}
|
||||
p.Delivery.Stopped, p.Delivery.StoppedWhy = by, why
|
||||
p.Delivery.LeftOut = said.LeftOut
|
||||
p.State = inventory.PlanFailed
|
||||
p.Note = fmt.Sprintf("stopped by %s at tier %d: %s", by, p.Tier, why)
|
||||
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||
return p, err
|
||||
if len(again) > 0 {
|
||||
batch, err := walkAgain(ctx, inv, p, again, now)
|
||||
if err != nil {
|
||||
return p, said, fmt.Errorf("%s is not stopped: its other merges could not be batched again: %w", p.ID, err)
|
||||
}
|
||||
said.WalkedAgainBy = batch.ID
|
||||
for _, m := range again {
|
||||
said.WalkedAgain = append(said.WalkedAgain, m.Repository+"@"+m.Commit)
|
||||
}
|
||||
p.Delivery.WalkedAgainBy = batch.ID
|
||||
p.Note += fmt.Sprintf("; its other merges are walked again without %s, by %s: %s", mergesLeftOutWords(said.LeftOut),
|
||||
batch.ID, strings.Join(said.WalkedAgain, ", "))
|
||||
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||
// The merges go back to the walk that still answers them, and the batch holds none: it is never walked.
|
||||
for _, m := range again {
|
||||
_ = inv.AnswerMerge(ctx, m.Repository, m.Commit, p.ID, m.Alone)
|
||||
}
|
||||
return p, said, err
|
||||
}
|
||||
fmt.Printf("%s stopped by %s; %s walks its other merges again: %s\n", p.ID, by, batch.ID,
|
||||
strings.Join(said.WalkedAgain, ", "))
|
||||
return p, said, nil
|
||||
}
|
||||
return p, nil
|
||||
if len(said.LeftOut) > 0 {
|
||||
p.Note += fmt.Sprintf("; left out %s, nothing else to walk", mergesLeftOutWords(said.LeftOut))
|
||||
}
|
||||
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||
return p, said, err
|
||||
}
|
||||
return p, said, nil
|
||||
}
|
||||
|
||||
// walkAgain puts merges of a stopped walk into a fresh batch of their kind, its window closed: kept before its
|
||||
// first merge names it, then each merge moved to it.
|
||||
func walkAgain(ctx context.Context, inv *inventory.Inventory, stopped inventory.Plan, again []inventory.BatchedMerge,
|
||||
now time.Time) (inventory.Plan, error) {
|
||||
own := false
|
||||
for _, m := range again {
|
||||
for _, n := range announcedOf(m).Moves {
|
||||
if _, on := onTheControllersPath[n]; on {
|
||||
own = true
|
||||
}
|
||||
}
|
||||
}
|
||||
first := again[0]
|
||||
b := inventory.Plan{ID: fmt.Sprintf("plan-%d", now.UnixNano()), Repository: first.Repository, Branch: first.Branch,
|
||||
Commit: first.Commit, Merged: first.Merged, Created: now, State: inventory.PlanAssembling,
|
||||
Tiers: [][]string{}, Modules: map[string]*inventory.PlanModule{},
|
||||
Delivery: &inventory.PlanDelivery{Batch: &inventory.PlanBatch{Own: own}}}
|
||||
if b.ID == stopped.ID {
|
||||
b.ID += "-again"
|
||||
}
|
||||
if err := inv.SavePlan(ctx, &b); err != nil {
|
||||
return b, err
|
||||
}
|
||||
for _, m := range again {
|
||||
if err := inv.AnswerMerge(ctx, m.Repository, m.Commit, b.ID, m.Alone); err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
// Its merges were heard before: the window, measured from the last of them, is closed, and the cutter cuts it
|
||||
// once no walk of its kind is open.
|
||||
return b, keepBatch(ctx, inv, &b, now, "")
|
||||
}
|
||||
|
||||
// leaveOut splits a walk's merges by the merges named to leave out, each `<repository>@<commit>` (a commit's
|
||||
// first characters will do): the merges walked again, and those left out — each named one, and each later merge
|
||||
// of its repository's branch, which contains it. A name the walk does not answer is refused.
|
||||
func leaveOut(merges []inventory.BatchedMerge, without []string) ([]inventory.BatchedMerge, []inventory.PlanLeftOut, error) {
|
||||
var stopped []inventory.BatchedMerge
|
||||
for _, w := range without {
|
||||
repository, commit, ok := strings.Cut(strings.TrimSpace(w), "@")
|
||||
if !ok || repository == "" || len(commit) < 7 {
|
||||
return nil, nil, fmt.Errorf("%q names no merge: <repository>@<commit>", w)
|
||||
}
|
||||
i := slices.IndexFunc(merges, func(m inventory.BatchedMerge) bool {
|
||||
return strings.EqualFold(m.Repository, repository) && strings.HasPrefix(m.Commit, commit)
|
||||
})
|
||||
if i < 0 {
|
||||
return nil, nil, fmt.Errorf("it answers no merge %s@%s", repository, commit)
|
||||
}
|
||||
stopped = append(stopped, merges[i])
|
||||
}
|
||||
// The earliest stopped merge of each repository's branch: every merge of that branch after it contains it.
|
||||
earliest := map[string]inventory.BatchedMerge{}
|
||||
for _, s := range stopped {
|
||||
k := strings.ToLower(s.Repository) + "\x00" + s.Branch
|
||||
if e, ok := earliest[k]; !ok || laterMerge(e, s) {
|
||||
earliest[k] = s
|
||||
}
|
||||
}
|
||||
var again []inventory.BatchedMerge
|
||||
var out []inventory.PlanLeftOut
|
||||
for _, m := range merges {
|
||||
named := slices.ContainsFunc(stopped, func(s inventory.BatchedMerge) bool {
|
||||
return strings.EqualFold(s.Repository, m.Repository) && s.Commit == m.Commit
|
||||
})
|
||||
e, has := earliest[strings.ToLower(m.Repository)+"\x00"+m.Branch]
|
||||
switch {
|
||||
case named:
|
||||
out = append(out, inventory.PlanLeftOut{Repository: m.Repository, Commit: m.Commit})
|
||||
case has && laterMerge(m, e):
|
||||
out = append(out, inventory.PlanLeftOut{Repository: m.Repository, Commit: m.Commit, Contains: e.Commit})
|
||||
default:
|
||||
again = append(again, m)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Contains == "" && out[j].Contains != "" })
|
||||
return again, out, nil
|
||||
}
|
||||
|
||||
// mergesLeftOutWords is the merges a stop left out, as a person reads them: each that contains a stopped one says so.
|
||||
func mergesLeftOutWords(out []inventory.PlanLeftOut) string {
|
||||
var words []string
|
||||
for _, o := range out {
|
||||
s := o.Repository + "@" + short(o.Commit)
|
||||
if o.Contains != "" {
|
||||
s += " (which contains " + short(o.Contains) + ")"
|
||||
}
|
||||
words = append(words, s)
|
||||
}
|
||||
return strings.Join(words, ", ")
|
||||
}
|
||||
|
||||
// orderMember is one member of a group, as mesh-delivery says it.
|
||||
@@ -466,6 +618,8 @@ func deliveryCommand(ctx context.Context, args []string) error {
|
||||
group := set.String("group", "", "a delivery group's id")
|
||||
by := set.String("by", catalogue.DeliverySeat, "who says it")
|
||||
why := set.String("why", "", "why")
|
||||
without := set.String("without", "", "merges a stop leaves out, <repository>@<commit>, comma-separated: the walk's "+
|
||||
"other merges are walked again without them (novox/hq ADR 0299)")
|
||||
limit := set.Int("n", 50, "how many ended walks to answer beside the open ones")
|
||||
planID := set.String("plan", "", "one walk")
|
||||
positionals, err := parseAround(set, rest)
|
||||
@@ -552,15 +706,21 @@ func deliveryCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
case "stop":
|
||||
if len(positionals) != 1 || strings.TrimSpace(*why) == "" {
|
||||
return errors.New("delivery stop <plan> --why <text> [--by <who>]")
|
||||
return errors.New("delivery stop <plan> --why <text> [--by <who>] [--without <repository>@<commit>,…]")
|
||||
}
|
||||
var p inventory.Plan
|
||||
var said stopAnswer
|
||||
if err := sayingOnTheBus(ctx, func() (err error) {
|
||||
p, err = stopWalk(ctx, inv, positionals[0], *by, strings.TrimSpace(*why))
|
||||
p, said, err = stopWalkWithout(ctx, inv, positionals[0], *by, strings.TrimSpace(*why), splitList(*without),
|
||||
time.Now().UTC())
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if *without != "" {
|
||||
// What it left out and what it walks again, as data: the delivery's owner reads it (ADR 0299).
|
||||
return answer(said)
|
||||
}
|
||||
fmt.Printf("%s stopped by %s at tier %d of %d; what was asked still builds and registers, nothing further "+
|
||||
"is asked or sent\n", p.ID, *by, p.Tier, len(p.Tiers))
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user