The operator's answer: mesh-build-go reading the checker's state inputs broke ADR 0596 §2. The run ask's input now names the verb seat:mesh-delivery-checker.input; the checker's tools serve it from the state inputs (Reader.Input, checked against its digest), and the state is the checker's own again, read by no other module. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
230 lines
9.7 KiB
Go
230 lines
9.7 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
"mesh-delivery-checker/rules"
|
|
)
|
|
|
|
// The checking program (novox/hq ADR 0644 §1, ADR 0656 §5; design 47's bootstrap delivery; issue 1921): every check has
|
|
// the program built from the group's mesh-delivery member head, else from mesh-delivery's trunk, and run in the build
|
|
// seat's sandbox through the seat node-build-go's ask `run`, with every member's head laid for it. It answers the heads'
|
|
// seat list first (`-seats`), which the manifest validator's and the composer's requests carry, and then the check's
|
|
// verdict of the facts the asking gathered (`-verdict`). The running checker asks, keeps and records; it judges nothing.
|
|
|
|
const (
|
|
// InputsStateName is the checker's own state of run inputs and InputsBucket its bucket: every input by the sha256 of
|
|
// its exact bytes, read by the checker alone. InputVerb is the seat verb a run ask's input names
|
|
// (delivery.RunInput.Verb), which the checker's tools serve from that state (Reader.Input): the Go build module asks it
|
|
// as its manifest's invokes grants it and checks the digest (delivery.RunInput.Read), never reading the state (ADR 0596
|
|
// §2; the operator's answer on milestone 47/B0).
|
|
InputsStateName = "inputs"
|
|
InputsBucket = delivery.CheckerModule + "_" + InputsStateName
|
|
InputVerb = "seat:" + catalogue.CheckerSeat + "." + delivery.CheckerInputVerb
|
|
// CheckingProgramDir and CheckingProgramPackage are where in mesh-delivery the checking program is built.
|
|
CheckingProgramDir = "modules/mesh-delivery-checker"
|
|
CheckingProgramPackage = "mesh-delivery-checker/cmd/mesh-checking-program"
|
|
// DefaultBeginBound is how long a run ask may wait for a build node to begin its step before it is put once more, and
|
|
// after that once more before the check fails naming the job: a build module that does not know the job `run` drops
|
|
// it unanswered (the lead's ruling on milestone 47/B0, 2026-11-06).
|
|
DefaultBeginBound = 5 * time.Minute
|
|
)
|
|
|
|
// Inputs keeps a run's input under its digest, exactly its bytes.
|
|
type Inputs interface {
|
|
Keep(ctx context.Context, digest string, body []byte) error
|
|
}
|
|
|
|
func (j *Examiner) beginBound() time.Duration {
|
|
if j.BeginBound > 0 {
|
|
return j.BeginBound
|
|
}
|
|
return DefaultBeginBound
|
|
}
|
|
|
|
// checkingProgram is where this check's checking program is built: the group's mesh-delivery member head, else
|
|
// mesh-delivery's trunk; false when neither is known.
|
|
func (st *state) checkingProgram() (delivery.RunProgram, string, bool) {
|
|
p := delivery.RunProgram{Repository: DeliveryRepository, Dir: CheckingProgramDir, Package: CheckingProgramPackage}
|
|
for _, pull := range st.c.Pulls {
|
|
if pull.Repository == DeliveryRepository {
|
|
p.Commit = pull.Head
|
|
return p, "the group's mesh-delivery head " + pull.Head, true
|
|
}
|
|
}
|
|
if st.j.Trunks != nil {
|
|
p.Commit = st.j.Trunks.Heads()[DeliveryRepository]
|
|
}
|
|
return p, "mesh-delivery's trunk " + p.Commit, p.Commit != ""
|
|
}
|
|
|
|
// runHeads are the members' heads, as the program reads them.
|
|
func (st *state) runHeads() []delivery.RunHead {
|
|
out := make([]delivery.RunHead, 0, len(st.c.Pulls))
|
|
for _, p := range st.c.Pulls {
|
|
out = append(out, delivery.RunHead{Repository: p.Repository, Commit: p.Head})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// runProgram has the checking program built and run with args over body, and answers its output and what judged; an
|
|
// error says why it did not answer: its input over its bound, no build node beginning the job, a step that did not
|
|
// pass, or a program that exited otherwise than 0.
|
|
func (st *state) runProgram(ctx context.Context, args []string, body []byte) (json.RawMessage, *delivery.JudgedBy, error) {
|
|
prog, at, ok := st.checkingProgram()
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("it cannot be built: the trunk head of %s is not known", DeliveryRepository)
|
|
}
|
|
if st.j.Inputs == nil {
|
|
return nil, nil, fmt.Errorf("its input cannot be kept: the checker keeps no state %s", InputsStateName)
|
|
}
|
|
in, err := delivery.RunInputOf(InputVerb, body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := st.j.Inputs.Keep(ctx, in.Digest, body); err != nil {
|
|
return nil, nil, fmt.Errorf("its input could not be kept in %s: %w", InputsStateName, err)
|
|
}
|
|
job := delivery.RunJob{Program: prog, Heads: st.runHeads(), Input: in, Args: args, Reader: "the checking program at " + at}
|
|
var ids []string
|
|
for range 2 {
|
|
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule,
|
|
At: st.j.now().UTC(), Packages: st.packagesOf(DeliveryRepository), Run: &job}
|
|
ids = append(ids, a.ID)
|
|
st.c.Progress.Set(fmt.Sprintf("the run ask %s of the checking program %s", a.ID, strings.Join(args, " ")), "")
|
|
st.c.Asked(a.ID)
|
|
if err := st.j.Asks.Put(ctx, a); err != nil {
|
|
return nil, nil, fmt.Errorf("its run ask could not be put: %w", err)
|
|
}
|
|
waiting := ctx
|
|
if watch := st.c.Watch; watch != nil {
|
|
id := a.ID
|
|
watch(id, nil) // a try names it at once (issue 1890)
|
|
waiting = buildrun.WithStepSeen(ctx, func(s delivery.Step) { watch(id, &s) })
|
|
}
|
|
step, begun, err := st.waitBegun(waiting, a)
|
|
if !begun {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("its run ask %s: %w", a.ID, err)
|
|
}
|
|
if step.State != delivery.StepPassed {
|
|
return nil, nil, fmt.Errorf("its run ask %s ended %s on %s: %s", a.ID, step.State, step.Node, step.Reason)
|
|
}
|
|
var answer delivery.RunAnswer
|
|
if err := json.Unmarshal(step.Result, &answer); err != nil {
|
|
return nil, nil, fmt.Errorf("its run ask %s answered no run answer: %w", a.ID, err)
|
|
}
|
|
if answer.Exit != 0 {
|
|
return nil, nil, fmt.Errorf("it exited %d on %s: %s", answer.Exit, step.Node, answer.Said)
|
|
}
|
|
return answer.Output, &delivery.JudgedBy{Program: prog, Key: answer.Key, Built: answer.Built, Heads: job.Heads}, nil
|
|
}
|
|
return nil, nil, fmt.Errorf("no build node began the job run of the asks %s within %s each: a build module that does not know the job run drops it",
|
|
strings.Join(ids, " and "), st.j.beginBound())
|
|
}
|
|
|
|
// waitBegun waits for a's step, and stops waiting, begun false, when no build node began it within the begin bound: a
|
|
// bound on the wait's context, no timer of its own.
|
|
func (st *state) waitBegun(ctx context.Context, a delivery.StepAsk) (delivery.Step, bool, error) {
|
|
wait, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
begin, stop := context.WithTimeout(ctx, st.j.beginBound())
|
|
defer stop()
|
|
seen := make(chan struct{})
|
|
var once sync.Once
|
|
type ended struct {
|
|
step delivery.Step
|
|
err error
|
|
}
|
|
done := make(chan ended, 1)
|
|
go func() {
|
|
step, err := st.j.Asks.Wait(buildrun.WithStepSeen(wait, func(delivery.Step) { once.Do(func() { close(seen) }) }), a)
|
|
done <- ended{step, err}
|
|
}()
|
|
select {
|
|
case e := <-done:
|
|
return e.step, true, e.err
|
|
case <-seen:
|
|
case <-begin.Done():
|
|
if ctx.Err() != nil {
|
|
return delivery.Step{}, true, ctx.Err()
|
|
}
|
|
return delivery.Step{}, false, nil
|
|
}
|
|
e := <-done
|
|
return e.step, true, e.err
|
|
}
|
|
|
|
// seatList asks the checking program for the heads' seat list, which the validator's and the composer's requests carry
|
|
// (ADR 0636 §4); the list and what it left out are the check's notes. A list it cannot make fails the check, naming why,
|
|
// and answers false: the check ends there, with no verdict asked of a program that could not answer its list (review
|
|
// round 1 of milestone 47/B0: a build node with no job run would hold the check two begin bounds more).
|
|
func (st *state) seatList(ctx context.Context) bool {
|
|
if st.narrowTry() {
|
|
return true
|
|
}
|
|
body, err := json.Marshal(st.factsHead())
|
|
if err == nil {
|
|
var out json.RawMessage
|
|
if out, _, err = st.runProgram(ctx, []string{"-seats"}, body); err == nil {
|
|
var a rules.SeatsAnswer
|
|
if err = json.Unmarshal(out, &a); err == nil && a.V != rules.SeatsVersion {
|
|
err = fmt.Errorf("it answered a seat list of version %d, not %d", a.V, rules.SeatsVersion)
|
|
}
|
|
if err == nil {
|
|
st.c.Seats = a.Seats
|
|
st.c.note(fmt.Sprintf("the heads' seat list holds %d seat definitions", len(a.Seats)))
|
|
for _, l := range a.Left {
|
|
st.c.note("the heads' seat list leaves out " + l.Path + ": " + l.Why)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
st.fail(goModule, "the heads' seat list could not be made by the checking program: "+err.Error())
|
|
return false
|
|
}
|
|
|
|
// factsHead are the facts that name the check, without what the asking gathered.
|
|
func (st *state) factsHead() rules.Facts {
|
|
f := rules.Facts{V: rules.FactsVersion, Group: st.c.Group, Heads: st.c.Heads, Trunk: st.c.Trunk}
|
|
for _, p := range st.c.Pulls {
|
|
f.Members = append(f.Members, rules.Member{Repository: p.Repository, Number: p.Number, Head: p.Head, Trunk: p.Trunk})
|
|
}
|
|
return f
|
|
}
|
|
|
|
// judged is the checking program's answer to the check's facts (`-verdict`), with what judged; a program that cannot be
|
|
// asked, built or run, or does not answer a verdict, fails the check naming the checking program, never the running
|
|
// checker's rules in its place: the facts gain the failure and the verdict is worded from them as a failure.
|
|
func (st *state) judged(ctx context.Context) (rules.Answer, *delivery.JudgedBy) {
|
|
body, err := json.Marshal(st.factsOf())
|
|
var by *delivery.JudgedBy
|
|
if err == nil {
|
|
var out json.RawMessage
|
|
if out, by, err = st.runProgram(ctx, []string{"-verdict"}, body); err == nil {
|
|
var a rules.Answer
|
|
if err = json.Unmarshal(out, &a); err == nil && a.V != rules.AnswerVersion {
|
|
err = fmt.Errorf("it answered a verdict of version %d, not %d", a.V, rules.AnswerVersion)
|
|
}
|
|
if err == nil {
|
|
return a, by
|
|
}
|
|
}
|
|
}
|
|
st.fail(goModule, "the check could not be judged by the checking program: "+err.Error())
|
|
return rules.Answer{V: rules.AnswerVersion, Verdict: rules.Judge(st.factsOf())}, by
|
|
}
|