Skip to content

feat(scheduler): retry NodeLock in Bind for PodGroup members - #2066

Merged
hami-robot[bot] merged 4 commits into
Project-HAMi:masterfrom
lin121291:feat/podgroup-nodelock-retry
Jul 17, 2026
Merged

feat(scheduler): retry NodeLock in Bind for PodGroup members#2066
hami-robot[bot] merged 4 commits into
Project-HAMi:masterfrom
lin121291:feat/podgroup-nodelock-retry

Conversation

@lin121291

@lin121291 lin121291 commented Jul 14, 2026

Copy link
Copy Markdown

What type of PR is this?

/kind feature

What this PR does / why we need it:

When multiple members of the same PodGroup are bound concurrently to
the same node, they contend on the hami.io/mutex.lock annotation.
The losing pod fails immediately and waits for kube-scheduler backoff
(~2s). This PR adds a retry loop in Bind so PodGroup pods wait for
the lock to clear (~22ms on real hardware with a device plugin) instead.

  • Add ErrNodeLockContention sentinel in nodelock package
  • Add IsPodGroupMember() based on scheduling.x-k8s.io/pod-group label
  • Add --node-lock-retry-timeout flag (default 28s, align with extender
    httpTimeout in KubeSchedulerConfiguration)
  • Non-PodGroup pod behavior is unchanged

Which issue(s) this PR fixes:
Fixes #1832

Special notes for your reviewer:

The retry timeout (28s) should be less than the extender httpTimeout
configured in KubeSchedulerConfiguration (HAMi chart default: 30s).

Does this PR introduce a user-facing change?:

Yes. New CLI flag --node-lock-retry-timeout (default 28s). No breaking changes.

Summary by CodeRabbit

  • New Features
    • Added --node-lock-retry-timeout to configure how long the scheduler retries node locks for grouped (PodGroup) pods (28s default; 0 disables).
  • Bug Fixes
    • Grouped pods now retry node-lock acquisition on contention until the configured timeout, and release locks correctly on failure.
    • Non-grouped pods and non-contention failures no longer trigger retry behavior.
  • Tests
    • Added unit tests covering contention, retry timing, and error handling for grouped vs non-grouped pods.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds PodGroup-aware node-lock retry configuration and centralized lock handling during scheduler binding. Contention is now programmatically identifiable, with tests covering retries, timeout cleanup, and non-contention errors.

Changes

PodGroup node-lock retry

Layer / File(s) Summary
Lock contention and PodGroup contracts
pkg/util/nodelock/nodelock.go, pkg/util/types.go, pkg/util/util.go
Adds a sentinel contention error, detection helper, PodGroup label constant, and membership helper.
Scheduler lock retry flow
pkg/scheduler/config/config.go, cmd/scheduler/main.go, pkg/scheduler/scheduler.go
Adds retry configuration, centralizes device locking, and retries contended locks for PodGroup members.
Bind retry validation
pkg/scheduler/scheduler_test.go
Tests non-group behavior, retries, timeout cleanup, and non-contention failures.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Bind
  participant Scheduler
  participant DevicesMap
  participant NodeLock
  Bind->>Scheduler: acquireNodeLocks(node, pod)
  Scheduler->>DevicesMap: LockNode for each device
  DevicesMap->>NodeLock: attempt node lock
  NodeLock-->>Scheduler: contention or success
  Scheduler->>DevicesMap: release locks before retry
  Scheduler->>DevicesMap: retry until timeout
  Scheduler-->>Bind: return result
Loading

Suggested reviewers: lengrongfu, mesutoezdil

Poem

A rabbit saw locks in a busy row,
“Try once more when they say no.”
PodGroups hop, contention clears,
Retries bound the waiting fears.
Clean locks, green tests, ears held high!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers the node-lock retry part of #1832 but leaves stale-pending eviction and podManager cleanup unaddressed. Implement the onAddPod stale-annotation eviction and Bind failure podManager cleanup, or mark this PR as retry-only scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main scheduler change.
Out of Scope Changes check ✅ Passed All changes support PodGroup lock-retry behavior or its tests, with no unrelated scope creep evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Add ErrNodeLockContention sentinel and retry loop in Bind
(--node-lock-retry-timeout, default 28s) for PodGroup pods.
Non-PodGroup behavior unchanged.

