Skip to content

Commit 0ec299e

Browse files
Fix node drain deadlock when device plugin holds kernel module
When draining a node running a KMM kernel module with an associated device plugin, a race condition between the Module reconciler and NMC reconciler can cause a deadlock: 1. Module reconciler removes the module from NMC spec (node unschedulable) 2. NMC reconciler creates an unloader pod (orphan status path) 3. The kmm-ready label stays on the node (removeOrphanedLabels requires both spec AND status to be absent, but status persists until unload succeeds) 4. Device plugin DaemonSet pod stays running (nodeSelector still matches and DaemonSet pods tolerate the unschedulable taint) 5. Unloader's modprobe -r fails indefinitely (device file held open) Fix: after processing orphan statuses, check if the node is unschedulable for each orphan module. If so, add the kmm-ready labels to the removal set. This ensures the device plugin pod is evicted, releasing the device file so the unloader can succeed. This fix was written by bug buddy - ai workflow Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9d626f4 commit 0ec299e

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

internal/controllers/nmc_reconciler.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ func (r *NMCReconciler) Reconcile(ctx context.Context, req reconcile.Request) (r
141141
}
142142
}
143143

144+
// Fix drain deadlock: when the Module reconciler removes the NMC spec before the NMC
145+
// reconciler processes the node-taint change, orphan statuses get unloader pods but the
146+
// kmm-ready label stays (removeOrphanedLabels requires status to be absent). This keeps
147+
// the device plugin DaemonSet pod running and holding the device file, deadlocking the
148+
// unloader. Remove kmm-ready labels for orphan statuses on unschedulable nodes so the
149+
// device plugin terminates and the unloader can eventually succeed.
150+
for _, status := range statusMap {
151+
if !r.nodeAPI.IsNodeSchedulable(&node, status.Tolerations) {
152+
readyLabelsToRemove[utils.GetKernelModuleReadyNodeLabel(status.Namespace, status.Name)] = ""
153+
readyLabelsToRemove[utils.GetKernelModuleVersionReadyNodeLabel(status.Namespace, status.Name)] = ""
154+
}
155+
}
156+
144157
// removing label of loaded kmods
145158
if len(readyLabelsToRemove) != 0 {
146159
if err := r.nodeAPI.UpdateLabels(ctx, &node, nil, readyLabelsToRemove); err != nil {

internal/controllers/nmc_reconciler_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,75 @@ var _ = Describe("NodeModulesConfigReconciler_Reconcile", func() {
236236
Expect(node).ToNot(Equal(expectedNode))
237237
})
238238

239+
It("should remove kmod labels for orphan statuses on unschedulable node to prevent drain deadlock", func() {
240+
const (
241+
modNamespace = "test-ns"
242+
modName = "test-module"
243+
)
244+
245+
orphanStatus := kmmv1beta1.NodeModuleStatus{
246+
ModuleItem: kmmv1beta1.ModuleItem{
247+
Namespace: modNamespace,
248+
Name: modName,
249+
},
250+
Config: kmmv1beta1.ModuleConfig{
251+
KernelVersion: "5.14.0",
252+
},
253+
}
254+
255+
nmc := &kmmv1beta1.NodeModulesConfig{
256+
ObjectMeta: metav1.ObjectMeta{Name: nmcName},
257+
Spec: kmmv1beta1.NodeModulesConfigSpec{},
258+
Status: kmmv1beta1.NodeModulesConfigStatus{
259+
Modules: []kmmv1beta1.NodeModuleStatus{orphanStatus},
260+
},
261+
}
262+
263+
node := v1.Node{
264+
ObjectMeta: metav1.ObjectMeta{
265+
Labels: map[string]string{
266+
kmodReadyLabel: "",
267+
labelNotToRemove: "",
268+
},
269+
},
270+
Spec: v1.NodeSpec{
271+
Taints: []v1.Taint{
272+
{
273+
Key: "node.kubernetes.io/unschedulable",
274+
Effect: v1.TaintEffectNoSchedule,
275+
},
276+
},
277+
},
278+
}
279+
280+
contextWithValueMatch := gomock.AssignableToTypeOf(
281+
reflect.TypeOf((*context.Context)(nil)).Elem(),
282+
)
283+
284+
gomock.InOrder(
285+
kubeClient.
286+
EXPECT().
287+
Get(ctx, nmcNsn, &kmmv1beta1.NodeModulesConfig{}).
288+
Do(func(_ context.Context, _ types.NamespacedName, kubeNmc ctrlclient.Object, _ ...ctrlclient.Options) {
289+
*kubeNmc.(*kmmv1beta1.NodeModulesConfig) = *nmc
290+
}),
291+
kubeClient.EXPECT().Get(ctx, types.NamespacedName{Name: nmc.Name}, &v1.Node{}).DoAndReturn(
292+
func(_ context.Context, _ types.NamespacedName, fetchedNode *v1.Node, _ ...ctrlclient.Options) error {
293+
*fetchedNode = node
294+
return nil
295+
},
296+
),
297+
wh.EXPECT().SyncStatus(ctx, nmc, &node),
298+
wh.EXPECT().ProcessUnconfiguredModuleStatus(contextWithValueMatch, nmc, &orphanStatus, &node),
299+
nm.EXPECT().IsNodeSchedulable(&node, nil).Return(false),
300+
nm.EXPECT().UpdateLabels(ctx, &node, nil, map[string]string{kmodReadyLabel: "", kmodVersionReadyLabel: ""}).Return(nil),
301+
)
302+
303+
res, err := r.Reconcile(ctx, req)
304+
Expect(err).ToNot(HaveOccurred())
305+
Expect(res).To(BeZero())
306+
})
307+
239308
It("should process spec entries and orphan statuses", func() {
240309
const (
241310
mod0Name = "mod0"
@@ -304,6 +373,7 @@ var _ = Describe("NodeModulesConfigReconciler_Reconcile", func() {
304373
nm.EXPECT().IsNodeSchedulable(&node, nil).Return(true),
305374
wh.EXPECT().ProcessModuleSpec(contextWithValueMatch, nmc, &spec1, nil, &node),
306375
wh.EXPECT().ProcessUnconfiguredModuleStatus(contextWithValueMatch, nmc, &status2, &node),
376+
nm.EXPECT().IsNodeSchedulable(&node, nil).Return(true),
307377
wh.EXPECT().GarbageCollectInUseLabels(ctx, nmc),
308378
wh.EXPECT().GarbageCollectWorkerPods(ctx, nmc),
309379
wh.EXPECT().UpdateNodeLabels(ctx, nmc, &node).Return(loaded, unloaded, err),
@@ -384,6 +454,7 @@ var _ = Describe("NodeModulesConfigReconciler_Reconcile", func() {
384454
nm.EXPECT().IsNodeSchedulable(&node, nil).Return(true),
385455
wh.EXPECT().ProcessModuleSpec(contextWithValueMatch, nmc, &spec0, &status0, &node).Return(errors.New(errorMeassge)),
386456
wh.EXPECT().ProcessUnconfiguredModuleStatus(contextWithValueMatch, nmc, &status2, &node).Return(errors.New(errorMeassge)),
457+
nm.EXPECT().IsNodeSchedulable(&node, nil).Return(true),
387458
wh.EXPECT().GarbageCollectInUseLabels(ctx, nmc).Return(errors.New(errorMeassge)),
388459
wh.EXPECT().GarbageCollectWorkerPods(ctx, nmc).Return(errors.New(errorMeassge)),
389460
wh.EXPECT().UpdateNodeLabels(ctx, nmc, &node).Return(nil, nil, errors.New(errorMeassge)),

0 commit comments

Comments
 (0)