Skip to content

Commit bf610a5

Browse files
authored
fix(validator): fail NCCL cleanup on namespace termination timeout (#2588)
1 parent 2897093 commit bf610a5

2 files changed

Lines changed: 59 additions & 26 deletions

File tree

validators/performance/nccl_all_reduce_bw_constraint.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2634,9 +2634,11 @@ func verifyTransportFromLogs(logs string, variant ncclVariant) error {
26342634
//
26352635
// uid pins the delete to the exact namespace instance runNCCLTrainJob
26362636
// created or reclaimed, so a recreated same-named namespace is left alone
2637-
// instead of silently deleted (see the check below for why it must be
2638-
// non-empty).
2639-
func cleanupNCCLResources(clientset kubernetes.Interface, namespace string, uid types.UID) error {
2637+
// instead of silently deleted.
2638+
//
2639+
// terminationWait bounds how long this waits for the namespace to actually
2640+
// disappear before failing.
2641+
func cleanupNCCLResources(clientset kubernetes.Interface, namespace string, uid types.UID, terminationWait time.Duration) error {
26402642
if uid == "" {
26412643
// Required, not just preferred: the fake client used in tests
26422644
// ignores delete preconditions and would otherwise silently
@@ -2670,18 +2672,18 @@ func cleanupNCCLResources(clientset kubernetes.Interface, namespace string, uid
26702672
fmt.Sprintf("failed to delete NCCL benchmark namespace %q", namespace), err)
26712673
}
26722674

2673-
// Same bound as ensureNamespace's wait on the create side. Only logged
2674-
// on timeout, not returned, since the Delete call above already
2675-
// succeeded, so a slow-but-real teardown (e.g. NVLS's DRA/IMEX
2676-
// finalizers) must not fail an otherwise-passing benchmark just because
2677-
// this observability wait ran out first.
2678-
waitCtx, waitCancel := context.WithTimeout(context.Background(), defaults.InferenceNamespaceTerminationWait)
2675+
// Unlike inference-perf's fixed-name namespace, this run's namespace is
2676+
// never reused by name, so there is no later "next run waits out the
2677+
// prior one's Terminating namespace" safety net to fall back on. A
2678+
// ComputeDomain or RoCE ResourceClaimTemplate stuck on a finalizer here
2679+
// would otherwise leak silently forever, with the log line claiming a
2680+
// clean "Deleted". Fail the check instead so a stuck DRA/IMEX teardown
2681+
// surfaces immediately rather than as an unexplained resource leak an
2682+
// operator has to find by hand.
2683+
waitCtx, waitCancel := context.WithTimeout(context.Background(), terminationWait)
26792684
defer waitCancel()
26802685
if err := waitForNamespaceGone(waitCtx, nsClient, namespace); err != nil {
2681-
slog.Warn("NCCL benchmark namespace did not finish terminating within the wait bound, "+
2682-
"deletion was accepted and its cascading GC continues in the background",
2683-
"namespace", namespace, "error", err)
2684-
return nil
2686+
return err
26852687
}
26862688

26872689
slog.Info("Deleted NCCL benchmark namespace", "namespace", namespace)
@@ -2718,7 +2720,7 @@ func cleanupNCCLRun(clientset kubernetes.Interface, dynamicClient dynamic.Interf
27182720
return benchErr
27192721
}
27202722

2721-
nsErr := cleanupNCCLResources(clientset, namespace, uid)
2723+
nsErr := cleanupNCCLResources(clientset, namespace, uid, defaults.InferenceNamespaceTerminationWait)
27222724
err := foldCleanupError(benchErr, nsErr, "NCCL benchmark succeeded but NCCL resource cleanup failed")
27232725
if nsErr != nil {
27242726
return err

validators/performance/nccl_roce_apply_test.go

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222
"time"
2323

24+
"github.com/NVIDIA/aicr/pkg/defaults"
2425
aicrErrors "github.com/NVIDIA/aicr/pkg/errors"
2526
"github.com/NVIDIA/aicr/validators"
2627
coordinationv1 "k8s.io/api/coordination/v1"
@@ -151,7 +152,7 @@ func testHeldLease(namespace string) *coordinationv1.Lease {
151152
func TestCleanupNCCLResources_ToleratesMissing(t *testing.T) {
152153
const ns = "aicr-nccl-perf-deadbeef"
153154
fakeClient := fake.NewClientset()
154-
if err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID); err != nil {
155+
if err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID, defaults.InferenceNamespaceTerminationWait); err != nil {
155156
t.Fatalf("cleanup of a namespace that was never created should not error, got: %v", err)
156157
}
157158
}
@@ -165,7 +166,7 @@ func TestCleanupNCCLResources_DeletesNamespace(t *testing.T) {
165166
const ns = "aicr-nccl-perf-deadbeef"
166167
fakeClient := fake.NewClientset(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns, UID: testNamespaceUID}})
167168

168-
if err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID); err != nil {
169+
if err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID, defaults.InferenceNamespaceTerminationWait); err != nil {
169170
t.Fatalf("cleanup should not error, got: %v", err)
170171
}
171172

@@ -185,7 +186,7 @@ func TestCleanupNCCLResources_ReturnsErrorOnDeleteFailure(t *testing.T) {
185186
return true, nil, apierrors.NewServiceUnavailable("apiserver is down")
186187
})
187188

188-
err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID)
189+
err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID, defaults.InferenceNamespaceTerminationWait)
189190
if err == nil {
190191
t.Fatal("expected an error from a non-NotFound namespace delete failure, got nil")
191192
}
@@ -203,7 +204,7 @@ func TestCleanupNCCLResources_RejectsEmptyUID(t *testing.T) {
203204
const ns = "aicr-nccl-perf-deadbeef"
204205
fakeClient := fake.NewClientset(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns, UID: testNamespaceUID}})
205206

