1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/computes/tests/test_reconciliation.py
catborise 37b87839c1 perf(computes): a VM list load writes nothing while the hosts match the database
Every VM list load reconciles each compute with libvirt. Even when nothing
had changed, that took the compute's claims and a transaction with
SELECT FOR UPDATE (and the ownership advisory lock on PostgreSQL): about
11 queries a compute, 4 of them writes. On SQLite each load queued behind
every other write of the app.

The reconcile now first compares libvirt's VM list with the database,
without a claim or a transaction, and stops when they match and no
removed record has expired. Otherwise it runs as before, reading libvirt
again under its claims, so two reconciles still cannot commit in reverse
order. With two matching hosts a list load goes from 38 queries (8
writes) to 18 (none).
2026-11-04 20:54:07 +03:00

193 lines
7.6 KiB
Python

from unittest.mock import MagicMock, PropertyMock, patch
from django.contrib.auth import get_user_model
from django.db import connection
from django.test import TestCase, TransactionTestCase
from django.test.utils import CaptureQueriesContext
from accounts.models import UserInstance
from computes.models import Compute
from computes.utils import refresh_instance_database
from instances.models import Instance
from instances.utils import refr
class MockDomain:
def __init__(self, name, uuid_str):
self._name = name
self._uuid_str = uuid_str
def name(self):
return self._name
def UUIDString(self):
return self._uuid_str
class InstanceReconciliationTestCase(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(username="test_reconcile_user", password="password")
self.compute = Compute.objects.create(
name="reconcile-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
def test_new_domain_created_in_libvirt_is_added_to_db(self):
dom1 = MockDomain(name="vm-alpha", uuid_str="11111111-1111-1111-1111-111111111111")
dom2 = MockDomain(name="vm-beta", uuid_str="22222222-2222-2222-2222-222222222222")
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [dom1, dom2]
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
instances = Instance.objects.filter(compute=self.compute).order_by("name")
self.assertEqual(instances.count(), 2)
self.assertEqual(instances[0].name, "vm-alpha")
self.assertEqual(instances[0].uuid, "11111111-1111-1111-1111-111111111111")
self.assertEqual(instances[1].name, "vm-beta")
self.assertEqual(instances[1].uuid, "22222222-2222-2222-2222-222222222222")
def test_domain_deleted_in_libvirt_is_deleted_from_db(self):
inst = Instance.objects.create(
compute=self.compute,
name="vm-to-delete",
uuid="33333333-3333-3333-3333-333333333333",
)
UserInstance.objects.create(instance=inst, user=self.user)
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = []
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
self.assertFalse(Instance.objects.filter(compute=self.compute, uuid="33333333-3333-3333-3333-333333333333").exists())
self.assertFalse(UserInstance.objects.filter(user=self.user).exists())
def test_domain_renamed_in_libvirt_preserves_userinstance_and_id(self):
uuid = "44444444-4444-4444-4444-444444444444"
inst = Instance.objects.create(
compute=self.compute,
name="vm-original-name",
uuid=uuid,
)
original_inst_id = inst.id
user_inst = UserInstance.objects.create(instance=inst, user=self.user, is_change=True)
dom_renamed = MockDomain(name="vm-new-name", uuid_str=uuid)
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [dom_renamed]
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
inst.refresh_from_db()
self.assertEqual(inst.id, original_inst_id)
self.assertEqual(inst.name, "vm-new-name")
self.assertEqual(inst.uuid, uuid)
# Verify UserInstance still exists and still points to the same instance!
user_inst.refresh_from_db()
self.assertEqual(user_inst.instance_id, original_inst_id)
self.assertTrue(user_inst.is_change)
def test_libvirt_connection_error_leaves_db_untouched(self):
inst = Instance.objects.create(
compute=self.compute,
name="vm-persistent",
uuid="55555555-5555-5555-5555-555555555555",
)
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.side_effect = Exception("Libvirt socket connection refused")
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
# Instance should NOT be deleted!
self.assertTrue(Instance.objects.filter(id=inst.id).exists())
def test_broken_domain_metadata_aborts_sync_safely(self):
inst = Instance.objects.create(
compute=self.compute,
name="vm-persistent-2",
uuid="66666666-6666-6666-6666-666666666666",
)
broken_dom = MagicMock()
broken_dom.UUIDString.side_effect = Exception("Corrupted domain metadata")
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [broken_dom]
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
self.assertTrue(Instance.objects.filter(id=inst.id).exists())
def test_refr_invokes_refresh_instance_database(self):
with patch("computes.utils.refresh_instance_database") as mock_refresh:
refr(self.compute)
mock_refresh.assert_called_once_with(self.compute)
class ReconciliationTransactionTestCase(TransactionTestCase):
"""libvirt is asked outside the DB transaction: on SQLite an open transaction
makes every other write of the app fail while a silent host is waited on."""
def setUp(self):
self.compute = Compute.objects.create(name="tx-compute", hostname="127.0.0.1", login="root", password="", type=1)
def proxy(self, list_domains):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.side_effect = list_domains
self.compute.__dict__["proxy"] = mock_proxy
def test_libvirt_is_called_outside_the_transaction(self):
seen = {}
def status():
seen["status"] = connection.in_atomic_block
return True
def list_domains():
seen["list"] = connection.in_atomic_block
return [MockDomain(name="vm-tx", uuid_str="77777777-7777-7777-7777-777777777777")]
self.proxy(list_domains)
with patch.object(Compute, "status", new_callable=PropertyMock, side_effect=status):
refresh_instance_database(self.compute)
self.assertEqual(seen, {"status": False, "list": False})
self.assertTrue(Instance.objects.filter(compute=self.compute, name="vm-tx").exists())
def test_a_compute_that_matches_the_database_writes_nothing(self):
Instance.objects.create(compute=self.compute, name="vm-same", uuid="99999999-9999-9999-9999-999999999999")
self.proxy(lambda: [MockDomain(name="vm-same", uuid_str="99999999-9999-9999-9999-999999999999")])
self.compute.__dict__["status"] = True
with CaptureQueriesContext(connection) as queries:
refresh_instance_database(self.compute)
self.assertEqual([q["sql"].split()[0] for q in queries.captured_queries], ["SELECT", "SELECT"])
def test_compute_deleted_while_listing_gets_no_instances(self):
def list_domains():
Compute.objects.filter(pk=self.compute.pk).delete()
return [MockDomain(name="vm-orphan", uuid_str="88888888-8888-8888-8888-888888888888")]
self.proxy(list_domains)
self.compute.__dict__["status"] = True
refresh_instance_database(self.compute)
self.assertFalse(Instance.objects.filter(uuid="88888888-8888-8888-8888-888888888888").exists())