Skip to content

Commit c3abeb3

Browse files
committed
fix(validator): separate an aborted run from a deployment that never completed
Two residual issues in waitForDeclaredTrainer, both the same class this PR exists to fix — reporting one condition under another's verdict. pollCtx.Done() fires for two reasons that mean opposite things: the 2m poll deadline, and cancellation of the parent context. Only the first means the delivered Trainer never became complete. The second means the run was aborted — catalog timeout, canceled phase, killed Job — and reporting that as a customer deployment defect is the misclassification the Unavailable-to-NotFound swap fixed, reintroduced one level down. The deadline branch now checks ctx.Err() first and returns Timeout for a canceled run. The loop also probed with pollCtx. getTrainerObject checks ctx.Err() at the top of every read and returns Timeout, so a deadline landing while a probe was in flight surfaced as a bare timeout rather than the NotFound-plus-diagnosis this function was built to produce. The probe runs on the parent context now, and the select owns the deadline exclusively, so the same cluster state cannot produce two different verdicts depending on where in the loop the clock ran out. That window was small but widening: the probe makes roughly six sequential Gets, each with its own DiagnosticTimeout, so on a loaded apiserver it can run for seconds. Adds a test for the cancellation path, and notes on the table that the not-declared-and-absent row is covered by e2e rather than being an oversight — it downloads and kustomize-builds the upstream archive. Refs #2297 Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
1 parent b09764c commit c3abeb3

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

validators/performance/trainer_ensure_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ func TestFoldCleanupError_PreservesCleanupCode(t *testing.T) {
247247
// break it: a recipe that never claimed a Trainer must still reuse one that happens
248248
// to be present, exactly as before.
249249
//
250-
// The not-declared + missing row is deliberately absent: it reaches installTrainer,
251-
// which downloads a release archive, so it belongs in an integration test rather
252-
// than here.
250+
// The not-declared + missing row is deliberately absent rather than overlooked: it
251+
// reaches installTrainer, which downloads and kustomize-builds the upstream release
252+
// archive, so it is covered by e2e rather than being unit-testable here.
253253
func TestEnsureTrainerInstalled_RecipeDrivenLifecycle(t *testing.T) {
254254
tests := []struct {
255255
name string
@@ -333,3 +333,32 @@ func withShortTrainerWait(t *testing.T) func() {
333333
trainerInstallWaitTimeout, trainerInstallPollInterval = oldTimeout, oldInterval
334334
}
335335
}
336+
337+
// TestWaitForDeclaredTrainer_CanceledRunIsNotADeploymentDefect pins the distinction
338+
// the poll deadline and parent cancellation would otherwise blur.
339+
//
340+
// Both expire the poll context, but they mean opposite things: the deadline means
341+
// the delivered Trainer never became complete, which is the customer's deployment
342+
// defect this PR exists to surface; cancellation means the run was aborted — a
343+
// catalog timeout, a canceled phase, a killed Job — which is not. Reporting the
344+
// second as the first is the same misclassification that made ErrCodeUnavailable
345+
// wrong for the deadline case.
346+
func TestWaitForDeclaredTrainer_CanceledRunIsNotADeploymentDefect(t *testing.T) {
347+
defer withShortTrainerWait(t)()
348+
client := newTrainerFakeClient()
349+
350+
ctx, cancel := context.WithCancel(context.Background())
351+
cancel()
352+
353+
_, err := waitForDeclaredTrainer(ctx, client)
354+
if err == nil {
355+
t.Fatal("expected an error when the run is canceled")
356+
}
357+
if stderrors.Is(err, aicrErrors.New(aicrErrors.ErrCodeNotFound, "")) {
358+
t.Errorf("canceled run reported as NotFound (%v); an aborted run is not a "+
359+
"failed deployment and must not be filed as one", err)
360+
}
361+
if !stderrors.Is(err, aicrErrors.New(aicrErrors.ErrCodeTimeout, "")) {
362+
t.Errorf("error code = %v, want ErrCodeTimeout", err)
363+
}
364+
}

validators/performance/trainer_lifecycle.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,12 @@ func waitForDeclaredTrainer(ctx context.Context, dynamicClient dynamic.Interface
563563
defer cancel()
564564

565565
for {
566-
install, ok, err := isTrainerInstalled(pollCtx, dynamicClient)
566+
// Probe with the parent context, not pollCtx: getTrainerObject checks
567+
// ctx.Err() at the top of every read and returns Timeout, so a deadline
568+
// landing mid-probe would surface as a bare timeout instead of the
569+
// NotFound-plus-diagnosis this function exists to produce. Letting the
570+
// select below own the deadline keeps the two conditions separable.
571+
install, ok, err := isTrainerInstalled(ctx, dynamicClient)
567572
if err != nil {
568573
return trainerInstall{}, aicrErrors.PropagateOrWrap(err, aicrErrors.ErrCodeInternal,
569574
"failed to check Kubeflow Trainer installation")
@@ -575,6 +580,16 @@ func waitForDeclaredTrainer(ctx context.Context, dynamicClient dynamic.Interface
575580

576581
select {
577582
case <-pollCtx.Done():
583+
// pollCtx expires for two different reasons and they mean opposite
584+
// things. A canceled parent means the run was aborted — catalog timeout,
585+
// phase cancellation, the Job killed — and reporting that as a customer
586+
// deployment defect is the same misclassification the NotFound code
587+
// above exists to avoid, one level down.
588+
if ctx.Err() != nil {
589+
return trainerInstall{}, aicrErrors.Wrap(aicrErrors.ErrCodeTimeout,
590+
"canceled while waiting for the recipe-declared Kubeflow Trainer", ctx.Err())
591+
}
592+
578593
// ErrCodeNotFound, not Unavailable: the read succeeded and the answer was
579594
// "not deployed". Unavailable is this package's code for a transport
580595
// failure — see the decision table on validators.Require — and using it

0 commit comments

Comments
 (0)