From ae03679b0e3e5813c9c6274cc13f71db0f5abe2f Mon Sep 17 00:00:00 2001 From: catborise Date: Thu, 5 Nov 2026 08:50:15 +0300 Subject: [PATCH] perf(instances): a clone request reads its source VM once The clone form's checks read the source's NICs from the live and the persistent definition, and each NIC read the running VM's addresses again since b2b2386d: a guest agent that is connected but hung held the request for its timeout once a NIC and definition. The checks now run in one cached_reads block, as the VM page does. --- instances/jobs.py | 4 +++- instances/tests/test_clone_job.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/instances/jobs.py b/instances/jobs.py index 69e4dc72..32464ff2 100644 --- a/instances/jobs.py +++ b/instances/jobs.py @@ -519,7 +519,9 @@ class CloneInstance(Kind): def plan(self, user, params): instance = _clonable(user, params["instance"]) - data, quota = clone_data(user, instance, params["fields"]) + # the source once a request: a running VM's NICs read its guest agent, which may take its whole timeout + with instance.proxy.cached_reads(): + data, quota = clone_data(user, instance, params["fields"]) keys = vm_claims(instance.compute, data["name"]) # the VM's definition is read again when the clone is defined keys[claims.instance_key(instance.uuid)] = Claim.EXCLUSIVE diff --git a/instances/tests/test_clone_job.py b/instances/tests/test_clone_job.py index 9db8fd38..99133049 100644 --- a/instances/tests/test_clone_job.py +++ b/instances/tests/test_clone_job.py @@ -1,5 +1,6 @@ """VM clone runs as a job: the page queues it, the worker clones and records it.""" +import contextlib from types import SimpleNamespace from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -124,6 +125,24 @@ class CloneJobTestCase(TransactionTestCase): "submission_key": "key-clone003", "clone-vcpu": "\u00b2"}) self.assertEqual([str(m) for m in get_messages(response.wsgi_request)], ["Enter a whole number of vCPUs for the clone"]) + def test_the_form_reads_the_vm_in_one_cached_reads_block(self): + inside, reads = [], [] + + @contextlib.contextmanager + def cached_reads(): + inside.append(True) + yield self.proxy + inside.pop() + + self.proxy.cached_reads = cached_reads + disks = self.proxy.get_disk_devices.return_value + self.proxy.get_disk_devices.side_effect = lambda *args, **kwargs: reads.append(bool(inside)) or disks + self.client.post(reverse("instances:clone", args=[self.source.pk]), + {"name": "copy", "disk-vda": "copy.qcow2", "clone-net-mac-0": "52:54:00:00:00:01", + "submission_key": "key-clone004"}) + self.assertTrue(Operation.objects.exists()) + self.assertEqual(reads, [True]) + def test_a_clone_that_cannot_start_is_made_with_a_warning(self): self.proxy.start.side_effect = libvirt.libvirtError("no memory") op, result = self.clone_with(**{"clone-start": "true"})