Skip to content

fix(scheduler): preserve concurrent node locks - #2197

Merged
hami-robot[bot] merged 3 commits into
Project-HAMi:masterfrom
ShiroKSH:fix/node-lock-retry-race
Aug 3, 2026
Merged

fix(scheduler): preserve concurrent node locks#2197
hami-robot[bot] merged 3 commits into
Project-HAMi:masterfrom
ShiroKSH:fix/node-lock-retry-race

Conversation

@ShiroKSH

@ShiroKSH ShiroKSH commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

/kind bug

What this PR does / why we need it:

Prevents concurrent scheduler replicas from replacing or clearing a node lock after a Kubernetes API conflict retry.

Which issue(s) this PR fixes:

Fixes none.

Special notes for your reviewer:

SetNodeLock and ReleaseNodeLock previously checked the annotation before entering retry.OnError. After a conflict, the retry fetched a newer Node and patched it without rechecking the lock. An acquisition could overwrite a concurrent holder; a release could remove it.

The retry now treats the annotation observed before patching as a compare-and-set target. Acquisition returns ErrNodeLockContention when a refreshed Node is locked. Release only removes the exact lock value it originally observed. Focused fake-client regression tests cover both conflict paths.

Validation:

  • make verify
  • make test
  • go test ./pkg/util/nodelock -run 'Test(SetNodeLockPreservesConcurrentLockAfterConflict|ReleaseNodeLockPreservesConcurrentLockAfterConflict)$' -count=1 -v -timeout=60s

This is scheduler-only; no device-allocation or in-container isolation path changed.

Does this PR introduce a user-facing change?:

No.

AI assistance disclosure:

AI assistance was used for repository exploration, race analysis, and drafting the focused implementation and tests. I reviewed the affected scheduler path and the validation results, and take responsibility for this contribution.

Summary by CodeRabbit

  • Bug Fixes

    • Improved node lock contention handling with clearer contention errors and no unnecessary retries.
    • Revalidated lock ownership before acquiring or releasing locks to prevent overwriting concurrent changes.
    • Preserved active locks during conflicting updates and improved compatibility with legacy lock records.
    • Updated release reporting so it occurs only when a lock is successfully removed.
  • Tests

    • Expanded coverage for concurrent lock acquisition, release conflicts, restamped locks, and retry behavior.

Signed-off-by: ShiroKSH <kushidashiro@gmail.com>
@hami-robot hami-robot Bot added kind/bug Something isn't working dco-signoff: yes labels Jul 29, 2026
@hami-robot
hami-robot Bot requested review from FouoF and wawa0210 July 29, 2026 13:01
@hami-robot

hami-robot Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Welcome @ShiroKSH! It looks like this is your first PR to Project-HAMi/HAMi 🎉

@hami-robot hami-robot Bot added the size/L label Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 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: 85709f59-de0f-4f47-9bb3-903a5096988e

📥 Commits

Reviewing files that changed from the base of the PR and between 31aafbe and e4c25be.

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

📝 Walkthrough

Walkthrough

Node lock acquisition now reports contention and re-checks ownership during retries. Release logic avoids clearing changed locks, supports same-pod restamped locks, and adds conflict-focused tests.

Changes

Node lock contention handling

Layer / File(s) Summary
Contention-aware lock acquisition
pkg/util/nodelock/nodelock.go, pkg/util/nodelock/nodelock_test.go
SetNodeLock wraps ErrNodeLockContention, retries only non-contention errors, re-checks annotations, and tests concurrent-lock preservation and non-conflict handling.
Conditional lock release
pkg/util/nodelock/nodelock.go, pkg/util/nodelock/nodelock_test.go
ReleaseNodeLock derives compatible owner matching, clears only the matching annotation, logs after successful release, and tests concurrent, replacement, and restamped locks.

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

Possibly related PRs

Suggested reviewers: dsfans2014, wawa0210

Poem

A rabbit checks each lock with care,
No changed owner disappears there.
Conflicts keep their rightful stay,
Restamped locks clear the proper way.
Patch attempts count, and logs ring clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preventing concurrent scheduler replicas from replacing or clearing node locks.
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.

@coderabbitai
coderabbitai Bot requested a review from DSFans2014 July 29, 2026 13:02

@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.

if you wouldn't mind doing merge commits instead of force pushing, that would make it a bit clearer what (if anything) has changed.

Comment thread pkg/util/nodelock/nodelock.go Outdated
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/util/nodelock/nodelock.go 90.90% 1 Missing and 1 partial ⚠️
Flag Coverage Δ
unittests 64.38% <90.90%> (+1.55%) ⬆️

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

Files with missing lines Coverage Δ
pkg/util/nodelock/nodelock.go 81.57% <90.90%> (+14.41%) ⬆️

... and 4 files with indirect coverage changes

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

