The validation run's answer is read from the step's end through steprun's ReadOutput, whole or from the build seat's output object, and the indexer invokes seat:node-build-go.output for it (A-254); the patch B5g6 left for the join, applied on the merged heads. Builds: design 47 G §712, §713; lead answers A-254, A-268 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
330 lines
13 KiB
Go
330 lines
13 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
|
)
|
|
|
|
// The pending revision's wait for its validation (design 47 G §713, A-232, A-247): the take-in puts the run ask of §712, writes
|
|
// `validation` on the revision's newest pending value and ends there; the build seat's step-ended of that ask, heard through the
|
|
// indexer's own durable consumer, resumes it: accepted takes the revision in as before, refused keeps it pending with `refused`,
|
|
// and an end that could not validate keeps it pending with `failed`, asked again by a later merge of the branch or by hand.
|
|
|
|
// ValidationAsks puts a run ask on the build seat (buildrun.Asker).
|
|
type ValidationAsks interface {
|
|
Put(ctx context.Context, a buildstep.StepAsk) error
|
|
}
|
|
|
|
// newestOfRevision is the index in ready of the revision's newest merge, the value that carries `validation` (A-247).
|
|
func newestOfRevision(ready []PendingEntry) int {
|
|
n := 0
|
|
for i, e := range ready {
|
|
if e.Value.Seq > ready[n].Value.Seq {
|
|
n = i
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// branchOfRevision is the watched branch the revision's merges landed on, its newest merged message's base (design 47 G §714);
|
|
// a message that names none is the trunk's. A pending value's own branch is its pull request's, never the branch validated.
|
|
func branchOfRevision(ready []PendingEntry) string {
|
|
if base := ready[newestOfRevision(ready)].Forge.Base; base != "" {
|
|
return base
|
|
}
|
|
return TrunkBranch
|
|
}
|
|
|
|
// validationGate says whether the ready revision waits for its validation (§713): it goes on to the take-in once its newest
|
|
// value's validation was accepted at that value's sequence; it waits while its ask is out, while it is refused, and while it
|
|
// could not validate; otherwise it puts a new ask, the next try, and waits. Without an asker nothing is validated.
|
|
func (ix *Indexer) validationGate(ctx context.Context, ready []PendingEntry, own string) (bool, string, error) {
|
|
if ix.ValidationAsks == nil {
|
|
return false, "", nil
|
|
}
|
|
newest := ready[newestOfRevision(ready)]
|
|
seq := newest.Value.Seq
|
|
if v := newest.Value.Validation; v != nil && v.Seq == seq {
|
|
switch {
|
|
case v.Accepted == seq:
|
|
return false, "", nil
|
|
case v.Failed != "":
|
|
return true, fmt.Sprintf("the validation of the revision at %d could not validate: %s", seq, v.Failed), nil
|
|
case newest.Value.Refused != "":
|
|
return true, fmt.Sprintf("the revision at %d is refused: %s", seq, newest.Value.Refused), nil
|
|
case v.Ask != "":
|
|
return true, fmt.Sprintf("the revision at %d waits for its validation, the ask %s", seq, v.Ask), nil
|
|
}
|
|
}
|
|
return true, "", ix.askValidation(ctx, ready, own)
|
|
}
|
|
|
|
// askValidation puts the revision's validation ask (§712) and writes it on its newest value, the try after every earlier try
|
|
// of the revision; an older value's validation goes (A-247: on the newest alone). A request or job that cannot be made could not
|
|
// validate: it is written as `failed`, said, and no ask is put.
|
|
func (ix *Indexer) askValidation(ctx context.Context, ready []PendingEntry, own string) error {
|
|
n := newestOfRevision(ready)
|
|
newest := ready[n]
|
|
try := 0
|
|
for _, e := range ready {
|
|
if v := e.Value.Validation; v != nil && v.Try > try {
|
|
try = v.Try
|
|
}
|
|
}
|
|
v := &takein.Validation{Seq: newest.Value.Seq, Try: try + 1, Asked: ix.now()}
|
|
branch := branchOfRevision(ready)
|
|
ask := buildstep.StepAsk{ID: buildstep.NewID(), Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: ValidationAsker, At: ix.now().UTC()}
|
|
req, entry, err := ix.validationRequest(ctx, branch, ready)
|
|
var raw []byte
|
|
if err == nil {
|
|
raw, err = json.Marshal(req)
|
|
}
|
|
var job buildstep.RunJob
|
|
if err == nil {
|
|
job, err = validationJob(entry, revisionHead(ready, entry.Repository, entry.Commit), branch, v.Seq, raw)
|
|
}
|
|
if err == nil {
|
|
v.Input, err = ix.KeepInput(ctx, ask.ID, raw)
|
|
}
|
|
if err != nil {
|
|
v.Failed = "the validation could not be asked: " + err.Error()
|
|
} else {
|
|
v.Ask, ask.Run = ask.ID, &job
|
|
}
|
|
if err := ix.writeValidation(ctx, ready, n, v, own); err != nil {
|
|
return err
|
|
}
|
|
if v.Failed != "" {
|
|
ix.sayValidation(ctx, ready, v)
|
|
return nil
|
|
}
|
|
if err := ix.ValidationAsks.Put(ctx, ask); err != nil {
|
|
return fmt.Errorf("the validation ask %s of the revision at %d could not be put: %w", ask.ID, v.Seq, err)
|
|
}
|
|
ix.logf("take-in: the revision at %d is validated by the ask %s (try %d); it waits for its end", v.Seq, ask.ID, v.Try)
|
|
return nil
|
|
}
|
|
|
|
// revisionHead is the revision's head of repository: the newest merge of it the revision covers, else the commit its entry stands at.
|
|
func revisionHead(ready []PendingEntry, repository, commit string) string {
|
|
var seq uint64
|
|
head := commit
|
|
for _, e := range ready {
|
|
if e.Forge.Merge != "" && strings.EqualFold(e.Forge.Repository, repository) && e.Value.Seq >= seq {
|
|
seq, head = e.Value.Seq, e.Forge.Merge
|
|
}
|
|
}
|
|
return head
|
|
}
|
|
|
|
// writeValidation writes v on ready[n] and takes it from every other value, each by compare-and-set; a new validation clears the
|
|
// value's refusal. A value lost under it is an error: the evaluation is made again.
|
|
func (ix *Indexer) writeValidation(ctx context.Context, ready []PendingEntry, n int, v *takein.Validation, own string) error {
|
|
for i, e := range ready {
|
|
if i != n && e.Value.Validation == nil {
|
|
continue
|
|
}
|
|
p := e.Value
|
|
p.Validation, p.LastWriter = nil, own
|
|
if i == n {
|
|
p.Validation, p.Refused = v, ""
|
|
}
|
|
data, err := encodePending(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ix.Pending.Update(ctx, e.Key, data, e.Revision); err != nil {
|
|
return fmt.Errorf("%s.%s: %w", PendingState, e.Key, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sayValidation says the revision's wait in take-in.pending with its refusal or why it could not validate.
|
|
func (ix *Indexer) sayValidation(ctx context.Context, ready []PendingEntry, v *takein.Validation) {
|
|
t := revisionWait(takein.TakeInPending, ready, "")
|
|
t.Failed = v.Failed
|
|
t.Refused = ready[newestOfRevision(ready)].Value.Refused
|
|
ix.sayTakeIn(ctx, t)
|
|
}
|
|
|
|
// errNoValidationAnswer is an end whose answer is no verdict of both cases.
|
|
var errNoValidationAnswer = errors.New("no answer")
|
|
|
|
// HearValidationEnded takes in the end of a validation ask (§713), heard on the build seat's step-ended through the indexer's own
|
|
// durable consumer: only an end of the indexer's asks outside a pipeline; an end whose ask no pending value names now is dropped,
|
|
// written in the journal. The answer is read from the event's own output or output object (A-247).
|
|
func (ix *Indexer) HearValidationEnded(ctx context.Context, e buildstep.StepEnded) error {
|
|
if e.Asker != ValidationAsker || e.Pipeline != "" || e.Ask == "" || ix.Pending == nil || !ix.pendingOnBus(ctx) {
|
|
return nil
|
|
}
|
|
own := ix.ownBuildNow(ctx)
|
|
ix.pendingMu.Lock()
|
|
defer ix.pendingMu.Unlock()
|
|
entries, err := ReadPending(ctx, ix.Pending, ix.logf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ready := WaitsOf(entries).Ready
|
|
n := -1
|
|
for i, p := range ready {
|
|
if p.Value.Validation != nil && p.Value.Validation.Ask == e.Ask {
|
|
n = i
|
|
}
|
|
}
|
|
if n < 0 {
|
|
ix.logf("take-in: the end of the validation ask %s (%s on %s) is dropped: no pending revision names that ask now", e.Ask, e.Outcome, e.Node)
|
|
return nil
|
|
}
|
|
ready, lost, _, _ := ix.loadNewest(ctx, ready)
|
|
if len(lost) > 0 {
|
|
return fmt.Errorf("the revision the validation ask %s ended for holds a merge that cannot be read", e.Ask)
|
|
}
|
|
n = newestOfRevision(ready)
|
|
v := *ready[n].Value.Validation
|
|
refused, failed := ix.validationAnswer(ctx, ready, e, v)
|
|
p := ready[n].Value
|
|
switch {
|
|
case failed != "":
|
|
v.Failed = failed
|
|
case refused != "":
|
|
p.Refused = refused
|
|
default:
|
|
v.Accepted = v.Seq
|
|
}
|
|
p.Validation, p.LastWriter = &v, own
|
|
data, err := encodePending(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ix.Pending.Update(ctx, ready[n].Key, data, ready[n].Revision); err != nil {
|
|
return fmt.Errorf("%s.%s: %w", PendingState, ready[n].Key, err)
|
|
}
|
|
if v.Accepted == 0 {
|
|
ready[n].Value = p
|
|
ix.sayValidation(ctx, ready, &v)
|
|
ix.logf("take-in: the validation ask %s of the revision at %d ended: refused %q, could not validate %q", e.Ask, v.Seq, refused, failed)
|
|
return nil
|
|
}
|
|
ix.logf("take-in: the validation ask %s accepted the revision at %d", e.Ask, v.Seq)
|
|
if own == "" {
|
|
return nil // the take-in waits for the build this program runs, as every take-in does
|
|
}
|
|
_, _, err = ix.evaluate(ctx, own, false)
|
|
return err
|
|
}
|
|
|
|
// validationAnswer reads the end's answer: the refusal of the revision, or why it could not validate (the step did not pass, the
|
|
// program exited other than 0, its answer is no verdict of both cases, or its request is no longer the one asked).
|
|
func (ix *Indexer) validationAnswer(ctx context.Context, ready []PendingEntry, e buildstep.StepEnded, v takein.Validation) (string, string) {
|
|
if e.Outcome != buildstep.OutcomePassed {
|
|
return "", strings.TrimSpace(e.Outcome + ": " + e.Said)
|
|
}
|
|
// The answer is the program's last line of standard output, inline or kept by the seat as an object (A-254); a program that
|
|
// exited other than 0 ended the step failed, read above.
|
|
out, err := e.ReadOutput(buildstep.AskGo, ix.AskVerb)
|
|
if err != nil {
|
|
return "", "the run's answer could not be read: " + err.Error()
|
|
}
|
|
var verdict composer.Verdict
|
|
if err := json.Unmarshal(out, &verdict); err != nil {
|
|
return "", fmt.Sprintf("%v: the manifest validator's output is no verdict: %v", errNoValidationAnswer, err)
|
|
}
|
|
req, _, err := ix.validationRequest(ctx, branchOfRevision(ready), ready)
|
|
var raw []byte
|
|
if err == nil {
|
|
raw, err = json.Marshal(req)
|
|
}
|
|
if err != nil {
|
|
return "", "the request cannot be made again to read the verdict: " + err.Error()
|
|
}
|
|
if runDigest(raw) != v.Input {
|
|
return "", "the revision's request is no longer the one the ask " + v.Ask + " was asked with"
|
|
}
|
|
refused, err := ValidationRefusal(req, verdict)
|
|
if err != nil {
|
|
return "", err.Error()
|
|
}
|
|
return RefusedWords(refused), ""
|
|
}
|
|
|
|
// ValidateAgainTool asks the validation of a revision that could not validate again by hand (§713).
|
|
const ValidateAgainTool = "indexer_validate_again"
|
|
|
|
// validationWords is the validation's state as the hand ask answers it.
|
|
func validationWords(v *takein.Validation, refused string) string {
|
|
switch {
|
|
case v == nil:
|
|
return "no validation is asked yet"
|
|
case v.Accepted != 0:
|
|
return fmt.Sprintf("accepted at %d (try %d, the ask %s)", v.Accepted, v.Try, v.Ask)
|
|
case v.Failed != "":
|
|
return fmt.Sprintf("could not validate at %d (try %d): %s", v.Seq, v.Try, v.Failed)
|
|
case refused != "":
|
|
return fmt.Sprintf("refused at %d (try %d): %s", v.Seq, v.Try, refused)
|
|
default:
|
|
return fmt.Sprintf("the ask %s is out since %s (try %d at %d)", v.Ask, v.Asked.UTC().Format(time.RFC3339), v.Try, v.Seq)
|
|
}
|
|
}
|
|
|
|
// ValidateAgainByHand is indexer_validate_again answered (§713): the ready revision under key (its oldest merge's m. key, as
|
|
// indexer_pending names it) whose validation could not validate is asked again, the next try; its answer says the ask's state.
|
|
// A revision accepted, refused or whose ask is out is asked nothing: a refusal is asked again only by a later merge of its branch.
|
|
func (ix *Indexer) ValidateAgainByHand(ctx context.Context, key, by, why string) TakenIn {
|
|
r := TakenIn{Module: key}
|
|
if ix.Pending == nil || ix.ValidationAsks == nil {
|
|
r.Answer = "this indexer asks no validation: nothing asked"
|
|
return r
|
|
}
|
|
own := ix.ownBuildNow(ctx)
|
|
ix.pendingMu.Lock()
|
|
defer ix.pendingMu.Unlock()
|
|
entries, err := ReadPending(ctx, ix.Pending, ix.logf)
|
|
if err != nil {
|
|
r.Answer = err.Error()
|
|
return r
|
|
}
|
|
ready := WaitsOf(entries).Ready
|
|
if len(ready) == 0 || ready[0].Key != key {
|
|
r.Answer = fmt.Sprintf("no revision waits under %s: indexer_pending names the waits", key)
|
|
return r
|
|
}
|
|
ready, lost, reasons, _ := ix.loadNewest(ctx, ready)
|
|
if len(lost) > 0 {
|
|
r.Answer = fmt.Sprintf("nothing asked: the message of %s is not read (%s)", lost[0].Key, reasons[lost[0].Key])
|
|
return r
|
|
}
|
|
newest := ready[newestOfRevision(ready)]
|
|
v := newest.Value.Validation
|
|
if v == nil || v.Seq != newest.Value.Seq || v.Failed == "" {
|
|
r.Answer = "nothing asked: the validation is " + validationWords(v, newest.Value.Refused) + "; only one that could not validate is asked again by hand"
|
|
return r
|
|
}
|
|
ix.logf("take-in: the validation of %s is asked again by hand by %s: %s", key, by, why)
|
|
if err := ix.askValidation(ctx, ready, own); err != nil {
|
|
r.Answer = "the validation was not asked again: " + err.Error()
|
|
return r
|
|
}
|
|
after, err := ReadPending(ctx, ix.Pending, ix.logf)
|
|
if err == nil {
|
|
ready = WaitsOf(after).Ready
|
|
}
|
|
if err != nil || len(ready) == 0 {
|
|
r.Answer = "asked again; its state is not read back"
|
|
return r
|
|
}
|
|
n := ready[newestOfRevision(ready)]
|
|
r.Stored = n.Value.Validation != nil && n.Value.Validation.Failed == ""
|
|
r.Answer = fmt.Sprintf("asked again by %s (%s): the validation is %s", by, why, validationWords(n.Value.Validation, n.Value.Refused))
|
|
return r
|
|
}
|