Signed-off-by: lin121291 <4jp33f9e@gmail.com>
@lin121291
lin121291 force-pushed the feat/podgroup-nodelock-retry branch from 07ba898 to 637b07c Compare July 14, 2026 09:29

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a retry mechanism for node locks when contended by another PodGroup member, allowing configurable timeout and retry behavior. It adds the NodeLockRetryTimeout configuration, helper functions to identify PodGroup members and node lock contentions, and comprehensive unit tests. Feedback on the changes highlights two issues in the retry loop: a potential partial lock leak if a non-contention error or timeout occurs mid-loop, and the blocking of scheduler shutdown due to time.Sleep. A code suggestion was provided to release locks immediately on failure and use a select block with s.stopCh instead of a direct sleep.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/scheduler/scheduler.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
pkg/scheduler/scheduler_test.go (2)

1871-1905: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

cleanup() never stops the informer factory / closes s.stopCh.

informerFactory.Start(s.stopCh) spins up background goroutines, but cleanup() only restores config.NodeLockRetryTimeout and device.DevicesMaps.stopCh is never closed. Each of the 4 new tests leaks its informer goroutines for the remainder of the test binary's life.

♻️ Proposed fix
 	cleanup := func() {
 		config.NodeLockRetryTimeout = oldRetry
 		device.DevicesMap = oldDevicesMap
+		close(s.stopCh)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/scheduler/scheduler_test.go` around lines 1871 - 1905, Update
setupBindLockRetryTest’s cleanup function to stop the scheduler’s informer
goroutines by closing or otherwise shutting down s.stopCh, while preserving the
existing config.NodeLockRetryTimeout and device.DevicesMap restoration. Ensure
cleanup remains safe for each test invocation.

1848-1854: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lint failure: use atomic.Int32 instead of manual int32 + sync/atomic calls.

Static analysis reports a lint failure on lockCalls/releaseCalls. Go 1.19+'s sync/atomic.Int32 type enforces atomic-only access and removes the need for explicit atomic.AddInt32/atomic.LoadInt32 calls.

🔧 Proposed fix using atomic.Int32
 type bindLockMockDevice struct {
 	registerMockDevice
 	lockErr      error
 	lockErrOnce  bool
-	lockCalls    int32
-	releaseCalls int32
+	lockCalls    atomic.Int32
+	releaseCalls atomic.Int32
 }

 func (m *bindLockMockDevice) CommonWord() string { return "bind-lock-mock" }
 func (m *bindLockMockDevice) LockNode(_ *corev1.Node, _ *corev1.Pod) error {
-	n := atomic.AddInt32(&m.lockCalls, 1)
+	n := m.lockCalls.Add(1)
 	if m.lockErr != nil && (!m.lockErrOnce || n == 1) {
 		return m.lockErr
 	}
 	return nil
 }
 func (m *bindLockMockDevice) ReleaseNodeLock(_ *corev1.Node, _ *corev1.Pod) error {
-	atomic.AddInt32(&m.releaseCalls, 1)
+	m.releaseCalls.Add(1)
 	return nil
 }

(callers would then use mock.lockCalls.Load() instead of atomic.LoadInt32(&mock.lockCalls))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/scheduler/scheduler_test.go` around lines 1848 - 1854, Update
bindLockMockDevice to declare lockCalls and releaseCalls as sync/atomic.Int32
values instead of int32 fields, then replace all corresponding atomic.AddInt32
and atomic.LoadInt32 usages with the fields’ Add and Load methods while
preserving existing call-count behavior.

Source: Linters/SAST tools

pkg/scheduler/scheduler.go (1)

760-781: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fixed-interval retry with no jitter risks thundering-herd contention.

Every contending PodGroup member retries every 100ms in lockstep with no jitter/backoff. Since issue #1832's scenario is specifically many gang members binding concurrently, this pattern means all losers wake and re-collide on the same node lock simultaneously, which can itself prolong contention rather than resolve it.

Consider adding jitter (e.g. 100ms + rand(0, 50ms)) or a small exponential backoff capped below the retry deadline.

♻️ Proposed jitter fix
-		s.releaseAllDevices(node, pod)
-		time.Sleep(100 * time.Millisecond)
+		s.releaseAllDevices(node, pod)
+		time.Sleep(100*time.Millisecond + time.Duration(rand.Intn(50))*time.Millisecond)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/scheduler/scheduler.go` around lines 760 - 781, Update acquireNodeLocks
to avoid synchronized retries by replacing the fixed 100ms sleep with a jittered
delay (for example, 100ms plus a random delay up to 50ms) or a small exponential
backoff capped by the remaining NodeLockRetryTimeout. Preserve the existing
contention detection, cleanup, timeout, and successful lock acquisition
behavior.
cmd/scheduler/main.go (1)

79-80: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No safeguard against --node-lock-retry-timeout exceeding the extender's actual httpTimeout.

The flag's help text documents that operators must align this with the extender's httpTimeout, but nothing enforces or warns about it at startup. If misconfigured (retry timeout ≥ extender timeout), the extender's HTTP client can time out mid-retry, while the scheduler goroutine keeps spinning in acquireNodeLocks — kube-scheduler may then treat the bind as failed and reschedule while the original attempt is still contending for the lock.

Consider logging a warning if NodeLockRetryTimeout exceeds some conservative bound (e.g. via a related --http-timeout flag or a hardcoded safety margin), so misconfiguration surfaces early rather than manifesting as intermittent duplicate-bind races.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/scheduler/main.go` around lines 79 - 80, In the scheduler startup flow
that registers and validates `config.NodeLockRetryTimeout`, add an early warning
when this duration exceeds the extender HTTP timeout or its established
conservative bound. Reuse the existing logger and timeout configuration symbols,
and make the warning clearly identify both values while preserving the current
flag defaults and lock-acquisition behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/scheduler/scheduler.go`:
- Around line 754-758: Update Scheduler.releaseAllDevices to capture the error
returned by each val.ReleaseNodeLock(node, pod) call and log release failures
with sufficient context for operators to identify the affected node, pod, and
device. Preserve releasing every device and the existing retry behavior.

---

Nitpick comments:
In `@cmd/scheduler/main.go`:
- Around line 79-80: In the scheduler startup flow that registers and validates
`config.NodeLockRetryTimeout`, add an early warning when this duration exceeds
the extender HTTP timeout or its established conservative bound. Reuse the
existing logger and timeout configuration symbols, and make the warning clearly
identify both values while preserving the current flag defaults and
lock-acquisition behavior.

In `@pkg/scheduler/scheduler_test.go`:
- Around line 1871-1905: Update setupBindLockRetryTest’s cleanup function to
stop the scheduler’s informer goroutines by closing or otherwise shutting down
s.stopCh, while preserving the existing config.NodeLockRetryTimeout and
device.DevicesMap restoration. Ensure cleanup remains safe for each test
invocation.
- Around line 1848-1854: Update bindLockMockDevice to declare lockCalls and
releaseCalls as sync/atomic.Int32 values instead of int32 fields, then replace
all corresponding atomic.AddInt32 and atomic.LoadInt32 usages with the fields’
Add and Load methods while preserving existing call-count behavior.

In `@pkg/scheduler/scheduler.go`:
- Around line 760-781: Update acquireNodeLocks to avoid synchronized retries by
replacing the fixed 100ms sleep with a jittered delay (for example, 100ms plus a
random delay up to 50ms) or a small exponential backoff capped by the remaining
NodeLockRetryTimeout. Preserve the existing contention detection, cleanup,
timeout, and successful lock acquisition behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a5c93dc6-e6af-43ad-8ca1-fb110e11e296

📥 Commits

Reviewing files that changed from the base of the PR and between a1b418c and 637b07c.

📒 Files selected for processing (7)
  • cmd/scheduler/main.go
  • pkg/scheduler/config/config.go
  • pkg/scheduler/scheduler.go
  • pkg/scheduler/scheduler_test.go
  • pkg/util/nodelock/nodelock.go
  • pkg/util/types.go
  • pkg/util/util.go

Comment thread pkg/scheduler/scheduler.go
- Release partial locks before returning on non-contention error or timeout
- Replace time.Sleep with select on stopCh for graceful shutdown
- Use atomic.Int32 in tests (modernize linter)

Signed-off-by: lin121291 <4jp33f9e@gmail.com>
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.68421% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/scheduler/scheduler.go 87.09% 3 Missing and 1 partial ⚠️
pkg/util/util.go 0.00% 4 Missing ⚠️
pkg/util/nodelock/nodelock.go 33.33% 2 Missing ⚠️
Flag Coverage Δ
unittests 59.90% <73.68%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/scheduler/config/config.go 78.15% <ø> (ø)
pkg/util/types.go 100.00% <ø> (ø)
pkg/util/nodelock/nodelock.go 67.16% <33.33%> (-1.02%) ⬇️
pkg/scheduler/scheduler.go 61.34% <87.09%> (+3.60%) ⬆️
pkg/util/util.go 68.48% <0.00%> (-1.71%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread pkg/scheduler/scheduler_test.go
Comment thread pkg/scheduler/scheduler.go Outdated
@mesutoezdil

mesutoezdil commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

and as i see it s only implements the lock retry goal from #1832, the onAddPod stale pending cache eviction and the podManager cleanup on the Bind failure path are still unaddressed,
might be worth changing fixes to part of #1832 so the issue does not auto close?

lin121291 added 2 commits July 15, 2026 23:47
Signed-off-by: lin121291 <4jp33f9e@gmail.com>
…e leak

Signed-off-by: lin121291 <4jp33f9e@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/scheduler/scheduler_test.go`:
- Around line 1879-1883: Update setupBindLockRetryTest cleanup to preserve and
restore the original client.KubeClient value, alongside the existing global
state restoration. Capture the value before the test overwrites it, then reset
client.KubeClient in cleanup before closing s.stopCh.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dcc98415-705e-42a8-80e1-f1223aaf5141

📥 Commits

Reviewing files that changed from the base of the PR and between 40e736e and 8e4282d.

📒 Files selected for processing (2)
  • pkg/scheduler/scheduler.go
  • pkg/scheduler/scheduler_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/scheduler/scheduler.go

Comment thread pkg/scheduler/scheduler_test.go
@mesutoezdil

Copy link
Copy Markdown
Contributor

still asking to change fixes #1832 to part of #1832 in the description since this pr only covers the lock retry goal, the other two goals in that issue are untouched and it would auto close prematurely

@lin121291

Copy link
Copy Markdown
Author

Thanks @mesutoezdil for the review.

On the scope of Fixes #1832: the fixes proposed in the original issue were written before real-hardware testing. The discussion on the issue narrowed things down to just the retry piece:

  • Kind test (gist) showed 5-min pending → this motivated the stale-annotation and Bind-cleanup fixes.
  • Real T4 test (gist) showed the 5-min was a kind artifact (no device plugin → Allocate() never runs). On real hardware Allocate → Release is ~22 ms and a 5-pod gang converges in 13 s via normal kube-scheduler backoff.
  • Consensus reached with @Shouren: only the retry gap is actionable.

The other two fixes also turn out to be already covered by existing self-healing paths in the scheduler (PodManager is UID-keyed and idempotent, Filter clears stale entries at the start of every round, LockNode has dangling-owner detection).

I'll update issue #1832 to reflect this narrower scope with the gist evidence linked, then this PR can stay as Fixes #1832.

Comment thread pkg/scheduler/scheduler_test.go

@mesutoezdil mesutoezdil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@archlitchi archlitchi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@hami-robot

hami-robot Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: archlitchi, lin121291, mesutoezdil

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hami-robot hami-robot Bot added the approved label Jul 17, 2026
@hami-robot
hami-robot Bot merged commit 52e4e7a into Project-HAMi:master Jul 17, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: support Kubernetes Coscheduling Plugin for PodGroup

3 participants