Skip to content

Commit a200381

Browse files
committed
Fix qemu pool eviction locking and stale removal races
1 parent 33cffb1 commit a200381

3 files changed

Lines changed: 65 additions & 21 deletions

File tree

lib/hypervisor/qemu/pool.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ func GetOrCreateForType(socketPath string, hypervisorType hypervisor.Type) (*QEM
4343
}
4444
delete(clientPool.clients, socketPath)
4545
if client.client != nil {
46-
_ = client.client.Close()
46+
// Match Remove's behavior so a stuck QMP close doesn't block the pool lock.
47+
go client.client.Close()
4748
}
4849
}
4950

@@ -57,29 +58,33 @@ func GetOrCreateForType(socketPath string, hypervisorType hypervisor.Type) (*QEM
5758
return client, nil
5859
}
5960

60-
// resetClient synchronously drops a pooled connection before a new QEMU
61+
// resetClient drops a pooled connection before a new QEMU
6162
// process reuses the same socket path.
6263
func resetClient(socketPath string) {
63-
clientPool.Lock()
64-
defer clientPool.Unlock()
65-
if client, ok := clientPool.clients[socketPath]; ok {
66-
delete(clientPool.clients, socketPath)
67-
if client.client != nil {
68-
_ = client.client.Close()
69-
}
70-
}
64+
removeIfCurrent(socketPath, nil)
7165
}
7266

7367
// Remove closes and removes a client from the pool.
7468
// Called automatically on errors to allow fresh reconnection.
7569
// Close is done asynchronously to avoid blocking if the connection is in a bad state.
7670
func Remove(socketPath string) {
71+
removeIfCurrent(socketPath, nil)
72+
}
73+
74+
// removeIfCurrent removes and closes the pooled client if the current entry
75+
// matches expected. Passing nil expected removes whatever is currently pooled.
76+
func removeIfCurrent(socketPath string, expected *QEMU) {
7777
clientPool.Lock()
7878
defer clientPool.Unlock()
7979

8080
if client, ok := clientPool.clients[socketPath]; ok {
81+
if expected != nil && client != expected {
82+
return
83+
}
8184
delete(clientPool.clients, socketPath)
82-
// Close asynchronously to avoid blocking on stuck connections
83-
go client.client.Close()
85+
// Close asynchronously to avoid blocking on stuck connections.
86+
if client.client != nil {
87+
go client.client.Close()
88+
}
8489
}
8590
}

lib/hypervisor/qemu/pool_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,42 @@ func TestGetOrCreateForTypeReplacesCachedBackendMismatch(t *testing.T) {
2626
clientPool.RUnlock()
2727
require.False(t, stillCached, "stale backend client must be removed before reconnect")
2828
}
29+
30+
func TestRemoveIfCurrentSkipsReplacementClient(t *testing.T) {
31+
socketPath := t.TempDir() + "/qemu.sock"
32+
stale := &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMU}
33+
replacement := &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMUMicroVM}
34+
35+
clientPool.Lock()
36+
clientPool.clients[socketPath] = replacement
37+
clientPool.Unlock()
38+
t.Cleanup(func() {
39+
clientPool.Lock()
40+
delete(clientPool.clients, socketPath)
41+
clientPool.Unlock()
42+
})
43+
44+
removeIfCurrent(socketPath, stale)
45+
46+
clientPool.RLock()
47+
cached, ok := clientPool.clients[socketPath]
48+
clientPool.RUnlock()
49+
require.True(t, ok, "replacement client should remain pooled")
50+
require.Same(t, replacement, cached, "stale remove must not evict replacement client")
51+
}
52+
53+
func TestRemoveIfCurrentRemovesMatchingClient(t *testing.T) {
54+
socketPath := t.TempDir() + "/qemu.sock"
55+
client := &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMU}
56+
57+
clientPool.Lock()
58+
clientPool.clients[socketPath] = client
59+
clientPool.Unlock()
60+
61+
removeIfCurrent(socketPath, client)
62+
63+
clientPool.RLock()
64+
_, ok := clientPool.clients[socketPath]
65+
clientPool.RUnlock()
66+
require.False(t, ok, "matching pooled client should be removed")
67+
}