206-
err := cleanupNCCLResources(fakeClient, ns, "")
207+
err := cleanupNCCLResources(fakeClient, ns, "", defaults.InferenceNamespaceTerminationWait)
207208
if err == nil {
208209
t.Fatal("expected an error for an empty owning UID, got nil")
209210
}
@@ -236,7 +237,7 @@ func TestCleanupNCCLResources_UIDMismatchPreventsDelete(t *testing.T) {
236237
stderrors.New("uid in precondition does not match uid in record"))
237238
})
238239

239-
if err := cleanupNCCLResources(fakeClient, ns, "wrong-uid"); err != nil {
240+
if err := cleanupNCCLResources(fakeClient, ns, "wrong-uid", defaults.InferenceNamespaceTerminationWait); err != nil {
240241
t.Fatalf("expected a UID mismatch to be treated as already-replaced, got err=%v", err)
241242
}
242243
if _, getErr := fakeClient.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}); getErr != nil {
@@ -289,7 +290,7 @@ func TestCleanupNCCLResources_WaitsForFinalizerHeldNamespace(t *testing.T) {
289290
_ = fakeClient.CoreV1().Namespaces().Delete(context.Background(), ns, metav1.DeleteOptions{})
290291
}()
291292

292-
if err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID); err != nil {
293+
if err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID, defaults.InferenceNamespaceTerminationWait); err != nil {
293294
t.Fatalf("cleanup should succeed once the finalizer clears, got: %v", err)
294295
}
295296
elapsed := time.Since(start)
@@ -306,12 +307,9 @@ func TestCleanupNCCLResources_WaitsForFinalizerHeldNamespace(t *testing.T) {
306307
}
307308
}
308309

309-
// TestWaitForNamespaceGone_TimesOutWhenNeverDeleted guards the bounded-wait
310-
// contract of waitForNamespaceGone itself. If finalizers never clear within
311-
// the deadline, it must return ErrCodeTimeout rather than hang indefinitely
312-
// (cleanupNCCLResources itself only logs this and returns nil). Calls it
313-
// directly with a short local context to avoid the real 5-minute
314-
// production bound.
310+
// TestWaitForNamespaceGone_TimesOutWhenNeverDeleted verifies that
311+
// waitForNamespaceGone returns ErrCodeTimeout, not a hang, when a
312+
// namespace's finalizers never clear before the context deadline.
315313
func TestWaitForNamespaceGone_TimesOutWhenNeverDeleted(t *testing.T) {
316314
const ns = "aicr-nccl-perf-deadbeef"
317315
now := metav1.Now()
@@ -321,6 +319,7 @@ func TestWaitForNamespaceGone_TimesOutWhenNeverDeleted(t *testing.T) {
321319
DeletionTimestamp: &now,
322320
}})
323321

322+
// Short deadline to avoid the real 5-minute production bound.
324323
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
325324
defer cancel()
326325

@@ -332,3 +331,35 @@ func TestWaitForNamespaceGone_TimesOutWhenNeverDeleted(t *testing.T) {
332331
t.Errorf("got %v, want an ErrCodeTimeout-wrapped wait failure", err)
333332
}
334333
}
334+
335+
// TestCleanupNCCLResources_ReturnsErrorOnTerminationTimeout verifies that
336+
// cleanupNCCLResources fails, rather than reporting a false "Deleted",
337+
// when a namespace is still held by a finalizer after the wait bound
338+
// expires.
339+
func TestCleanupNCCLResources_ReturnsErrorOnTerminationTimeout(t *testing.T) {
340+
const ns = "aicr-nccl-perf-deadbeef"
341+
now := metav1.Now()
342+
fakeClient := fake.NewClientset(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{
343+
Name: ns, UID: testNamespaceUID,
344+
}})
345+
fakeClient.PrependReactor("delete", "namespaces", func(k8stesting.Action) (bool, runtime.Object, error) {
346+
// Accept the delete but never actually remove the object, simulating
347+
// a finalizer that never clears within the wait bound below.
348+
held := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{
349+
Name: ns, UID: testNamespaceUID, Finalizers: []string{"kubernetes"}, DeletionTimestamp: &now,
350+
}}
351+
nsGVR := v1.SchemeGroupVersion.WithResource("namespaces")
352+
if err := fakeClient.Tracker().Update(nsGVR, held, ""); err != nil {
353+
return true, nil, err
354+
}
355+
return true, nil, nil
356+
})
357+
358+
err := cleanupNCCLResources(fakeClient, ns, testNamespaceUID, 100*time.Millisecond)
359+
if err == nil {
360+
t.Fatal("expected cleanup to fail when the namespace never finishes terminating, got nil")
361+
}
362+
if !stderrors.Is(err, aicrErrors.New(aicrErrors.ErrCodeTimeout, "")) {
363+
t.Errorf("got %v, want an ErrCodeTimeout-wrapped cleanup failure", err)
364+
}
365+
}

0 commit comments

Comments
 (0)