1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00

fix(vrtManager): the libvirt event loop starts per process, on first use

The connection manager registered libvirt's event implementation and
started the event loop thread when vrtManager.connection was imported.
novncd imports it and then forks a child per console (websockify's
multiprocessing), so every child was forked from a process with a
running libvirt thread, which libvirt does not support, and the child
had no loop thread of its own: keepalive never ran there. Measured in
the test setup: in such a child, a call on a host that went silent
still waited after 70 s.

- The loop starts with the first get_connection of a process. The
  state is per process and shared by all managers; libvirt's event
  implementation is registered once and a child inherits it.
- After a fork, the child renews the lock and starts its own loop on
  first use. It stops using the inherited connections, which are the
  parent's sockets, but keeps them: releasing them would run libvirt
  destructors in the child. No process connects before forking today
  (novncd's parent does not connect, gunicorn does not preload); this
  keeps such a case safe.

With the change, novncd's parent has no libvirt thread, the console
child starts its own, and the same call fails after 29 s with a
keepalive timeout. A test forks a real child; it fails without the
fork handling.
This commit is contained in:
catborise 2026-10-05 14:41:31 +03:00
parent 01a42079be
commit eab27f8aba
2 changed files with 100 additions and 8 deletions

View file

@ -13,6 +13,7 @@ if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
import libvirt
from vrtManager import connection
from vrtManager.connection import CONN_SSH, CONN_TCP, SSH_COMMAND, wvmConnectionManager
@ -206,5 +207,63 @@ class BoundedOpenTestCase(unittest.TestCase):
self.assertIn("ConnectTimeout", script.read())
class EventLoopTestCase(unittest.TestCase):
"""The libvirt event loop thread starts with the first connection of a
process, not at import: novncd forks a child per console after importing
this module, and libvirt is not safe to use after a fork of a process
with running libvirt threads. A child starts its own loop, so its
keepalive works."""
def setUp(self):
for name in ("wvmEventLoop", "libvirt.virEventRegisterDefaultImpl", "libvirt.openAuth"):
patcher = patch(f"vrtManager.connection.{name}")
setattr(self, name.split(".")[-1], patcher.start())
self.addCleanup(patcher.stop)
self.openAuth.side_effect = lambda *args: alive_connection()
state = patch.dict(connection._event_loop, {"lock": threading.Lock(), "running": False, "registered": False})
state.start()
self.addCleanup(state.stop)
def test_no_thread_until_the_first_connection(self):
manager = wvmConnectionManager()
self.wvmEventLoop.assert_not_called()
self.virEventRegisterDefaultImpl.assert_not_called()
manager.get_connection("h", "u", "p", CONN_TCP)
wvmConnectionManager().get_connection("h2", "u", "p", CONN_TCP) # one loop per process
self.wvmEventLoop.return_value.start.assert_called_once()
self.virEventRegisterDefaultImpl.assert_called_once()
def test_a_forked_child_starts_its_own_loop_with_its_own_connections(self):
manager = connection.connection_manager
parent_conn = manager.get_connection("fork-host", "u", "p", CONN_TCP)
read_end, write_end = os.pipe()
pid = os.fork()
if pid == 0: # child: report and leave without running the test runner
try:
child_conn = manager.get_connection("fork-host", "u", "p", CONN_TCP)
kept = any(
entry.connection is parent_conn
for connections in manager._inherited
for entries in connections.values()
for entry in entries
)
report = (
f"{self.wvmEventLoop.return_value.start.call_count} "
f"{self.virEventRegisterDefaultImpl.call_count} {child_conn is parent_conn} {kept}"
)
except BaseException as e:
report = repr(e)
os.write(write_end, report.encode())
os._exit(0)
os.close(write_end)
os.waitpid(pid, 0)
report = os.read(read_end, 200).decode()
os.close(read_end)
# a second loop started in the child, on the inherited registration;
# the child opened its own connection instead of the parent's socket,
# and kept the parent's (releasing it would run libvirt destructors)
self.assertEqual(report, "2 1 False True")
if __name__ == "__main__":
unittest.main()