From 6d60953f981362cf12e97f3508039fb089034157 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 19:58:39 +0100 Subject: [PATCH 1/5] A running try names its asks and where each stands; build_usage says a seat's CPU-bound capacity The checker wrote a try's asks only at its end, so a 31-minute try could not be told queued from running (issue 1890). The try's record now names each ask as it is put, then the node that took it, its state and the phase its step says, written on the step key's own changes. build_usage names a running try's asks and takes a build module's capacity. (configured bounded by its node's build CPU, issue 1806's follow-up) when it is current with mesh-config. --- .../cmd/mesh-delivery-checker-tools/main.go | 3 +- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 +- .../internal/check/case.go | 5 ++ .../internal/check/judge.go | 8 +- .../internal/check/try.go | 87 ++++++++++++++++++- .../internal/check/try_test.go | 25 +++++- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 +- .../mesh-delivery-view/internal/view/usage.go | 48 +++++++--- .../internal/view/usage_test.go | 21 +++++ 11 files changed, 187 insertions(+), 22 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 8e5ec44..4422450 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -135,7 +135,8 @@ func TryTools(tr Trying) []stdio.Tool { }}, {Name: check.TryReadTool, Description: "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, " + - "the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", + "the head and the trunk commit it was merged onto, the time it took and what failed first; while it runs, each ask it put as soon as it is put, " + + "with the node that took it, its state and the phase its step says (fetch and merge, a level's digest, lint, tests) and since when. Read-only.", Input: map[string]any{"type": "object", "required": []string{"id"}, "properties": map[string]any{ "id": map[string]any{"type": "string", "description": "the try's id, try-"}}}, Run: func(args map[string]any) (any, error) { diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 4e2c71e..cc0ac5a 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142434-65ee43f07583 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index e793069..9e114f3 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142434-65ee43f07583 h1:ikD/ggf9/5k23sxJOX1PFcGzb4M3+QeC66AT0MyQ9Zs= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142434-65ee43f07583/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 h1:mSBxF9KgIJyNL0gOKYq5naWewUj2yazU/tGOh12Tze0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/internal/check/case.go b/modules/mesh-delivery-checker/internal/check/case.go index 169d55e..8670f7c 100644 --- a/modules/mesh-delivery-checker/internal/check/case.go +++ b/modules/mesh-delivery-checker/internal/check/case.go @@ -46,6 +46,11 @@ type Case struct { notes []string // asks are the ids of the asks the check put on the build seats' streams, in order: a try names them (Try.Asks). asks []string + // Watch, when set, hears each ask the check puts, as it is put (step nil), and each value its step key takes while + // the check waits for it: the node that took it, the phase it says, its end. A try writes them into its record, so a + // running try names its asks and where each stands (issue 1890). Called from the check's goroutines; it must not block + // for long. + Watch func(askID string, step *delivery.Step) // Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at. Progress *Progress // Kind is what asked the check (KindNew, KindNodes, KindTrunk): a carry after a trunk or sibling move is asked on the diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index f493289..ec2f1c5 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -427,8 +427,14 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l if err := st.j.Asks.Put(ctx, a); err != nil { return fail(t.module, fmt.Sprintf("the check of %s#%d could not be asked: %v", p.Repository, p.Number, err)) } + waiting := ctx + if watch := st.c.Watch; watch != nil { + id := a.ID + watch(id, nil) // put: a try names it at once, not only when it ends (issue 1890) + waiting = buildrun.WithStepSeen(ctx, func(s delivery.Step) { watch(id, &s) }) + } var err error - step, err = st.j.Asks.Wait(ctx, a) + step, err = st.j.Asks.Wait(waiting, a) if err != nil { if ctx.Err() != nil { return fail(t.module, ctx.Err().Error()) diff --git a/modules/mesh-delivery-checker/internal/check/try.go b/modules/mesh-delivery-checker/internal/check/try.go index af0b409..a44b3b4 100644 --- a/modules/mesh-delivery-checker/internal/check/try.go +++ b/modules/mesh-delivery-checker/internal/check/try.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "regexp" + "slices" "sort" "strconv" "strings" @@ -79,12 +80,92 @@ type Try struct { Machines []string `json:"machines,omitempty"` // Asks are the ids of the asks the try put on the build seats' streams, each ending on the step key . of its // build module: the delivery view counts those steps as a try's, not a candidate's check (build_usage). - Asks []string `json:"asks,omitempty"` + Asks []string `json:"asks,omitempty"` + // Steps say where each ask stands while the try runs and after: asked until a node takes it, then the node, its + // state, the phase its step says and when (issue 1890: a 31-minute try could not be told queued from running). + Steps []TryStep `json:"steps,omitempty"` Asked time.Time `json:"asked"` Started time.Time `json:"started,omitzero"` Ended time.Time `json:"ended,omitzero"` } +// TryAsked is a TryStep's state before a build node took its ask: it waits in the seat's stream. +const TryAsked = "asked" + +// TryStep is one ask of a try as its step key last said it. +type TryStep struct { + Ask string `json:"ask"` + Node string `json:"node,omitempty"` + State string `json:"state"` + Phase string `json:"phase,omitempty"` + Started time.Time `json:"started,omitzero"` + PhaseAt time.Time `json:"phase-at,omitzero"` + Ended time.Time `json:"ended,omitzero"` +} + +// tryWriter writes a running try's record again as its asks are put and their steps move: one writer per try, holding +// the revision it wrote last, so the try's end is written at that revision. A write refused (the try ended by hand) +// stops the writes; the end's own write then says it. +type tryWriter struct { + mu sync.Mutex + tr *Tryer + ctx context.Context + key string + t Try + rev uint64 + stopped bool +} + +// heard takes an ask put (step nil) or a value of its step key in, and writes the record when it changed. +func (w *tryWriter) heard(askID string, step *delivery.Step) { + w.mu.Lock() + defer w.mu.Unlock() + if w.stopped { + return + } + changed := false + if !slices.Contains(w.t.Asks, askID) { + w.t.Asks, changed = append(w.t.Asks, askID), true + } + i := slices.IndexFunc(w.t.Steps, func(s TryStep) bool { return s.Ask == askID }) + if i < 0 { + w.t.Steps, changed = append(w.t.Steps, TryStep{Ask: askID, State: TryAsked}), true + i = len(w.t.Steps) - 1 + } + if step != nil { + now := TryStep{Ask: askID, Node: step.Node, State: step.State, Phase: step.Phase, Started: step.Started, + PhaseAt: step.PhaseAt, Ended: step.Ended} + changed = changed || now != w.t.Steps[i] + w.t.Steps[i] = now + } + if changed { + w.write() + } +} + +// write writes the record at the revision written last; a refusal stops the writes. +func (w *tryWriter) write() { + data, err := encodeTry(w.t) + if err != nil { + return + } + rev, err := w.tr.Tries.Update(w.ctx, w.key, data, w.rev) + if err != nil { + w.stopped = true + w.tr.logf("%s: its asks' progress is no longer written: %v", w.key, err) + return + } + w.rev = rev +} + +// end stops the writes and answers the record and its revision, for the try's end. +func (w *tryWriter) end() (Try, uint64) { + w.mu.Lock() + defer w.mu.Unlock() + w.stopped = true + return w.t, w.rev +} + // Over says whether the try came to its end. func (t Try) Over() bool { return t.State == TryPass || t.State == TryFail || t.State == TryError } @@ -395,7 +476,8 @@ func (tr *Tryer) run(ctx context.Context, key string) { } return } - cs := &Case{Key: key, Group: key, + progress := &tryWriter{tr: tr, ctx: ctx, key: key, t: t, rev: rev} + cs := &Case{Key: key, Group: key, Watch: progress.heard, Pulls: []Pull{{Repository: t.Repository, Head: t.Head, Trunk: t.Trunk}}, Trunk: map[string]string{t.Repository: t.Trunk}, Queued: t.Asked, @@ -409,6 +491,7 @@ func (tr *Tryer) run(ctx context.Context, key string) { if ctx.Err() != nil { return // left running: the checker takes it over when it starts; or ended by hand, which wrote its end } + t, rev = progress.end() v := out.Verdict t.Ended = tr.now().UTC() t.Took = t.Ended.Sub(t.Started).Round(time.Second).String() diff --git a/modules/mesh-delivery-checker/internal/check/try_test.go b/modules/mesh-delivery-checker/internal/check/try_test.go index 27eb986..cee1241 100644 --- a/modules/mesh-delivery-checker/internal/check/try_test.go +++ b/modules/mesh-delivery-checker/internal/check/try_test.go @@ -23,6 +23,19 @@ func (m memTries) Create(key string, value []byte) error { return err } +// writtenTries is a memKV that keeps every running try it was given to write, in order. +type writtenTries struct { + *memKV + running []Try +} + +func (w *writtenTries) Update(ctx context.Context, key string, value []byte, rev uint64) (uint64, error) { + if t, err := DecodeTry(value); err == nil && !t.Over() { + w.running = append(w.running, t) + } + return w.memKV.Update(ctx, key, value, rev) +} + var tryTrunks = map[string]string{repoA: trunkA, repoSDK: trunkS, hqRepo: trunkA} func readTry(t testing.TB, kv *memKV, id string) Try { @@ -89,7 +102,8 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { / t.Fatal(err) } b := newBench(t) - tr := &Tryer{Tries: kv, Judge: b.ex} + written := &writtenTries{memKV: kv} + tr := &Tryer{Tries: written, Judge: b.ex} raw, _, _, _ := kv.Get(ctx, id) tr.Put(ctx, id, raw) tr.Wait() @@ -97,6 +111,15 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { / if got.State != TryPass || got.Took == "" || got.Head != head1 || got.Trunk != trunkA || got.Said == "" || got.Levels[delivery.LevelUnit].Run != 2 { t.Fatalf("try = %+v", got) } + // Issue 1890: while it runs, the try names its ask as soon as it is put, and then where its step stands; the end + // keeps each ask's node and state. + if len(written.running) < 3 || len(written.running[1].Asks) != 1 || written.running[1].Steps[0].State != TryAsked || + written.running[2].Steps[0].Node != "shanks" { + t.Errorf("the running try's writes = %+v, want its ask named while it ran, then its node", written.running) + } + if len(got.Steps) != 1 || got.Steps[0].Node != "shanks" || got.Steps[0].State != delivery.StepPassed { + t.Errorf("the ended try's steps = %+v", got.Steps) + } // The try names the asks it put, so the delivery view counts their steps as a try's (build_usage). if len(got.Asks) != 1 { t.Errorf("asks = %v, want the one check ask the try put", got.Asks) diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index ae85fd6..c44cc9b 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026125731-2aae6f5580f2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index f754077..9e114f3 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026125731-2aae6f5580f2 h1:s1WcG7Ll1OkEwwvMKEabdWvPvTiFCHghlXiY1LoIJDA= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026125731-2aae6f5580f2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 h1:mSBxF9KgIJyNL0gOKYq5naWewUj2yazU/tGOh12Tze0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/internal/view/usage.go b/modules/mesh-delivery-view/internal/view/usage.go index 19d43ea..7849d80 100644 --- a/modules/mesh-delivery-view/internal/view/usage.go +++ b/modules/mesh-delivery-view/internal/view/usage.go @@ -67,7 +67,8 @@ type UsageAnswer struct { At time.Time `json:"at"` Nodes []NodeUsage `json:"nodes"` Backlog []BacklogRow `json:"backlog"` - // TriesRunning are the tries the checker runs now; their asks are counted under check until the try ends and names them. + // TriesRunning are the tries the checker runs now, each with its asks and where each stands (a checker that names its + // asks as they are put, issue 1890; an older one names them only at the end, and they are counted under check until then). TriesRunning []TryRunning `json:"tries_running"` Notes []string `json:"notes,omitempty"` } @@ -77,6 +78,9 @@ type TryRunning struct { ID string `json:"id"` Repository string `json:"repository"` Started time.Time `json:"started,omitzero"` + Asks []string `json:"asks,omitempty"` + // Steps are the try's own words for each ask: asked, or the node that took it, its state and the phase its step says. + Steps json.RawMessage `json:"steps,omitempty"` } // NodeUsage is one node: each build seat it holds or is set to build for, whether it is eligible to build, and its CPU. @@ -101,7 +105,11 @@ type SeatUsage struct { // Capacity is the asks it runs at once; CapacityFrom where that came from. Capacity int `json:"capacity"` CapacityFrom string `json:"capacity_from"` - Paused bool `json:"paused"` + // Configured is mesh-config's capacity when the module says it judges by a lower one (capacity. of its pauses + // state: the configured capacity bounded by its node's build CPU, issue 1806's follow-up); Capacity is then the + // module's and CapacityFrom its why. + Configured int `json:"configured,omitempty"` + Paused bool `json:"paused"` // Eligible says it takes asks: it holds the seat, has a capacity above 0 and is not paused; WhyNot says why not. // EligibleKnown is false when this tool cannot tell (it cannot read mesh-config's capacities): Eligible is then // absent, never false, and WhyNot starts "not known: ". @@ -384,8 +392,9 @@ func (u UsageReader) Usage() UsageAnswer { } } - // Pauses: paused. of each module. + // Pauses: paused. of each module; and capacity., the capacity the module says it judges by. paused := map[string]bool{} // . + judged := map[string]delivery.Capacity{} for _, lang := range delivery.AskLanguages { module, _ := delivery.BuildModuleOf(lang) pr, ok := u.Pauses[lang] @@ -411,6 +420,13 @@ func (u UsageReader) Usage() UsageAnswer { paused[node+"."+lang] = p.Paused } } + if key, err := delivery.CapacityKey(node); err == nil { + if raw, err := pr.Get(key); err == nil && len(raw) > 0 { + if c, err := delivery.DecodeCapacity(raw); err == nil { + judged[node+"."+lang] = c + } + } + } } } @@ -452,6 +468,11 @@ func (u UsageReader) Usage() UsageAnswer { s.Capacity = (&buildrun.Places{Capacity: caps}).CapacityOf(node) s.CapacityFrom = fmt.Sprintf("the modules' default %q: mesh-config holds no resolved value for %s on %s", buildstep.DefaultCapacities, s.Module, node) } + if c, ok := judged[node+"."+lang]; ok && s.Holds && unknown == "" && !isBad && c.Configured == s.Capacity && c.Effective != c.Configured { + // The module running on the node judges by less than mesh-config gives: its node's build CPU bounds it. + s.Configured, s.Capacity = s.Capacity, c.Effective + s.CapacityFrom = fmt.Sprintf("%s, as %s on %s judges it (said %s)", c.Why, s.Module, node, c.At.UTC().Format(time.RFC3339)) + } s.Paused = paused[node+"."+lang] s.EligibleKnown = true switch { @@ -517,6 +538,7 @@ func (u UsageReader) tries(now time.Time, ans *UsageAnswer, note func(string, .. want = append(want, k) } } + unnamed := 0 raws, err := u.Tries.values(want) if err != nil { note("the checker's tries cannot all be read: %v", err) @@ -526,11 +548,12 @@ func (u UsageReader) tries(now time.Time, ans *UsageAnswer, note func(string, .. continue } var t struct { - ID string `json:"id"` - Repository string `json:"repository"` - State string `json:"state"` - Started time.Time `json:"started"` - Asks []string `json:"asks"` + ID string `json:"id"` + Repository string `json:"repository"` + State string `json:"state"` + Started time.Time `json:"started"` + Asks []string `json:"asks"` + Steps json.RawMessage `json:"steps"` } if json.Unmarshal(raws[i], &t) != nil { continue @@ -539,11 +562,14 @@ func (u UsageReader) tries(now time.Time, ans *UsageAnswer, note func(string, .. asks[a] = true } if t.State == "running" { - ans.TriesRunning = append(ans.TriesRunning, TryRunning{ID: t.ID, Repository: t.Repository, Started: t.Started}) + ans.TriesRunning = append(ans.TriesRunning, TryRunning{ID: t.ID, Repository: t.Repository, Started: t.Started, Asks: t.Asks, Steps: t.Steps}) + if len(t.Asks) == 0 { + unnamed++ + } } } - if len(ans.TriesRunning) > 0 { - note("a running try's asks are counted under check until the try ends and names them") + if unnamed > 0 { + note("%d running tries name no ask yet (none put yet, or a checker that names them only at the end): their asks are counted under check until then", unnamed) } return asks } diff --git a/modules/mesh-delivery-view/internal/view/usage_test.go b/modules/mesh-delivery-view/internal/view/usage_test.go index 580018a..78cab03 100644 --- a/modules/mesh-delivery-view/internal/view/usage_test.go +++ b/modules/mesh-delivery-view/internal/view/usage_test.go @@ -191,4 +191,25 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { if verdict["shanks"] != "not known" || verdict["ace"] != "not eligible" { t.Errorf("verdicts with mesh-config unreadable = %v, want shanks not known, ace not eligible (paused)", verdict) } + + // Issue 1806's follow-up: the module on shanks says it judges by 1 of mesh-config's 2 (its build CPU bounds it); the + // seat says the module's capacity and why. And issue 1890: a running try that names its asks is said with them. + u = usageFixture() + u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":1,"configured":2,"quota-from":"the cpu.max of /meshyield.slice","quota-cores":6,"cores-per-step":6,"by-cpu":1,"why":"1, bounded by the CPU","at":"2026-10-26T11:00:00Z"}`, + "capacity.ace": `{"v":1,"effective":2,"configured":3,"why":"stale: mesh-config says 2 now"}`}) + u.Tries = states(map[string]string{"try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","asks":["` + tryAsk0 + `"],"steps":[{"ask":"` + tryAsk0 + `","node":"shanks","state":"running","phase":"unit: tests of 3 packages"}]}`}) + bound := u.Usage() + sc := seatOf(t, bound, "shanks", "node-build-go") + if sc.Capacity != 1 || sc.Configured != 2 || !strings.HasPrefix(sc.CapacityFrom, "1, bounded by the CPU, as mesh-build-go on shanks judges it") { + t.Errorf("a seat its CPU bounds: capacity %d of %d, from %q", sc.Capacity, sc.Configured, sc.CapacityFrom) + } + if sa := seatOf(t, bound, "ace", "node-build-go"); sa.Capacity != 2 || sa.Configured != 0 { + t.Errorf("a module's word older than mesh-config's value is not taken: %+v", sa) + } + if len(bound.TriesRunning) != 1 || len(bound.TriesRunning[0].Asks) != 1 || !strings.Contains(string(bound.TriesRunning[0].Steps), "unit: tests") || + strings.Contains(strings.Join(bound.Notes, " | "), "name no ask yet") { + t.Errorf("a running try naming its asks: %+v, notes %v", bound.TriesRunning, bound.Notes) + } } + +var tryAsk0 = stepID(at("11:58"), "a00010") -- 2.54.0 From c0a4b139a2733b3a7a28cf09e8a9f347db6281dc Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 20:05:51 +0100 Subject: [PATCH 2/5] A try queued behind the build seat's checks no longer blocks the next try Only tries a build node holds count toward the 4 at once; tries waiting for a node are bounded apart at 8, and the refusal names both counts (issue 1890: at 19:53 four tries queued behind 44 checks for up to 47 minutes refused every further try). --- .../cmd/mesh-delivery-checker-tools/main.go | 2 +- .../internal/check/try.go | 42 ++++++++++++++++--- .../internal/check/try_test.go | 39 +++++++++++++---- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 4422450..0bc71c9 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -105,7 +105,7 @@ func TryTools(tr Trying) []stdio.Tool { {Name: check.TryTool, Description: "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, " + "its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; " + check.TryReadTool + " reads " + - "where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide. A repository " + + "where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 run on a build node and 8 wait for one at once, mesh-wide (a try queued behind the seat's checks holds no node). A repository " + "the checker checks no level of (one whose trunk the gate does not name) is refused. A try whose ask no build node took within the step " + "bound ends as error; its ask stays in the build seat's stream and may still run later, judged by nobody (issue 734).", Input: map[string]any{"type": "object", "required": []string{"repository", "head"}, "properties": map[string]any{ diff --git a/modules/mesh-delivery-checker/internal/check/try.go b/modules/mesh-delivery-checker/internal/check/try.go index a44b3b4..c008011 100644 --- a/modules/mesh-delivery-checker/internal/check/try.go +++ b/modules/mesh-delivery-checker/internal/check/try.go @@ -30,8 +30,13 @@ const ( // TriesState is the checker's state of tries: the bucket mesh-delivery-checker_tries, keys try-. TriesState = "tries" TriesBucket = delivery.CheckerModule + "_" + TriesState - // MaxTries is how many tries may wait or run at once, mesh-wide: each holds a build node for minutes. + // MaxTries is how many tries may hold a build node at once, mesh-wide: each holds one for minutes. A try whose asks no + // node has taken yet waits behind the seat's other asks and holds none, so it counts toward MaxTriesWaiting instead + // (issue 1890: at 19:53 on 2026-10-26 four tries sat queued behind 44 checks for up to 47 minutes and no fifth could be + // asked). MaxTries = 4 + // MaxTriesWaiting is how many tries may wait at once, queued or with their asks not yet taken by a node. + MaxTriesWaiting = 8 // TryScan is how many of the newest tries a new ask reads to find the ones that wait or run (issue 1827). TryScan = 64 // TryKeep is how long an ended try's key is kept before the checker removes it. @@ -166,6 +171,18 @@ func (w *tryWriter) end() (Try, uint64) { return w.t, w.rev } +// HoldsANode says whether the try holds a build node now: it runs, and a step of its asks runs on a node, or it names +// no step (a checker that writes its asks only at the end, or one about to put its first). +func (t Try) HoldsANode() bool { + if t.State != TryRunning { + return false + } + if len(t.Steps) == 0 { + return true + } + return slices.ContainsFunc(t.Steps, func(s TryStep) bool { return s.State == delivery.StepRunning }) +} + // Over says whether the try came to its end. func (t Try) Over() bool { return t.State == TryPass || t.State == TryFail || t.State == TryError } @@ -269,7 +286,7 @@ func AskTry(store TryStore, trunks map[string]string, a TryAsk, now time.Time) ( }); err != nil { return "", err } - var open []string + var holding, waiting []string for i, cur := range tries { if !live[i] { continue @@ -277,10 +294,17 @@ func AskTry(store TryStore, trunks map[string]string, a TryAsk, now time.Time) ( if cur.Repository == t.Repository && cur.Head == t.Head && cur.Trunk == t.Trunk && cur.Tests == t.Tests && cur.Level == t.Level { return cur.ID, nil } - open = append(open, fmt.Sprintf("%s (%s@%.12s, %s)", cur.ID, cur.Repository, cur.Head, cur.State)) + said := fmt.Sprintf("%s (%s@%.12s, %s)", cur.ID, cur.Repository, cur.Head, cur.State) + if cur.HoldsANode() { + holding = append(holding, said) + } else { + waiting = append(waiting, said) + } } - if len(open) >= MaxTries { - return "", fmt.Errorf("%d tries wait or run, the most at once: %s; ask again when one ends", len(open), strings.Join(open, ", ")) + // Tries on a build node and tries waiting for one are bounded apart: a try queued behind the seat's checks holds no node. + if len(holding) >= MaxTries || len(waiting) >= MaxTriesWaiting { + return "", fmt.Errorf("%d tries run on a build node (the most at once: %d) and %d wait for one (the most: %d): running %s; waiting %s; ask again when one ends", + len(holding), MaxTries, len(waiting), MaxTriesWaiting, orNone(holding), orNone(waiting)) } t.ID = "try-" + strconv.FormatInt(now.UnixNano(), 10) data, err := encodeTry(t) @@ -553,3 +577,11 @@ func judgeWithin(ctx context.Context, j Judge, cs *Case, bound time.Duration) (O } return out, true } + +// orNone joins the tries said, or says none. +func orNone(said []string) string { + if len(said) == 0 { + return "none" + } + return strings.Join(said, ", ") +} diff --git a/modules/mesh-delivery-checker/internal/check/try_test.go b/modules/mesh-delivery-checker/internal/check/try_test.go index cee1241..e6b2427 100644 --- a/modules/mesh-delivery-checker/internal/check/try_test.go +++ b/modules/mesh-delivery-checker/internal/check/try_test.go @@ -3,6 +3,7 @@ package check import ( "context" "encoding/json" + "fmt" "strings" "testing" "time" @@ -67,14 +68,38 @@ func TestATryIsAskedQueuedOncePerHeadAndAtMostFourAtOnce(t *testing.T) { // ADR if err != nil || again != id { t.Errorf("a second ask of the same head answered %q, %v, want %q", again, err, id) } - heads := []string{head2, trunkA2, trunkS2} - for i, h := range heads { - if _, err := AskTry(store, tryTrunks, TryAsk{Repository: repoA, Head: h, Trunk: trunkA}, at.Add(time.Duration(i+2)*time.Second)); err != nil { - t.Fatalf("try %d: %v", i+2, err) - } + // Issue 1890: tries on a build node (a step of theirs running, or none named yet) and tries waiting for one (queued, or + // their asks not taken) are bounded apart, MaxTries and MaxTriesWaiting; the refusal names both counts. + onNode := func(i int) Try { + return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning, + Steps: []TryStep{{Ask: "a", State: delivery.StepRunning}}} } - if _, err := AskTry(store, tryTrunks, TryAsk{Repository: repoSDK, Head: head1}, at.Add(time.Minute)); err == nil || !strings.Contains(err.Error(), "4 tries wait or run") { - t.Errorf("a fifth try answered %v, want refused naming the four", err) + waitingFor := func(i int) Try { + return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning, + Steps: []TryStep{{Ask: "a", State: TryAsked}}} + } + for _, c := range []struct { + what string + holding, wait int + refused string + }{ + {"four queued behind the checks, none on a node: a fifth is taken", 0, 4, ""}, + {"four on a node", MaxTries, 0, "4 tries run on a build node (the most at once: 4) and 0 wait"}, + {"three on a node, eight waiting", 3, MaxTriesWaiting, "3 tries run on a build node (the most at once: 4) and 8 wait for one (the most: 8)"}, + } { + kv := newMemKV() + for i := range c.holding + c.wait { + tr := waitingFor(i + 1) + if i < c.holding { + tr = onNode(i + 1) + } + data, _ := encodeTry(tr) + _, _ = kv.Create(context.Background(), tr.ID, data) + } + _, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoSDK, Head: head1}, at.Add(time.Minute)) + if (c.refused == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.refused)) { + t.Errorf("%s: answered %v, want refused %q", c.what, err, c.refused) + } } } -- 2.54.0 From d6bd31a1668a16aed633a60979a19729be892afc Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 20:11:08 +0100 Subject: [PATCH 3/5] Say the try limits as what they are: a check when a try is asked Tries let in while waiting may later all run at once; each node's build capacity bounds what runs on it (review of issue 1890's try cap). --- .../cmd/mesh-delivery-checker-tools/main.go | 2 +- .../internal/check/try.go | 23 ++++++++++--------- .../internal/check/try_test.go | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 0bc71c9..a6a6936 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -105,7 +105,7 @@ func TryTools(tr Trying) []stdio.Tool { {Name: check.TryTool, Description: "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, " + "its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; " + check.TryReadTool + " reads " + - "where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 run on a build node and 8 wait for one at once, mesh-wide (a try queued behind the seat's checks holds no node). A repository " + + "where it stands. A try of the same head that waits or runs is the answer to a second ask; a new try is refused while 4 tries run on a build node or 8 wait for one, mesh-wide; tries let in while waiting may later all run at once, and each node's build capacity bounds what runs on it. A repository " + "the checker checks no level of (one whose trunk the gate does not name) is refused. A try whose ask no build node took within the step " + "bound ends as error; its ask stays in the build seat's stream and may still run later, judged by nobody (issue 734).", Input: map[string]any{"type": "object", "required": []string{"repository", "head"}, "properties": map[string]any{ diff --git a/modules/mesh-delivery-checker/internal/check/try.go b/modules/mesh-delivery-checker/internal/check/try.go index c008011..757ce4e 100644 --- a/modules/mesh-delivery-checker/internal/check/try.go +++ b/modules/mesh-delivery-checker/internal/check/try.go @@ -30,12 +30,12 @@ const ( // TriesState is the checker's state of tries: the bucket mesh-delivery-checker_tries, keys try-. TriesState = "tries" TriesBucket = delivery.CheckerModule + "_" + TriesState - // MaxTries is how many tries may hold a build node at once, mesh-wide: each holds one for minutes. A try whose asks no - // node has taken yet waits behind the seat's other asks and holds none, so it counts toward MaxTriesWaiting instead - // (issue 1890: at 19:53 on 2026-10-26 four tries sat queued behind 44 checks for up to 47 minutes and no fifth could be - // asked). - MaxTries = 4 - // MaxTriesWaiting is how many tries may wait at once, queued or with their asks not yet taken by a node. + // MaxTries and MaxTriesWaiting are checked when a try is asked, mesh-wide: a new try is refused while MaxTries tries run + // on a build node or MaxTriesWaiting wait for one (queued, or their asks not yet taken by a node). It is no bound on + // what runs later: tries let in while waiting may later all run at once, and each node's build capacity bounds what + // runs on it. A try waiting behind the seat's other asks holds no node, so it no longer blocks the next try (issue + // 1890: at 19:53 on 2026-10-26 four tries sat queued behind 44 checks for up to 47 minutes and no fifth could be asked). + MaxTries = 4 MaxTriesWaiting = 8 // TryScan is how many of the newest tries a new ask reads to find the ones that wait or run (issue 1827). TryScan = 64 @@ -222,8 +222,8 @@ func NotChecked(repository string) error { } // AskTry writes a new try, queued, and answers its id: a try of the same head that waits or runs is the answer to a second -// ask, and at most MaxTries wait or run at once. trunks are the gate's trunk heads of every repository (source.main): a -// repository with none is no repository the checker checks. A trunk asked by name is its trunk head now. +// ask, and a new try is refused while MaxTries tries run on a build node or MaxTriesWaiting wait for one. trunks are the +// gate's trunk heads of every repository (source.main): a repository with none is no repository the checker checks. A trunk asked by name is its trunk head now. func AskTry(store TryStore, trunks map[string]string, a TryAsk, now time.Time) (string, error) { repo := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(a.Repository), ".git")) head := strings.ToLower(strings.TrimSpace(a.Head)) @@ -301,10 +301,11 @@ func AskTry(store TryStore, trunks map[string]string, a TryAsk, now time.Time) ( waiting = append(waiting, said) } } - // Tries on a build node and tries waiting for one are bounded apart: a try queued behind the seat's checks holds no node. + // Checked at this ask only: a try queued behind the seat's checks holds no node; tries let in while waiting may later + // all run at once, and each node's build capacity bounds what runs on it. if len(holding) >= MaxTries || len(waiting) >= MaxTriesWaiting { - return "", fmt.Errorf("%d tries run on a build node (the most at once: %d) and %d wait for one (the most: %d): running %s; waiting %s; ask again when one ends", - len(holding), MaxTries, len(waiting), MaxTriesWaiting, orNone(holding), orNone(waiting)) + return "", fmt.Errorf("a new try is refused while %d tries run on a build node or %d wait for one; now %d run and %d wait: running %s; waiting %s; ask again when one ends", + MaxTries, MaxTriesWaiting, len(holding), len(waiting), orNone(holding), orNone(waiting)) } t.ID = "try-" + strconv.FormatInt(now.UnixNano(), 10) data, err := encodeTry(t) diff --git a/modules/mesh-delivery-checker/internal/check/try_test.go b/modules/mesh-delivery-checker/internal/check/try_test.go index e6b2427..e617056 100644 --- a/modules/mesh-delivery-checker/internal/check/try_test.go +++ b/modules/mesh-delivery-checker/internal/check/try_test.go @@ -84,8 +84,8 @@ func TestATryIsAskedQueuedOncePerHeadAndAtMostFourAtOnce(t *testing.T) { // ADR refused string }{ {"four queued behind the checks, none on a node: a fifth is taken", 0, 4, ""}, - {"four on a node", MaxTries, 0, "4 tries run on a build node (the most at once: 4) and 0 wait"}, - {"three on a node, eight waiting", 3, MaxTriesWaiting, "3 tries run on a build node (the most at once: 4) and 8 wait for one (the most: 8)"}, + {"four on a node", MaxTries, 0, "now 4 run and 0 wait"}, + {"three on a node, eight waiting", 3, MaxTriesWaiting, "a new try is refused while 4 tries run on a build node or 8 wait for one; now 3 run and 8 wait"}, } { kv := newMemKV() for i := range c.holding + c.wait { -- 2.54.0 From c47afd8f4b88ff5aacb764411cff7f33d6cbab73 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 21:20:22 +0100 Subject: [PATCH 4/5] build_usage says a seat's build hours, and when it is outside them A build module's capacity. now carries its node's build window (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00); the seat shows it, outside it says it takes nothing new, and a value the module cannot read shows as any hour with the value and why. The checker's and the view's SDK pin moves to mesh-sdk d00dac1. --- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 +-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 +-- .../mesh-delivery-view/internal/view/usage.go | 27 ++++++++++++++----- .../internal/view/usage_test.go | 11 ++++++++ 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index cc0ac5a..a92d288 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 9e114f3..cffc507 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 h1:mSBxF9KgIJyNL0gOKYq5naWewUj2yazU/tGOh12Tze0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 h1:QIcGyV96AHZ6t/4AgBDlUOcs5tq40PppC7pwEpmzZco= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index c44cc9b..9fb5a30 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 9e114f3..cffc507 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642 h1:mSBxF9KgIJyNL0gOKYq5naWewUj2yazU/tGOh12Tze0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026185254-02f50bd9d642/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 h1:QIcGyV96AHZ6t/4AgBDlUOcs5tq40PppC7pwEpmzZco= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/internal/view/usage.go b/modules/mesh-delivery-view/internal/view/usage.go index 7849d80..35e1fbc 100644 --- a/modules/mesh-delivery-view/internal/view/usage.go +++ b/modules/mesh-delivery-view/internal/view/usage.go @@ -108,8 +108,14 @@ type SeatUsage struct { // Configured is mesh-config's capacity when the module says it judges by a lower one (capacity. of its pauses // state: the configured capacity bounded by its node's build CPU, issue 1806's follow-up); Capacity is then the // module's and CapacityFrom its why. - Configured int `json:"configured,omitempty"` - Paused bool `json:"paused"` + Configured int `json:"configured,omitempty"` + // BuildHours is the node's build window as the module says it (HH:MM-HH:MM and its zone), absent when it builds at any + // hour; OutsideHours says the module judged the time outside it, so it takes nothing new (the operator, 2026-10-26). + BuildHours string `json:"build_hours,omitempty"` + OutsideHours bool `json:"outside_hours,omitempty"` + // BuildHoursFault is why the module cannot read its node's build-hours value, naming it: it then builds at any hour. + BuildHoursFault string `json:"build_hours_fault,omitempty"` + Paused bool `json:"paused"` // Eligible says it takes asks: it holds the seat, has a capacity above 0 and is not paused; WhyNot says why not. // EligibleKnown is false when this tool cannot tell (it cannot read mesh-config's capacities): Eligible is then // absent, never false, and WhyNot starts "not known: ". @@ -468,10 +474,17 @@ func (u UsageReader) Usage() UsageAnswer { s.Capacity = (&buildrun.Places{Capacity: caps}).CapacityOf(node) s.CapacityFrom = fmt.Sprintf("the modules' default %q: mesh-config holds no resolved value for %s on %s", buildstep.DefaultCapacities, s.Module, node) } - if c, ok := judged[node+"."+lang]; ok && s.Holds && unknown == "" && !isBad && c.Configured == s.Capacity && c.Effective != c.Configured { - // The module running on the node judges by less than mesh-config gives: its node's build CPU bounds it. - s.Configured, s.Capacity = s.Capacity, c.Effective - s.CapacityFrom = fmt.Sprintf("%s, as %s on %s judges it (said %s)", c.Why, s.Module, node, c.At.UTC().Format(time.RFC3339)) + if c, ok := judged[node+"."+lang]; ok && s.Holds && unknown == "" && !isBad && c.Configured == s.Capacity { + // The module running on the node says its build hours, and judges by less than mesh-config gives when its + // node's build CPU bounds it or the time is outside its build hours. + s.BuildHours, s.OutsideHours = c.BuildHours, c.OutsideHours + if c.BuildHoursFault != "" { + s.BuildHours, s.BuildHoursFault = "any hour", c.BuildHoursFault + } + if c.Effective != c.Configured { + s.Configured, s.Capacity = s.Capacity, c.Effective + s.CapacityFrom = fmt.Sprintf("%s, as %s on %s judges it (said %s)", c.Why, s.Module, node, c.At.UTC().Format(time.RFC3339)) + } } s.Paused = paused[node+"."+lang] s.EligibleKnown = true @@ -487,6 +500,8 @@ func (u UsageReader) Usage() UsageAnswer { s.WhyNot = "not known: this tool cannot read its capacity (" + unknown + ")" case isBad: s.WhyNot = "its capacity on " + node + " cannot be read, so its puller uses 0" + case s.OutsideHours: + s.WhyNot = "outside its build hours " + s.BuildHours + " on " + node + ": it takes nothing new until they open" case s.Capacity == 0: s.WhyNot = "its capacity on " + node + " is 0" } diff --git a/modules/mesh-delivery-view/internal/view/usage_test.go b/modules/mesh-delivery-view/internal/view/usage_test.go index 78cab03..dc33ff1 100644 --- a/modules/mesh-delivery-view/internal/view/usage_test.go +++ b/modules/mesh-delivery-view/internal/view/usage_test.go @@ -206,6 +206,17 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) { if sa := seatOf(t, bound, "ace", "node-build-go"); sa.Capacity != 2 || sa.Configured != 0 { t.Errorf("a module's word older than mesh-config's value is not taken: %+v", sa) } + // The operator, 2026-10-26: outside its build hours the seat takes nothing new, and says so. + u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":0,"configured":2,"build-hours":"18:00-06:00 CET","outside-hours":true,"why":"0, outside its build hours 18:00-06:00 CET until 18:00 (otherwise 2)","at":"2026-10-26T11:00:00Z"}`}) + if so := seatOf(t, u.Usage(), "shanks", "node-build-go"); so.Capacity != 0 || !so.OutsideHours || so.Eligible == nil || *so.Eligible || + so.WhyNot != "outside its build hours 18:00-06:00 CET on shanks: it takes nothing new until they open" { + t.Errorf("a seat outside its build hours: %+v", so) + } + // A build-hours value the module cannot read: it builds at any hour, and the seat says the value and why. + u.Pauses["go"] = states(map[string]string{"capacity.shanks": `{"v":1,"effective":2,"configured":2,"build-hours-fault":"the build hours \"shanks=18:00-6\": \"6\" is not a time HH:MM","why":"2; at any hour, because its build hours cannot be read","at":"2026-10-26T11:00:00Z"}`}) + if sf := seatOf(t, u.Usage(), "shanks", "node-build-go"); sf.BuildHours != "any hour" || !strings.Contains(sf.BuildHoursFault, "shanks=18:00-6") || sf.Eligible == nil || !*sf.Eligible { + t.Errorf("a seat whose build hours cannot be read: %+v", sf) + } if len(bound.TriesRunning) != 1 || len(bound.TriesRunning[0].Asks) != 1 || !strings.Contains(string(bound.TriesRunning[0].Steps), "unit: tests") || strings.Contains(strings.Join(bound.Notes, " | "), "name no ask yet") { t.Errorf("a running try naming its asks: %+v, notes %v", bound.TriesRunning, bound.Notes) -- 2.54.0 From c19468b04eae94d791b28c9a643fab9ae8612ea5 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 21:29:44 +0100 Subject: [PATCH 5/5] Pin the checker and the view at mesh-sdk 805b4d9: the capacity work merged with sdk main --- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index a92d288..3b83a31 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index cffc507..c6d0c7b 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 h1:QIcGyV96AHZ6t/4AgBDlUOcs5tq40PppC7pwEpmzZco= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 h1:+SoDdDXF7Tgj38WP4kRpUSps6DJVl6P+w5u/KfdtwnQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 9fb5a30..5222ff5 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index cffc507..c6d0c7b 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03 h1:QIcGyV96AHZ6t/4AgBDlUOcs5tq40PppC7pwEpmzZco= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202137-d00dac140b03/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 h1:+SoDdDXF7Tgj38WP4kRpUSps6DJVl6P+w5u/KfdtwnQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= -- 2.54.0