From c3bfbb52c0be6f2ce6b127673b26a9097a275898 Mon Sep 17 00:00:00 2001
From: Mark Syms <mark.syms@citrix.com>
Date: Mon, 6 Oct 2025 11:00:40 +0100
Subject: [PATCH] CA-416486: leaf coalesce wait for GC

In coalesce leaf, after triggering the regular GC, ensure it has
completed before proceeding with the VM suspend.

Signed-off-by: Mark Syms <mark.syms@citrix.com>
diff --git a/drivers/cleanup.py b/drivers/cleanup.py
index ae69dde..bdc4a92 100755
--- a/drivers/cleanup.py
+++ b/drivers/cleanup.py
@@ -3360,6 +3360,11 @@ def start_gc_service(sr_uuid, wait=False):
     subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
 
 
+def wait_for_completion(sr_uuid):
+    while get_state(sr_uuid):
+        time.sleep(5)
+
+
 def gc_force(session, srUuid, force=False, dryRun=False, lockSR=False):
     """Garbage collect all deleted VDIs in SR "srUuid". The caller must ensure
     the SR lock is held.
diff --git a/drivers/coalesce-leaf b/drivers/coalesce-leaf
index f45e248..206a2d5 100755
--- a/drivers/coalesce-leaf
+++ b/drivers/coalesce-leaf
@@ -185,6 +185,10 @@ def vm_leaf_coalesce(session, vm_uuid):
         # do regular GC now to minimize downtime
         cleanup.gc(session, sr_uuid, False)
 
+    for sr_uuid in coalesceable_vdis:
+        # wait for the GC process for this SR to complete
+        cleanup.wait_for_completion(sr_uuid)
+
     suspended = False
     if vm_rec["power_state"] == "Running":
         log_msg("Suspending VM %s" % vm_rec["uuid"])
diff --git a/tests/test_cleanup.py b/tests/test_cleanup.py
index aad8552..f796ed7 100644
--- a/tests/test_cleanup.py
+++ b/tests/test_cleanup.py
@@ -1,5 +1,6 @@
 import errno
 import signal
+import subprocess
 import unittest
 import unittest.mock as mock
 import uuid
@@ -2029,3 +2030,52 @@ class TestFileSR(TestSR):
         self.xapi_mock.forgetVDI.side_effect = forgetVdi
 
         self.mock_sr._finishInterruptedCoalesceLeaf(child_vdi_uuid, parent_vdi_uuid)
+
+
+class TestService(unittest.TestCase):
+
+    def setUp(self):
+        self.addCleanup(mock.patch.stopall)
+        run_patcher = mock.patch("cleanup.subprocess.run", autospec=True)
+        self.mock_run = run_patcher.start()
+
+        sleep_patcher = mock.patch("cleanup.time.sleep", autospec=True)
+        self.mock_sleep = sleep_patcher.start()
+
+    def test_wait_for_completion_noop(self):
+        # Arrange
+        sr_uuid = str(uuid4())
+        sr_uuid_esc = sr_uuid.replace("-", "\\x2d")
+        self.mock_run.return_value = subprocess.CompletedProcess("", 0, b"unknown")
+
+        # Act
+        cleanup.wait_for_completion(sr_uuid)
+
+        # Assert
+        self.mock_run.assert_called_once_with(
+            ["/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"],
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
+
+    def test_wait_for_completion_wait_2(self):
+        # Arrange
+        sr_uuid = str(uuid4())
+        sr_uuid_esc = sr_uuid.replace("-", "\\x2d")
+
+        activating_process = subprocess.CompletedProcess("", 0, b"activating")
+        finished_process = subprocess.CompletedProcess("", 0, b"unknown")
+        self.mock_run.side_effect = [activating_process, activating_process, finished_process]
+
+        # Act
+        cleanup.wait_for_completion(sr_uuid)
+
+        # Assert
+        self.mock_run.assert_has_calls([
+            mock.call(
+                ["/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"],
+                stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True),
+            mock.call(
+                ["/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"],
+                stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True),
+            mock.call(
+                ["/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"],
+                stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)])