Signed-off-by: ShiroKSH <kushidashiro@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/util/nodelock/nodelock_test.go (1)

47-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist the simulated lock state in the fake tracker.

Handled GET reactors return objects only for those requests; they do not write the fake client’s ObjectTracker. The retry reads see the original tracked node (no lock or holderA), so these conflict tests don’t observe the injected lock unless the getter reactor also stores holderB in the tracker when returning it.

Update the tracker in the same place where the synthetic Node is returned:

  • pkg/util/nodelock/nodelock_test.go#L47-L76: persist holderB before allowing the retry GET.
  • pkg/util/nodelock/nodelock_test.go#L93-L121: persist holderB before allowing the retry GET.
  • pkg/util/nodelock/nodelock_test.go#L137-L169: persist restampedLock before allowing the retry GET, so both the retry GET and second merge-patch use the restamped version.
🤖 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/util/nodelock/nodelock_test.go` around lines 47 - 76, Update the GET
reactors in pkg/util/nodelock/nodelock_test.go at lines 47-76 and 93-121 to
persist the synthetic node with holderB in the fake client tracker before
returning it; at lines 137-169, persist the node with restampedLock before
returning it so subsequent GET and merge-patch operations observe the restamped
state. Use the existing clientSet tracker and reactor context in each test
without changing the conflict assertions.
🤖 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/util/nodelock/nodelock.go`:
- Around line 213-215: Update the lock ownership check in the nodelock retry
path so timestamp-only legacy currentLock values must equal lockStr before
proceeding, while retaining suffix matching for owner-encoded locks containing
NodeLockSep. Ensure a concurrently replaced legacy lock returns nil instead of
being cleared.

---

Outside diff comments:
In `@pkg/util/nodelock/nodelock_test.go`:
- Around line 47-76: Update the GET reactors in
pkg/util/nodelock/nodelock_test.go at lines 47-76 and 93-121 to persist the
synthetic node with holderB in the fake client tracker before returning it; at
lines 137-169, persist the node with restampedLock before returning it so
subsequent GET and merge-patch operations observe the restamped state. Use the
existing clientSet tracker and reactor context in each test without changing the
conflict assertions.
🪄 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: c647e1d7-b3ae-416d-9cf4-b0a479b49051

📥 Commits

Reviewing files that changed from the base of the PR and between 2764a58 and 31aafbe.

📒 Files selected for processing (2)
  • pkg/util/nodelock/nodelock.go
  • pkg/util/nodelock/nodelock_test.go

Comment thread pkg/util/nodelock/nodelock.go Outdated
Comment thread pkg/util/nodelock/nodelock.go
Signed-off-by: ShiroKSH <kushidashiro@gmail.com>
@Norway-02

Copy link
Copy Markdown

Looked through the unresolved review threads:

Re: the restamped-lock silent-skip in ReleaseNodeLock (@mesutoezdil's inline comment)

The issue is in this retry block in ReleaseNodeLock:

if skipNodeLockOwnerCheck || !strings.Contains(currentLock, NodeLockSep) {
    if currentLock != lockStr {
        return nil  // silent exit
    }
}

If skipNodeLockOwnerCheck is true, or the lock is legacy (no separator), but the scheduler has since re-stamped the same pod's lock with a new timestamp, currentLock != lockStr is true and the release silently aborts — leaving the node locked indefinitely.

The outer check for owner-encoded locks correctly uses suffix matching (pod ns/name), but this branch compares the exact original lock string, which breaks when the timestamp refreshes.

A straightforward fix would be to apply the same suffix check here:

if !skipNodeLockOwnerCheck {
    if strings.Contains(currentLock, NodeLockSep) {
        if !strings.HasSuffix(currentLock, lockOwner) {
            return nil
        }
    } else if currentLock != lockStr {
        // legacy format: only abort if another holder took over
        return nil
    }
}

This is consistent with CodeRabbit's suggestion that was marked resolved, but mesutoezdil's non-line comment suggests the issue still exists in the current diff.

Re: line 157 (SetNodeLock) — The current code already returns nil when the suffix matches this pod's lock, so that case looks correctly handled in the current version of the PR.

Happy to assist test or iterate on this if helpful.

@Shouren Shouren 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.

Prevents concurrent scheduler replicas from replacing or clearing a node lock after a Kubernetes API conflict retry.

@ShiroKSH HAMi does not currently support active-active scheduler replicas. Its high-availability model uses leader election, so only the leader is expected to perform scheduling and binding while the other replicas remain standby. Therefore, concurrent node-lock updates from multiple scheduler replicas are not expected in a supported deployment, and the scenario this PR aims to fix does not appear to exist.

@mesutoezdil

Copy link
Copy Markdown
Contributor

/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 Aug 3, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: archlitchi, ShiroKSH

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

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.

5 participants