From c8fb778d08d2e8a440f127f82cfbd1b816a7490f Mon Sep 17 00:00:00 2001 From: catborise Date: Thu, 5 Nov 2026 18:38:57 +0300 Subject: [PATCH] fix(instances): recording a migration holds the ownership lock record_migration moved or merged a VM's owners without lock_ownership(), which every other step that moves owners takes; on PostgreSQL a sync of a third compute with the same UUID could move them at the same time, and owners moved onto the source record went with its delete. --- instances/tests/test_migrate_job.py | 10 ++++++++++ instances/utils.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/instances/tests/test_migrate_job.py b/instances/tests/test_migrate_job.py index 3bb01429..275e3e23 100644 --- a/instances/tests/test_migrate_job.py +++ b/instances/tests/test_migrate_job.py @@ -228,3 +228,13 @@ class MigrateJobTestCase(TransactionTestCase): resource_label="mig-src/mig-vm") self.assertEqual(MigrateInstance().log_entry(queued, Operation.CANCELLED), ("mig-src", "mig-vm", UUID, "VM mig-vm: migrate to mig-dst (cancelled)")) + + def test_recording_a_migration_holds_the_ownership_lock(self): + # a sync of a third compute must not move the owners at the same time + from instances import utils + + with patch.object(utils, "lock_ownership") as lock: + utils.record_migration(self.vm, self.dest) + lock.assert_called_once_with() + self.vm.refresh_from_db() + self.assertEqual(self.vm.compute, self.dest) diff --git a/instances/utils.py b/instances/utils.py index 6ba709ac..4c94d317 100644 --- a/instances/utils.py +++ b/instances/utils.py @@ -7,6 +7,7 @@ from collections import namedtuple from accounts.models import UserInstance, UserAttributes from appsettings.settings import app_settings from computes.models import Compute +from computes.utils import lock_ownership from django.conf import settings from django.db import transaction from django.utils.translation import gettext_lazy as _ @@ -321,6 +322,8 @@ def record_migration(instance, new_compute): """Moves instance to new_compute in the database. A record the list of new_compute made for it meanwhile takes its owners and template mark.""" with transaction.atomic(): + # owners are moved here: not while a sync of a third compute moves them too + lock_ownership() target_inst = Instance.objects.filter(compute=new_compute, uuid=instance.uuid).first() if target_inst and target_inst.id != instance.id: for ui in UserInstance.objects.filter(instance=instance):