Merge pull request 'plans names the hold a waiting walk waits on (issue 529)' (#245) from fix/529-a-waiting-walk-is-answered-or-named into main
This commit was merged in pull request #245.
This commit is contained in:
@@ -82,6 +82,38 @@ func waitingNote(p inventory.Plan) string {
|
||||
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, unheld, other []string
|
||||
for _, l := range lines {
|
||||
switch l.State {
|
||||
case "held":
|
||||
held = append(held, l.Says)
|
||||
case "unheld":
|
||||
unheld = append(unheld, l.Says)
|
||||
default:
|
||||
other = append(other, l.Says)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case 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, "; "))
|
||||
case len(other) > 0:
|
||||
// Not at its turn: it goes when its turn comes. A hand start here would skip its group's order (review of
|
||||
// novox/hq issue 529), so none is offered.
|
||||
return fmt.Sprintf("published; its walk waits for %s's word: %s", p.Delivery.Awaits, strings.Join(other, "; "))
|
||||
}
|
||||
// Held by nothing: nothing will give it its word, so the hand start is the way on.
|
||||
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(unheld, "; "), p.ID)
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 int `json:"number,omitempty"`
|
||||
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:
|
||||
// "mesh/merge-gate pending since <UTC time>", "mesh/repo-check never set" (novox/hq issue 438).
|
||||
Waiting []string `json:"waiting,omitempty"`
|
||||
@@ -125,6 +128,34 @@ func deliveriesStalled(ctx context.Context, conn *nats.Conn, held bool) ([]stall
|
||||
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.
|
||||
func stalledObservations(lines []stalledLine) []conditions.Observation {
|
||||
out := make([]conditions.Observation, 0, len(lines))
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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"}}}
|
||||
line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits)
|
||||
if !strings.Contains(line, "not at its turn") || !strings.Contains(line, "novox/mesh-tools@3f0c1a6cec18") {
|
||||
t.Fatalf("the line reads %q", line)
|
||||
}
|
||||
// No hand start offered: it would skip the group's order (review of issue 529).
|
||||
if strings.Contains(line, "plans go") {
|
||||
t.Fatalf("a walk not at its turn points at a hand start: %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
// A walk nothing holds — the nzbgeek case, its merges' deliveries ended as nothing-to-move — is said so, and the hand
|
||||
// start is the way on.
|
||||
func TestAWalkHeldByNothingSaysWhyAndTheHandStart(t *testing.T) {
|
||||
now := time.Date(2026, 10, 11, 20, 30, 0, 0, time.UTC)
|
||||
p := waitingWalk(now)
|
||||
waits := walkWaits{p.ID: {{ID: "novox/mesh-media-catalog@cdcdcdcd0000", State: "unheld", Walk: p.ID,
|
||||
Says: "held by nothing: its merges' deliveries ended as nothing-to-move (novox/mesh-media-catalog@abababab0000), " +
|
||||
"yet the walk moves nzbgeek, nzbget — nothing gives it its word"}}}
|
||||
line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits)
|
||||
if !strings.Contains(line, "ended as nothing-to-move") || !strings.Contains(line, "plans go "+p.ID) {
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"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
|
||||
// module; nil names no machine.
|
||||
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)
|
||||
where := fmt.Sprintf("tier %d of %d", min(p.Tier+1, len(p.Tiers)), len(p.Tiers))
|
||||
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)
|
||||
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, 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) {
|
||||
// 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
|
||||
}
|
||||
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 {
|
||||
on, _ := inv.Running(ctx, module)
|
||||
return on
|
||||
}))
|
||||
}, waitsOf(ctx, inv, []inventory.Plan{p})))
|
||||
if r := p.Release; r != nil {
|
||||
// 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, ", "),
|
||||
@@ -1630,15 +1639,41 @@ func plansCommand(ctx context.Context, args []string) error {
|
||||
on, _ := inv.Running(ctx, module)
|
||||
return on
|
||||
}
|
||||
waits := waitsOf(ctx, inv, plans)
|
||||
for _, p := range plans {
|
||||
if p.Batch() {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user