Skip to content

fix(scheduler): exponential backoff for acquireNodeLocks retries - #2594

Closed
Brijesh-Thakkar wants to merge 3 commits into
Project-HAMi:masterfrom
Brijesh-Thakkar:fix/acquireNodeLocks-backoff-jitter
Closed

fix(scheduler): exponential backoff for acquireNodeLocks retries#2594
Brijesh-Thakkar wants to merge 3 commits into
Project-HAMi:masterfrom
Brijesh-Thakkar:fix/acquireNodeLocks-backoff-jitter

Conversation

@Brijesh-Thakkar

@Brijesh-Thakkar Brijesh-Thakkar commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR changes the pod allocation lock retry strategy from a fixed 100ms delay to an exponential backoff with jitter.

Under contention, a fixed retry interval causes multiple pods to retry at the same time, increasing API-server load and creating unnecessary contention. Exponential backoff spreads retries over time, reducing synchronized retries and improving allocation latency as the number of contending pods increases.

Changes

  • Replace the fixed 100ms lock retry interval with exponential backoff.

  • Add jitter to retry delays to reduce synchronized retry behavior.

  • Preserve the existing lock ownership semantics and retry behavior.

  • Update the benchmark to accurately model lock acquisition and release under contention.

Benchmark Results

Update (post-review): An earlier version of the benchmark had a mock bug where losing pods could clear the winning pod's lock — this was caught by CodeRabbit. After fixing the lock-ownership semantics in the test mock, the corrected results confirm that this is a genuine latency improvement under realistic contention, rather than simply an API-load tradeoff.

Contending pods | Fixed 100ms (before) | Exp backoff (after) | Δ -- | -- | -- | -- 2 | 105.6 ms | 132.5 ms | +25% 4 | 306.5 ms | 255.0 ms | -17% 8 | 708.2 ms | 535.5 ms | -24% 16 | 1,511 ms | 1,163 ms | -23%

The results show that while exponential backoff introduces a small overhead at very low contention (2 pods), it provides a significant latency reduction once contention becomes realistic (4+ pods), with improvements of approximately 17–24%.

Why

With a fixed retry interval, contending pods tend to retry simultaneously:

Pod A ──retry──retry──retry──retry──>
Pod B ──retry──retry──retry──retry──>
Pod C ──retry──retry──retry──retry──>

This creates synchronized bursts of lock requests.

With exponential backoff and jitter, retries become more distributed:

Pod A ──retry──────retry────────retry──>
Pod B ─────retry────────retry──────────>
Pod C ─────────retry──────retry────────>

This reduces contention on the lock/API server and allows the winning pod to make progress with less interference.

Testing

  • Updated and validated the lock contention benchmark.

  • Fixed the benchmark mock to ensure only the lock owner can release the lock.

  • Verified behavior across multiple contention levels.

  • Confirmed the latency improvement is reproducible under 4+ contending pods.

Fixes #2589

Copilot AI lite review requested due to automatic review settings August 11, 2026 14:37
@hami-robot
hami-robot Bot requested review from DSFans2014 and lengrongfu August 11, 2026 14:37
@hami-robot

hami-robot Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Brijesh-Thakkar
Once this PR has been reviewed and has the lgtm label, please assign dsfans2014 for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found 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 size/L label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8958408f-f4d1-4ecb-aef2-76e4bd9b01eb

📥 Commits

Reviewing files that changed from the base of the PR and between ad89ad9 and 58b20ce.

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

📝 Walkthrough

Walkthrough

The scheduler replaces fixed node-lock retry delays with deadline-bounded exponential backoff, jitter, logging, and a one-second cap. Tests and benchmarks cover timing, retry budgets, desynchronization, and concurrent contention.

Changes

Node-lock retry backoff

