140 lines
5.1 KiB
Go
140 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
"git.novox.be/novox/mesh-sdk/go/process"
|
|
|
|
"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 = health.FindingsBucket(delivery.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 health.Severity
|
|
}
|
|
|
|
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 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.
|
|
func (f *findings) raise(ctx context.Context, fd finding) {
|
|
key := health.FindingKey(fd.scope, fd.id, fd.kind)
|
|
kv := f.bucket()
|
|
if kv == nil {
|
|
f.logf("finding %s (not written, %s is not on the bus yet): %s", key, findingsBucket, fd.summary)
|
|
return
|
|
}
|
|
if e, err := kv.Get(ctx, key); err == nil {
|
|
if old, err := health.DecodeFinding(e.Value()); err == nil && old.Summary == fd.summary {
|
|
return
|
|
}
|
|
}
|
|
at := f.now().UTC()
|
|
sev := fd.severity
|
|
if sev == "" {
|
|
sev = health.Warning
|
|
}
|
|
value, err := health.EncodeFinding(health.Finding{Key: key, Kind: fd.kind, Subject: health.Subject{Scope: fd.scope, ID: fd.id},
|
|
Severity: sev, Summary: fd.summary, Headline: fd.headline, Explanation: fd.explanation, Resolved: fd.resolved, Needs: fd.needs,
|
|
Evidence: []health.Evidence{{At: at, Said: fd.summary}}, Source: "builder." + fd.kind, Observer: delivery.BuilderModule, At: at})
|
|
if err == nil {
|
|
_, err = kv.Put(ctx, key, value)
|
|
}
|
|
if err != nil {
|
|
f.logf("the finding %s cannot be written: %v", 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
|
|
}
|
|
key := health.FindingKey(scope, id, kind)
|
|
if _, err := kv.Get(ctx, key); errors.Is(err, jetstream.ErrKeyNotFound) {
|
|
return
|
|
}
|
|
if err := kv.Delete(ctx, key); err != nil && !errors.Is(err, jetstream.ErrKeyNotFound) {
|
|
f.logf("the finding %s cannot be cleared: %v", key, err)
|
|
}
|
|
}
|
|
|
|
// 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, health.ScopeModule, w.Name)
|
|
return
|
|
}
|
|
f.raise(ctx, finding{kind: versions.FloorSilent, scope: health.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."})
|
|
}
|
|
}
|