Files
mesh-delivery/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go
T

186 lines
6.6 KiB
Go

package main
import (
"context"
"errors"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
"mesh-delivery-builder/internal/versions"
)
// findingsBucket is the builder's findings (novox/hq ADR 0564 §2): what it sees wrong now, written at its raise and
// deleted at its clear, which the condition keeper raises and clears.
var findingsBucket = meshconditionsclient.FindingsBucket(builds.BuilderModule)
// findings writes the builder's findings into its state, once bound; until then each is said in the log only.
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.
type finding struct {
kind, scope, id string
summary, headline, explanation, resolved, needs string
severity string
}
func (f *findings) bucket() findingStore {
switch {
case f == nil:
return nil
case f.store != nil:
return f.store
case f.kv == nil:
return nil
}
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 builds.BuildLogEntry) {
if e.Event != builds.BuildEndedEvent || e.Ref != "" {
return
}
switch e.Outcome {
case builds.BuildFailed:
f.raise(ctx, finding{kind: BuildFailed, scope: meshconditionsclient.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 builds.BuildDone:
f.clear(ctx, BuildFailed, meshconditionsclient.ScopeModule, e.Module)
}
}
// raise writes fd; it is written again only when its words changed, so the keeper's condition moves only then.
func (f *findings) raise(ctx context.Context, fd finding) {
o := meshconditionsclient.Observation{Scope: fd.scope, ID: fd.id, Kind: fd.kind, Severity: fd.severity,
Summary: fd.summary, Headline: fd.headline, Explanation: fd.explanation, Resolved: fd.resolved, Needs: fd.needs,
Source: "builder." + fd.kind}
kv := f.bucket()
if kv == nil {
f.logf("finding %s (not written, %s is not on the bus yet): %s", o.Key(), findingsBucket, fd.summary)
return
}
if _, err := meshconditionsclient.Raise(findingBucket{ctx, kv}, builds.BuilderModule, "", o, f.now()); err != nil {
f.logf("the finding %s cannot be written: %v", o.Key(), err)
}
}
// clear deletes the finding of kind about scope and id; one not there is nothing to clear.
func (f *findings) clear(ctx context.Context, kind, scope, id string) {
kv := f.bucket()
if kv == nil {
return
}
if _, err := meshconditionsclient.Clear(findingBucket{ctx, kv}, scope, id, kind); err != nil {
f.logf("the finding %s cannot be cleared: %v", meshconditionsclient.Key(scope, id, kind), err)
}
}
// findingBucket is the builder's findings store as the bucket the conditions client raises and clears in.
type findingBucket struct {
ctx context.Context
kv findingStore
}
func (b findingBucket) Get(key string) ([]byte, bool, error) {
e, err := b.kv.Get(b.ctx, key)
switch {
case errors.Is(err, jetstream.ErrKeyNotFound):
return nil, false, nil
case err != nil:
return nil, false, err
}
return e.Value(), true, nil
}
func (b findingBucket) Put(key string, value []byte) error {
_, err := b.kv.Put(b.ctx, key, value)
return err
}
func (b findingBucket) Delete(key string) error {
if err := b.kv.Delete(b.ctx, key); err != nil && !errors.Is(err, jetstream.ErrKeyNotFound) {
return err
}
return nil
}
// notHeard is the writer of the builder's finding of a family it does not hear (heard.Writer, ADR 0615 §5): written into
// its findings once bound, said in the log until then.
func (f *findings) notHeard(ctx context.Context) heard.Writer {
on := func(do func(findingStore)) {
switch {
case f.store != nil:
do(f.store)
case f.kv != nil:
f.kv.Ready(func(kv jetstream.KeyValue) { do(kv) })
}
}
return heard.Writer{Observer: builds.BuilderModule, Logf: f.logf, Now: f.now,
Put: func(key string, value []byte) error {
on(func(kv findingStore) {
if _, err := kv.Put(ctx, key, value); err != nil {
f.logf("the finding %s cannot be written: %v", key, err)
}
})
return nil
},
Delete: func(key string) error {
on(func(kv findingStore) {
if _, err := kv.Get(ctx, key); err != nil {
return // none stands
}
if err := kv.Delete(ctx, key); err != nil {
f.logf("the finding %s cannot be cleared: %v", key, err)
}
})
return nil
}}
}
// floorSaid is the floor finding (design 51 *Health and conditions*: a floor source silent): raised for a name waiting on
// its floor, about that name, cleared once all three sources answered.
func (f *findings) floorSaid(ctx context.Context) func(versions.FloorWait, bool) {
return func(w versions.FloorWait, raised bool) {
if !raised {
f.clear(ctx, versions.FloorSilent, meshconditionsclient.ScopeModule, w.Name)
return
}
f.raise(ctx, finding{kind: versions.FloorSilent, scope: meshconditionsclient.ScopeModule, id: w.Name, summary: w.Finding(),
headline: "A version's floor is not known",
explanation: "The builder numbers a name only once its registry seat, the forge bridge's tags and the indexer's requirements all answered; one of them did not.",
resolved: "All three answered.",
needs: "builder_floor_waits names the silent source; builder_floor_again asks again by hand."})
}
}