B1g-3: a run's rerun asks with the candidate's catalog half, not the branch's

Builds: design 47 G A-222, A-193, A-205
This commit is contained in:
2026-11-12 06:15:38 +01:00
parent 0cd1ab79e4
commit cbb678f42e
3 changed files with 75 additions and 4 deletions
@@ -9,6 +9,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
catalog "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshpipelinerunner"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -65,5 +66,8 @@ func dropBound(ctx context.Context, bounds jetstream.KeyValue) func(string) erro
// writeVerbs are the verbs over the hearer and the starter, their hand acts recorded through the seat mesh-hand-acts.
func writeVerbs(ctx context.Context, h *pipeline.Hearer, s *pipeline.Starter, view func(string) (pipeline.Half, bool),
calls *process.Conn, bounds jetstream.KeyValue) pipeline.Verbs {
return pipeline.Verbs{Hearer: h, Starter: s, View: view, Acts: meshhandactsclient.Recorder{Ask: calls.Call}, Drop: dropBound(ctx, bounds)}
candidate := func(group, branch string, revision int64) (pipeline.Half, error) {
return readCandidate(calls.CallKept, catalog.CatalogRevisionEvent{Kind: "candidate", Group: group, Branch: branch, Revision: revision})
}
return pipeline.Verbs{Hearer: h, Starter: s, View: view, Candidate: candidate, Acts: meshhandactsclient.Recorder{Ask: calls.Call}, Drop: dropBound(ctx, bounds)}
}
@@ -10,6 +10,7 @@ import (
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshpipelinerunner"
wants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -21,6 +22,9 @@ type Verbs struct {
// Starter asks a step again (rerun), as it asks the steps of a start; View reads a branch's catalog half for the ask.
Starter *Starter
View func(branch string) (Half, bool)
// Candidate reads a run's candidate's catalog half, its group `run-<id>`, on branch at the revision its pipeline was placed
// from, for a rerun of a run's step (A-222): a run's steps are the candidate's, never the branch's.
Candidate func(group, branch string, revision int64) (Half, error)
// Acts records a hand act before the verb does it (rerun, accept, cancel); a nil Acts refuses the verb, nothing recorded.
Acts views.HandActs
// Drop drops the bound of an ask a cancelled pipeline no longer waits on.
@@ -296,15 +300,25 @@ func (v Verbs) Rerun(caller string, in seat.RerunInput) (seat.RerunAnswer, error
// askOf is the ask to put on the serving seat for step s again: the one Place makes for it, under a new id (an ask put twice
// under one id is kept once), saying it is a rerun and by whom.
func (v Verbs) askOf(p seat.Pipeline, s seat.StepResult, caller string, now time.Time) (*Asked, error) {
if v.Starter == nil || v.Starter.Registration == nil || v.Starter.Steps == nil || v.View == nil {
if v.Starter == nil || v.Starter.Registration == nil || v.Starter.Steps == nil || (v.View == nil && p.Run == "") {
return nil, errors.New("the runner cannot ask a step: it holds no registration or no ask")
}
reg, ok := v.Starter.Registration(p.Module)
if !ok {
return nil, fmt.Errorf("%s has no registration", p.Module)
}
half, ok := v.View(p.Branch)
if !ok {
var half Half
if p.Run != "" {
if v.Candidate == nil {
return nil, errors.New("the runner cannot read a run's candidate")
}
var err error
if half, err = v.Candidate(RunGroupPrefix+p.Run, keys.EscapeName(p.Branch), p.Revision); err != nil {
return nil, fmt.Errorf("the candidate %s%s's revision %d is not read, so its step is not asked: %w", RunGroupPrefix, p.Run, p.Revision, err)
}
} else if h, ok := v.View(p.Branch); ok {
half = h
} else {
return nil, fmt.Errorf("the catalog half of %s is not held yet: the step is asked when it is", p.Branch)
}
_, placed := Place(p, reg, half.Entries, now, v.Starter.Name)
@@ -155,6 +155,59 @@ func TestAcceptRerunAndCancelWriteByCompareAndSetAndRecordTheirHandAct(t *testin
}
}
// A-222: a rerun of a run's step asks with the candidate's catalog half (its entries, here the directory), never the branch's,
// and is refused, nothing asked, when the candidate cannot be read.
func TestRerunOfARunAsksWithItsCandidatesHalf(t *testing.T) {
v, store, _, asks, _, _ := verbsFixture()
p, rev, _ := store.Get(pkey)
p.Run, p.Branch, p.Revision = "0a1b2c3d4e5f", "fix/1", 12
for i := range p.Steps {
if p.Steps[i].Step == "lint" && p.Steps[i].Artifact == "app:program" {
p.Steps[i].Outcome = seat.StepResultOutcomeFailed
}
}
store.Update(pkey, p, rev)
_, cand := placeFixture()
e := cand["app"]
e.Path = "candidate/app"
cand["app"] = e
var read struct {
group, branch string
revision int64
}
v.Candidate = func(group, branch string, revision int64) (Half, error) {
read.group, read.branch, read.revision = group, branch, revision
return Half{Revision: revision, Entries: cand}, nil
}
in := seat.RerunInput{Pipeline: pkey, Step: "lint", Artifact: "app:program", Why: "retry"}
if _, err := v.Rerun("operator", in); err != nil {
t.Fatal(err)
}
if b, ok := asks.asks[0].(nodebuildgo.StepAsk); !ok || b.Step == nil || b.Step.Fetch.Dir != "candidate/app" {
t.Errorf("the ask = %+v, want the candidate's directory", asks.asks[0])
}
if read.group != "run-0a1b2c3d4e5f" || read.branch != "fix/1" || read.revision != 12 {
t.Errorf("the candidate read = %+v", read)
}
p, rev, _ = store.Get(pkey)
p.Steps[stepIndex(p, "lint", "app:program")].Outcome = seat.StepResultOutcomeFailed
store.Update(pkey, p, rev)
v.Candidate = func(string, string, int64) (Half, error) { return Half{}, errors.New("no answer") }
n := len(asks.asks)
if _, err := v.Rerun("operator", in); err == nil || len(asks.asks) != n {
t.Errorf("a rerun with no candidate read: %v, asked %d more", err, len(asks.asks)-n)
}
}
func stepIndex(p seat.Pipeline, step, artifact string) int {
for i, s := range p.Steps {
if s.Step == step && s.Artifact == artifact {
return i
}
}
return -1
}
// A hand act the log does not take is a verb that does nothing (ADR 0565 clause 11).
func TestAVerbWhoseHandActIsNotRecordedDoesNothing(t *testing.T) {
v, store, _, _, _, _ := verbsFixture()