fix9-6: the view's build usage finds the build seats and their holders, never a build module's name
build_usage reads each build seat's steps and pauses through the seat; the view's tools follow the seat list (view_seat_list, view_seat_list_again) and ask the holder of each seat of the seat mesh-module-assigner. Builds: design 47 G A-82, A-74, §204
This commit is contained in:
@@ -13,14 +13,18 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -405,6 +409,29 @@ func ownersRead(ask func(key string, body any) (json.RawMessage, error)) view.Re
|
||||
GraphState: indexer("indexer_read_graph")}
|
||||
}
|
||||
|
||||
// holderOf is a seat's holder as the seat mesh-module-assigner's verb assignments answers it with the seats (design 47 G A-82,
|
||||
// A-74): the module the seat list names for the seat, and false while the seat has none or the list cannot be read.
|
||||
func holderOf(ask func(key string, body any) (json.RawMessage, error)) func(seat string) (string, bool) {
|
||||
return func(seat string) (string, bool) {
|
||||
raw, err := ask("seat:"+mma.Seat+"."+mma.VerbAssignments, mma.AssignmentsInput{Seats: mma.AssignmentsInputSeatsTrue})
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
var held struct {
|
||||
Seats []mma.SeatHolders `json:"seats"`
|
||||
}
|
||||
if json.Unmarshal(raw, &held) != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, s := range held.Seats {
|
||||
if s.Seat == seat && len(s.Holders) > 0 {
|
||||
return s.Holders[0].Module, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := ownersRead(stdio.Ask)
|
||||
r.Builds, r.HavesState = read(stdio.State("mesh-delivery-builder.builds")), read(stdio.State("node-engine.have"))
|
||||
@@ -416,16 +443,28 @@ func main() {
|
||||
r.ProvisionsState = read(stdio.State(view.ProvisionsRead))
|
||||
r.BuildLog = stdio.Log(builds.BuildLogRead).Since
|
||||
r.Pending = pendingOf
|
||||
// The build modules are no names (design 47 G A-82): the build seats on the seat list, each held by the module the seat
|
||||
// list names for it; the seat list and its two tools are wired once by mesh-sdk (§204).
|
||||
gate := seatlist.NewPart("view", []string{stdio.AnyPerson, "node-tools", "mesh-healers"})
|
||||
go func() {
|
||||
<-stdio.Served()
|
||||
gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] "+f+"\n", a...) })
|
||||
}()
|
||||
seats := buildrun.NewBuildSeats(func() map[string]seatdef.Definition {
|
||||
if rd := gate.Reader(); rd != nil {
|
||||
return rd.Definitions()
|
||||
}
|
||||
return nil
|
||||
}, holderOf(stdio.Ask))
|
||||
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
|
||||
Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())}
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
u.Steps[lang] = read(stdio.State(module + "." + view.StepsState))
|
||||
u.Pauses[lang] = read(stdio.State(module + "." + buildstep.PausesState))
|
||||
}
|
||||
Seats: seats,
|
||||
StepsOf: func(seat string) view.StateRead { return read(stdio.State(seat + "." + view.StepsState)) },
|
||||
PausesOf: func(seat string) view.StateRead { return read(stdio.State(seat + "." + buildstep.PausesState)) },
|
||||
Backlog: backlogOf(socket())}
|
||||
// view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and
|
||||
// the assigner's states (ADR 0615 §6), and a re-read by hand.
|
||||
tools := append(Tools(r, u), views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
||||
tools := append(Tools(r, u), gate.Tools()...)
|
||||
tools = append(tools, views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
|
||||
if err := stdio.Serve("", tools); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mesh-delivery-view/internal/view"
|
||||
"reflect"
|
||||
@@ -625,3 +626,26 @@ func manifestReadsAsksEveryNode(t *testing.T) {
|
||||
t.Errorf("node_manifest_reads = %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G A-82, A-74: a build seat's holder is the module the seat mesh-module-assigner's verb assignments names for it with
|
||||
// the seats, and none while the seat has no holder or the list cannot be read.
|
||||
func TestABuildSeatsHolderIsTheOneTheSeatListNames(t *testing.T) {
|
||||
var asked []string
|
||||
ask := func(key string, _ any) (json.RawMessage, error) {
|
||||
asked = append(asked, key)
|
||||
return json.RawMessage(`{"seats":[{"seat":"node-build-go","scope":"node","holders":[{"module":"acme-go","node":"n1"}]},{"seat":"node-build-node","scope":"node","holders":[]}]}`), nil
|
||||
}
|
||||
holder := holderOf(ask)
|
||||
if m, ok := holder("node-build-go"); !ok || m != "acme-go" {
|
||||
t.Errorf("the holder of node-build-go = %q, %v; want acme-go", m, ok)
|
||||
}
|
||||
if m, ok := holder("node-build-node"); ok {
|
||||
t.Errorf("a seat with no holder has %q", m)
|
||||
}
|
||||
if len(asked) != 2 || asked[0] != "seat:mesh-module-assigner.assignments" {
|
||||
t.Errorf("asked %v; want the seat mesh-module-assigner's assignments", asked)
|
||||
}
|
||||
if _, ok := holderOf(func(string, any) (json.RawMessage, error) { return nil, errors.New("down") })("node-build-go"); ok {
|
||||
t.Error("a seat list that cannot be read names a holder")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
buildseats "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
@@ -50,9 +51,11 @@ const (
|
||||
|
||||
// UsageReader reads what build_usage answers from.
|
||||
type UsageReader struct {
|
||||
// Steps and Pauses are each build module's states by language (go, node, image).
|
||||
Steps map[string]StateRead
|
||||
Pauses map[string]StateRead
|
||||
// Seats are the build seats, each by the acting rule that builds, with the module holding it on the seat list (design 47 G
|
||||
// A-82): no build module is named here. StepsOf and PausesOf read a build seat's steps and pauses through the seat.
|
||||
Seats *buildseats.BuildSeats
|
||||
StepsOf func(seat string) StateRead
|
||||
PausesOf func(seat string) StateRead
|
||||
// Resolved is mesh-config's resolved values (<node>.<module>.<key>); Haves the node-engine's have.
|
||||
Resolved StateRead
|
||||
Haves StateRead
|
||||
@@ -209,6 +212,16 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
note := func(f string, a ...any) { ans.Notes = append(ans.Notes, fmt.Sprintf(f, a...)) }
|
||||
|
||||
tries := u.tries(now, &ans, note)
|
||||
// The build seats once for this answer: each asks the seat list for its holder (design 47 G A-82).
|
||||
buildSeats := u.Seats.All()
|
||||
buildSeatOfModule := func(module string) buildseats.BuildModule {
|
||||
for _, m := range buildSeats {
|
||||
if m.Module == module && module != "" {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return buildseats.BuildModule{}
|
||||
}
|
||||
seats := map[string]map[string]*SeatUsage{} // node -> language -> seat
|
||||
seat := func(node, lang string) *SeatUsage {
|
||||
if seats[node] == nil {
|
||||
@@ -217,9 +230,13 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
if s := seats[node][lang]; s != nil {
|
||||
return s
|
||||
}
|
||||
name, _ := buildstep.BuildSeat(lang)
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
s := &SeatUsage{Seat: name, Module: module, Running: RunningUsage{ByKind: map[string]int{}}, LeftRunning: []StepUsage{}, EndedStale: []StepUsage{}, Steps: []StepUsage{}}
|
||||
var bm buildseats.BuildModule
|
||||
for _, m := range buildSeats {
|
||||
if m.AskLanguage() == lang {
|
||||
bm = m
|
||||
}
|
||||
}
|
||||
s := &SeatUsage{Seat: bm.Seat, Module: bm.Module, Running: RunningUsage{ByKind: map[string]int{}}, LeftRunning: []StepUsage{}, EndedStale: []StepUsage{}, Steps: []StepUsage{}}
|
||||
seats[node][lang] = s
|
||||
return s
|
||||
}
|
||||
@@ -233,10 +250,8 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
} else {
|
||||
var want []string
|
||||
for _, k := range keys {
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
if m, _ := buildstep.BuildModuleOf(lang); strings.HasSuffix(k, "."+m) {
|
||||
want = append(want, k)
|
||||
}
|
||||
if _, module, _ := strings.Cut(k, "."); buildSeatOfModule(module).Seat != "" {
|
||||
want = append(want, k)
|
||||
}
|
||||
}
|
||||
raws, err := u.Haves.values(want)
|
||||
@@ -256,8 +271,8 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
}
|
||||
for k, h := range haves {
|
||||
node, module, _ := strings.Cut(k, ".")
|
||||
lang := strings.TrimPrefix(module, "mesh-build-")
|
||||
s := seat(node, lang)
|
||||
bm := buildSeatOfModule(module)
|
||||
s := seat(node, bm.AskLanguage())
|
||||
s.Phase, s.Holds = string(h.Phase), h.Phase == nodeengineclient.PhaseRunning
|
||||
}
|
||||
|
||||
@@ -310,19 +325,27 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
}
|
||||
for key := range unreadable {
|
||||
node, module, _ := strings.Cut(key, ".")
|
||||
seat(node, strings.TrimPrefix(module, "mesh-build-"))
|
||||
bm := buildSeatOfModule(module)
|
||||
seat(node, bm.AskLanguage())
|
||||
}
|
||||
// Every node with a resolved capacity is listed, so "is this node eligible" is answered for it.
|
||||
for key := range resolved {
|
||||
node, module, _ := strings.Cut(key, ".")
|
||||
seat(node, strings.TrimPrefix(module, "mesh-build-"))
|
||||
bm := buildSeatOfModule(module)
|
||||
seat(node, bm.AskLanguage())
|
||||
}
|
||||
if len(buildSeats) == 0 {
|
||||
note("no build seat is known from the seat definitions and the seat list, so no steps or pauses are read")
|
||||
}
|
||||
// The steps of each build module.
|
||||
steps := map[string]map[string][]usageStep{} // node -> language -> steps
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
sr, ok := u.Steps[lang]
|
||||
if !ok || sr.Keys == nil || sr.Get == nil {
|
||||
for _, bm := range buildSeats {
|
||||
lang, module := bm.AskLanguage(), cmpOr(bm.Module, bm.Seat)
|
||||
var sr StateRead
|
||||
if u.StepsOf != nil {
|
||||
sr = u.StepsOf(bm.Seat)
|
||||
}
|
||||
if sr.Keys == nil || sr.Get == nil {
|
||||
note("the steps of %s are not readable here", module)
|
||||
continue
|
||||
}
|
||||
@@ -379,10 +402,13 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
// Pauses: paused.<node> of each module; and capacity.<node>, the capacity the module says it judges by.
|
||||
paused := map[string]bool{} // <node>.<language>
|
||||
judged := map[string]buildstep.Capacity{}
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
module, _ := buildstep.BuildModuleOf(lang)
|
||||
pr, ok := u.Pauses[lang]
|
||||
if !ok || pr.Get == nil {
|
||||
for _, bm := range buildSeats {
|
||||
lang, module := bm.AskLanguage(), cmpOr(bm.Module, bm.Seat)
|
||||
var pr StateRead
|
||||
if u.PausesOf != nil {
|
||||
pr = u.PausesOf(bm.Seat)
|
||||
}
|
||||
if pr.Get == nil {
|
||||
note("the pauses of %s are not readable here, so none is said paused", module)
|
||||
continue
|
||||
}
|
||||
@@ -422,7 +448,8 @@ func (u UsageReader) Usage() UsageAnswer {
|
||||
for _, node := range nodes {
|
||||
nu := NodeUsage{Node: node, Seats: []SeatUsage{}, Said: []string{},
|
||||
CPU: "not on the bus: the disk-load module keeps no state; " + node + "/disk-load.cpu_top reads it (the build modules' share among its units)"}
|
||||
for _, lang := range buildstep.AskLanguages {
|
||||
for _, bm := range buildSeats {
|
||||
lang := bm.AskLanguage()
|
||||
s := seats[node][lang]
|
||||
if s == nil {
|
||||
continue
|
||||
@@ -696,3 +723,11 @@ func sumMedian(d []int64) (sum, median int64) {
|
||||
}
|
||||
return sum, s[(len(s)-1)/2]
|
||||
}
|
||||
|
||||
// cmpOr is the first of two names that is not empty.
|
||||
func cmpOr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
buildseats "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -82,8 +84,19 @@ func usageFixture() UsageReader {
|
||||
"shanks.mesh-config.other": `{"value":"x"}`,
|
||||
"g14.mesh-build-node.capacity": `{"node":"g14","module":"mesh-build-node","key":"capacity","source":"unreadable"}`,
|
||||
}),
|
||||
Steps: map[string]StateRead{"go": states(goSteps), "node": states(map[string]string{}), "image": states(map[string]string{})},
|
||||
Pauses: map[string]StateRead{"go": states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`}), "node": states(nil), "image": states(nil)},
|
||||
Seats: buildSeatsOf(),
|
||||
StepsOf: func(seat string) StateRead {
|
||||
if seat == "node-build-go" {
|
||||
return states(goSteps)
|
||||
}
|
||||
return states(map[string]string{})
|
||||
},
|
||||
PausesOf: func(seat string) StateRead {
|
||||
if seat == "node-build-go" {
|
||||
return states(map[string]string{"paused.ace": `{"v":1,"paused":true,"by":"lead@shanks","at":"2026-10-26T09:00:00Z"}`})
|
||||
}
|
||||
return states(nil)
|
||||
},
|
||||
Tries: states(map[string]string{
|
||||
"try-" + strconv.FormatInt(at("11:29").UnixNano(), 10): `{"id":"try-1","repository":"novox/mesh-sdk","head":"h","trunk":"t","state":"pass","asks":["` + tryAsk + `"]}`,
|
||||
"try-" + strconv.FormatInt(at("11:58").UnixNano(), 10): `{"id":"try-2","repository":"novox/mesh-delivery","head":"h","trunk":"t","state":"running","started":"2026-10-26T11:58:00Z"}`,
|
||||
@@ -167,7 +180,7 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
// Nothing readable: each state is named in a note, the backlog's refusal too, and nothing is said held.
|
||||
none := UsageReader{Now: func() time.Time { return usageNow }, Backlog: func() ([]BacklogRow, error) { return nil, errors.New("no socket") }}.Usage()
|
||||
notes := strings.Join(none.Notes, " | ")
|
||||
for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "steps of mesh-build-go are not readable", "tries are not readable", "did not answer: no socket"} {
|
||||
for _, want := range []string{"have is not readable", "capacity unknown to this tool: mesh-config's resolved values are not readable", "no build seat is known", "tries are not readable", "did not answer: no socket"} {
|
||||
if !strings.Contains(notes, want) {
|
||||
t.Errorf("the notes %q do not say %q", notes, want)
|
||||
}
|
||||
@@ -196,8 +209,8 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
// 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"}`})
|
||||
setPauses(&u, "node-build-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")
|
||||
@@ -208,13 +221,13 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
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"}`})
|
||||
setPauses(&u, "node-build-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"}`})
|
||||
setPauses(&u, "node-build-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)
|
||||
}
|
||||
@@ -225,3 +238,25 @@ func TestBuildUsageAnswersEveryNodesUseFromTheStates(t *testing.T) {
|
||||
}
|
||||
|
||||
var tryAsk0 = stepID(at("11:58"), "a00010")
|
||||
|
||||
// buildSeatsOf is the build seats as the seat definitions and the seat list give them (design 47 G A-82): the seats whose
|
||||
// acting rule builds, each held by the module the seat list names.
|
||||
func buildSeatsOf() *buildseats.BuildSeats {
|
||||
rule := func(on string) seatdef.Definition {
|
||||
return seatdef.Definition{Acts: []seatdef.ActingRule{{Step: seatdef.StepBuild, On: on}}}
|
||||
}
|
||||
defs := map[string]seatdef.Definition{"node-build-go": rule("go"), "node-build-node": rule("typescript"), "node-build-image": rule(seatdef.OnKindImage)}
|
||||
holders := map[string]string{"node-build-go": "mesh-build-go", "node-build-node": "mesh-build-node", "node-build-image": "mesh-build-image"}
|
||||
return buildseats.NewBuildSeats(func() map[string]seatdef.Definition { return defs }, func(seat string) (string, bool) { h, ok := holders[seat]; return h, ok })
|
||||
}
|
||||
|
||||
// setPauses replaces what one build seat's pauses read.
|
||||
func setPauses(u *UsageReader, seat string, st StateRead) {
|
||||
prev := u.PausesOf
|
||||
u.PausesOf = func(s string) StateRead {
|
||||
if s == seat {
|
||||
return st
|
||||
}
|
||||
return prev(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
if m.Module != "mesh-delivery-view" {
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-engine.have"} // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
want := []string{"mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-build-go.pauses", "node-build-go.steps", "node-build-image.pauses", "node-build-image.steps", "node-build-node.pauses", "node-build-node.steps", "node-engine.have"} // the build seats' states by seat, never a build module (design 47 G A-82); // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -124,7 +124,9 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
"mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_pending",
|
||||
"mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken",
|
||||
"seat:mesh-catalog.revision", // ADR 0663: the modules are the catalog half's, read through the seat's verb revision, never the indexer's
|
||||
"seat:mesh-catalog.seat-definitions", // design 47 G A-82, §204: the build seats are read from the seat list
|
||||
"seat:mesh-hand-acts.hand-act", // the join: its first view asked again by hand is a hand act (views.StatusTools)
|
||||
"seat:mesh-module-assigner.assignments", // A-82: the holder of each build seat
|
||||
"seat:node-engine.manifest-reads"}; !reflect.DeepEqual(m.Invokes, want) { // ADR 0618 §3: node_manifest_reads asks the node-engine seat's verb of every node; 1585/T24: view_tiers asks indexer_pending, a verb edge (ADR 0614 §2)
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
@@ -139,7 +141,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
t.Errorf("version = %q, want 0.1", m.Version)
|
||||
}
|
||||
if want := []string{"mesh-conditions.findings-asked", "mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.wants",
|
||||
"mesh-catalog.catalog-revision", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
// The one event the view says: a source revision is delivered (1906/D9, ADR 0539 §17), derived by it from the wants and the haves.
|
||||
@@ -164,7 +166,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
}
|
||||
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
|
||||
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads", "view_seat_list", "view_seat_list_again"}; !reflect.DeepEqual(m.Tools, want) {
|
||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||
}
|
||||
// D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"view_tiers",
|
||||
"view_first_views",
|
||||
"view_first_view_again",
|
||||
"node_manifest_reads"
|
||||
"node_manifest_reads",
|
||||
"view_seat_list",
|
||||
"view_seat_list_again"
|
||||
],
|
||||
"emits": [
|
||||
"delivered",
|
||||
@@ -56,6 +58,8 @@
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-assigner.views.wants",
|
||||
"mesh-catalog.catalog-revision",
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-delivery-indexer.views.candidates",
|
||||
"mesh-delivery-indexer.views.taken"
|
||||
],
|
||||
@@ -73,16 +77,18 @@
|
||||
"mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_taken",
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-module-assigner.assignments",
|
||||
"seat:node-engine.manifest-reads"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-build-go.pauses",
|
||||
"mesh-build-go.steps",
|
||||
"mesh-build-image.pauses",
|
||||
"mesh-build-image.steps",
|
||||
"mesh-build-node.pauses",
|
||||
"mesh-build-node.steps",
|
||||
"node-build-go.pauses",
|
||||
"node-build-go.steps",
|
||||
"node-build-image.pauses",
|
||||
"node-build-image.steps",
|
||||
"node-build-node.pauses",
|
||||
"node-build-node.steps",
|
||||
"mesh-config.resolved",
|
||||
"mesh-delivery-builder.build-log",
|
||||
"mesh-delivery-builder.builds",
|
||||
|
||||
Reference in New Issue
Block a user