Runner (A-328): never hold the runner's own pipeline for its new version

A take-in that changes the runner held its own pipeline with the others, so its new version could not be wanted
before the 30-minute bound and its change was judged by the old runner. The runner's own pipeline now runs on the
running runner, as a build module's running version builds its successor; its pass lets the new runner be wanted,
whose running releases the held ones. The bound stays as the failure bound, with its finding.

Builds: design 47 G §29, §65, §270; A-328

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-13 02:12:39 +01:00
parent e557168e6e
commit a90412c7a6
2 changed files with 25 additions and 8 deletions
@@ -56,9 +56,20 @@ func TestPipelinesAreHeldForTheNewRunnerAndReleasedWhenItRuns(t *testing.T) {
if p.Held == nil || p.Held.Fingerprint != "src2:new" || len(p.Steps) != 0 || len(asks.asks) != 0 {
t.Fatalf("held = %+v, steps %d, asks %d", p.Held, len(p.Steps), len(asks.asks))
}
if strings.Join(said, "|") != "wait-held.app.main.c1|wait-held.runner.main.c2" || len(bounds) != 2 || bounds[0] != "src2:new" {
// The runner's own pipeline is never held: it runs on the running runner, so its pass lets the new runner be wanted (A-328).
if p, _, _ := store.Get("main.4.runner"); p.Held != nil || p.Outcome != seat.PipelineOutcomeRunning {
t.Errorf("the runner's own pipeline is held %+v, want it running on the running runner", p.Held)
}
if strings.Join(said, "|") != "wait-held.app.main.c1|pipeline-started.runner.main.c2" || len(bounds) != 1 || bounds[0] != "src2:new" {
t.Errorf("said %v, bounds %v", said, bounds)
}
// A later take-in of the runner's own module while one is held is not held either (A-328).
if n, err := s.TakenIn(catalog.TakeInTaken{Key: "m.3", Branch: "main", Source: 6, Entries: []catalog.TakeInEntry{{Module: "runner", Commit: "c4", Fingerprint: "src2:new"}}}, view); err != nil || n != 1 {
t.Fatalf("a take-in of the runner alone = %d, %v", n, err)
}
if p, _, _ := store.Get("main.6.runner"); p.Held != nil {
t.Errorf("the runner's own later pipeline is held %+v", p.Held)
}
// A later take-in not naming the runner is held for the same version.
if n, err := s.TakenIn(catalog.TakeInTaken{Key: "m.2", Branch: "main", Source: 5, Entries: []catalog.TakeInEntry{{Module: "app", Commit: "c3", Fingerprint: "src2:bb"}}}, view); err != nil || n != 1 {
t.Fatalf("second take-in = %d, %v", n, err)
@@ -72,7 +83,7 @@ func TestPipelinesAreHeldForTheNewRunnerAndReleasedWhenItRuns(t *testing.T) {
}
said = nil
n, err = s.Release(store.memStore, "src2:new", "ran", view)
if err != nil || n != 3 {
if err != nil || n != 2 {
t.Fatalf("release = %d, %v", n, err)
}
p, _, _ = store.Get("main.4.app")
@@ -114,7 +125,7 @@ func TestReleaseVerbStartsTheHeldPipelinesByHand(t *testing.T) {
}
said = nil
a, err := v.Release("operator", seat.ReleaseInput{Why: "the new runner will not come"})
if err != nil || a.Released != 2 {
if err != nil || a.Released != 1 { // the runner's own pipeline was never held (A-328)
t.Fatalf("release = %+v, %v", a, err)
}
if p, _, _ := store.Get("main.4.app"); p.Held != nil || len(p.Steps) == 0 || len(asks.asks) == 0 {
@@ -193,7 +204,7 @@ func TestHeldWaitCountsFromTheBuildEndOfTheAwaitedRunner(t *testing.T) {
if _, err := s.TakenIn(taken, view); err != nil {
t.Fatal(err)
}
if len(waits) != 2 || waits[0].ClockFrom != nil || len(armed) != 0 {
if len(waits) != 1 || waits[0].ClockFrom != nil || len(armed) != 0 { // the runner's own pipeline is never held (A-328)
t.Fatalf("waits %+v, armed %v", waits, armed)
}
// A failed end starts no clock; a done end does.
@@ -204,10 +215,10 @@ func TestHeldWaitCountsFromTheBuildEndOfTheAwaitedRunner(t *testing.T) {
t.Fatalf("heard %q %q %v", m, fp, ok)
}
waits = nil
if n, err := s.Clocked(store.memStore, "runner", "src2:new"); err != nil || n != 2 {
if n, err := s.Clocked(store.memStore, "runner", "src2:new"); err != nil || n != 1 {
t.Fatalf("clocked = %d, %v", n, err)
}
if len(waits) != 2 || waits[0].ClockFrom == nil || !waits[0].ClockFrom.Equal(end) || len(armed) != 1 || armed[0] != "src2:new@2026-11-11T12:00:00Z" {
if len(waits) != 1 || waits[0].ClockFrom == nil || !waits[0].ClockFrom.Equal(end) || len(armed) != 1 || armed[0] != "src2:new@2026-11-11T12:00:00Z" {
t.Errorf("waits %+v, armed %v", waits, armed)
}
// A pipeline held after the end is known says the clock at once.
@@ -122,7 +122,12 @@ func (s *Starter) Start(b Begin) (bool, error) {
if err := s.Fills.refuseStart(key); err != nil {
return false, err
}
if b.Await == "" && s.Awaiting != nil {
// The runner's own pipeline is never held: it runs on the running runner, as a build module's running version builds its
// successor (§270, A-328), so its pass lets the new runner be wanted (§29).
if b.Module != "" && b.Module == s.Self {
b.Await = ""
}
if b.Await == "" && s.Awaiting != nil && b.Module != s.Self {
if fp, ok := s.Awaiting(); ok {
b.Await = fp
}
@@ -403,7 +408,8 @@ func (s *Starter) TakenIn(ev catalog.TakeInTaken, view func(branch string) (Half
half, _ := view(ev.Branch)
started := 0
var first error
// A take-in giving the runner's own module a new fingerprint holds every pipeline it starts for that runner (§65, A-195).
// A take-in giving the runner's own module a new fingerprint holds every other pipeline it starts for that runner (§65,
// A-195, A-328); the runner's own runs now (Start).
await := ""
for _, e := range ev.Entries {
if s.changesSelf(e.Module, e.Fingerprint) {