BL17d: builder_build answers a module's build keys whole; builder_builds_since takes module; builder_stuck's fingerprint pinned whole
builder_build reads the builds and asked states, and the tools bundle follows the catalog half itself (registrations for the watched branches, revision per branch) for `newest`; the manifest names the tool and the two seat verbs. Builds: design 47 G §589, A-87 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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:<hex>"}}},
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, <asker>.<node>.<id> or <node>.<id>: %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, <asker>.<node>.<id> or <node>.<id>: %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}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user