Skip to content

fix(scheduler): prevent concurrent over-allocation and quota leak in Filter (#1330, #1896) - #2523

Closed
AyushSrivastava1818 wants to merge 5 commits into
Project-HAMi:masterfrom
AyushSrivastava1818:fix/filter-concurrency-bug-1330-1896
Closed

fix(scheduler): prevent concurrent over-allocation and quota leak in Filter (#1330, #1896)#2523
AyushSrivastava1818 wants to merge 5 commits into
Project-HAMi:masterfrom
AyushSrivastava1818:fix/filter-concurrency-bug-1330-1896

Conversation

@AyushSrivastava1818

@AyushSrivastava1818 AyushSrivastava1818 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind bug

What this PR does / why we need it

This PR fixes two concurrency and state-consistency issues in the HAMi Kubernetes scheduler's Filter() extender path.

  1. Bug vGPU count on single GPU is over the splitcount limit #1330 — Concurrent capacity check and commit race

    Scheduler.Filter() previously performed Fit / FitQuota checks separately from podManager.AddPod / quotaManager.AddUsage. Since Filter() can be invoked concurrently for different pods, multiple requests could pass the same capacity check before either one committed its allocation.

    This could result in GPU memory, device cores, or quota resources being over-allocated beyond the available capacity.

  2. Bug scheduler: quota usage may not be rolled back when PatchPodAnnotations fails in Filter #1896 — Missing quota rollback on annotation patch failure

    If util.PatchPodAnnotations() failed after the allocation had already been committed, the existing error path could remove the pod allocation without rolling back the corresponding quota usage.

    This left stale quota accounting in memory and could cause subsequent pods to be incorrectly rejected even though the resources were actually available.

Which issue(s) this PR fixes

Fixes #1330
Fixes #1896


Key Technical Changes

  1. Atomic Filter() allocation with filterMutex

    • Added filterMutex sync.Mutex to Scheduler.
    • Protected the complete allocation critical section from stale allocation cleanup through candidate evaluation, node scoring, and AddPod / AddUsage.
    • Structured the allocation flow into tryAllocate() and rollbackAllocation().
  2. Deadlock-safe stale allocation cleanup

    • Split cleanupStalePodAllocation() into:

      • cleanupStalePodAllocationLocked() for callers already holding filterMutex.
      • cleanupStalePodAllocation() as the locking entry point.
    • This avoids recursive locking while keeping the existing cleanup path safe.

  3. Symmetric allocation rollback

    • If PatchPodAnnotations() fails after a successful allocation, rollbackAllocation() now removes the pod from podManager and rolls back its usage from quotaManager under the same mutex.
    • This keeps scheduler and quota state consistent after failed scheduling attempts.
  4. Informer event accounting audit

    • Verified that onAddPod only calls quotaManager.AddUsage() when podManager.AddPod() actually records a new pod.
    • Existing pods are therefore not double-counted during informer resync/events.

Verification — Race Detector

Verified under a CGO-enabled Linux environment (go1.26.5 linux/amd64, 12 logical cores).

The following scheduler tests pass under the Go race detector:

=== RUN   Test_Filter_Bug1896_QuotaRollbackOnPatchFailure
--- PASS: Test_Filter_Bug1896_QuotaRollbackOnPatchFailure (0.45s)

=== RUN   Test_Filter_Bug1330_NoConcurrentOverAllocation
--- PASS: Test_Filter_Bug1330_NoConcurrentOverAllocation (2.29s)

=== RUN   Test_Filter_NonContendingPodsBothEventuallyAdmitted
--- PASS: Test_Filter_NonContendingPodsBothEventuallyAdmitted (0.02s)

=== RUN   Test_Filter_FitFailureReleasesLockCleanly
--- PASS: Test_Filter_FitFailureReleasesLockCleanly (0.01s)

=== RUN   Test_onAddPod_AlreadyTrackedPodDoesNotDoubleCountQuota
--- PASS: Test_onAddPod_AlreadyTrackedPodDoesNotDoubleCountQuota (0.00s)

PASS
ok github.com/Project-HAMi/HAMi/pkg/scheduler 3.818s

The tests cover concurrent over-allocation prevention, quota rollback after patch failure, non-contending scheduling, lock release on failed allocation, and duplicate quota accounting during informer events.

AI Disclosure

AI tools like Claude were used during the development of this PR for code review, debugging assistance, and exploring potential concurrency issues.

All proposed changes were manually reviewed, implemented, and verified by the contributor. The fix was validated with the Go race detector and the relevant scheduler tests.

Summary by CodeRabbit

  • Bug Fixes

    • Improved support for nodes with names exceeding Kubernetes label limits.
    • Prevented scheduler race conditions that could cause over-allocation or duplicate quota accounting.
    • Ensured failed allocation updates correctly roll back capacity and quota changes.
    • Preserved independent pod admissions during concurrent scheduling.
  • Reliability

    • Added deterministic sanitization for node labels while preserving valid names.
    • Improved handling of missing pods and annotation updates.

…hPodAnnotations

Kubernetes node names can be up to 253 characters (RFC 1123 DNS subdomain),
but label values are hard-limited to 63 characters and must match
(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?. PatchPodAnnotations was
previously copying the raw node name from annotations[AssignedNodeAnnotations]
directly into a pod label, which caused the Kubernetes API server to reject
the entire patch request with a validation error whenever the node name
exceeded 63 characters -- breaking scheduling for any pod bound to such a node.

Fix:
- Use k8s.io/apimachinery/pkg/util/validation.IsValidLabelValue to validate
  the node name before setting label[AssignedNodeAnnotations].
- If valid (the common case for short node names), set the label as before.
- If invalid (e.g. node name > 63 chars or illegal characters), skip setting
  that label and emit a klog.Warningf noting the pod, node name, and reason.
- Annotation patching is unchanged in all cases.

Tests:
- Extended TestPatchPodAnnotations in pkg/util/util_test.go with three new
  table-driven cases:
  * short (valid) node name -> label set correctly
  * node name > 63 chars -> patch succeeds, annotation set, label absent
  * node name with invalid characters (<= 63 chars) -> same as above

All existing pkg/util tests continue to pass.

Signed-off-by: AyushSrivastava1818 <ayush.sri0705@gmail.com>
…arification comment

Signed-off-by: AyushSrivastava1818 <ayush.sri0705@gmail.com>
…nd vGPUmonitor pod lister

- Implement exported helper util.SafeLabelValue(value string) string that deterministically derives a valid k8s label value (<= 63 chars) using a prefix + sha256 hash.
- Update PatchPodAnnotations to ALWAYS set label[AssignedNodeAnnotations] = SafeLabelValue(v) (never skipping the label). Log V(4) info when derived value differs from original.
- Update cmd/vGPUmonitor/metrics.go (collectPodAndContainerInfo and collectPodAndContainerMigInfo) to wrap
odeName with util.SafeLabelValue(nodeName) when constructing the pod lister selector.
- Update unit tests in pkg/util/util_test.go and cmd/vGPUmonitor/metrics_test.go to assert label presence and write/read selector consistency for long node names.

Signed-off-by: AyushSrivastava1818 <ayush.sri0705@gmail.com>
…ect-HAMi#1330, Project-HAMi#1896)

- Protect Filter() critical section with filterMutex across capacity check and allocation commit to prevent concurrent over-allocation (Project-HAMi#1330).
- Roll back both podManager and quotaManager allocations when PatchPodAnnotations fails (Project-HAMi#1896).
- Add regression unit tests and BenchmarkFilter.

Signed-off-by: AyushSrivastava1818 <ayush.sri0705@gmail.com>
@hami-robot hami-robot Bot added kind/bug Something isn't working dco-signoff: yes labels Aug 9, 2026
@hami-robot
hami-robot Bot requested review from FouoF and chaunceyjiang August 9, 2026 20:06
@hami-robot

hami-robot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: AyushSrivastava1818
Once this PR has been reviewed and has the lgtm label, please assign wawa0210 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/XL label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 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: 08f41029-54db-496b-8dcb-d0ccbbcf7299

📥 Commits

Reviewing files that changed from the base of the PR and between 110efe2 and 8c079b9.

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

📝 Walkthrough

Walkthrough

The change adds deterministic Kubernetes label sanitization for node names and applies it to metric selectors and pod annotations. It also serializes scheduler allocation state, restores quota usage after annotation failures, and adds concurrency and regression tests.

Changes

Allocation consistency

Layer / File(s) Summary
Safe node labels
pkg/util/util.go, pkg/util/util_test.go, cmd/vGPUmonitor/metrics.go, cmd/vGPUmonitor/metrics_test.go
SafeLabelValue preserves valid values and hashes invalid or overlength values. Pod annotations and metric selectors use the converted node name.
Atomic scheduler allocation
pkg/scheduler/scheduler.go
filterMutex serializes allocation and cleanup state. Filter patches annotations outside the lock and rolls back pod and quota state when patching fails.
Scheduler regression coverage
pkg/scheduler/scheduler_test.go
Tests cover rollback, concurrent admission, fit failures, independent admissions, benchmark workloads, and duplicate quota accounting.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant podManager
  participant quotaManager
  participant KubernetesAPI
  Scheduler->>podManager: select and record device allocation
  Scheduler->>quotaManager: record quota usage
  Scheduler->>KubernetesAPI: patch pod annotations
  KubernetesAPI-->>Scheduler: patch result
  alt patch succeeds
    Scheduler-->>Scheduler: report selected node
  else patch fails
    Scheduler->>podManager: remove allocation
    Scheduler->>quotaManager: remove quota usage
    Scheduler-->>Scheduler: return patch error
  end
Loading

Possibly related issues

  • Project-HAMi/HAMi#2522 — Directly covers the atomic allocation, concurrency locking, and quota rollback changes.

Possibly related PRs

Suggested reviewers: fouof, chaunceyjiang, peachest

Poem

A rabbit counts each GPU share,
And locks the path when pods compete.
Long node names become labels fair,
Failed patches clear the quota slate.
The scheduler keeps its state complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The SafeLabelValue changes and vGPU monitor selector updates are not related to the objectives of issues #1330 or #1896. Move the SafeLabelValue, annotation-label sanitization, and vGPU monitor selector changes to a separate pull request.
✅ 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 primary fixes: preventing concurrent over-allocation and quota leaks in the scheduler Filter path.
Linked Issues check ✅ Passed The changes address synchronized allocation for #1330 and quota rollback after annotation patch failure for #1896, with matching regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 peachest August 9, 2026 20:06

@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: 3

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

2687-2689: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the quota assertion the doc comment promises.

The function comment states that the test verifies no phantom state in podManager or quotaManager. The body only checks podManager. Add the matching quota check.

💚 Proposed addition
 	// Ensure no phantom allocation was created in podManager.
 	_, inCache := s.podManager.GetPod(pod)
 	require.False(t, inCache, "pod must not be recorded in podManager cache when Fit fails")
+
+	// Ensure no phantom quota usage was recorded.
+	for _, dq := range s.quotaManager.GetResourceQuota() {
+		for name, q := range *dq {
+			require.Equal(t, int64(0), q.Used, "quota %s must remain unused when Fit fails", name)
+		}
+	}
🤖 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 2687 - 2689, Add the missing
quotaManager assertion in the test alongside the existing podManager check,
using the relevant quota lookup to verify no phantom quota allocation is
recorded when Fit fails. Preserve the existing podManager assertion and failure
message style.

2462-2466: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Skip the 100-iteration loop in short mode.

Each iteration builds a scheduler, a fake clientset, an informer factory, and five pods, then runs five goroutines. Under -race this adds significant time to the package test run. The repository test convention uses -short.

♻️ Proposed change
 	const (
 		ns          = "race-test-ns"
 		concurrency = 5   // pods racing to claim the only GPU slot
 		iterations  = 100 // repeat to expose statistical races
 	)
+
+	loops := iterations
+	if testing.Short() {
+		loops = 5
+	}

Then use loops as the loop bound.

As per coding guidelines, "Unit tests should be runnable with the race detector and repository test conventions, such as go test ... -short --race -count=1."

🤖 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 2462 - 2466, The race test’s
fixed 100-iteration loop should honor Go short-test mode. In the test around
constants ns, concurrency, and iterations, derive a loops bound that skips the
iterations when testing.Short() is true, then use loops as the loop bound while
preserving the existing full iteration count otherwise.

Source: Coding guidelines


2742-2800: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move pod construction and creation out of the timed region.

Both sub-benchmarks build a corev1.Pod and call Pods().Create() inside the measured loop. The fake clientset Create performs a deep copy, takes the object-tracker lock, and walks the reactor chain. That cost is attributed to Filter(). Under RunParallel the tracker lock also adds contention that is unrelated to filterMutex, so the benchmark cannot show the lock throughput it is meant to measure.

Pre-create the pods into a slice before b.ResetTimer() and index into it inside the loop.

A second point: every successful Filter() commits an allocation into podManager, so getNodesUsage cost grows with b.N. The benchmark is therefore not measuring steady state. Consider clearing the pod cache periodically or documenting this in the comment.

🤖 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 2742 - 2800, Move pod
construction and fake-clientset creation out of the timed sections of both
“Uncontended” and “ContendedParallel” benchmarks by pre-creating pods in slices
before b.ResetTimer(), then index the appropriate pod inside each measured
iteration. Prevent podManager state from growing across iterations by
periodically clearing the pod cache, or document the intentional accumulation if
that behavior must remain.

2395-2403: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the patch failure explicit with a reactor.

Replace the implicit fake-clientset "missing pod causes NotFound" setup with an explicit PrependReactor("patch", "pods", ...). Add a t.Cleanup to 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 2395 - 2403, Update the test
setup around the fake KubeClient to register an explicit PrependReactor for
patch operations on pods that returns the intended failure, rather than relying
on a missing pod to produce NotFound. Add t.Cleanup to close s.stopCh after the
test, while preserving the existing informer setup and failure scenario.
pkg/scheduler/scheduler.go (4)

1028-1037: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the bestNode dereference.

The caller treats err == nil && filterRes == nil as proof that bestNode is non-nil. tryAllocate satisfies this contract today. A future edit that adds an early return nil, nil, nil path would produce a nil-pointer panic at Line 1036. Add an explicit check.

🛡️ Proposed guard
 	filterRes, bestNode, err := s.tryAllocate(args, resourceReqs)
 	if err != nil || filterRes != nil {
 		return filterRes, err
 	}
+	if bestNode == nil {
+		return nil, fmt.Errorf("tryAllocate returned no node for pod %s/%s", args.Pod.Namespace, args.Pod.Name)
+	}
🤖 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 1028 - 1037, Add an explicit nil
check for bestNode after the tryAllocate result handling and before the
klog.InfoS dereference. Preserve the existing return behavior for errors or
filter results, and handle a nil bestNode safely without allowing the scheduling
path to panic.

942-980: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

tryAllocate serializes full node scoring; confirm the scale target.

The mutex now covers getNodesUsage and calcScore for every candidate node. All concurrent Filter() calls are serialized for the full duration of scoring, not only for the commit. This is the correct fix for over-allocation, but it converts filtering into a single-threaded stage. BenchmarkFilter uses 200 nodes; large clusters with many candidate nodes and a high pod arrival rate will queue behind this lock.

If the benchmark shows acceptable latency at your target cluster size, no change is needed. Otherwise consider a two-phase approach: score without the lock, then re-verify fit and commit under the lock, retrying when the re-check fails.

🤖 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 942 - 980, Benchmark tryAllocate and
the Filter path at the target cluster size and pod arrival rate, including the
200-node BenchmarkFilter scenario, to verify whether holding filterMutex across
getNodesUsage and calcScore meets latency requirements. If contention is
unacceptable, refactor tryAllocate into an unlocked scoring phase followed by
locked fit re-validation and allocation commit, retrying when the re-check
fails; otherwise leave the current serialization unchanged.

982-987: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Four call sites re-implement the same locked cleanup sequence. cleanupStalePodAllocationLocked and its wrapper already express "take the pod from podManager and remove its quota usage under filterMutex". Three other sites repeat that sequence inline, and two of them use explicit Unlock() instead of defer, so a panic inside TakeAndDeletePod, RmUsage, or AddUsage would leave filterMutex held and block every later Filter() call.

  • pkg/scheduler/scheduler.go#L982-L987: replace the body of rollbackAllocation with a call to cleanupStalePodAllocation(pod).
  • pkg/scheduler/scheduler.go#L155-L181: replace the terminated-pod branch with s.cleanupStalePodAllocation(pod), and move the AddPod/AddUsage block into a small method that uses defer s.filterMutex.Unlock().
  • pkg/scheduler/scheduler.go#L212-L216: replace the block with s.cleanupStalePodAllocation(pod).
🤖 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 982 - 987, Centralize pod allocation
cleanup and ensure mutexes are released on panic: in pkg/scheduler/scheduler.go
lines 982-987, replace rollbackAllocation’s locked body with
cleanupStalePodAllocation(pod); in lines 155-181, use
cleanupStalePodAllocation(pod) for the terminated-pod branch and move the
AddPod/AddUsage logic into a small method that unlocks filterMutex with defer;
in lines 212-216, replace the repeated cleanup block with
cleanupStalePodAllocation(pod).

982-987: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

rollbackAllocation duplicates cleanupStalePodAllocation.

Both methods take filterMutex and call cleanupStalePodAllocationLocked. The bodies are identical. Keep one implementation, and if the rollback name adds value at the call site, define it as a direct call to cleanupStalePodAllocation.

♻️ Proposed refactor
 // rollbackAllocation rolls back podManager and quotaManager allocations on annotation patch failure (upstream `#1896`).
 func (s *Scheduler) rollbackAllocation(pod *corev1.Pod) {
-	s.filterMutex.Lock()
-	defer s.filterMutex.Unlock()
-	s.cleanupStalePodAllocationLocked(pod)
+	s.cleanupStalePodAllocation(pod)
 }
🤖 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 982 - 987, Remove the duplicated
locking and cleanup implementation from rollbackAllocation, and reuse
cleanupStalePodAllocation directly. If rollbackAllocation remains for call-site
clarity, make it a thin delegation to cleanupStalePodAllocation while preserving
the existing locking behavior in the single shared implementation.
🤖 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 2461-2468: Fix lint issues in pkg/scheduler/scheduler_test.go at
lines 2461-2468, 2691-2697, and 2721-2723: terminate the Run with comment with a
period, gofmt-align the const block, and modernize the iterations loop to range
iterations; preserve the intentional empty critical section by adding a
//lint:ignore SA2001 directive or use filterMutex.TryLock with a matching
Unlock; modernize the nodeCount loop to range nodeCount.
- Around line 2555-2561: Update the admission assertion in the test loop around
admitted.Load() to require exactly one admitted pod, replacing the at-most-one
check while preserving the existing diagnostic message. If a valid execution
path can admit zero pods per iteration, retain the upper-bound assertion and add
an aggregate assertion ensuring at least one pod is admitted across all
iterations.
- Around line 2826-2875: Update the test setup around podDev and onAddPod to
encode podDev with device.EncodePodDevices and attach the resulting annotations
to pod.Annotations, exercising informer device decoding. Assert the second
podManager.AddPod call returns false, quota usage remains unchanged after
onAddPod, and the cached PodInfo.Devices still equals podDev.

---

Nitpick comments:
In `@pkg/scheduler/scheduler_test.go`:
- Around line 2687-2689: Add the missing quotaManager assertion in the test
alongside the existing podManager check, using the relevant quota lookup to
verify no phantom quota allocation is recorded when Fit fails. Preserve the
existing podManager assertion and failure message style.
- Around line 2462-2466: The race test’s fixed 100-iteration loop should honor
Go short-test mode. In the test around constants ns, concurrency, and
iterations, derive a loops bound that skips the iterations when testing.Short()
is true, then use loops as the loop bound while preserving the existing full
iteration count otherwise.
- Around line 2742-2800: Move pod construction and fake-clientset creation out
of the timed sections of both “Uncontended” and “ContendedParallel” benchmarks
by pre-creating pods in slices before b.ResetTimer(), then index the appropriate
pod inside each measured iteration. Prevent podManager state from growing across
iterations by periodically clearing the pod cache, or document the intentional
accumulation if that behavior must remain.
- Around line 2395-2403: Update the test setup around the fake KubeClient to
register an explicit PrependReactor for patch operations on pods that returns
the intended failure, rather than relying on a missing pod to produce NotFound.
Add t.Cleanup to close s.stopCh after the test, while preserving the existing
informer setup and failure scenario.

In `@pkg/scheduler/scheduler.go`:
- Around line 1028-1037: Add an explicit nil check for bestNode after the
tryAllocate result handling and before the klog.InfoS dereference. Preserve the
existing return behavior for errors or filter results, and handle a nil bestNode
safely without allowing the scheduling path to panic.
- Around line 942-980: Benchmark tryAllocate and the Filter path at the target
cluster size and pod arrival rate, including the 200-node BenchmarkFilter
scenario, to verify whether holding filterMutex across getNodesUsage and
calcScore meets latency requirements. If contention is unacceptable, refactor
tryAllocate into an unlocked scoring phase followed by locked fit re-validation
and allocation commit, retrying when the re-check fails; otherwise leave the
current serialization unchanged.
- Around line 982-987: Centralize pod allocation cleanup and ensure mutexes are
released on panic: in pkg/scheduler/scheduler.go lines 982-987, replace
rollbackAllocation’s locked body with cleanupStalePodAllocation(pod); in lines
155-181, use cleanupStalePodAllocation(pod) for the terminated-pod branch and
move the AddPod/AddUsage logic into a small method that unlocks filterMutex with
defer; in lines 212-216, replace the repeated cleanup block with
cleanupStalePodAllocation(pod).
- Around line 982-987: Remove the duplicated locking and cleanup implementation
from rollbackAllocation, and reuse cleanupStalePodAllocation directly. If
rollbackAllocation remains for call-site clarity, make it a thin delegation to
cleanupStalePodAllocation while preserving the existing locking behavior in the
single shared implementation.
🪄 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: 224f4496-34f0-4e6d-bf88-1fca1b9dd875

📥 Commits

Reviewing files that changed from the base of the PR and between 3616313 and 110efe2.

📒 Files selected for processing (6)
  • cmd/vGPUmonitor/metrics.go
  • cmd/vGPUmonitor/metrics_test.go
  • pkg/scheduler/scheduler.go
  • pkg/scheduler/scheduler_test.go
  • pkg/util/util.go
  • pkg/util/util_test.go

Comment thread pkg/scheduler/scheduler_test.go Outdated
Comment on lines +2555 to +2561
admittedCount := admitted.Load()
require.LessOrEqualf(t, admittedCount, int32(1),
"iteration %d: admitted %d pods against a node/quota with capacity for 1 (Bug #1330)",
iter, admittedCount)

close(s.stopCh)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert exactly one admission, not at most one.

require.LessOrEqualf(admittedCount, int32(1), ...) passes when admittedCount is 0. A regression that makes Filter() reject every pod would keep this test green. The node and the quota both have capacity for exactly one pod, and filterMutex serializes the calls, so exactly one pod must be admitted.

💚 Proposed fix
 		admittedCount := admitted.Load()
-		require.LessOrEqualf(t, admittedCount, int32(1),
-			"iteration %d: admitted %d pods against a node/quota with capacity for 1 (Bug `#1330`)",
-			iter, admittedCount)
+		require.Equalf(t, int32(1), admittedCount,
+			"iteration %d: admitted %d pods against a node/quota with capacity for exactly 1 (Bug `#1330`)",
+			iter, admittedCount)

If a legitimate path can admit zero pods, keep LessOrEqual and add a separate assertion that at least one pod was admitted across all iterations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
admittedCount := admitted.Load()
require.LessOrEqualf(t, admittedCount, int32(1),
"iteration %d: admitted %d pods against a node/quota with capacity for 1 (Bug #1330)",
iter, admittedCount)
close(s.stopCh)
}
admittedCount := admitted.Load()
require.Equalf(t, int32(1), admittedCount,
"iteration %d: admitted %d pods against a node/quota with capacity for exactly 1 (Bug `#1330`)",
iter, admittedCount)
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 2555 - 2561, Update the
admission assertion in the test loop around admitted.Load() to require exactly
one admitted pod, replacing the at-most-one check while preserving the existing
diagnostic message. If a valid execution path can admit zero pods per iteration,
retain the upper-bound assertion and add an aggregate assertion ensuring at
least one pod is admitted across all iterations.

Comment on lines +2826 to +2875
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "tracked-pod",
Namespace: ns,
UID: "tracked-uid",
Annotations: map[string]string{
util.AssignedNodeAnnotations: "node1",
},
},
}

podDev := device.PodDevices{
nvidia.NvidiaGPUDevice: device.PodSingleDevice{
{
{
Idx: 0,
UUID: "gpu0",
Type: nvidia.NvidiaGPUDevice,
Usedmem: 2048,
Usedcores: 20,
},
},
},
}

// 1. Add pod to podManager and quotaManager directly (simulating prior Filter allocation).
added := s.podManager.AddPod(pod, "node1", podDev)
require.True(t, added, "initial AddPod must return true for new pod")
s.quotaManager.AddUsage(pod, podDev)

getUsage := func() int64 {
q := s.quotaManager.GetResourceQuota()
if nsq, ok := q[ns]; ok {
if entry, ok := (*nsq)[memResourceName]; ok {
return entry.Used
}
}
return 0
}

initialUsage := getUsage()
require.Greater(t, initialUsage, int64(0), "quota usage should be recorded initially")

// 2. Trigger onAddPod (simulating informer resync or event for already-tracked pod).
s.onAddPod(pod)

usageAfterInformerEvent := getUsage()
require.Equal(t, initialUsage, usageAfterInformerEvent,
"quota usage must NOT be double-counted when onAddPod runs for an already-tracked pod")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm DecodePodDevices behavior for annotations with no device keys,
# and confirm countPodDevices returns empty for empty PodDevices.
ast-grep run --pattern 'func DecodePodDevices($$$) { $$$ }' --lang go pkg/device
ast-grep run --pattern 'func countPodDevices($$$) { $$$ }' --lang go pkg/device
rg -nP --type=go -C3 'func EncodePodDevices' pkg/device

Repository: Project-HAMi/HAMi

Length of output: 532


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)scheduler_test\.go$|(^|/)devices\.go$|(^|/)quota|podManager|addusage' | head -100

echo
echo "== relevant test context =="
sed -n '2700,2890p' pkg/scheduler/scheduler_test.go

echo
echo "== onAddPod implementation =="
rg -n -C4 'func \(s \*Scheduler\) onAddPod|AddUsage|DecodePodDevices|countPodDevices' pkg/scheduler -S

echo
echo "== device APIs outline/sections =="
sed -n '360,490p' pkg/device/devices.go
rg -n -C4 'func countPodDevices|type PodDevices|DecodePodDevices|EncodePodDevices' pkg/device pkg/scheduler devices.go -S

Repository: Project-HAMi/HAMi

Length of output: 25960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== scheduler.go relevant onAddPod section =="
sed -n '120,185p' pkg/scheduler/scheduler.go

echo
echo "== device quota countPodDevices/AddUsage sections =="
sed -n '60,150p' pkg/device/quota.go

echo
echo "== podManager AddPod/GetPod sections =="
rg -n -C4 'func \(m \*.*PodManager\) AddPod|func \(m \*.*PodManager\) GetPod|type .*PodManager' pkg/scheduler pkg/device -S

echo
echo "== existing Issue1368 test context =="
sed -n '1990,2065p' pkg/scheduler/scheduler_test.go

echo
echo "== imports in scheduler_test.go for required helper =="
sed -n '1,80p' pkg/scheduler/scheduler_test.go

Repository: Project-HAMi/HAMi

Length of output: 10114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '54,125p' pkg/device/pods.go
sed -n '430,540p' pkg/device/devices.go

Repository: Project-HAMi/HAMi

Length of output: 5104


Encode device annotations so this test covers the informer path.

onAddPod decodes devices from device.EncodePodDevices(device.SupportDevices, ...), so this test currently passes podDev directly into AddPod without exercising that decode path. Copy the encoded annotations onto the pod, then assert AddPod returns false, quota usage is unchanged, and the cached PodInfo.Devices still matches podDev.

[functional_and_code_integrity]

🤖 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 2826 - 2875, Update the test
setup around podDev and onAddPod to encode podDev with device.EncodePodDevices
and attach the resulting annotations to pod.Annotations, exercising informer
device decoding. Assert the second podManager.AddPod call returns false, quota
usage remains unchanged after onAddPod, and the cached PodInfo.Devices still
equals podDev.

- Add missing periods to comments per godot rules.
- Format range loop statements to use range over int values per modernize.
- Add nolint annotation for SA2001 empty critical section intentional sync point.

Signed-off-by: AyushSrivastava1818 <ayush.sri0705@gmail.com>
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.93151% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/scheduler/scheduler.go 82.69% 6 Missing and 3 partials ⚠️
pkg/util/util.go 88.23% 1 Missing and 1 partial ⚠️
Flag Coverage Δ
unittests 64.40% <84.93%> (+1.02%) ⬆️

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

Files with missing lines Coverage Δ
cmd/vGPUmonitor/metrics.go 43.87% <100.00%> (+0.38%) ⬆️
pkg/util/util.go 74.26% <88.23%> (+0.95%) ⬆️
pkg/scheduler/scheduler.go 71.07% <82.69%> (+1.87%) ⬆️

... and 7 files with indirect coverage changes

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

@FouoF

FouoF commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The quota rollback after a failed annotation patch is a real local-state bug, but the current branch is not a reviewable fix for it. It fully stacks #2516, serializes all Filter calls with a process-local mutex that cannot solve the multi-scheduler-replica case, and changes the success-message score list. Please remove the #2516 and concurrency changes and retain only the symmetric #1896 quota rollback with a focused regression test. The #1330 cross-replica problem needs a separate supported-topology design rather than this mutex.

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: quota usage may not be rolled back when PatchPodAnnotations fails in Filter vGPU count on single GPU is over the splitcount limit

2 participants