tests/functional: add a stress test for monitor hot unplug
When unplugging a monitor there is a careful synchronization dance between the monitor handling the "object-del" command and the command processing for the monitor being deleted. The stress test runs a busy loop of 'query-qmp-schema' on a second monitor, while the primary monitor requests its deletion. Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com> Tested-by: Peter Krempa <pkrempa@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> Message-ID: <20260706135824.2623960-31-berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
Daniel P. Berrangé committed
Jul 6, 2026 at 14:58 UTC
8b142ecb34f0a75bac9bed475c92ee9037a96f46
1 file changed
+62
tests/functional/generic/test_monitor_hotplug.py
+62
@@ -6,11 +6,16 @@
6
#
7
# Copyright (c) 2026 Christian Brauner
8
9
+import asyncio
10
import os
11
+import random
12
+import threading
13
+import time
14
15
from qemu_test import QemuSystemTest
16
17
from qemu.qmp.legacy import QEMUMonitorProtocol
18
+from qemu.qmp import QMPClient
19
20
21
class MonitorHotplug(QemuSystemTest):
@@ -163,6 +168,63 @@ class MonitorHotplug(QemuSystemTest):
168
qmp.close()
169
self._remove_monitor()
170
171
+ def stress_mon(self, sock):
172
+ async def main():
173
+ qmp = QMPClient('testvm')
174
+ await qmp.connect(sock)
175
+ # Run query-version in a tight loop so that the
176
+ # monitor thread/dispatcher is very busy at the
177
+ # time we try to delete the monitor
178
+ while True:
179
+ try:
180
+ # A command which returns a lot of data to make
181
+ # it more likely we're in the I/O reply path
182
+ # when deleting the monitor
183
+ res = await qmp.execute('query-qmp-schema')
184
+ # Some commands which generate async events
185
+ # as those can trigger different code paths
186
+ res = await qmp.execute('stop')
187
+ res = await qmp.execute('cont')
188
+ except:
189
+ # we'll get here if the monitor is terminated
190
+ # by QEMU in which case we must disconnect
191
+ # out side, but....
192
+ try:
193
+ await qmp.disconnect()
194
+ except (ConnectionResetError, EOFError, BrokenPipeError):
195
+ # ... disconnect() will probably see
196
+ # errors too, but we must try to call it
197
+ # regardless to cleanup asyncio state
198
+ # and prevent python warnings at GC time
199
+ pass
200
+ return
201
+ asyncio.run(main())
202
+
203
+ def test_hotplug_stress(self):
204
+ """
205
+ Repeatedly hotplug and unplug a monitor, while another thread
206
+ concurrently issues commands on that monitor. This stresses
207
+ the synchronization with the monitor thread during cleanup
208
+ """
209
+ self.set_machine('none')
210
+ self.vm.add_args('-nodefaults')
211
+ self.vm.launch()
212
+
213
+ # Each loop sleeps at most 0.5 seconds, so this should
214
+ # give an upper bound of approx 5 seconds execution
215
+ # time which is reasonable to run by default
216
+ repeat = 10
217
+ for i in range(repeat):
218
+ # First cycle
219
+ sock = self._add_monitor()
220
+ print ("# stress cycle %02d/%02d" % (i, repeat))
221
+ stress = threading.Thread(target=self.stress_mon, args=[sock])
222
+ stress.start()
223
+ # Sleep upto 1/2 second to vary the races
224
+ time.sleep(random.random() / 2)
225
+ self._remove_monitor()
226
+ stress.join()
227
+
228
229
if __name__ == '__main__':
230
QemuSystemTest.main()