Layer / File(s) Summary
Implement bounded retry backoff
pkg/scheduler/scheduler.go
acquireNodeLocks uses exponential delays with jitter, a one-second cap, deadline limiting, contention logging, and shutdown interruption handling.
Validate contention behavior
pkg/scheduler/scheduler_test.go
Timing-aware and shared-lock mocks support tests for backoff limits, retry budgets, jitter desynchronization, timeout behavior, and concurrent contention benchmarks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers: fouof

Sequence Diagram(s)

sequenceDiagram
  participant acquireNodeLocks
  participant node_lock_device
  participant retry_backoff
  acquireNodeLocks->>node_lock_device: Attempt node-lock acquisition
  node_lock_device-->>acquireNodeLocks: Return contention error
  acquireNodeLocks->>retry_backoff: Calculate jittered exponential delay
  retry_backoff-->>acquireNodeLocks: Return deadline-bounded delay
  retry_backoff-->>acquireNodeLocks: Wait or receive shutdown interruption
  acquireNodeLocks->>node_lock_device: Retry acquisition
Loading

Poem

A rabbit checks the node-lock gate,
Then waits with jitter, not too late.
Retry steps grow, but stop at one,
While tests track every hop begun.
Contention fades through timed springs,
And shared locks guard concurrent things.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing fixed acquireNodeLocks retry delays with exponential backoff.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

Replace fixed 100ms retry interval with exponential backoff + jitter,
matching the DefaultStrategy pattern already used in nodelock.go.
Reduces API-server patch volume ~3-4x under PodGroup contention at
the cost of increased per-pod bind latency during active contention.
See PR description for full benchmark tradeoff.

Signed-off-by: Brijesh-Thakkar <brijeshthakkariitian5126@gmail.com>
@coderabbitai
coderabbitai Bot requested a review from FouoF August 11, 2026 14:38
@Brijesh-Thakkar
Brijesh-Thakkar force-pushed the fix/acquireNodeLocks-backoff-jitter branch from 7ecc495 to 6b54181 Compare August 11, 2026 14:38
@Brijesh-Thakkar

Copy link
Copy Markdown
Contributor Author

@archlitchi
Please review this PR
and if any changes or reviews are there please share it
thank you

@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 2499-2507: The contention benchmark mock must model lock ownership
by pod UID. In pkg/scheduler/scheduler_test.go lines 2499-2507, update
sharedLockMockDevice.LockNode and ReleaseNodeLock to record the acquiring pod’s
UID and only clear the holder when the releasing pod owns it; in lines
2483-2484, release the lock after each successful acquisition so benchmark pods
can complete.
🪄 Autofix

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: c096ac99-0e15-436e-9b37-bf50be0e9660

📥 Commits

Reviewing files that changed from the base of the PR and between 634bf2b and 6b54181.

📒 Files selected for processing (2)
  • pkg/scheduler/scheduler.go
  • pkg/scheduler/scheduler_test.go

Comment thread pkg/scheduler/scheduler_test.go Outdated

Copilot AI 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.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Implements exponential backoff + jitter for acquireNodeLocks contention retries (with a 1s cap), adds shutdown-aware waiting, and introduces unit/benchmark coverage to validate timing/budget/desync behavior.

