DLV-B7: a failed trunk build is the builder's finding build-failed, cleared by a later done build
Read from the builder's own build log entries at their end; a candidate's build is the check's to say. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -21,6 +21,15 @@ type findings struct {
|
||||
kv *process.AwaitedBucket
|
||||
logf func(string, ...any)
|
||||
now func() time.Time
|
||||
// store stands in for kv in a test.
|
||||
store findingStore
|
||||
}
|
||||
|
||||
// findingStore is the part of the findings bucket written.
|
||||
type findingStore interface {
|
||||
Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error)
|
||||
Put(ctx context.Context, key string, value []byte) (uint64, error)
|
||||
Delete(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error
|
||||
}
|
||||
|
||||
// finding is one of the builder's findings: its kind, what it is about, and its words.
|
||||
@@ -30,11 +39,41 @@ type finding struct {
|
||||
severity health.Severity
|
||||
}
|
||||
|
||||
func (f *findings) bucket() jetstream.KeyValue {
|
||||
if f == nil || f.kv == nil {
|
||||
func (f *findings) bucket() findingStore {
|
||||
switch {
|
||||
case f == nil:
|
||||
return nil
|
||||
case f.store != nil:
|
||||
return f.store
|
||||
case f.kv == nil:
|
||||
return nil
|
||||
}
|
||||
return f.kv.KV()
|
||||
if kv := f.kv.KV(); kv != nil {
|
||||
return kv
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildFailed is the finding of a failed trunk build (DLV-B7, design 25), about its module.
|
||||
const BuildFailed = "build-failed"
|
||||
|
||||
// buildEnded raises the module's build-failed for a trunk build that failed and clears it at a later done one; a
|
||||
// candidate's build (its ref named) is the check's to say.
|
||||
func (f *findings) buildEnded(ctx context.Context, e delivery.BuildLogEntry) {
|
||||
if e.Event != delivery.BuildEndedEvent || e.Ref != "" {
|
||||
return
|
||||
}
|
||||
switch e.Outcome {
|
||||
case delivery.BuildFailed:
|
||||
f.raise(ctx, finding{kind: BuildFailed, scope: health.ScopeModule, id: e.Module,
|
||||
summary: "the trunk build of " + e.Module + " at " + e.FP + " (commit " + e.Commit + ") failed",
|
||||
headline: "A module's trunk build failed",
|
||||
explanation: "The builder built " + e.Module + " from the trunk and the build failed, so its new version is not delivered.",
|
||||
resolved: "A later trunk build of " + e.Module + " is done.",
|
||||
needs: "builder_builds_since shows the build; builder_build_again builds it again."})
|
||||
case delivery.BuildDone:
|
||||
f.clear(ctx, BuildFailed, health.ScopeModule, e.Module)
|
||||
}
|
||||
}
|
||||
|
||||
// raise writes fd; it is written again only when its words changed, so the keeper's condition moves only then.
|
||||
|
||||
@@ -168,10 +168,13 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
b.Trunk = trunks.head
|
||||
// The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the
|
||||
// indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since.
|
||||
found := &findings{kv: process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings are said in its log only", logger.Printf), logf: logger.Printf, now: time.Now}
|
||||
b.Log = func(e delivery.BuildLogEntry) {
|
||||
if err := appendLog(context.WithoutCancel(ctx), js, cred, e); err != nil {
|
||||
logger.Printf("%v", err)
|
||||
}
|
||||
// A failed trunk build is the builder's finding build-failed, cleared by a later done one (DLV-B7).
|
||||
found.buildEnded(context.WithoutCancel(ctx), e)
|
||||
}
|
||||
b.Said = func(e build.AskedAgain) {
|
||||
if err := emitAskedAgain(context.WithoutCancel(ctx), js, cred, e); err != nil {
|
||||
@@ -314,7 +317,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// A branch's end removes its tagged versions (design 51 *Tagged versions*, DLV-B6): the forge bridge's merged, a pull's
|
||||
// closed or branch-deleted on the builder's durable consumer of its stream; each forge message also wakes a floor whose
|
||||
// bridge_tags was silent. The checker's tag-wanted is kept in tag-asks. A consumer not there yet is said.
|
||||
found := &findings{kv: process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings are said in its log only", logger.Printf), logf: logger.Printf, now: time.Now}
|
||||
ends := &branchEnds{acts: acts, findings: found, tagAsks: tagAsksKV.KV, logf: logger.Printf, now: time.Now,
|
||||
wake: func(ctx context.Context) {
|
||||
(&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeForge, "", "")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
// issue 774).
|
||||
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
||||
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
|
||||
t.Run("a failed trunk build is a finding until a later done one (DLV-B7)", aFailedTrunkBuildIsAFinding)
|
||||
t.Run("a branch's end is acknowledged once its tagged versions are removed (design 51 Tagged versions)", aBranchsEndIsHeard)
|
||||
set := &buildrun.ToolchainSet{}
|
||||
var logs bytes.Buffer
|
||||
@@ -194,3 +196,51 @@ type failingVersions struct{ noVersions }
|
||||
func (failingVersions) KeysOf(context.Context, string) ([]string, error) {
|
||||
return nil, errors.New("down")
|
||||
}
|
||||
|
||||
func aFailedTrunkBuildIsAFinding(t *testing.T) {
|
||||
st := &memFindings{vals: map[string][]byte{}}
|
||||
f := &findings{store: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }}
|
||||
ended := func(outcome, ref string) delivery.BuildLogEntry {
|
||||
return delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome}
|
||||
}
|
||||
key := health.FindingKey(health.ScopeModule, "m", BuildFailed)
|
||||
f.buildEnded(context.Background(), ended(delivery.BuildFailed, "refs/pull/3/head"))
|
||||
if len(st.vals) != 0 {
|
||||
t.Fatalf("a candidate's failed build is a finding: %v", st.vals)
|
||||
}
|
||||
f.buildEnded(context.Background(), ended(delivery.BuildFailed, ""))
|
||||
got, err := health.DecodeFinding(st.vals[key])
|
||||
if err != nil || got.Observer != delivery.BuilderModule || !strings.Contains(got.Summary, "trunk build of m at f1") {
|
||||
t.Fatalf("the finding = %+v, %v", got, err)
|
||||
}
|
||||
f.buildEnded(context.Background(), ended(delivery.BuildDone, ""))
|
||||
if _, ok := st.vals[key]; ok {
|
||||
t.Error("a later done trunk build leaves build-failed")
|
||||
}
|
||||
}
|
||||
|
||||
// memFindings is a findings bucket in memory.
|
||||
type memFindings struct{ vals map[string][]byte }
|
||||
|
||||
type memEntry struct {
|
||||
jetstream.KeyValueEntry
|
||||
v []byte
|
||||
}
|
||||
|
||||
func (e memEntry) Value() []byte { return e.v }
|
||||
|
||||
func (m *memFindings) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
|
||||
v, ok := m.vals[key]
|
||||
if !ok {
|
||||
return nil, jetstream.ErrKeyNotFound
|
||||
}
|
||||
return memEntry{v: v}, nil
|
||||
}
|
||||
func (m *memFindings) Put(_ context.Context, key string, v []byte) (uint64, error) {
|
||||
m.vals[key] = v
|
||||
return 1, nil
|
||||
}
|
||||
func (m *memFindings) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
|
||||
delete(m.vals, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user