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
ifclient.client!=nil {
46-
_=client.client.Close()
46+
// Match Remove's behavior so a stuck QMP close doesn't block the pool lock.
47+
goclient.client.Close()
4748
}
4849
}
4950

@@ -57,29 +58,33 @@ func GetOrCreateForType(socketPath string, hypervisorType hypervisor.Type) (*QEM
5758
returnclient, 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
funcresetClient(socketPathstring) {
63-
clientPool.Lock()
64-
deferclientPool.Unlock()
65-
ifclient, ok:=clientPool.clients[socketPath]; ok {
66-
delete(clientPool.clients, socketPath)
67-
ifclient.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
funcRemove(socketPathstring) {
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+
funcremoveIfCurrent(socketPathstring, expected*QEMU) {
7777
clientPool.Lock()
7878
deferclientPool.Unlock()
7979

8080
ifclient, ok:=clientPool.clients[socketPath]; ok {
81+
ifexpected!=nil&&client!=expected {
82+
return
83+
}
8184
delete(clientPool.clients, socketPath)
82-
// Close asynchronously to avoid blocking on stuck connections
83-
goclient.client.Close()
85+
// Close asynchronously to avoid blocking on stuck connections.
86+
ifclient.client!=nil {
87+
goclient.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+
funcTestRemoveIfCurrentSkipsReplacementClient(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+
funcTestRemoveIfCurrentRemovesMatchingClient(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
iferr:=q.client.SystemPowerdown(); err!=nil {
74-
Remove(q.socketPath)
74+
removeIfCurrent(q.socketPath, q)
7575
returnerr
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
iferr:=q.client.Quit(); err!=nil {
84-
Remove(q.socketPath)
84+
removeIfCurrent(q.socketPath, q)
8585
returnerr
8686
}
8787
// Connection is gone after quit, remove from pool
88-
Remove(q.socketPath)
88+
removeIfCurrent(q.socketPath, q)
8989
clearBalloonTargetCache(q.socketPath)
9090
returnnil
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
iferr!=nil {
97-
Remove(q.socketPath)
97+
removeIfCurrent(q.socketPath, q)
9898
returnnil, 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
iferr:=q.client.Stop(); err!=nil {
132-
Remove(q.socketPath)
132+
removeIfCurrent(q.socketPath, q)
133133
returnerr
134134
}
135135
returnnil
@@ -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
iferr:=q.client.Continue(); err!=nil {
141-
Remove(q.socketPath)
141+
removeIfCurrent(q.socketPath, q)
142142
returnerr
143143
}
144144
returnnil
@@ -153,13 +153,13 @@ func (q *QEMU) Snapshot(ctx context.Context, destPath string) error {
153153
memoryFile:=destPath+"/memory"
154154
uri:="exec:cat > "+memoryFile
155155
iferr:=q.client.Migrate(uri); err!=nil {
156-
Remove(q.socketPath)
156+
removeIfCurrent(q.socketPath, q)
157157
returnfmt.Errorf("migrate: %w", err)
158158
}
159159

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

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

0 commit comments

Comments
 (0)