console: the cut branch of ready checks supersession by stamp as the other waits do
A maintenance round the cut found outstanding and an operator round superseded is abandoned, so restage writes the cut's own round instead of waiting out the allowance. TestOperatorRoundSupersedesTheJobsWait/ready, after the cut (RED before).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+39
−0
internal/console/selfserve_job.go+6 −0internal/e2e/selfserve/job_races_test.go+33 −0
diff --git a/internal/console/selfserve_job.go b/internal/console/selfserve_job.go| index 1ff64be53..aa0f00fd5 100644 |
| --- a/internal/console/selfserve_job.go |
| +++ b/internal/console/selfserve_job.go |
| @@ -1285,6 +1285,12 @@ func (s *Server) selfServeReady(ctx context.Context, t *leaseToken, row *SelfSer |
| _, err := s.selfServeApply(ctx, t, row, request{Kind: "maintain", Phase: ssReady}, nil, selfServeRestageMut(row.ID)) |
| return s.selfServePreOfflineRefusal(row, err, now) |
| } |
| + // An operator round superseded the outstanding one: it is |
| + // abandoned, and the next step finds none and restage writes the |
| + // cut's own round. |
| + if sup, err := s.selfServeSuperseded(row, mv); sup || err != nil { |
| + return err |
| + } |
| ev, err := s.readOfflineEvidence(ctx, row.Host, row.ID, mv) |
| if err != nil { |
| return err |
diff --git a/internal/e2e/selfserve/job_races_test.go b/internal/e2e/selfserve/job_races_test.go| index b49e3d360..e3ce7c221 100644 |
| --- a/internal/e2e/selfserve/job_races_test.go |
| +++ b/internal/e2e/selfserve/job_races_test.go |
| @@ -302,6 +302,39 @@ func TestOperatorRoundSupersedesTheJobsWait(t *testing.T) { |
| supersede(t, w, row.ID, "stage", true) |
| w.phase(row.ID, "ready", 60) |
| }) |
| + t.Run("ready, after the cut", func(t *testing.T) { |
| + // The cut found the maintenance round outstanding and waits on it |
| + // as the cut's first round; an operator round supersedes it. The |
| + // job abandons it and restage writes the cut's own round. |
| + w := newJobWorld(t) |
| + row := w.admit() |
| + w.watch = row.ID |
| + w.phase(row.ID, "ready", 60) |
| + w.advance(11 * time.Minute) |
| + w.down() |
| + w.consoleUntil(5, "a round confirmed", func() bool { |
| + r := w.row(row.ID) |
| + return r.Request.Kind == "maintain" && r.Request.Confirmed && !r.Request.Consumed |
| + }) |
| + if !w.markCut(row.ID) { |
| + t.Fatal("the cut was refused") |
| + } |
| + w.edit(func(rec *provision.Record) error { |
| + return provision.OfflineRestage(row.ID, stamp(rec), provision.Receipt{})(rec) |
| + }) |
| + w.up() |
| + w.boxes() // the stager answers the operator's round only |
| + w.down() |
| + w.consoleUntil(4, "restage", func() bool { return w.row(row.ID).Phase == "restage" }) |
| + if r := w.row(row.ID); r.PhaseStep != "issue" { |
| + t.Fatalf("the superseded round was adopted: %s", describe(r)) |
| + } |
| + w.up() |
| + w.phase(row.ID, "barrier", 20) |
| + if n := w.nonces("stage", "restage"); len(n) != 1 { |
| + t.Fatalf("restage's own rounds: %v", n) |
| + } |
| + }) |
| t.Run("ready", func(t *testing.T) { |
| w := newJobWorld(t) |
| row := w.admit() |