lib/hypervisor/qemu/qemu.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func capabilities(hypervisorType hypervisor.Type) hypervisor.Capabilities {
7171
// This sends a graceful shutdown signal to the guest.
7272
func (q *QEMU) DeleteVM(ctx context.Context) error {
7373
if err := q.client.SystemPowerdown(); err != nil {
74-
Remove(q.socketPath)
74+
removeIfCurrent(q.socketPath, q)
7575
return err
7676
}
7777
clearBalloonTargetCache(q.socketPath)
@@ -81,11 +81,11 @@ func (q *QEMU) DeleteVM(ctx context.Context) error {
8181
// Shutdown stops the QEMU process.
8282
func (q *QEMU) Shutdown(ctx context.Context) error {
8383
if err := q.client.Quit(); err != nil {
84-
Remove(q.socketPath)
84+
removeIfCurrent(q.socketPath, q)
8585
return err
8686
}
8787
// Connection is gone after quit, remove from pool
88-
Remove(q.socketPath)
88+
removeIfCurrent(q.socketPath, q)
8989
clearBalloonTargetCache(q.socketPath)
9090
return nil
9191
}
@@ -94,7 +94,7 @@ func (q *QEMU) Shutdown(ctx context.Context) error {
9494
func (q *QEMU) GetVMInfo(ctx context.Context) (*hypervisor.VMInfo, error) {
9595
status, err := q.client.Status()
9696
if err != nil {
97-
Remove(q.socketPath)
97+
removeIfCurrent(q.socketPath, q)
9898
return nil, fmt.Errorf("query status: %w", err)
9999
}
100100

@@ -129,7 +129,7 @@ func (q *QEMU) GetVMInfo(ctx context.Context) (*hypervisor.VMInfo, error) {
129129
// Pause suspends VM execution.
130130
func (q *QEMU) Pause(ctx context.Context) error {
131131
if err := q.client.Stop(); err != nil {
132-
Remove(q.socketPath)
132+
removeIfCurrent(q.socketPath, q)
133133
return err
134134
}
135135
return nil
@@ -138,7 +138,7 @@ func (q *QEMU) Pause(ctx context.Context) error {
138138
// Resume continues VM execution.
139139
func (q *QEMU) Resume(ctx context.Context) error {
140140
if err := q.client.Continue(); err != nil {
141-
Remove(q.socketPath)
141+
removeIfCurrent(q.socketPath, q)
142142
return err
143143
}
144144
return nil
@@ -153,13 +153,13 @@ func (q *QEMU) Snapshot(ctx context.Context, destPath string) error {
153153
memoryFile := destPath + "/memory"
154154
uri := "exec:cat > " + memoryFile
155155
if err := q.client.Migrate(uri); err != nil {
156-
Remove(q.socketPath)
156+
removeIfCurrent(q.socketPath, q)
157157
return fmt.Errorf("migrate: %w", err)
158158
}
159159

160160
// Wait for migration to complete
161161
if err := q.client.WaitMigration(ctx, migrationTimeout); err != nil {
162-
Remove(q.socketPath)
162+
removeIfCurrent(q.socketPath, q)
163163
return fmt.Errorf("wait migration: %w", err)
164164
}
165165

@@ -197,7 +197,7 @@ func (q *QEMU) SetTargetGuestMemoryBytes(ctx context.Context, bytes int64) error
197197
return fmt.Errorf("target guest memory %d must be non-negative", bytes)
198198
}
199199
if err := q.client.Balloon(bytes); err != nil {
200-
Remove(q.socketPath)
200+
removeIfCurrent(q.socketPath, q)
201201
return fmt.Errorf("set balloon target: %w", err)
202202
}
203203
balloonTargetCache.Store(q.socketPath, bytes)

0 commit comments

Comments
 (0)