Name the hold a waiting walk waits on in plans (issue 529)
mesh/delivery-group group fix/529-a-waiting-walk-is-answered-or-named checking: 0 of 2 member(s) ready
mesh/merge-gate pass: builds mesh-controller → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group fix/529-a-waiting-walk-is-answered-or-named checking: 0 of 2 member(s) ready
mesh/merge-gate pass: builds mesh-controller → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
A walk whose delivery was held for a person said only that it waits for mesh-delivery's word and that plans go starts it by hand, pointing the reader at a hand start instead of the person's decision. Ask mesh-delivery what holds each waiting walk and say the held delivery, its reason and mesh-delivery.release, or the delivery not at its turn.
This commit is contained in:
@@ -82,6 +82,29 @@ func waitingNote(p inventory.Plan) string {
|
|||||||
p.Delivery.Awaits, p.ID)
|
p.Delivery.Awaits, p.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitingSaid is what a walk waiting for its word says when the delivery's owner said what holds it (novox/hq
|
||||||
|
// issue 529): a delivery held for a person, its reason and the verb that releases it — the person's decision, not
|
||||||
|
// a hand start — or the deliveries not at their turn. Told nothing, it is waitingNote.
|
||||||
|
func waitingSaid(p inventory.Plan, lines []stalledLine) string {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return waitingNote(p)
|
||||||
|
}
|
||||||
|
var held, other []string
|
||||||
|
for _, l := range lines {
|
||||||
|
if l.State == "held" {
|
||||||
|
held = append(held, l.Says)
|
||||||
|
} else {
|
||||||
|
other = append(other, l.Says)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(held) > 0 {
|
||||||
|
return fmt.Sprintf("published; its walk waits for %s's word, and a delivery it carries is held for a person: %s",
|
||||||
|
p.Delivery.Awaits, strings.Join(held, "; "))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("published; its walk waits for %s's word: %s — `plans go %s --why …` starts it by hand",
|
||||||
|
p.Delivery.Awaits, strings.Join(other, "; "), p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// letGo gives a waiting walk its word: by the delivery's owner, or a person. The next advance asks its
|
// letGo gives a waiting walk its word: by the delivery's owner, or a person. The next advance asks its
|
||||||
// first tier. Refused for a walk that does not wait.
|
// first tier. Refused for a walk that does not wait.
|
||||||
func letGo(ctx context.Context, inv *inventory.Inventory, id, by, why string) (inventory.Plan, error) {
|
func letGo(ctx context.Context, inv *inventory.Inventory, id, by, why string) (inventory.Plan, error) {
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ type stalledLine struct {
|
|||||||
// Number is the pull request's, for a line of a head the forge never announced (novox/hq issue 347).
|
// Number is the pull request's, for a line of a head the forge never announced (novox/hq issue 347).
|
||||||
Number int `json:"number,omitempty"`
|
Number int `json:"number,omitempty"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
// Walk is the walk a line is about: a waiting walk no live delivery holds, or, asked with `walks`, each walk
|
||||||
|
// waiting for the owner's word and what holds it (novox/hq issues 362, 529).
|
||||||
|
Walk string `json:"walk,omitempty"`
|
||||||
// Waiting are the merge checks a line of the state `unanswered` waits on, as mesh-delivery says each:
|
// Waiting are the merge checks a line of the state `unanswered` waits on, as mesh-delivery says each:
|
||||||
// "mesh/merge-gate pending since <UTC time>", "mesh/repo-check never set" (novox/hq issue 438).
|
// "mesh/merge-gate pending since <UTC time>", "mesh/repo-check never set" (novox/hq issue 438).
|
||||||
Waiting []string `json:"waiting,omitempty"`
|
Waiting []string `json:"waiting,omitempty"`
|
||||||
@@ -125,6 +128,34 @@ func deliveriesStalled(ctx context.Context, conn *nats.Conn, held bool) ([]stall
|
|||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// walkWaits are, by walk, the lines mesh-delivery says of a walk waiting for its word: what holds it.
|
||||||
|
type walkWaits map[string][]stalledLine
|
||||||
|
|
||||||
|
// walksWaiting asks the delivery's owner what holds each walk waiting for its word (novox/hq issue 529): its
|
||||||
|
// `stalled` asked with `walks`, so a line of `plans` names a held delivery, its reason and the verb that releases
|
||||||
|
// it, instead of pointing at a hand start. Nil when no holder is on record or none answers: the line then reads as
|
||||||
|
// before. An owner from before this answers its stalled lines, of which only those naming a walk are kept.
|
||||||
|
func walksWaiting(ctx context.Context, conn *nats.Conn, held bool) walkWaits {
|
||||||
|
if !held {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, err := askDeliveryOwner(ctx, conn, "stalled", map[string]any{"walks": true})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var lines []stalledLine
|
||||||
|
if json.Unmarshal(raw, &lines) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := walkWaits{}
|
||||||
|
for _, l := range lines {
|
||||||
|
if l.Walk != "" {
|
||||||
|
out[l.Walk] = append(out[l.Walk], l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// stalledObservations are the conditions of what is stalled.
|
// stalledObservations are the conditions of what is stalled.
|
||||||
func stalledObservations(lines []stalledLine) []conditions.Observation {
|
func stalledObservations(lines []stalledLine) []conditions.Observation {
|
||||||
out := make([]conditions.Observation, 0, len(lines))
|
out := make([]conditions.Observation, 0, len(lines))
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq issue 529: a walk waiting because its delivery is held for a person said only "its walk waits for
|
||||||
|
// mesh-delivery's word — `plans go … --why …` starts it by hand", pointing a reader at a hand start instead of at
|
||||||
|
// the person's decision. The line names what mesh-delivery says holds the walk: the held delivery, its reason and
|
||||||
|
// `mesh-delivery.release`; or the delivery not at its turn.
|
||||||
|
|
||||||
|
func waitingWalk(now time.Time) inventory.Plan {
|
||||||
|
return inventory.Plan{ID: "plan-1791793277221379309", Repository: "novox/mesh-sdk", Branch: "main",
|
||||||
|
Commit: "9747f3e1b1ac", State: inventory.PlanBuilding, Tiers: [][]string{{"mesh-sdk"}},
|
||||||
|
Created: now.Add(-9 * time.Minute), Modules: map[string]*inventory.PlanModule{},
|
||||||
|
Delivery: &inventory.PlanDelivery{Awaits: "mesh-delivery"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWalkWaitingOnAHeldDeliveryNamesTheHold(t *testing.T) {
|
||||||
|
now := time.Date(2026, 10, 12, 8, 35, 0, 0, time.UTC)
|
||||||
|
p := waitingWalk(now)
|
||||||
|
held := "novox/mesh-sdk@d41d255f28ad held since 2026-10-12T08:23:00Z: its group adr-0307-state-history's composed " +
|
||||||
|
"check did not pass for the heads that merged — a person decides: `mesh-delivery.release` (id " +
|
||||||
|
"novox/mesh-sdk@d41d255f28ad, why) lets it go, `mesh-delivery.stop` ends it"
|
||||||
|
waits := walkWaits{p.ID: {{ID: "novox/mesh-sdk@d41d255f28ad", State: "held", Walk: p.ID, Says: held}}}
|
||||||
|
line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits)
|
||||||
|
for _, want := range []string{"held for a person", "composed check did not pass", "mesh-delivery.release"} {
|
||||||
|
if !strings.Contains(line, want) {
|
||||||
|
t.Fatalf("the line does not say %q: %q", want, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(line, "starts it by hand") {
|
||||||
|
t.Fatalf("a held walk's line points at a hand start: %q", line)
|
||||||
|
}
|
||||||
|
// Told nothing — an older mesh-delivery, or none answering — it reads as before.
|
||||||
|
if line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, nil); !strings.Contains(line, "starts it by hand") {
|
||||||
|
t.Fatalf("told nothing, the line reads %q", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWalkWaitingOnADeliveryNotAtItsTurnSaysSo(t *testing.T) {
|
||||||
|
now := time.Date(2026, 10, 12, 8, 57, 0, 0, time.UTC)
|
||||||
|
p := waitingWalk(now)
|
||||||
|
waits := walkWaits{p.ID: {{ID: "novox/mesh-sdk@9747f3e1b1ac", State: "published", Walk: p.ID,
|
||||||
|
Says: "not let go yet: novox/mesh-tools@3f0c1a6cec18 (published) carried by it not at its turn"}}}
|
||||||
|
if line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits); !strings.Contains(line, "not at its turn") ||
|
||||||
|
!strings.Contains(line, "novox/mesh-tools@3f0c1a6cec18") {
|
||||||
|
t.Fatalf("the line reads %q", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What holds each waiting walk is asked of mesh-delivery's `stalled` with `walks`, a verb the controller's grant
|
||||||
|
// already names; only lines naming a walk are kept, by walk.
|
||||||
|
func TestWhatHoldsTheWaitingWalksIsAskedOfTheDeliveryOwner(t *testing.T) {
|
||||||
|
was := askDeliveryOwner
|
||||||
|
defer func() { askDeliveryOwner = was }()
|
||||||
|
var asked map[string]any
|
||||||
|
askDeliveryOwner = func(_ context.Context, _ *nats.Conn, verb string, args map[string]any) (json.RawMessage, error) {
|
||||||
|
if verb != "stalled" {
|
||||||
|
t.Fatalf("asked %s", verb)
|
||||||
|
}
|
||||||
|
asked = args
|
||||||
|
return json.Marshal([]stalledLine{{ID: "novox/a@1", State: "held", Walk: "plan-1", Says: "held: why"},
|
||||||
|
{ID: "novox/b@2", State: "held", Says: "a delivery past its bound, no walk"}})
|
||||||
|
}
|
||||||
|
got := walksWaiting(t.Context(), nil, true)
|
||||||
|
if asked["walks"] != true {
|
||||||
|
t.Fatalf("stalled was asked with %v", asked)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || len(got["plan-1"]) != 1 || got["plan-1"][0].Says != "held: why" {
|
||||||
|
t.Fatalf("the waiting walks are %+v", got)
|
||||||
|
}
|
||||||
|
if got := walksWaiting(t.Context(), nil, false); got != nil {
|
||||||
|
t.Fatalf("with no holder on record it asked: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
)
|
)
|
||||||
@@ -1258,6 +1260,13 @@ func planLineWith(p inventory.Plan, now time.Time, pause pauseView, bound time.D
|
|||||||
// planLineOn is planLineWith naming the plan by what it moves and where (planHeadline), given what runs each
|
// planLineOn is planLineWith naming the plan by what it moves and where (planHeadline), given what runs each
|
||||||
// module; nil names no machine.
|
// module; nil names no machine.
|
||||||
func planLineOn(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration, running func(string) []string) string {
|
func planLineOn(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration, running func(string) []string) string {
|
||||||
|
return planLineSaid(p, now, pause, bound, running, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// planLineSaid is planLineOn saying, of a walk waiting for its delivery's word, what the delivery's owner says
|
||||||
|
// holds it (novox/hq issue 529); nil waits says it as before.
|
||||||
|
func planLineSaid(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration, running func(string) []string,
|
||||||
|
waits walkWaits) string {
|
||||||
name := planHeadline(p, running)
|
name := planHeadline(p, running)
|
||||||
where := fmt.Sprintf("tier %d of %d", min(p.Tier+1, len(p.Tiers)), len(p.Tiers))
|
where := fmt.Sprintf("tier %d of %d", min(p.Tier+1, len(p.Tiers)), len(p.Tiers))
|
||||||
switch p.State {
|
switch p.State {
|
||||||
@@ -1274,7 +1283,7 @@ func planLineOn(p inventory.Plan, now time.Time, pause pauseView, bound time.Dur
|
|||||||
since := now.Sub(inTierSince(p)).Round(time.Second)
|
since := now.Sub(inTierSince(p)).Round(time.Second)
|
||||||
if p.Waiting() {
|
if p.Waiting() {
|
||||||
// Waiting for its delivery's word is no lateness of the walk's (novox/hq ADR 0239).
|
// Waiting for its delivery's word is no lateness of the walk's (novox/hq ADR 0239).
|
||||||
return fmt.Sprintf("%s · %s, %s, for %s", name, where, waitingNote(p), since)
|
return fmt.Sprintf("%s · %s, %s, for %s", name, where, waitingSaid(p, waits[p.ID]), since)
|
||||||
}
|
}
|
||||||
if deferred(p) {
|
if deferred(p) {
|
||||||
// Nor is waiting for the walk before it to end (one walk at a time, novox/hq ADR 0276).
|
// Nor is waiting for the walk before it to end (one walk at a time, novox/hq ADR 0276).
|
||||||
@@ -1479,11 +1488,11 @@ func plansCommand(ctx context.Context, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bounds := readTierBounds(ctx, inv, now)
|
bounds := readTierBounds(ctx, inv, now)
|
||||||
fmt.Printf("%s — %s\n", p.ID, planLineOn(p, now, buildSeatPause(ctx, inv, []inventory.Plan{p}),
|
fmt.Printf("%s — %s\n", p.ID, planLineSaid(p, now, buildSeatPause(ctx, inv, []inventory.Plan{p}),
|
||||||
bounds.of(p.Repository), func(module string) []string {
|
bounds.of(p.Repository), func(module string) []string {
|
||||||
on, _ := inv.Running(ctx, module)
|
on, _ := inv.Running(ctx, module)
|
||||||
return on
|
return on
|
||||||
}))
|
}, waitsOf(ctx, inv, []inventory.Plan{p})))
|
||||||
if r := p.Release; r != nil {
|
if r := p.Release; r != nil {
|
||||||
// A release plan's walk (ADR 0236): machines done, the one judged, those to come.
|
// A release plan's walk (ADR 0236): machines done, the one judged, those to come.
|
||||||
fmt.Printf(" machines in order: %s; done: %s; skipped: %s\n", strings.Join(r.Order, ", "),
|
fmt.Printf(" machines in order: %s; done: %s; skipped: %s\n", strings.Join(r.Order, ", "),
|
||||||
@@ -1630,15 +1639,41 @@ func plansCommand(ctx context.Context, args []string) error {
|
|||||||
on, _ := inv.Running(ctx, module)
|
on, _ := inv.Running(ctx, module)
|
||||||
return on
|
return on
|
||||||
}
|
}
|
||||||
|
waits := waitsOf(ctx, inv, plans)
|
||||||
for _, p := range plans {
|
for _, p := range plans {
|
||||||
if p.Batch() {
|
if p.Batch() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("%-28s %s\n", p.ID, planLineOn(p, now, pause, bounds.of(p.Repository), running))
|
fmt.Printf("%-28s %s\n", p.ID, planLineSaid(p, now, pause, bounds.of(p.Repository), running, waits))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitsOf asks the delivery's owner what holds the walks among these that wait for its word (novox/hq issue 529),
|
||||||
|
// once, and only when one waits; nil when none waits, no holder is on record, or the bus or the owner does not
|
||||||
|
// answer within a few seconds — the lines then read as before.
|
||||||
|
func waitsOf(ctx context.Context, inv *inventory.Inventory, plans []inventory.Plan) walkWaits {
|
||||||
|
waiting := false
|
||||||
|
for _, p := range plans {
|
||||||
|
waiting = waiting || p.Open() && p.Waiting()
|
||||||
|
}
|
||||||
|
if !waiting {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, err := inv.Catalogued(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var waits walkWaits
|
||||||
|
_ = onTheBus(func(conn *nats.Conn) error {
|
||||||
|
waits = walksWaiting(ctx, conn, deliverySeatHeld(entries))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return waits
|
||||||
|
}
|
||||||
|
|
||||||
// planWhatIf is the plan a merge would produce, computed the way the merge handler computes one
|
// planWhatIf is the plan a merge would produce, computed the way the merge handler computes one
|
||||||
// and saved nowhere: the modules the repository's changed files touch (or the modules named), what
|
// and saved nowhere: the modules the repository's changed files touch (or the modules named), what
|
||||||
// packages their source, everything reachable from them, in tiers. For reading before merging.
|
// packages their source, everything reachable from them, in tiers. For reading before merging.
|
||||||
|
|||||||
Reference in New Issue
Block a user