diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/build.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/build.go new file mode 100644 index 00000000..a91ec8a2 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/build.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/buscalls" + + "mesh-delivery-builder/internal/build" +) + +// catalogWithin is how long a call waits for the catalog half's first view of one branch. +var catalogWithin = 30 * time.Second + +// buildTool is builder_build (design 47 G §589): a module's build keys whole. +func buildTool(r *build.BuildReader) stdio.Tool { + return stdio.Tool{Name: build.BuildTool, + Description: "A module's build keys, whole, newest first (at most 20): per key its fingerprint (whole, as the build-input fingerprint the catalog graph " + + "entry carries it), state (queued, building, done or failed), revision and since when, the step and its node, the error whole, event-builds " + + "(the revisions events started since the last a person started; after three the operator is asked) and the open ask; and newest, the watched " + + "branches whose newest catalog graph entry of the module names that fingerprint, read from the catalog half. A module none of whose keys the newest " + + "entry names says so, naming the entry's fingerprint. With fingerprint, that key alone. Which node wants which fingerprint is the assigner's " + + "to answer (the seat mesh-wants). Read-only.", + Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module"}, + "fingerprint": map[string]any{"type": "string", "description": "one key: its fingerprint, src2:"}}}, + Run: func(args map[string]any) (any, error) { + module, _ := args["module"].(string) + fp, _ := args["fingerprint"].(string) + return r.Build(module, fp) + }} +} + +// newestVia reads the fingerprint of a module's newest catalog graph entry on each of its watched branches, from the catalog +// half through the seat mesh-catalog (the verb registrations for the branches, the verb revision for each branch's view), as +// the delivery view's tools read it: once per call (design 47 G §532, §589, A-87). +func newestVia(ask meshcatalogclient.Call) func(module string) (map[string]string, error) { + return func(module string) (map[string]string, error) { + raw, err := ask(meshcatalogclient.RegistrationsVerb, meshcatalog.RegistrationsInput{Module: module}) + if err != nil { + return nil, fmt.Errorf("the registrations of %s: %w", module, err) + } + answer, said, ok := buscalls.Unwrap(raw) + if !ok { + return nil, fmt.Errorf("%s answered: %s", meshcatalogclient.RegistrationsVerb, said) + } + var regs meshcatalog.RegistrationsAnswer + if err := json.Unmarshal(answer, ®s); err != nil { + return nil, fmt.Errorf("%s answered no registrations: %w", meshcatalogclient.RegistrationsVerb, err) + } + out := map[string]string{} + for _, reg := range regs.Registrations { + if reg.Module != module { + continue + } + for _, branch := range reg.Branches { + c, err := meshcatalogclient.NewCatalog(ask, branch) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), catalogWithin) + err = c.Start(ctx) + cancel() + if err != nil { + return nil, fmt.Errorf("the catalog half of %s has no first view: %w", branch, err) + } + if e, ok := c.Entry(module); ok && e.Fp != "" { + out[branch] = e.Fp + } + } + } + return out, nil + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/build_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/build_test.go new file mode 100644 index 00000000..f18c8ad0 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/build_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "encoding/json" + "strings" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// Design 47 G §589, A-87: the tools bundle reads each watched branch's newest catalog graph entry of the module from the catalog +// half itself: the verb registrations for the branches, the verb revision for each branch's view. +func TestNewestIsReadFromTheCatalogHalfOfEachWatchedBranch(t *testing.T) { + fp := map[string]string{"main": "src2:" + strings.Repeat("1", 64), "feature": "src2:" + strings.Repeat("2", 64)} + ask := func(key string, body any) (json.RawMessage, error) { + switch key { + case meshcatalogclient.RegistrationsVerb: + return json.Marshal(meshcatalog.RegistrationsAnswer{Registrations: []meshcatalog.Registration{{Module: "m", Branches: []string{"main", "feature"}}}}) + case meshcatalogclient.RevisionVerb: + in := body.(meshcatalog.RevisionInput) + return json.Marshal(meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Revision: 3, Kind: meshcatalogclient.KindTrunk}, + Entries: []meshcatalog.CatalogEntry{{Module: "m", Fp: fp[in.Branch]}, {Module: "z", Fp: "src2:" + strings.Repeat("9", 64)}}}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + got, err := newestVia(ask)("m") + if err != nil || len(got) != 2 || got["main"] != fp["main"] || got["feature"] != fp["feature"] { + t.Fatalf("newest = %v, %v, want each branch's entry fingerprint whole", got, err) + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index cc4309b2..b42c7e53 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -156,15 +156,16 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q "fingerprint, source revision, repository, directory, commit and ref, the build node, when it ended, its outcome (done, failed or " + "queued), the manifest built, the bases it was built against and each artifact made with its digest and reference; a failed " + "build carries its error and the step it was at. more says the " + - "log held more than one answer carries: ask again from the last build's time. What the operator's command nox builds reads. Read-only.", + "log held more than one answer carries: ask again from the last build's time. With module, that module's builds only. What the operator's command nox builds reads. Read-only.", Input: map[string]any{"type": "object", "required": []string{"since"}, "properties": map[string]any{ - "since": map[string]any{"type": "string", "description": "RFC 3339 time; the builds that ended at or after it"}}}, + "since": map[string]any{"type": "string", "description": "RFC 3339 time; the builds that ended at or after it"}, + "module": map[string]any{"type": "string", "description": "only this module's builds"}}}, Run: func(args map[string]any) (any, error) { at, err := time.Parse(time.RFC3339Nano, str(args, "since")) if err != nil { return nil, fmt.Errorf("since %q is not an RFC 3339 time: %w", str(args, "since"), err) } - return build.BuildsSince(since, at) + return build.BuildsSince(since, at, str(args, "module")) }}, {Name: build.AnnounceAgainTool, Description: "Announce a done build again (take in again, compose again): the builder writes the done key of module and fingerprint " + @@ -363,6 +364,8 @@ func main() { return e.Value, true, nil }} tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr) + tools = append(tools, buildTool(&build.BuildReader{Builds: read(stdio.State("builds")), Entry: entryOf(stdio.State("builds")), Asked: read(stdio.State("asked")), + Newest: newestVia(stdio.Ask), Steps: r})) tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...) qs, as := stdio.State(versions.PublishQueueState), stdio.State(versions.ActedState) tools = append(tools, republishTools(readerOf(qs), readerOf(as), buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...) @@ -394,3 +397,14 @@ func main() { os.Exit(1) } } + +// entryOf reads a key with its revision and when it was written. +func entryOf(s stdio.KeptState) func(key string) (json.RawMessage, uint64, time.Time, error) { + return func(key string) (json.RawMessage, uint64, time.Time, error) { + e, err := s.Get(key) + if err != nil || e == nil { + return nil, 0, time.Time{}, err + } + return e.Value, e.Revision, written(s, key, e.Revision), nil + } +} diff --git a/modules/mesh-delivery-builder/internal/build/buildread.go b/modules/mesh-delivery-builder/internal/build/buildread.go new file mode 100644 index 00000000..1d9ec73a --- /dev/null +++ b/modules/mesh-delivery-builder/internal/build/buildread.go @@ -0,0 +1,222 @@ +package build + +import ( + "encoding/json" + "errors" + "fmt" + "slices" + "sort" + "strings" + "time" + + occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" +) + +// **builder_build answers a module's build keys whole** (design 47 G §589, A-87): with `fingerprint` that key whole, with +// `module` alone the module's keys newest first, at most MostBuilds, each with its fingerprint whole, its state, revision and +// since when, its step and node, its error whole, event-builds and the open ask (§568), and `newest` naming each watched branch +// whose newest catalog graph entry of the module names that fingerprint, read from the catalog half by the tools bundle. + +// BuildTool is the builder's read tool of a module's build keys. +const BuildTool = "builder_build" + +// MostBuilds is how many keys of a module builder_build answers by module alone. +const MostBuilds = 20 + +// BuildRow is one build key as builder_build answers it. +type BuildRow struct { + Key string `json:"key"` + Fingerprint string `json:"fingerprint"` + State string `json:"state"` + Revision uint64 `json:"revision,omitempty"` + Since time.Time `json:"since,omitzero"` + Step string `json:"step,omitempty"` + Node string `json:"node,omitempty"` + Error string `json:"error,omitempty"` + EventBuilds int `json:"event-builds"` + // Ask is the open ask of the operator about the key (§568). + Ask *OpenAsk `json:"ask,omitempty"` + // Newest names each watched branch whose newest catalog graph entry of the module names this fingerprint. + Newest []string `json:"newest,omitempty"` +} + +// OpenAsk is the ask the operator has not answered. +type OpenAsk struct { + ID string `json:"id"` + Asked string `json:"asked"` + Expires string `json:"expires"` +} + +// BuildAnswer is what builder_build answers. +type BuildAnswer struct { + Module string `json:"module"` + Builds []BuildRow `json:"builds"` + // Newest is the fingerprint of the module's newest catalog graph entry on each watched branch. + Newest map[string]string `json:"newest-entries,omitempty"` + // Note says what no key shows: that none of the keys is the newest entry's, naming its fingerprint. + Note string `json:"note,omitempty"` +} + +// BuildReader reads what builder_build answers. +type BuildReader struct { + Builds StateRead + // Entry is a key's value with its revision and when it was written; nil leaves both out. + Entry func(key string) (json.RawMessage, uint64, time.Time, error) + // Asked is the builder's state asked, which holds the asks the operator was asked. + Asked StateRead + // Newest is the fingerprint of the module's newest catalog graph entry on each watched branch, read from the catalog half. + Newest func(module string) (map[string]string, error) + Steps StuckReader + index map[string]map[string]string +} + +// Build answers for module, and for one fingerprint when fingerprint is not empty. +func (r *BuildReader) Build(module, fingerprint string) (BuildAnswer, error) { + out := BuildAnswer{Module: module, Builds: []BuildRow{}} + if module == "" { + return out, errors.New("name the module (module)") + } + all, err := r.Builds.Keys() + if err != nil { + return out, err + } + var rows []BuildRow + for _, key := range all { + mod, fp, err := builds.ParseBuilderBuildKey(key) + if err != nil || mod != module || (fingerprint != "" && fp != fingerprint) { + continue + } + raw, rev, at, err := r.read(key) + if err != nil { + return out, fmt.Errorf("%s cannot be read: %w", key, err) + } + if raw == nil { + continue + } + v, err := builds.DecodeBuilderBuild(raw) + if err != nil { + return out, fmt.Errorf("%s holds what is not a build: %w", key, err) + } + row := BuildRow{Key: key, Fingerprint: fp, State: v.State, Revision: rev, Since: at, Step: v.Step, Error: v.Error, EventBuilds: v.EventBuilds} + if _, _, node, _, ok := builds.TransportFaultOf(v.Error); ok && node != "no node" { + row.Node = node + } + if row.Node == "" && v.State == builds.BuildBuilding { + row.Node = r.stepNode(v) + } + rows = append(rows, row) + } + if fingerprint != "" && len(rows) == 0 { + return out, fmt.Errorf("the builder holds no build key of %s at %s", module, fingerprint) + } + sort.Slice(rows, func(i, j int) bool { + if !rows[i].Since.Equal(rows[j].Since) { + return rows[i].Since.After(rows[j].Since) + } + return rows[i].Key > rows[j].Key + }) + if len(rows) > MostBuilds { + rows = rows[:MostBuilds] + } + asks := r.openAsks() + for i := range rows { + rows[i].Ask = asks[rows[i].Key] + } + if r.Newest != nil { + newest, err := r.Newest(module) + if err != nil { + return out, fmt.Errorf("the catalog half's newest entry of %s cannot be read: %w", module, err) + } + out.Newest = newest + named := false + for i := range rows { + for branch, fp := range newest { + if fp == rows[i].Fingerprint { + rows[i].Newest, named = append(rows[i].Newest, branch), true + } + } + slices.Sort(rows[i].Newest) + } + if !named && len(newest) > 0 { + var says []string + for branch, fp := range newest { + says = append(says, branch+" names "+fp) + } + slices.Sort(says) + out.Note = "no build key of " + module + " is the newest entry's: " + strings.Join(says, "; ") + } + } + out.Builds = append(out.Builds, rows...) + return out, nil +} + +// stepNode is the node a building key's step runs on, read from its asks' steps in the build modules' steps; empty when none runs. +func (r *BuildReader) stepNode(v builds.BuilderBuild) string { + if len(v.Asks) == 0 || len(r.Steps.Steps) == 0 { + return "" + } + if r.index == nil { + idx, err := r.Steps.stepIndex() + if err != nil { + return "" + } + r.index = idx + } + languages := make([]string, 0, len(v.Asks)) + for l := range v.Asks { + languages = append(languages, l) + } + slices.Sort(languages) + for _, l := range languages { + bm, _ := buildstep.BuildModuleOf(l) + st, ok := r.Steps.Steps[bm] + k, found := r.index[bm][v.Asks[l]] + if !ok || !found { + continue + } + if raw, _, err := st.Get(k); err == nil && raw != nil { + if step, err := buildstep.DecodeStep(raw); err == nil && step.Node != "" { + return step.Node + } + } + } + return "" +} + +func (r *BuildReader) read(key string) (json.RawMessage, uint64, time.Time, error) { + if r.Entry != nil { + return r.Entry(key) + } + raw, at, err := r.Builds.Get(key) + return raw, 0, at, err +} + +// openAsks are the asks kept in the state asked that nobody answered yet, by the key each is about. +func (r *BuildReader) openAsks() map[string]*OpenAsk { + out := map[string]*OpenAsk{} + if r.Asked.Keys == nil { + return out + } + keys, err := r.Asked.Keys() + if err != nil { + return out + } + for _, k := range keys { + raw, _, err := r.Asked.Get(k) + if err != nil || raw == nil { + continue + } + var kept occ.Kept + if json.Unmarshal(raw, &kept) != nil || kept.Ended != "" || kept.Acted || kept.Ask.About == "" { + continue + } + if t, err := occ.Time(kept.Ask.Expires); err == nil && !t.IsZero() && time.Now().After(t) { + continue + } + out[kept.Ask.About] = &OpenAsk{ID: kept.Ask.ID, Asked: kept.Asked.UTC().Format(time.RFC3339), Expires: kept.Ask.Expires} + } + return out +} diff --git a/modules/mesh-delivery-builder/internal/build/buildread_test.go b/modules/mesh-delivery-builder/internal/build/buildread_test.go new file mode 100644 index 00000000..92be0c20 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/build/buildread_test.go @@ -0,0 +1,83 @@ +package build + +import ( + "encoding/json" + "sort" + "strings" + "testing" + "time" + + occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" +) + +const fp2 = "src2:2222222222222222222222222222222222222222222222222222222222222222" + +func stateOf(vals map[string][]byte) StateRead { + return StateRead{ + Keys: func() ([]string, error) { + var ks []string + for k := range vals { + ks = append(ks, k) + } + sort.Strings(ks) + return ks, nil + }, + Get: func(k string) (json.RawMessage, time.Time, error) { return vals[k], time.Time{}, nil }} +} + +// Design 47 G §589: builder_build by module answers its keys newest first with whole fingerprints, event-builds and the open +// ask, and names the watched branches whose newest entry names a key's fingerprint; a module none of whose keys the newest +// entry names says so; with a fingerprint, that key alone. +func TestBuilderBuildAnswersAModulesKeysWhole(t *testing.T) { + failed, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildFailed, EventBuilds: 3, Manifest: mdigest('a'), + Error: "transport fault: connection, node-build-go ace, dial tcp: connection refused"}) + done, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: mdigest('a'), Artifacts: []builds.BuiltArtifact{{Name: "x", Kind: "file", Digest: "sha256:" + strings.Repeat("a", 64)}}}) + k1, _ := builds.BuilderBuildKey("m", fp1) + k2, _ := builds.BuilderBuildKey("m", fp2) + other, _ := builds.BuilderBuildKey("other", fp1) + vals := map[string][]byte{k1: failed, k2: done, other: done} + ask := occ.Kept{Ask: oc.Ask{ID: "retake-1", About: k1, Expires: occ.FormatTime(time.Now().Add(time.Hour))}, Asked: time.Now()} + askRaw, _ := json.Marshal(ask) + r := &BuildReader{Builds: stateOf(vals), Asked: stateOf(map[string][]byte{"ask.retake-1": askRaw}), + Newest: func(string) (map[string]string, error) { return map[string]string{"main": fp1, "feature": fp2}, nil }} + a, err := r.Build("m", "") + if err != nil || len(a.Builds) != 2 { + t.Fatalf("builds = %+v, %v, want the module's two keys", a, err) + } + var row BuildRow + for _, b := range a.Builds { + if b.Fingerprint == fp1 { + row = b + } + } + if row.Fingerprint != fp1 || len(row.Fingerprint) != len("src2:")+64 || row.State != builds.BuildFailed || row.EventBuilds != 3 || row.Node != "ace" || + !strings.HasPrefix(row.Error, "transport fault: connection, node-build-go ace, dial tcp") || row.Ask == nil || row.Ask.ID != "retake-1" || len(row.Newest) != 1 || row.Newest[0] != "main" { + t.Errorf("the failed key = %+v, want its fingerprint whole, its error whole, event-builds 3, the open ask and newest main", row) + } + if one, err := r.Build("m", fp2); err != nil || len(one.Builds) != 1 || one.Builds[0].Fingerprint != fp2 || one.Builds[0].Newest[0] != "feature" { + t.Errorf("one key = %+v, %v", one, err) + } + r.Newest = func(string) (map[string]string, error) { + return map[string]string{"main": "src2:" + strings.Repeat("c", 64)}, nil + } + if n, err := r.Build("m", ""); err != nil || !strings.Contains(n.Note, "no build key of m is the newest entry's") || !strings.Contains(n.Note, "main names src2:"+strings.Repeat("c", 64)) { + t.Errorf("a module none of whose keys the newest entry names: %+v, %v", n, err) + } +} + +// Design 47 G §589: builder_stuck answers each key's fingerprint whole, as the build-input fingerprint the catalog graph entry carries it. +func TestBuilderStuckAnswersTheFingerprintWhole(t *testing.T) { + building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, Step: "asked", Manifest: mdigest('a')}) + k, _ := builds.BuilderBuildKey("m", fp1) + out, err := StuckReader{Builds: stateOf(map[string][]byte{k: building})}.Stuck("m") + if err != nil { + t.Fatal(err) + } + rows := out["stuck"].([]Stuck) + if len(rows) != 1 || rows[0].FP != fp1 { + t.Fatalf("stuck = %+v, want the fingerprint %s whole", rows, fp1) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index ce3d66f3..3955c800 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -270,12 +270,19 @@ func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7 page.Entries = append(page.Entries, stdio.LogEntry{Sequence: uint64(i + 1), Key: e.Module, Value: raw, At: e.At}) } page.Next = 3 - since, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At) + since, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, "") if err != nil || len(since.Builds) != 1 || since.Builds[0].Event != builds.BuildEndedEvent || len(since.Builds[0].Made) != 3 || !since.More { t.Errorf("builds since = %+v, %v, want the one end and more", since, err) } + // With module (design 47 G §589) the builds of that module alone. + if other, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, "another"); err != nil || len(other.Builds) != 0 { + t.Errorf("builds since of another module = %+v, %v, want none", other, err) + } + if mine, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, logged[0].Module); err != nil || len(mine.Builds) != 1 { + t.Errorf("builds since of the module = %+v, %v, want its one end", mine, err) + } page.Entries[0].Value = []byte(`{"v":0}`) - if _, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At); err == nil { + if _, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, ""); err == nil { t.Error("an entry that does not read was skipped, want it refused") } if v.State != builds.BuildDone || v.By != "novox.builder" || v.Manifest != manifestD || len(v.Artifacts) != 3 { diff --git a/modules/mesh-delivery-builder/internal/build/since.go b/modules/mesh-delivery-builder/internal/build/since.go index 8875b37d..d459a9b8 100644 --- a/modules/mesh-delivery-builder/internal/build/since.go +++ b/modules/mesh-delivery-builder/internal/build/since.go @@ -2,6 +2,7 @@ package build import ( "fmt" + "slices" "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" @@ -16,9 +17,9 @@ type LogSince func(at time.Time, keys string, limit int) (*stdio.LogPage, error) // BuildsSince answers builds.BuildsSinceTool (ADR 0571 clause 9): every ended build in the builder's log from since on, // oldest first, read through the runtime's log verbs; More when the page was not the log's last, so the caller asks again -// from the last build's time. An entry the log holds that does not read is refused, naming its sequence: a reader that +// from the last build's time. With module (design 47 G §589) only that module's builds are answered. An entry the log holds that does not read is refused, naming its sequence: a reader that // skipped it would register too little. -func BuildsSince(read LogSince, since time.Time) (builds.BuildsSince, error) { +func BuildsSince(read LogSince, since time.Time, module string) (builds.BuildsSince, error) { out := builds.BuildsSince{Since: since.UTC(), Builds: []builds.BuildLogEntry{}} page, err := read(since, "", BuildsSinceLimit) if err != nil { @@ -38,6 +39,9 @@ func BuildsSince(read LogSince, since time.Time) (builds.BuildsSince, error) { if ended := builds.EndedSince(entries, since); ended != nil { out.Builds = ended } + if module != "" { + out.Builds = slices.DeleteFunc(out.Builds, func(e builds.BuildLogEntry) bool { return e.Module != module }) + } out.More = page.Next != 0 return out, nil } diff --git a/modules/mesh-delivery-builder/internal/build/stuck.go b/modules/mesh-delivery-builder/internal/build/stuck.go index eafe7379..5a6666b9 100644 --- a/modules/mesh-delivery-builder/internal/build/stuck.go +++ b/modules/mesh-delivery-builder/internal/build/stuck.go @@ -313,26 +313,9 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { if r.Now != nil { now = r.Now } - // Steps are found by the ask's id, which their key ends in: one index per build module, no value read until an ask names it. - stepKeys := map[string]map[string]string{} - for bm, s := range r.Steps { - ks, err := s.Keys() - if err != nil { - return nil, fmt.Errorf("the steps of %s: %w", bm, err) - } - byID := make(map[string]string, len(ks)) - for _, k := range ks { - _, _, id, err := buildstep.ParseStepKey(k) - if err != nil { - // A key neither form parses is named, never passed over in silence (design 47 G §484). - if r.Logf != nil { - r.Logf("builder_stuck: the key %q of the steps of %s is a step key of neither form, .. or .: %v", k, bm, err) - } - continue - } - byID[id] = k - } - stepKeys[bm] = byID + stepKeys, err := r.stepIndex() + if err != nil { + return nil, err } // Narrowed by the key (the module) before any value is read; the state is in the value, so every matching key is read, in // parallel, at most MaxReads of them. @@ -429,6 +412,32 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { return answer, nil } +// stepIndex finds the build modules' steps by the ask's id, which their key ends in: one index per build module, no value read +// until an ask names it. +func (r StuckReader) stepIndex() (map[string]map[string]string, error) { + stepKeys := map[string]map[string]string{} + for bm, s := range r.Steps { + ks, err := s.Keys() + if err != nil { + return nil, fmt.Errorf("the steps of %s: %w", bm, err) + } + byID := make(map[string]string, len(ks)) + for _, k := range ks { + _, _, id, err := buildstep.ParseStepKey(k) + if err != nil { + // A key neither form parses is named, never passed over in silence (design 47 G §484). + if r.Logf != nil { + r.Logf("builder_stuck: the key %q of the steps of %s is a step key of neither form, .. or .: %v", k, bm, err) + } + continue + } + byID[id] = k + } + stepKeys[bm] = byID + } + return stepKeys, nil +} + // StuckBuildModules are the build modules whose steps builder_stuck reads. var StuckBuildModules = []string{buildrun.GoBuildModule, buildrun.NodeBuildModule, ImageBuildModule} diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 887e62c0..533f28e6 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -144,7 +144,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "seat:git.tag", - "seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.entry", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove", + "seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove", "seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } @@ -171,7 +171,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 t.Errorf("secrets-owner = %q", m.SecretsOwner) } // 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again. - if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views", + if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views", "builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits", "reserved"}; !reflect.DeepEqual(sorted(m.Tools), want) { t.Errorf("tools = %v, want %v", m.Tools, want) } diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 069a757d..6994fab0 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -15,6 +15,7 @@ "builder_announce_again", "builder_ask_again", "builder_bounds", + "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", @@ -85,6 +86,8 @@ "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.entry", + "seat:mesh-catalog.registrations", + "seat:mesh-catalog.revision", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments",