186 lines
6.6 KiB
Go
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."})
|
|
}
|
|
}
|