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

feat(instances): a running migration can be cancelled; its time limit goes

This commit is contained in:
catborise 2026-10-15 20:42:50 +03:00
parent 31cbe60aa4
commit c2d3285e0f
7 changed files with 69 additions and 127 deletions

View file

@ -5,7 +5,6 @@ import logging
import os.path
import re
import string
import threading
import time
logger = logging.getLogger(__name__)
@ -77,7 +76,7 @@ def _rate(before, after, seconds, scale=1):
class wvmInstances(wvmConnect):
def moveto(self, conn, name, live, unsafe, offline, autoconverge=False, compress=False, uri=None, timeout=None):
def moveto(self, conn, name, live, unsafe, offline, autoconverge=False, compress=False, uri=None):
"""Migrates the VM of conn (a wvmInstance on the source) here.
The mode follows the VM's state, read once: a running VM migrates
@ -86,9 +85,8 @@ class wvmInstances(wvmConnect):
fit the state is refused. unsafe, autoconverge (live only) and
compress apply to a running VM. uri: the native migration URI
(tcp://address) on the destination; None lets libvirt use the
destination's own hostname. timeout: seconds after which a migration
that has not finished is cancelled (None: no limit). Returns the mode
and options used, for the log."""
destination's own hostname. Returns the mode and options used, for
the log."""
state = conn.get_status()
if state == 5:
if live:
@ -150,7 +148,7 @@ class wvmInstances(wvmConnect):
# dom.migrate.
self.move_definition(dom)
else:
self.migrate_within(dom, flags, uri, timeout)
dom.migrate(self.wvm, flags, None, uri, 0)
return ", ".join(mode)
@staticmethod
@ -172,40 +170,6 @@ class wvmInstances(wvmConnect):
"host. Start the VM and migrate it live."
)
def migrate_within(self, dom, flags, uri, timeout):
"""dom.migrate, cancelled when it has not finished after timeout
seconds: a VM that changes its memory faster than the network carries
it never finishes, and the request would be ended by the web server
with no word. Cancelled, the VM stays on the source."""
cancelled = threading.Event()
def cancel():
with contextlib.suppress(libvirtError): # finished meanwhile
dom.abortJob()
cancelled.set()
timer = threading.Timer(timeout, cancel) if timeout else None
if timer:
timer.daemon = True
timer.start()
try:
dom.migrate(self.wvm, flags, None, uri, 0)
except libvirtError as error:
if timer:
timer.cancel()
timer.join() # a cancel under way ends before the locks go
if cancelled.is_set():
raise util.OperationError(
f"The migration did not finish within {timeout} s and was cancelled ({error}); the VM stays on "
"the source host. A VM that changes its memory faster than the network carries it needs auto "
"converge."
) from error
raise
finally:
if timer:
timer.cancel()
timer.join()
def missing_disks(self, definitions):
"""(target, source) of the disks in the VM definitions (XML) that are
not volumes of this host's active storage pools: the VM's own files,

View file

@ -1,9 +1,7 @@
"""Migration: the source definition never stays behind; a shut-off VM's
definition is moved by the app, not by libvirt's offline migration."""
import threading
import unittest
from unittest.mock import patch
import libvirt
from django.conf import settings
@ -427,64 +425,3 @@ class DestinationDisksTestCase(unittest.TestCase):
xml = with_disks("<disk type='volume' device='disk'><source pool='p' volume='v'/><target dev='vdc'/></disk>")
with self.assertRaisesRegex(util.OperationError, "vdc \\(p/v\\)"):
self.migrate(host, xml)
class SlowDomain(FakeDomain):
"""A migration that does not finish until its job is aborted."""
def __init__(self, **kw):
super().__init__(**kw)
self.aborted = threading.Event()
def migrate(self, dconn, flags, dname, uri, bandwidth):
if not self.aborted.wait(5):
raise AssertionError("the migration was not aborted")
raise libvirt.libvirtError("operation aborted: job 'migration out' canceled by client")
def abortJob(self):
self.aborted.set()
class MigrationTimeLimitTestCase(unittest.TestCase):
"""A migration that does not converge is cancelled after the time limit,
before the web server ends the request; the VM stays on the source."""
def test_a_migration_over_the_limit_is_cancelled(self):
source = FakeSource(1)
source.instance = SlowDomain()
with self.assertRaisesRegex(util.OperationError, "did not finish within 0.05 s and was cancelled.*auto converge"):
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=0.05)
self.assertTrue(source.instance.aborted.is_set())
def test_a_migration_within_the_limit_stops_its_timer(self):
timers = []
class Timer(threading.Timer):
def __init__(self, *args):
super().__init__(*args)
self.joined = False
timers.append(self)
def join(self, timeout=None):
self.joined = True
super().join(timeout)
source = FakeSource(1)
with patch("vrtManager.instance.threading.Timer", Timer):
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=5)
self.assertEqual(len(source.instance.calls), 1)
self.assertTrue(timers[0].finished.is_set() and timers[0].joined)
self.assertFalse(timers[0].is_alive())
def test_a_failure_the_cancel_did_not_cause_keeps_its_error(self):
source = FakeSource(1)
source.instance = SlowDomain()
def refused():
source.instance.aborted.set() # migrate() gives up, but the abort itself failed
raise libvirt.libvirtError("cannot abort migration in confirm phase")
source.instance.abortJob = refused
with self.assertRaisesRegex(libvirt.libvirtError, "operation aborted") as caught:
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=0.05)
self.assertNotIsInstance(caught.exception, util.OperationError)