Changes:

  • Replace fixed 100ms retry sleep with exponential backoff (100ms → … → 1s cap) + jitter in acquireNodeLocks
  • Add s.stopCh-aware waiting and V=4 structured logging for contention retries
  • Add tests/benchmarks to validate cap, timeout budget adherence, and jitter desynchronization

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
pkg/scheduler/scheduler.go Adds a reusable backoff policy and integrates it into acquireNodeLocks with jitter + shutdown-aware waiting
pkg/scheduler/scheduler_test.go Adds timing-based tests for the new backoff behavior plus a contention benchmark

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/scheduler/scheduler.go
Comment thread pkg/scheduler/scheduler.go Outdated
Comment on lines +939 to +946
@@ -929,10 +940,17 @@ func (s *Scheduler) acquireNodeLocks(node *corev1.Node, pod *corev1.Pod) error {
return fmt.Errorf("timed out after %v waiting for node %s to be unlocked: %w",
config.NodeLockRetryTimeout, node.Name, nodelockutil.ErrNodeLockContention)
}
delay := backoff.Step()
if remaining := time.Until(deadline); delay > remaining {
delay = remaining
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed. The timeout check now uses time.Until(deadline) <= 0, and delay <= 0 is treated as an immediate timeout instead of falling through to a near-zero sleep. Verified with -race -count=10 with no regressions.

Comment on lines 950 to 954
select {
case <-s.stopCh:
return fmt.Errorf("scheduler shutting down while waiting for node lock: %w", nodelockutil.ErrNodeLockContention)
case <-time.After(100 * time.Millisecond):
case <-time.After(delay):
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair point, but I'm leaving this as-is for this PR. time.After only allocates during active lock contention, not on the hot path, so the churn is bounded to retry scenarios. Happy to revisit with time.NewTimer/Reset if this becomes a concern, but I didn't want to add that complexity for a rarely-hit path in a small PR.

}
results := make([]runResult, numRuns)

for i := range numRuns {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified that this repository targets Go 1.26.5, so range-over-integer is fully supported. No change needed.

func BenchmarkAcquireNodeLocksContention(b *testing.B) {
for _, numPods := range []int{2, 4, 8, 16} {
b.Run(fmt.Sprintf("pods=%d", numPods), func(b *testing.B) {
for range b.N {
device.DevicesMap = map[string]device.Devices{"shared-lock-mock": mock}

done := make(chan struct{})
for i := range numPods {
done <- struct{}{}
}()
}
for range numPods {
Comment on lines +2436 to +2439
// With 50% jitter on a ~400ms base, the spread across 6 runs should be > 50ms.
require.Greater(t, spread, 50*time.Millisecond,
"retry intervals at gap %d should vary due to jitter, but spread was only %v (intervals: %v)",
checkIdx, spread, intervals)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Understand the concern with the wall-clock-based assertion. I ran TestAcquireNodeLocks_BackoffDesync 10x with -race -count=10, and it passed cleanly every time. The 50ms threshold has wide headroom over the ~400ms base with 50% jitter. I'm open to a deterministic RNG-injection approach if preferred, but that would be a larger refactor of the backoff configuration for this PR's scope.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/scheduler/scheduler.go 91.66% 0 Missing and 1 partial ⚠️
Flag Coverage Δ
unittests 62.34% <91.66%> (-0.02%) ⬇️

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

Files with missing lines Coverage Δ
pkg/scheduler/scheduler.go 67.67% <91.66%> (-0.38%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Use time.Until(deadline) <= 0 instead of time.Now().After(deadline) for
the timeout check, and treat delay <= 0 as an immediate timeout rather
than allowing a near-zero sleep to continue the retry loop. Addresses
review feedback from GitHub Copilot.

Signed-off-by: Brijesh-Thakkar <brijeshthakkariitian5126@gmail.com>
Signed-off-by: Brijesh-Thakkar <brijeshthakkariitian5126@gmail.com>
@mesutoezdil

Copy link
Copy Markdown
Contributor

This is being closed because it does not comply with the contribution guidelines.

@Brijesh-Thakkar

Copy link
Copy Markdown
Contributor Author

@mesutoezdil
Thanks for the review. I've updated the PR description above — it had a stale
inconsistency: the original benchmark numbers were invalidated by a mock bug
(fixed after CodeRabbit's feedback), and the corrected numbers actually show a
genuine latency improvement, not just a tradeoff as originally stated. I should
have reconciled the PR description with that update instead of leaving it only
in the issue comments — that's on me. If there's anything else that doesn't
meet guidelines, I'd appreciate specifics so I can address them properly. Happy
to reopen this PR or resubmit if that's preferred.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

scheduler: acquireNodeLocks retry uses fixed 100ms interval, causing thundering herd for PodGroup pods

3 participants