725 lines
27 KiB
Go
725 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Which merges a walk answers (novox/hq issue 362, ADR 0276).
|
|
//
|
|
// On 2026-10-10 two merges of the catalogue 18 seconds apart were walked by one walk at the later merge's
|
|
// commit, which superseded the first. This owner found the walk a delivery follows by the walk's id or by the
|
|
// commit it was merged as, final or not: the surviving walk matched the dunst delivery, which the supersession
|
|
// had already ended, so nothing gave it its word, and the claude-code delivery, merged as another commit,
|
|
// matched no walk and was held for a person after ten minutes, for a reason that named neither.
|
|
//
|
|
// A walk answers every merge it carries: the merges it names (`delivery.merges`, said by a controller that
|
|
// batches merges), the merge of its own commit, a merge whose walk it took over, and a merge of its repository
|
|
// and branch the forge says its commit contains. A delivery follows the newest walk that answers it, and only a
|
|
// delivery not yet ended is ever tied to a walk by what it answers: an ended delivery is moved only by the
|
|
// record of its own walk, which no longer moves it.
|
|
|
|
// seenWalk is a walk's last record and when it was heard.
|
|
type seenWalk struct {
|
|
walk Walk
|
|
at time.Time
|
|
}
|
|
|
|
// keptWalkFor is how long a walk no longer open is kept for a merge heard after it ended; a batch is kept as
|
|
// long, past its window's latest close.
|
|
const keptWalkFor = 10 * time.Minute
|
|
|
|
// answer is a delivery a walk answers and the commit of its repository the walk carries for it.
|
|
type answer struct {
|
|
d *Delivery
|
|
carried string
|
|
}
|
|
|
|
// walkAnswers is whether a walk answers a delivery's merge, and the commit it carries for it.
|
|
func walkAnswers(w Walk, d *Delivery) (string, bool) {
|
|
if w.Release != nil || w.IsBatch() || d.MergedAs == "" {
|
|
return "", false
|
|
}
|
|
if c, ok := w.carriedFor(d.Repository, d.MergedAs); ok {
|
|
return c, true
|
|
}
|
|
if d.Walk != nil && d.Walk.TakenOverBy == w.ID && namesNoMerges(w) && strings.EqualFold(w.Repository, d.Repository) &&
|
|
sameBranch(w.Branch, d.Base) {
|
|
// The walk that took its walk over, from a controller that names no merges: it answers every merge the one
|
|
// it took over answered, by its commit of the same repository and branch. A walk that names its merges
|
|
// answers only those (carriedFor, above).
|
|
return w.Commit, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// containedAnswer is, for a controller that does not name the merges a walk answers, whether an open walk
|
|
// answers a ready merged delivery by containment: of its repository and branch, made for a merge after it, the
|
|
// forge says its commit contains this merge, and it moves every module this merge's plan moves. A later walk that
|
|
// moves only what its own merge moved does not deliver this one, however much of the trunk it contains.
|
|
func (h *Holder) containedAnswer(w Walk, d *Delivery) (string, bool) {
|
|
if !h.mayContain(w, d) || !d.ContainedIn[w.Commit] {
|
|
return "", false
|
|
}
|
|
return w.Commit, true
|
|
}
|
|
|
|
// mayContain is whether an open walk is one to ask the forge about for a ready merged delivery (containedAnswer).
|
|
func (h *Holder) mayContain(w Walk, d *Delivery) bool {
|
|
if w.Release != nil || w.IsBatch() || !w.Open() || !namesNoMerges(w) || d.State != Ready || d.MergedAs == "" || d.Walk != nil ||
|
|
d.Plan == nil || len(d.Plan.Moved) == 0 || !strings.EqualFold(w.Repository, d.Repository) ||
|
|
!sameBranch(w.Branch, d.Base) || sameCommit(w.Commit, d.MergedAs) {
|
|
return false
|
|
}
|
|
for _, m := range d.Plan.Moved {
|
|
if _, moves := w.Modules[m]; !moves {
|
|
return false
|
|
}
|
|
}
|
|
// Made for a merge after this one: the walk's own merge, where this owner holds it, or else the walk itself.
|
|
after := w.Created
|
|
for _, o := range h.deliveries {
|
|
if strings.EqualFold(o.Repository, w.Repository) && sameCommit(o.MergedAs, w.Commit) && !o.MergedAt.IsZero() {
|
|
after = o.MergedAt
|
|
}
|
|
}
|
|
return after.After(d.MergedAt)
|
|
}
|
|
|
|
// namesNoMerges is whether a walk is from a controller that does not name the merges it answers.
|
|
func namesNoMerges(w Walk) bool { return w.Delivery == nil || len(w.Delivery.Merges) == 0 }
|
|
|
|
func sameBranch(a, b string) bool { return a == "" || b == "" || strings.EqualFold(a, b) }
|
|
|
|
// follows is whether a delivery takes a walk that answers it as the one it follows: it has none, it is that walk,
|
|
// that walk took its own over, or that walk is newer than the one it follows. A walk record kept before its
|
|
// creation was read is followed past only once it is superseded or failed.
|
|
func follows(d *Delivery, w Walk) bool {
|
|
switch {
|
|
case d.Walk == nil || d.Walk.ID == w.ID || d.Walk.TakenOverBy == w.ID:
|
|
return true
|
|
case d.Walk.Created.IsZero():
|
|
return d.Walk.State == walkSuperseded || d.Walk.State == walkFailed
|
|
}
|
|
return w.Created.After(d.Walk.Created)
|
|
}
|
|
|
|
// deliveriesWalking are the deliveries a walk record moves: every delivery that already follows it — ended or
|
|
// not, its own record — and every delivery not ended that it answers and that follows it now. In id order.
|
|
func (h *Holder) deliveriesWalking(w Walk) []answer {
|
|
var out []answer
|
|
for _, d := range h.deliveries {
|
|
if d.Walk != nil && d.Walk.ID == w.ID {
|
|
carried := d.AnsweredBy
|
|
if c, ok := walkAnswers(w, d); ok {
|
|
carried = c
|
|
}
|
|
out = append(out, answer{d: d, carried: carried})
|
|
continue
|
|
}
|
|
if d.State.Final() || !follows(d, w) {
|
|
continue
|
|
}
|
|
if c, ok := walkAnswers(w, d); ok {
|
|
out = append(out, answer{d: d, carried: c})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].d.ID < out[j].d.ID })
|
|
return out
|
|
}
|
|
|
|
// accounted is whether any delivery, ended or not, follows the walk or is answered by it: a walk known for
|
|
// what it answers is never made a delivery of its own.
|
|
func (h *Holder) accounted(w Walk) bool {
|
|
for _, d := range h.deliveries {
|
|
if d.Walk != nil && d.Walk.ID == w.ID {
|
|
return true
|
|
}
|
|
if _, ok := walkAnswers(w, d); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// held is whether a live delivery follows the walk, or a live delivery it answers follows a newer one. A walk whose
|
|
// word only stops that lapsed give (lapsedOn, novox/hq issue 753) is not held: it stays under the orphans' timer, so
|
|
// past the grace without its word it is listed by `stalled`, saying why it was not given.
|
|
func (h *Holder) heldWalk(w Walk) bool {
|
|
for _, d := range h.deliveries {
|
|
if d.State.Final() {
|
|
continue
|
|
}
|
|
if d.Walk != nil && d.Walk.ID == w.ID {
|
|
return true
|
|
}
|
|
if _, ok := walkAnswers(w, d); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// pickWalk is the newest walk heard that answers a merged delivery with no walk yet; nil for none.
|
|
func (h *Holder) pickWalk(d *Delivery) (Walk, string, bool) {
|
|
var best Walk
|
|
var carried string
|
|
found := false
|
|
for _, s := range h.walks {
|
|
if !follows(d, s.walk) {
|
|
continue
|
|
}
|
|
c, ok := walkAnswers(s.walk, d)
|
|
if !ok {
|
|
c, ok = h.containedAnswer(s.walk, d)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !found || s.walk.Created.After(best.Created) || s.walk.Created.Equal(best.Created) && s.walk.ID > best.ID {
|
|
best, carried, found = s.walk, c, true
|
|
}
|
|
}
|
|
return best, carried, found
|
|
}
|
|
|
|
// linkAnswering ties a merged delivery with no walk to the newest walk heard that answers it; under the lock.
|
|
func (h *Holder) linkAnswering(d *Delivery) bool {
|
|
if d.State.Final() || d.MergedAs == "" || d.Walk != nil {
|
|
return false
|
|
}
|
|
w, carried, ok := h.pickWalk(d)
|
|
if !ok {
|
|
return false
|
|
}
|
|
h.linkWalk(d, w, carried)
|
|
delete(h.unmatched, w.ID)
|
|
return true
|
|
}
|
|
|
|
// containsAsks are the forge's asks whether an open walk of a merged delivery's repository and branch, at
|
|
// another commit, contains its merge: for a controller that does not name the merges a walk answers (issue 362),
|
|
// asked once per pair, the answer kept on the delivery.
|
|
func (h *Holder) containsAsks(d *Delivery, now time.Time) []func() {
|
|
var asks []func()
|
|
for _, s := range h.walks {
|
|
w := s.walk
|
|
if !h.mayContain(w, d) {
|
|
continue
|
|
}
|
|
if _, known := d.ContainedIn[w.Commit]; known {
|
|
continue
|
|
}
|
|
if a := h.askContains(d, w.Commit, now); a != nil {
|
|
asks = append(asks, a)
|
|
}
|
|
}
|
|
return asks
|
|
}
|
|
|
|
// batchOf is the batch a merged delivery's merge is in, when the controller said one; false for none.
|
|
func (h *Holder) batchOf(d *Delivery) (Walk, bool) {
|
|
if d.MergedAs == "" {
|
|
return Walk{}, false
|
|
}
|
|
for _, s := range h.walks {
|
|
if !s.walk.IsBatch() || s.walk.Delivery == nil {
|
|
continue
|
|
}
|
|
for _, m := range s.walk.Delivery.Merges {
|
|
if strings.EqualFold(m.Repository, d.Repository) && sameCommit(m.Commit, d.MergedAs) {
|
|
return s.walk, true
|
|
}
|
|
}
|
|
}
|
|
return Walk{}, false
|
|
}
|
|
|
|
// batchSaid is what a batch holding a delivery's merge waits for, in words.
|
|
func batchSaid(b Walk, now time.Time) string {
|
|
said := "its batch " + b.ID + ": " + b.State
|
|
bt := b.Delivery.Batch
|
|
switch {
|
|
case b.State == walkQueued && bt != nil && bt.Behind != "":
|
|
said += " behind " + bt.Behind
|
|
case b.State == walkAssembling && bt != nil && bt.ClosesAt != nil:
|
|
left := bt.ClosesAt.Sub(now).Round(time.Second)
|
|
if left < 0 {
|
|
left = 0
|
|
}
|
|
said += fmt.Sprintf(", %s left", left)
|
|
if bt.AtMost != nil {
|
|
said += " (at the latest " + bt.AtMost.Local().Format("15:04:05") + ")"
|
|
}
|
|
}
|
|
return said + "; plan not yet calculated"
|
|
}
|
|
|
|
// unwalkedWhy is why a merged delivery no walk answers is held: the open walks of its repository and that none
|
|
// answers its merge, or that the controller holds none — what happened, not what might have.
|
|
func (h *Holder) unwalkedWhy(d *Delivery) string {
|
|
var open []string
|
|
for _, s := range h.walks {
|
|
w := s.walk
|
|
if w.Release == nil && !w.IsBatch() && w.Open() && strings.EqualFold(w.Repository, d.Repository) {
|
|
open = append(open, fmt.Sprintf("%s at %s", w.ID, shortOf(w.Commit, 8)))
|
|
}
|
|
}
|
|
sort.Strings(open)
|
|
head := fmt.Sprintf("merged as %s, and no walk answers its merge within %s: ", shortOf(d.MergedAs, 8), unwalked)
|
|
if len(open) == 0 {
|
|
return head + "the controller holds no open walk of " + d.Repository + " — its merge was not heard, or " +
|
|
"nothing the mesh holds follows " + orNone(d.Base) + ": a person decides"
|
|
}
|
|
return head + "the open walk(s) of " + d.Repository + " — " + strings.Join(open, ", ") + " — name no merge of " +
|
|
"it and the forge did not say they contain it: a person decides"
|
|
}
|
|
|
|
func orNone(s string) string {
|
|
if s == "" {
|
|
return "its branch"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// orphanLines are the waiting walks no live delivery holds, past the grace a merge is given to be heard, as
|
|
// stalled lines in the state Unheld: nothing would ever give such a walk its word (issue 362). Under the lock.
|
|
func (h *Holder) orphanLines(now time.Time) []StalledLine {
|
|
var out []StalledLine
|
|
for id, first := range h.orphans {
|
|
s, known := h.walks[id]
|
|
if !known || now.Sub(first) < matchGrace {
|
|
continue
|
|
}
|
|
w := s.walk
|
|
if lapsed, behind := h.lapsedOf(w); len(lapsed) > 0 || len(behind) > 0 {
|
|
// Owed its word by stops that lapsed, and not given it past the grace (review of issue 753): said, why.
|
|
says := fmt.Sprintf("the walk %s of %s at %s waits for this owner's word, which the stopped deliveries whose "+
|
|
"merge it answers owe it (novox/hq issue 753), and it has not started", w.ID, w.Repository, shortOf(w.Commit, 8))
|
|
if why := h.lapsedSaid(w); why != "" {
|
|
says += ": " + why
|
|
}
|
|
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Published, Walk: w.ID,
|
|
For: now.Sub(first).Round(time.Second).String(), Bound: matchGrace.String(),
|
|
H2: "none: its word is given at its turn — the operator's past the bound", Says: says})
|
|
continue
|
|
}
|
|
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Unheld, Walk: w.ID,
|
|
For: now.Sub(first).Round(time.Second).String(), Bound: matchGrace.String(),
|
|
H2: "none: no delivery holds the walk, so nothing gives it its word — the operator's",
|
|
Says: fmt.Sprintf("the walk %s of %s at %s waits for this owner's word, and no delivery that has not ended "+
|
|
"follows it or is answered by it: `mesh-delivery.show` for the deliveries of %s, and `plans go %s --why …` "+
|
|
"starts it by hand", w.ID, w.Repository, shortOf(w.Commit, 8), w.Repository, w.ID)})
|
|
out[len(out)-1].Says += h.endedSaid(w)
|
|
if hold := h.coverHeld(id); hold != "" {
|
|
out[len(out)-1].Says += "; it is held back meanwhile: " + hold
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Unheld is a waiting walk no live delivery holds: not a state of the table — only what a stalled line says.
|
|
const Unheld State = "unheld"
|
|
|
|
// seeOrphans keeps when each waiting walk no live delivery holds was first seen so, and forgets the rest; a
|
|
// walk still unmatched waits for its merge or its adoption instead. Under the lock.
|
|
func (h *Holder) seeOrphans(now time.Time) {
|
|
for id, s := range h.walks {
|
|
w := s.walk
|
|
_, unmatched := h.unmatched[id]
|
|
if unmatched || w.Release != nil || w.IsBatch() || !w.Open() || !w.Seen().Waits || h.heldWalk(w) {
|
|
delete(h.orphans, id)
|
|
continue
|
|
}
|
|
if _, was := h.orphans[id]; !was {
|
|
h.orphans[id] = now
|
|
h.Logf("[mesh-delivery] the walk %s waits for this owner's word and no live delivery holds it: said once "+
|
|
"it is %s so", id, matchGrace)
|
|
}
|
|
}
|
|
for id := range h.orphans {
|
|
if _, known := h.walks[id]; !known {
|
|
delete(h.orphans, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// forgetWalks drops a walk record no longer open after keptWalkFor, and a batch heard of that long past its
|
|
// window's latest close. Under the lock.
|
|
func (h *Holder) forgetWalks(now time.Time) {
|
|
for id, s := range h.walks {
|
|
w := s.walk
|
|
switch {
|
|
case w.IsBatch():
|
|
end := s.at
|
|
if w.Delivery != nil && w.Delivery.Batch != nil && w.Delivery.Batch.AtMost != nil && w.Delivery.Batch.AtMost.After(end) {
|
|
end = *w.Delivery.Batch.AtMost
|
|
}
|
|
if now.Sub(end) > 3*time.Hour {
|
|
delete(h.walks, id)
|
|
}
|
|
case !w.Open() && now.Sub(s.at) > keptWalkFor:
|
|
delete(h.walks, id)
|
|
delete(h.deliverErr, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- the word, once per walk ------------------------------------------------------------------------------
|
|
|
|
// turnWant is a walk whose turn came for some of the deliveries it carries, and why.
|
|
type turnWant struct {
|
|
ids []string
|
|
whys []string
|
|
}
|
|
|
|
// endedSaid names the ended deliveries a waiting walk answers or that follow it, with their states, so a walk no live
|
|
// delivery holds says what it carries instead of only that nothing holds it (novox/hq issue 753); empty for none.
|
|
func (h *Holder) endedSaid(w Walk) string {
|
|
var ended []string
|
|
for _, d := range h.deliveries {
|
|
if !d.State.Final() {
|
|
continue
|
|
}
|
|
_, answers := walkAnswers(w, d)
|
|
if answers || d.Walk != nil && d.Walk.ID == w.ID {
|
|
ended = append(ended, fmt.Sprintf("%s (%s)", d.ID, d.State))
|
|
}
|
|
}
|
|
if len(ended) == 0 {
|
|
return ""
|
|
}
|
|
sort.Strings(ended)
|
|
return "; every delivery it carries ended: " + strings.Join(ended, ", ")
|
|
}
|
|
|
|
// lapsedOn are the deliveries, by id, whose word an open walk waiting for this owner's word takes from a stop that
|
|
// lapsed (novox/hq issue 753): each was stopped by its group for a member delivered since, so its own change waits for
|
|
// a later walk of its repository that contains its merge (issue 747), and this walk answers that merge — it names it,
|
|
// or is made at it — and has not left it out. Such a walk is the one that delivery waits for: it gives it its word, so
|
|
// a walk whose every other delivery ended is never left without one. A walk at the merge of a delivery stopped for
|
|
// itself, or by a person, gets nothing from it. Under the lock.
|
|
//
|
|
// In its group's order (review of issue 753): a member gives its word only once every member ordered before it reached
|
|
// the machines, or is itself a stop that lapsed whose merge this same walk answers — the walk then delivers both, in
|
|
// the group's order among its tiers, as members sharing one walk do. A member behind one still undelivered gives
|
|
// nothing; lapsedBehind names that one.
|
|
func (h *Holder) lapsedOn(w Walk) []string {
|
|
out, _ := h.lapsedOf(w)
|
|
return out
|
|
}
|
|
|
|
// lapsedOf is lapsedOn, and, by delivery, the member before it in its group that holds back the word of a stop that
|
|
// lapsed whose merge the walk answers. Under the lock.
|
|
func (h *Holder) lapsedOf(w Walk) ([]string, map[string]string) {
|
|
if w.Release != nil || w.IsBatch() || !w.Open() || !w.Seen().Waits {
|
|
return nil, nil
|
|
}
|
|
var out []string
|
|
behind := map[string]string{}
|
|
for _, d := range h.deliveries {
|
|
if !h.lapsedAnswered(w, d) {
|
|
continue
|
|
}
|
|
if before := h.lapsedBehind(w, d); before != "" {
|
|
behind[d.ID] = before
|
|
continue
|
|
}
|
|
out = append(out, d.ID)
|
|
}
|
|
sort.Strings(out)
|
|
return out, behind
|
|
}
|
|
|
|
// lapsedAnswered is whether a delivery's stop lapsed and the walk answers its merge, not left out.
|
|
func (h *Holder) lapsedAnswered(w Walk, d *Delivery) bool {
|
|
if d.MergedAs == "" || !h.stopLapsed(d) {
|
|
return false
|
|
}
|
|
if _, ok := walkAnswers(w, d); !ok {
|
|
return false
|
|
}
|
|
_, left := w.leftOut(d.Repository, d.MergedAs)
|
|
return !left
|
|
}
|
|
|
|
// lapsedBehind is the member ordered before a delivery in its group that has not reached the machines and that this
|
|
// walk does not deliver with it, with its state; empty for none.
|
|
func (h *Holder) lapsedBehind(w Walk, d *Delivery) string {
|
|
g := h.groups[d.Group]
|
|
if d.Group == "" || g == nil {
|
|
return ""
|
|
}
|
|
members := h.membersOf(g)
|
|
byID := map[string]*Delivery{}
|
|
for _, m := range members {
|
|
byID[m.ID] = m
|
|
}
|
|
for _, m := range h.inOrder(g, members) {
|
|
if m.ID == d.ID {
|
|
return ""
|
|
}
|
|
o := byID[m.ID]
|
|
if o == nil || memberReached(o) || h.lapsedAnswered(w, o) {
|
|
continue
|
|
}
|
|
return fmt.Sprintf("%s (%s)", o.ID, o.State)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// lapsedSaid is why a walk whose word stops that lapsed give still waits: the members of their groups not yet
|
|
// delivered before them, what the last look held it for, or the controller's refusal of the word. Under the lock.
|
|
func (h *Holder) lapsedSaid(w Walk) string {
|
|
var parts []string
|
|
_, behind := h.lapsedOf(w)
|
|
ids := make([]string, 0, len(behind))
|
|
for id := range behind {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
for _, id := range ids {
|
|
parts = append(parts, fmt.Sprintf("%s waits for %s, ordered before it in its group", id, behind[id]))
|
|
}
|
|
if why := h.waitsWhy[w.ID]; why != "" {
|
|
parts = append(parts, "not let go yet: "+why)
|
|
}
|
|
if err := h.deliverErr[w.ID]; err != "" {
|
|
parts = append(parts, "the controller refused its word: "+err)
|
|
}
|
|
lapsed, _ := h.lapsedOf(w)
|
|
for _, id := range lapsed {
|
|
if d := h.deliveries[id]; d != nil && strings.Contains(d.LetGoWhy, lapsedWord(w.ID)) {
|
|
parts = append(parts, fmt.Sprintf("its word was given for %s at %s, and the controller has not started it",
|
|
id, d.LetGo.Local().Format("15:04:05")))
|
|
}
|
|
}
|
|
return strings.Join(parts, "; ")
|
|
}
|
|
|
|
// lapsedOwed is whether a walk's word is owed by stops that lapsed: given now, or once a member before one is delivered.
|
|
func (h *Holder) lapsedOwed(w Walk) bool {
|
|
lapsed, behind := h.lapsedOf(w)
|
|
return len(lapsed) > 0 || len(behind) > 0
|
|
}
|
|
|
|
// lapsedTurns registers, for every open walk waiting for this owner's word, the turn of each delivery whose stop
|
|
// lapsed and whose merge it answers (lapsedOn, novox/hq issue 753). The word is then given as for any walk: once,
|
|
// when every live delivery it carries is at its turn too and it carries no stopped change still held back (words).
|
|
// Given once per walk: the walk's id is in the delivery's LetGoWhy once its word was given. Under the lock.
|
|
func (h *Holder) lapsedTurns() {
|
|
for id, s := range h.walks {
|
|
for _, did := range h.lapsedOn(s.walk) {
|
|
d := h.deliveries[did]
|
|
if strings.Contains(d.LetGoWhy, lapsedWord(id)) {
|
|
continue
|
|
}
|
|
if h.turning == nil {
|
|
h.turning = map[string]*turnWant{}
|
|
}
|
|
want := h.turning[id]
|
|
if want == nil {
|
|
want = &turnWant{}
|
|
h.turning[id] = want
|
|
}
|
|
if !containsString(want.ids, d.ID) {
|
|
want.ids = append(want.ids, d.ID)
|
|
want.whys = append(want.whys, fmt.Sprintf("%s: %s its merge %s, stopped by its group for %s, whose change "+
|
|
"was delivered since; it waits for a later walk of %s that contains its merge, and this is that "+
|
|
"walk (novox/hq issue 753)", d.ID, lapsedWord(id), shortOf(d.MergedAs, 8), d.stoppedFor(), d.Repository))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// lapsedWord is how the word a lapsed stop gives a walk names that walk.
|
|
func lapsedWord(walk string) string { return "the walk " + walk + " answers" }
|
|
|
|
// words are the asks of the word of every walk whose turn came for every live delivery it carries: one ask per
|
|
// walk, however many deliveries it carries, and none while one of them is not at its turn (ADR 0276 decision 5).
|
|
// Under the lock.
|
|
func (h *Holder) words() []func() {
|
|
walks := make([]string, 0, len(h.turning))
|
|
for walk := range h.turning {
|
|
walks = append(walks, walk)
|
|
}
|
|
sort.Strings(walks)
|
|
var asks []func()
|
|
h.waitsWhy = map[string]string{}
|
|
for _, walk := range walks {
|
|
want := h.turning[walk]
|
|
var waiting []string
|
|
for _, d := range h.deliveries {
|
|
if d.State.Final() || d.Walk == nil || d.Walk.ID != walk || containsString(want.ids, d.ID) {
|
|
continue
|
|
}
|
|
waiting = append(waiting, fmt.Sprintf("%s (%s)", d.ID, d.State))
|
|
}
|
|
if len(waiting) > 0 {
|
|
sort.Strings(waiting)
|
|
h.waitsWhy[walk] = strings.Join(waiting, ", ") + " carried by it not at its turn"
|
|
h.Logf("[mesh-delivery] the walk %s is not let go yet: %s", walk, h.waitsWhy[walk])
|
|
continue
|
|
}
|
|
ids, why := append([]string(nil), want.ids...), strings.Join(want.whys, "; ")
|
|
// **Never the word for a walk that carries a stopped change** (ADR 0299, review of #221): it is asked to
|
|
// leave the merge out first (leaveStopped).
|
|
if stopped, _ := h.carriesStopped(walk); len(stopped) > 0 {
|
|
h.waitsWhy[walk] = "it carries " + strings.Join(stopped, ", ") + ", whose delivery was stopped: it is " +
|
|
"asked to leave that merge out first"
|
|
if hold := h.coverHeld(walk); hold != "" {
|
|
h.waitsWhy[walk] = hold
|
|
}
|
|
h.Logf("[mesh-delivery] the walk %s is not let go: it carries %s, whose delivery was stopped", walk,
|
|
strings.Join(stopped, ", "))
|
|
continue
|
|
}
|
|
if h.asking[walk] {
|
|
continue // its word is being asked
|
|
}
|
|
h.asking[walk] = true
|
|
walk := walk
|
|
asks = append(asks, func() {
|
|
err := h.Controller.Deliver(walk, why)
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
delete(h.asking, walk)
|
|
if err != nil {
|
|
if h.deliverErr == nil {
|
|
h.deliverErr = map[string]string{}
|
|
}
|
|
h.deliverErr[walk] = err.Error() // said by `stalled` past the grace (review of novox/hq issue 753)
|
|
h.Logf("[mesh-delivery] the walk %s could not be let go yet: %v", walk, err)
|
|
h.again("a walk's word the controller refused or did not answer")
|
|
return
|
|
}
|
|
delete(h.deliverErr, walk)
|
|
for _, id := range ids {
|
|
d := h.deliveries[id]
|
|
if d == nil {
|
|
continue
|
|
}
|
|
d.LetGo, d.LetGoWhy = h.Now(), why
|
|
if d.Walk != nil && d.Walk.ID == walk {
|
|
d.Walk.Waits, d.Walk.GoBy = false, byOwner
|
|
}
|
|
h.settleAndKeep(d)
|
|
}
|
|
})
|
|
}
|
|
h.turning = nil
|
|
return asks
|
|
}
|
|
|
|
// endedMovingNothing is whether a delivery ended delivered with nothing for a walk to move: merged, its plan
|
|
// moving no module the mesh holds, and no walk followed.
|
|
func endedMovingNothing(d *Delivery) bool {
|
|
return d.State == Delivered && d.MergedAs != "" && d.Walk == nil && d.MovesNothing()
|
|
}
|
|
|
|
// endedAsNothingToMove are the deliveries a walk answers that ended delivered as nothing for a walk to move (novox/hq
|
|
// issues 362, 529: the nzbgeek case); empty when it answers any other. Such a walk is held by nothing: said, never
|
|
// let go by this owner. Under the lock.
|
|
func (h *Holder) endedAsNothingToMove(w Walk) []string {
|
|
var out []string
|
|
for _, d := range h.deliveries {
|
|
_, answers := walkAnswers(w, d)
|
|
if !answers && (d.Walk == nil || d.Walk.ID != w.ID) {
|
|
continue
|
|
}
|
|
if !endedMovingNothing(d) {
|
|
return nil
|
|
}
|
|
out = append(out, d.ID)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// WalksWaiting is `stalled` asked with `walks` (novox/hq issue 529): for each walk waiting for this owner's word,
|
|
// what holds it, in the shape of a stalled line naming the walk — each held delivery it carries (State held, its
|
|
// reason and the verb that releases it), else why its word was not given at the last look (State published), else
|
|
// that no live delivery holds it (State unheld). A walk whose word is being given is not listed. The controller's
|
|
// `plans` says these instead of pointing at a hand start; the self-check never raises a condition for them.
|
|
func (h *Holder) WalksWaiting() []StalledLine {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
now := h.Now()
|
|
var out []StalledLine
|
|
for id, s := range h.walks {
|
|
w := s.walk
|
|
if w.Release != nil || w.IsBatch() || !w.Open() || !w.Seen().Waits || h.asking[id] {
|
|
continue
|
|
}
|
|
waited := now.Sub(w.Created).Round(time.Second).String()
|
|
var held []StalledLine
|
|
live := false
|
|
for _, d := range h.deliveries {
|
|
if d.State.Final() || d.Walk == nil || d.Walk.ID != id {
|
|
continue
|
|
}
|
|
live = true
|
|
if d.State == Held {
|
|
held = append(held, StalledLine{ID: d.ID, Number: d.Number, State: Held, Walk: id, For: waited,
|
|
Bound: "0s", H2: "none: held for a person — the operator's", Says: HeldLine(d)})
|
|
}
|
|
}
|
|
switch {
|
|
case len(held) > 0:
|
|
out = append(out, held...)
|
|
case h.coverHeld(id) != "":
|
|
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Published, Walk: id, For: waited,
|
|
Bound: coverAskBound.String(), H2: "none: it is asked to leave the merge out past the bound",
|
|
Says: "held back: " + h.coverHeld(id)})
|
|
case h.waitsWhy[id] != "":
|
|
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Published, Walk: id, For: waited,
|
|
Bound: "0s", H2: "none: it goes at its turn", Says: "not let go yet: " + h.waitsWhy[id]})
|
|
case !live && h.lapsedOwed(w):
|
|
// Its word comes from the stopped deliveries whose merge it answers (novox/hq issue 753): said, never
|
|
// unheld, while the controller has not taken it, with what holds it.
|
|
lapsed, _ := h.lapsedOf(w)
|
|
says := "its word is owed by the stopped deliveries whose merge it answers, each stopped for a member " +
|
|
"delivered since (novox/hq issue 753)"
|
|
if len(lapsed) > 0 {
|
|
says = "its word is given for " + strings.Join(lapsed, ", ") + ", stopped for a member delivered since, " +
|
|
"whose merge it answers (novox/hq issue 753)"
|
|
}
|
|
if why := h.lapsedSaid(w); why != "" {
|
|
says += "; " + why
|
|
}
|
|
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Published, Walk: id, For: waited,
|
|
Bound: "0s", H2: "none: it goes at its turn", Says: says})
|
|
case !live:
|
|
says := "no delivery that has not ended follows it or is answered by it, so nothing gives it its word: " +
|
|
"`mesh-delivery.show` for the deliveries of " + w.Repository
|
|
if ended := h.endedAsNothingToMove(w); len(ended) > 0 {
|
|
// The nzbgeek case (novox/hq issues 362, 529): its merges' deliveries ended as nothing for a walk to
|
|
// move, yet the walk moves modules — nothing judged those, so nothing here lets it go.
|
|
says = "held by nothing: its merges' deliveries ended as nothing-to-move (" + strings.Join(ended, ", ") +
|
|
"), yet the walk moves " + strings.Join(walkModules(w), ", ") + " — nothing gives it its word"
|
|
}
|
|
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Unheld, Walk: id, For: waited,
|
|
Bound: "0s", H2: "none: no delivery holds the walk — the operator's", Says: says + h.endedSaid(w)})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Walk != out[j].Walk {
|
|
return out[i].Walk < out[j].Walk
|
|
}
|
|
return out[i].ID < out[j].ID
|
|
})
|
|
return out
|
|
}
|
|
|
|
// walkModules are the modules a walk's record names, sorted.
|
|
func walkModules(w Walk) []string {
|
|
out := make([]string, 0, len(w.Modules))
|
|
for m := range w.Modules {
|
|
out = append(out, m)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|