-
Notifications
You must be signed in to change notification settings - Fork 795
fix(util): make same-pod node lock acquisition idempotent #2256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2091,6 +2091,74 @@ func (m *bindLockMockDevice) ReleaseNodeLock(_ *corev1.Node, _ *corev1.Pod) erro | |
| return nil | ||
| } | ||
|
|
||
| // sharedLockMockDevice mirrors backends (e.g. nvidia, ascend, hygon, metax) that | ||
| // all acquire the same node-lock annotation key "hami.io/mutex.lock" through the | ||
| // shared pkg/util/nodelock helper. | ||
| type sharedLockMockDevice struct { | ||
| registerMockDevice | ||
| vendor string | ||
| } | ||
|
|
||
| func (m *sharedLockMockDevice) CommonWord() string { return m.vendor } | ||
| func (m *sharedLockMockDevice) LockNode(n *corev1.Node, p *corev1.Pod) error { | ||
| return nodelockutil.LockNode(n.Name, nodelockutil.NodeLockKey, p) | ||
| } | ||
| func (m *sharedLockMockDevice) ReleaseNodeLock(n *corev1.Node, p *corev1.Pod) error { | ||
| return nodelockutil.ReleaseNodeLock(n.Name, nodelockutil.NodeLockKey, p, false) | ||
| } | ||
|
|
||
| // Test_Bind_MultiDeviceBackendsSharingNodeLock reproduces #2243: a pod requesting | ||
| // two device types whose backends both lock the same node annotation must not | ||
| // contend with itself during Bind. | ||
| func Test_Bind_MultiDeviceBackendsSharingNodeLock(t *testing.T) { | ||
| pod := &corev1.Pod{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "pod-multi-device", Namespace: "default", UID: types.UID("uid-multi-device"), | ||
| }, | ||
| } | ||
| mockA := &sharedLockMockDevice{vendor: "shared-lock-a"} | ||
| mockB := &sharedLockMockDevice{vendor: "shared-lock-b"} | ||
|
|
||
| oldRetry := config.NodeLockRetryTimeout | ||
| config.NodeLockRetryTimeout = 500 * time.Millisecond | ||
| oldDevicesMap := device.DevicesMap | ||
| device.DevicesMap = map[string]device.Devices{mockA.vendor: mockA, mockB.vendor: mockB} | ||
| t.Cleanup(func() { | ||
| config.NodeLockRetryTimeout = oldRetry | ||
| device.DevicesMap = oldDevicesMap | ||
| }) | ||
|
|
||
| s := NewScheduler() | ||
| t.Cleanup(func() { close(s.stopCh) }) | ||
| scheme := runtime.NewScheme() | ||
| _ = corev1.AddToScheme(scheme) | ||
| s.eventRecorder = record.NewBroadcaster().NewRecorder(scheme, corev1.EventSource{}) | ||
|
|
||
| node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node1"}} | ||
| fakeClient := fake.NewSimpleClientset(pod, node) | ||
| s.kubeClient = fakeClient | ||
| client.KubeClient = fakeClient | ||
|
|
||
| // lockAllDevices must succeed when both backends lock the same node for the | ||
| // same pod, and the resulting annotation must reference the locking pod. | ||
| if err := s.lockAllDevices(node, pod); err != nil { | ||
| t.Fatalf("lockAllDevices failed for same-pod multi-backend lock: %v", err) | ||
| } | ||
| nodeAfter, err := fakeClient.CoreV1().Nodes().Get(context.TODO(), node.Name, metav1.GetOptions{}) | ||
| require.NoError(t, err) | ||
| lockValue, ok := nodeAfter.Annotations[nodelockutil.NodeLockKey] | ||
| require.True(t, ok, "node lock annotation must be set") | ||
| if !strings.HasSuffix(lockValue, nodelockutil.NodeLockSep+nodelockutil.GeneratePodNamespaceName(pod, nodelockutil.NodeLockSep)) { | ||
| t.Fatalf("node lock %q does not reference the locking pod", lockValue) | ||
| } | ||
|
|
||
| s.releaseAllDevices(node, pod) | ||
| nodeAfter, err = fakeClient.CoreV1().Nodes().Get(context.TODO(), node.Name, metav1.GetOptions{}) | ||
| require.NoError(t, err) | ||
| _, ok = nodeAfter.Annotations[nodelockutil.NodeLockKey] | ||
| require.False(t, ok, "node lock annotation must be released after all backends finish") | ||
|
Comment on lines
+2155
to
+2159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline pkg/scheduler/scheduler.go --match releaseAllDevices --view expanded
rg -n -A60 -B5 '\bfunc \(.*\) releaseAllDevices\b' pkg/scheduler/scheduler.go
rg -n -A90 -B5 '\bfunc ReleaseNodeLock\b' pkg/util/nodelock/nodelock.goRepository: Project-HAMi/HAMi Length of output: 6813 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,220p' pkg/util/nodelock/nodelock.go
printf '\n--- lock/release node usages ---\n'
rg -n -A20 -B10 '\bReleaseNodeLock\(|\bSetNodeLock\(|NodeLockKey|NodeLockSep|GeneratePodNamespaceName|ParseNodeLock' pkg -g '*.go'
printf '\n--- relevant tests ---\n'
rg -n -A35 -B15 'releaseAllDevices|Node lock annotation must be released|Node lock released|Multiple.*node lock|Multi-device|node lock' pkg/scheduler pkg/util/nodelock -g '*_test.go'Repository: Project-HAMi/HAMi Length of output: 50374 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- nodelock helpers ---'
rg -n -A40 -B5 'func GenerateNodeLockKeyByPod|func GeneratePodNamespaceName|func ParseNodeLock|func ' pkg/util/nodelock/nodelock.go
printf '%s\n' '--- locked release behavior ---'
python3 - <<'PY'
from pathlib import Path
text = Path('pkg/util/nodelock/nodelock.go').read_text()
idx = text.find('func ReleaseNodeLock')
print(text[idx:idx+1800])
PY
printf '%s\n' '--- release lifecycle comment context ---'
sed -n '2130,2160p' pkg/scheduler/scheduler_test.goRepository: Project-HAMi/HAMi Length of output: 13008 Guard shared node-lock release until the pod has no remaining backends.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| var errContention = fmt.Errorf("contended: %w", nodelockutil.ErrNodeLockContention) | ||
|
|
||
| func setupBindLockRetryTest(t *testing.T, retryTimeout time.Duration, pod *corev1.Pod, mock *bindLockMockDevice) (*Scheduler, extenderv1.ExtenderBindingArgs, func()) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,6 +229,14 @@ func LockNode(nodeName string, lockname string, pods *corev1.Pod) error { | |
| return err | ||
| } | ||
|
|
||
| // A lock already held by this same pod (e.g., a multi-device pod whose | ||
| // earlier backend locked the node through the shared annotation key) is an | ||
| // idempotent re-acquisition, not contention. | ||
| if ns != "" && previousPodName != "" && ns == pods.Namespace && previousPodName == pods.Name { | ||
| klog.InfoS("Node lock already held by this pod", "node", nodeName, "podName", pods.Name, "podNamespace", pods.Namespace) | ||
| return nil | ||
| } | ||
|
Comment on lines
+232
to
+238
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Use Pod UID for same-pod ownership. A namespace and name match can identify a replacement Pod after the original Pod is deleted.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| var skipOwnerCheck = false | ||
| if time.Since(lockTime) > NodeLockTimeout { | ||
| klog.InfoS("Node lock expired", "node", nodeName, "lockTime", lockTime, "timeout", NodeLockTimeout) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Restore
client.KubeClientduring cleanup.Line 2140 overwrites the package-global Kubernetes client. The cleanup restores
config.NodeLockRetryTimeoutanddevice.DevicesMap, but not this client. Later tests can use this fake client and its mutated objects.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents