A failed candidate build no longer needs mesh-delivery.recheck, an exception verb (ADR 0421 §12): the checker hears the builder's keys the failed verdict names and, once each is written done (builder_build_again), retakes the check of the same heads as it does after a failure of the nodes, at its start too. When the tests have already failed the check composes at once and names the builds it did not wait on, so its red status is never held back by a build. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
171 lines
6.4 KiB
Go
171 lines
6.4 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The states of a build the status line says (ADR 0484 clause 7).
|
|
const (
|
|
BuildBuilt = "built"
|
|
BuildFailed = "failed"
|
|
BuildWaiting = "waiting"
|
|
)
|
|
|
|
// BuildStates reads the builder's build of a module at a fingerprint of the new form (builds.<module>.<fingerprint>): the
|
|
// value and whether the key exists.
|
|
type BuildStates interface {
|
|
Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error)
|
|
}
|
|
|
|
// NoteOf is the state of a build as the line says it: done is built, failed is failed, and one queued, building or not
|
|
// begun is waiting.
|
|
func NoteOf(b delivery.BuilderBuild, found bool) string {
|
|
switch {
|
|
case !found:
|
|
return BuildWaiting
|
|
case b.State == delivery.BuildDone:
|
|
return BuildBuilt
|
|
case b.State == delivery.BuildFailed:
|
|
return BuildFailed
|
|
}
|
|
return BuildWaiting
|
|
}
|
|
|
|
// buildNotes are the state of each build the case's modules carry a fingerprint of, by module, sorted. A module two pull
|
|
// requests give is said once.
|
|
func (j *Examiner) buildNotes(ctx context.Context, c *Case) []BuildNote {
|
|
if j.Builds == nil {
|
|
return nil
|
|
}
|
|
fps := map[string]string{}
|
|
for _, p := range c.Pulls {
|
|
for module, cm := range p.Fps {
|
|
if cm.FP2 != "" {
|
|
fps[module] = cm.FP2
|
|
}
|
|
}
|
|
}
|
|
modules := make([]string, 0, len(fps))
|
|
for m := range fps {
|
|
modules = append(modules, m)
|
|
}
|
|
sort.Strings(modules)
|
|
var notes []BuildNote
|
|
for _, m := range modules {
|
|
b, found, err := j.Builds.Build(ctx, m, fps[m])
|
|
if err != nil {
|
|
j.logf("the build of %s at %s cannot be read: %v", m, fps[m], err)
|
|
found = false
|
|
}
|
|
notes = append(notes, BuildNote{Module: m, State: NoteOf(b, found)})
|
|
}
|
|
return notes
|
|
}
|
|
|
|
// DefaultBuildWait is how long a check waits for the candidate's builds to end before it composes (the setting
|
|
// build-wait-bound, novox/hq ADR 0603 §10): past it the check composes without the builds not ended and names them.
|
|
const DefaultBuildWait = 30 * time.Minute
|
|
|
|
// BuildEnds waits on the builder's build keys (builds.<module>.<fingerprint>), hearing each key's changes on the bus as the
|
|
// builder writes them: the builder owns the fact, and nothing is polled.
|
|
type BuildEnds interface {
|
|
// Ended waits until the build of every module at its fingerprint (module to fingerprint) is done or failed, or ctx ends,
|
|
// and answers the builds that ended, by module. An error is only builds that cannot be heard at all.
|
|
Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error)
|
|
// Done waits until the build of every module at its fingerprint is done (a failed one built again, builder_build_again),
|
|
// and says whether they all are; false when ctx ends first or they cannot be heard.
|
|
Done(ctx context.Context, builds map[string]string) bool
|
|
}
|
|
|
|
// BuildFailedTest is what a failure of the check names a failed build of the candidate by: the test `build: <module>`.
|
|
// A verdict failed on builds is asked again by the checker itself once each is built again (Checker.Rebuilt).
|
|
const BuildFailedTest = "build: "
|
|
|
|
// failedBuilds are the candidate's builds a failed verdict names as failed (BuildFailedTest), by module, at the fingerprint
|
|
// the case carries for it; none for a verdict that names no failed build.
|
|
func failedBuilds(v delivery.Verdict, cs *Case) map[string]string {
|
|
if v.State != delivery.VerdictFail {
|
|
return nil
|
|
}
|
|
out := map[string]string{}
|
|
for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) {
|
|
module, named := strings.CutPrefix(f.Test, BuildFailedTest)
|
|
if !named {
|
|
continue
|
|
}
|
|
for _, p := range cs.Pulls {
|
|
if cm, ok := p.Fps[module]; ok && cm.FP2 != "" {
|
|
out[module] = cm.FP2
|
|
}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (j *Examiner) buildWait() time.Duration {
|
|
if j.BuildWait > 0 {
|
|
return j.BuildWait
|
|
}
|
|
return DefaultBuildWait
|
|
}
|
|
|
|
// awaitBuilds waits for the candidate's builds before the check composes (ADR 0603 §10): the check builds what the builder
|
|
// builds of the candidate and composes with it. A failed build fails the check, naming the module and the builder's words;
|
|
// a build not ended within the bound is named, and the check composes without it. It answers the words of the composition's
|
|
// warnings: what the check composes without waiting on its build, and why.
|
|
func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string {
|
|
wait, words := st.buildsOfTheCheck(ctx)
|
|
if len(wait) == 0 {
|
|
return words
|
|
}
|
|
modules := slices.Sorted(maps.Keys(wait))
|
|
if v.State == delivery.VerdictFail {
|
|
// The check has failed already: its red status is never held back by a build (the lead, 2026-11-03).
|
|
return append(words, fmt.Sprintf("the check failed before its builds were waited on, so it composes at once, without waiting on the builds of %s (ADR 0603 §10)", strings.Join(modules, ", ")))
|
|
}
|
|
if st.j.BuildEnds == nil {
|
|
return append(words, fmt.Sprintf("the builds of %s cannot be heard, so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", ")))
|
|
}
|
|
bound := st.j.buildWait()
|
|
waits := fmt.Sprintf("waits on the builder's builds of %s before it composes, at most %s (ADR 0603 §10)", strings.Join(modules, ", "), bound)
|
|
st.c.note(waits)
|
|
st.j.logf("%s %s", st.c.Key, waits)
|
|
at, cancel := context.WithTimeout(ctx, bound)
|
|
ended, err := st.j.BuildEnds.Ended(at, wait)
|
|
cancel()
|
|
if err != nil {
|
|
st.j.logf("%s: the builds of %s cannot be heard: %v", st.c.Key, strings.Join(modules, ", "), err)
|
|
return append(words, fmt.Sprintf("the builds of %s cannot be heard (%v), so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "), err))
|
|
}
|
|
var late []string
|
|
for _, module := range modules {
|
|
b, done := ended[module]
|
|
switch {
|
|
case !done:
|
|
late = append(late, module)
|
|
case b.State == delivery.BuildFailed:
|
|
f := delivery.FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)}
|
|
if v.State != delivery.VerdictFail {
|
|
v.State, v.Failed = delivery.VerdictFail, f
|
|
continue
|
|
}
|
|
addFailure(v, f)
|
|
}
|
|
}
|
|
if len(late) > 0 && ctx.Err() == nil {
|
|
words = append(words, fmt.Sprintf("the builds of %s did not end within the build-wait-bound of %s, so the check composes without them (ADR 0603 §10)", strings.Join(late, ", "), bound))
|
|
}
|
|
return words
|
|
}
|