Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/vGPUmonitor/feedback.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ func CheckPriority(utSwitchOn map[string]UtilizationPerDevice, p int, c *nvidia.
return false
}

var observeTestHook func()

func Observe(lister *nvidia.ContainerLister) {
if observeTestHook != nil {
observeTestHook()
}
lister.Lock()

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.

issue #2573 says pod add/update informer events call update() at the same time as observe(). this file only registers deletefunc, and that one only logs, it never touches containers. where does the real concurrent write come from today? the lock is still correct and safe to add either way, just checking the reproduction story is accurate.

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.

The issue description doesn't quite get the informer events right. Update() and Observe() just run sequentially on the ticker loop, so they don't race with each other.

The real race is between Observe() modifying c.Info in the background and Prometheus concurrently scraping the /metrics endpoint. Since Collect() already grabs the lister lock when responding to Prometheus scrapes, grabbing the same lock in Observe() properly synchronizes the writes.

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.

@mesutoezdil I've answered your question inline! Let me know if you need anything else from my end.

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.

pr #2311 hit a real deadlock adding a lock inside listcontainers, because metrics.go already holds the lock when it calls listcontainers. why does this lock here not hit the same issue?

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.

The issue in #2311 happened because metrics.go already held the lock. When it called ListContainers(), adding a lock inside there made the exact same code try to lock it twice, which causes a deadlock.

The lock here is safe because metrics.go never actually calls Observe(). Observe() only runs in its own separate background loop watchAndFeedback. Since they run totally separate from each other, they just wait their turn for the lock. No double-locking can happen here. @mesutoezdil hope you liked it

defer lister.UnLock()

utSwitchOn := map[string]UtilizationPerDevice{}
containers := lister.ListContainers()

Expand Down
44 changes: 44 additions & 0 deletions cmd/vGPUmonitor/feedback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"testing"
"time"

"github.com/Project-HAMi/HAMi/pkg/monitor/nvidia"
)
Expand Down Expand Up @@ -160,3 +161,46 @@ func TestCheckBlocking_MultiDevice(t *testing.T) {
})
}
}

func TestObserve(t *testing.T) {
// Call Observe with an empty lister to cover the missing lines for codecov
// and to ensure no panics occur with locking/unlocking.
lister := &nvidia.ContainerLister{}

// Test that Observe actually acquires the lock deterministically.
lister.Lock()

reachedLock := make(chan struct{})
observeTestHook = func() {
close(reachedLock)
}
defer func() { observeTestHook = nil }()

done := make(chan struct{})
go func() {
Observe(lister)
close(done)
}()

// Wait until Observe reaches the lock boundary
<-reachedLock

// Ensure Observe is now blocked on the lock
select {
case <-done:
t.Fatal("Observe completed while lock was held by another goroutine, indicating it did not acquire the lock!")
default:
// Expected: Observe is blocked
}

// Release the lock
lister.UnLock()

// Now Observe should complete
select {
case <-done:
// Success
case <-time.After(1 * time.Second):
t.Fatal("Observe did not complete after lock was released")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading