Files
mesh-controller/internal/inventory/busy_test.go
T
jschoubben b1546ed28a
mesh/delivery-group group feat/335-a-busy-container-is-replaced-when-idle rejected: its order contradicts itself: novox/mesh-catalog@190608193764, novox/mesh-c…
mesh/merge-gate out of date: judged against trunk 56d6313262e8, the trunk is now cbac703e817d; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 56d6313262e8, the trunk is now cbac703e817d; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
Hold a walk's first-machine gate while its busy container finishes its build
A merge into mesh-controller replaced every build agent and threw away the
checks in flight (issue 594). The manifest's container busy is checked and
sent only to an engine that reads it; a report's busy waits are kept, the
gate starts at the report that says the container replaced, and the wait is
a phase of the delivery time. mesh-builder busy drains the agent and answers
while a build is in hand (novox/hq ADR 0335).
2026-10-13 00:53:10 +02:00

143 lines
6.0 KiB
Go

package inventory
import (
"strings"
"testing"
"time"
)
// A walk whose first machine waited for a busy container counts the wait as a phase of its own, between the
// send to the first machine and the judging, and its delivery time holds it (novox/hq ADR 0335 §4): the
// recorded walk again, its judging started only at 17:54:07, a minute after the send, when the machine said
// the container replaced.
func TestABusyWaitIsAPhaseOfTheDeliveryTime(t *testing.T) {
raw := strings.Replace(recordedWalk, `"gate": {"machines": ["novox"], "since": "2026-10-10T17:53:07.287136827Z",`,
`"gate": {"machines": ["novox"], "since": "2026-10-10T17:54:07.287136827Z",
"busy": {"machine": "novox", "module": "mesh-delivery", "container": "mesh-delivery",
"since": "2026-10-10T17:53:07Z", "until": "2026-10-10T18:08:07Z", "ended": "2026-10-10T17:54:07.287136827Z"},`, 1)
w := walkOf(t, raw).WalkPhases(time.Date(2026, 10, 10, 18, 0, 0, 0, time.UTC), nil)
sumsUp(t, w)
if w.TotalMS != 282894 {
t.Fatalf("the wait changed the delivery's total: %dms", w.TotalMS)
}
busy := phase(w, PhaseBusy, 0)
if busy.State != PhaseMeasured || busy.TookMS != 60000 {
t.Fatalf("the busy wait is not a measured phase of a minute: %+v", busy)
}
if judged := phase(w, PhaseJudge, 0); judged.TookMS != 148607-60000 {
t.Fatalf("the judging still counts from the send, not from the replacement: %+v", judged)
}
var names []string
for _, ph := range w.Phases {
names = append(names, ph.Name)
}
if !strings.Contains(strings.Join(names, " "), PhaseSend+" "+PhaseBusy+" "+PhaseJudge) {
t.Fatalf("the busy wait is not between the send and the judging: %v", names)
}
}
// While the wait goes on, the walk says it waits for a busy container there; a walk where nothing waited has no
// such phase.
func TestAnOpenBusyWaitIsSaid(t *testing.T) {
raw := strings.Replace(recordedWalk, `"gate": {"machines": ["novox"], "since": "2026-10-10T17:53:07.287136827Z", "passes": 3,
"verdict": "passed", "judged_at": "2026-10-10T17:55:35.894985053Z"}`,
`"gate": {"machines": ["novox"], "since": "2026-10-10T17:53:07.287136827Z",
"busy": {"machine": "novox", "module": "mesh-delivery", "container": "mesh-delivery",
"since": "2026-10-10T17:53:07Z", "until": "2026-10-10T18:08:07Z"}}`, 1)
raw = strings.Replace(raw, `"state": "done"`, `"state": "rolling"`, 1)
p := walkOf(t, raw)
if !p.Modules["mesh-delivery"].Gate.Waiting() {
t.Fatal("a gate whose busy wait has not ended does not say it waits")
}
w := p.WalkPhases(time.Date(2026, 10, 10, 17, 58, 0, 0, time.UTC), nil)
busy := phase(w, PhaseBusy, 0)
if busy.State != PhaseUnknown || !strings.Contains(busy.Said, "waits for a busy container on novox") {
t.Fatalf("an open busy wait is not said: %+v", busy)
}
if plain := walkOf(t, recordedWalk).WalkPhases(time.Date(2026, 10, 10, 18, 0, 0, 0, time.UTC), nil); phase(plain, PhaseBusy, 0).Name != "" {
t.Fatalf("a walk where nothing waited has a busy phase: %+v", plain.Phases)
}
}
// A container a report lists waiting is its module's by its id; its output alone is not a new account.
func TestABusyContainerIsItsModules(t *testing.T) {
list := []BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: BusyWaiting, Said: "a"},
{ID: "web.server", State: "replaced-when-idle"}}
if b, ok := WaitingFor(list, "build-agent"); !ok || b.Container != "mesh-build-agent" {
t.Fatalf("build-agent's container is not found waiting: %+v", b)
}
if _, ok := WaitingFor(list, "web"); ok {
t.Error("a container replaced reads as waiting")
}
if _, ok := WaitingFor(list, "build"); ok {
t.Error("another module's container is found by a prefix of its name")
}
again := []BusyWait{{ID: "build-agent.server", State: BusyWaiting, Said: "b, a minute on"}, list[1]}
if !sameBusy(list, again) {
t.Error("a wait whose output changed is a new account")
}
if sameBusy(list, list[1:]) || !(Doing{Busy: list}).Waiting() || (Doing{Busy: list[1:]}).Waiting() {
t.Error("a wait that ended is not a new account")
}
}
// A machine's report of a busy container is kept with its last account and read back beside it, and its
// starting and its end are news; its output alone is not (novox/hq ADR 0335). Needs the store.
func TestAReportOfABusyContainerIsKeptAndReadBack(t *testing.T) {
inv, node := aNodeWithModules(t)
ctx := t.Context()
record, err := inv.NodeByName(ctx, node)
if err != nil {
t.Fatal(err)
}
since := time.Date(2026, 10, 13, 1, 0, 0, 0, time.UTC)
waiting := []BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: BusyWaiting,
Since: since, Until: since.Add(15 * time.Minute), Said: "busy: a check"}}
keep := func(busy []BusyWait) bool {
t.Helper()
news, err := inv.RecordOrderedDoing(ctx, record.ID, Doing{Outcome: OutcomeApplied, Declared: "d1", Applied: 1,
Busy: busy}, ReportOrder{Epoch: 1, Sequence: 1}, func(ReportOrder) bool { return false })
if err != nil {
t.Fatal(err)
}
return news
}
read := func() []BusyWait {
t.Helper()
reports, err := inv.LastReports(ctx)
if err != nil {
t.Fatal(err)
}
for _, r := range reports {
if r.Node == node {
return r.Busy
}
}
t.Fatal("the machine's report is not read back")
return nil
}
keep(nil)
if got := read(); len(got) != 0 {
t.Fatalf("a report with nothing busy reads back %+v", got)
}
if !keep(waiting) {
t.Error("a container starting to wait is not news")
}
got := read()
if b, ok := WaitingFor(got, "build-agent"); !ok || !b.Since.Equal(since) || b.Said != "busy: a check" {
t.Fatalf("the wait is not read back: %+v", got)
}
again := []BusyWait{waiting[0]}
again[0].Said = "busy: a check, a minute on"
if keep(again) {
t.Error("a wait whose output changed is news")
}
if !keep([]BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: "replaced-when-idle",
Since: since, Until: since.Add(15 * time.Minute)}}) {
t.Error("a container replaced after it waited is not news")
}
if _, ok := WaitingFor(read(), "build-agent"); ok {
t.Error("a container replaced still reads as waiting")
}
}