173 lines
6.7 KiB
Go
173 lines
6.7 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
)
|
|
|
|
// 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) (builds.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 builds.BuilderBuild, found bool) string {
|
|
switch {
|
|
case !found:
|
|
return BuildWaiting
|
|
case b.State == builds.BuildDone:
|
|
return BuildBuilt
|
|
case b.State == builds.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]builds.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 meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case) map[string]string {
|
|
if v.State != meshdeliverycheckerclient.VerdictFail {
|
|
return nil
|
|
}
|
|
out := map[string]string{}
|
|
for _, f := range append([]FailedFirst{failedOf(v)}, failedList(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 *meshdeliverychecker.VerdictsByGroupHeadsValue) []string {
|
|
wait, words := st.buildsOfTheCheck(ctx)
|
|
if len(wait) == 0 {
|
|
return words
|
|
}
|
|
modules := slices.Sorted(maps.Keys(wait))
|
|
if v.State == meshdeliverycheckerclient.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 == builds.BuildFailed:
|
|
f := 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 != meshdeliverycheckerclient.VerdictFail {
|
|
v.State, v.Failed = meshdeliverycheckerclient.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
|
|
}
|