This is an automated email from the ASF dual-hosted git repository.

DaanHoogland pushed a commit to branch ghi14391-SQLIntegrityConstraintViolation
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit edc2a39f11527687ca89d63d644048c84862b6f3
Author: Daan Hoogland <[email protected]>
AuthorDate: Mon Oct 19 15:39:27 2026 +0200

    ssvm: prevent sql constraint violation then a SVM was removed during 
network oiutage
---
 .../src/main/java/com/cloud/host/HostVO.java       |  4 ++
 .../src/main/java/com/cloud/host/dao/HostDao.java  |  2 +
 .../main/java/com/cloud/host/dao/HostDaoImpl.java  |  6 +++
 .../com/cloud/resource/ResourceManagerImpl.java    | 22 ++++++++++
 .../cloud/resource/ResourceManagerImplTest.java    | 47 ++++++++++++++++++++++
 5 files changed, 81 insertions(+)

diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java 
b/engine/schema/src/main/java/com/cloud/host/HostVO.java
index a449eb450cf..ae51be83e97 100644
--- a/engine/schema/src/main/java/com/cloud/host/HostVO.java
+++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java
@@ -600,6 +600,10 @@ public class HostVO implements Host {
         return removed;
     }
 
+    public void setRemoved(Date removed) {
+        this.removed = removed;
+    }
+
     @Override
     public String getVersion() {
         return version;
diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java 
b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
index 57ac35d3eb1..47019a76c18 100644
--- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
+++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
@@ -79,6 +79,8 @@ public interface HostDao extends GenericDao<HostVO, Long>, 
StateDao<Status, Stat
 
     HostVO findByGuid(String guid);
 
+    HostVO findByGuidIncludingRemoved(String guid);
+
     HostVO findByTypeNameAndZoneId(long zoneId, String name, Host.Type type);
 
     List<HostVO> findHypervisorHostInCluster(long clusterId);
diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java 
b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
index 8c3604d352b..221c4b14693 100644
--- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
@@ -609,6 +609,12 @@ public class HostDaoImpl extends GenericDaoBase<HostVO, 
Long> implements HostDao
         return findOneBy(sc);
     }
 
+    @Override
+    public HostVO findByGuidIncludingRemoved(String guid) {
+        SearchCriteria<HostVO> sc = GuidSearch.create("guid", guid);
+        return findOneIncludingRemovedBy(sc);
+    }
+
     /*
      * Find hosts which is in Disconnected, Down, Alert and ping timeout and 
server is not null, set server to null
      */
diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java 
b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
index a6a75fc3961..67af99a2895 100755
--- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
+++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
@@ -2281,10 +2281,32 @@ public class ResourceManagerImpl extends ManagerBase 
implements ResourceManager,
             return host;
         }
 
+        host = reviveRemovedHostByGuid(fullGuid);
+        if (host != null) {
+            return host;
+        }
+
         logger.debug(String.format("Could not find Host by guid %s", 
fullGuid));
         return null;
     }
 
+    /**
+     * A host can be marked removed (e.g. a system VM force-stopped and 
destroyed from the UI) while the
+     * underlying resource is still alive and later reconnects with the same 
guid. The `guid` column is
+     * uniquely constrained regardless of removal, so blindly inserting a new 
row for it fails with a
+     * SQLIntegrityConstraintViolationException. Revive the removed row 
instead of creating a duplicate.
+     */
+    private HostVO reviveRemovedHostByGuid(String guid) {
+        HostVO removedHost = _hostDao.findByGuidIncludingRemoved(guid);
+        if (removedHost == null || removedHost.getRemoved() == null) {
+            return null;
+        }
+        logger.warn(String.format("Host with guid %s reconnected but was 
previously removed (id: %s); reviving the removed host record instead of 
creating a duplicate",
+                guid, removedHost.getId()));
+        removedHost.setRemoved(null);
+        return removedHost;
+    }
+
     protected void validateExistingHostLocationImmutable(final HostVO host, 
final boolean newHost,
             final long dcId, final Long podId, final Long clusterId, final 
StartupCommand startup) {
         if (newHost || host == null || host.getType() != Host.Type.Routing) {
diff --git 
a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java 
b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
index a28d3aaeac0..c50cfb55e45 100644
--- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
+++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
@@ -64,9 +64,11 @@ import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.UUID;
 
@@ -683,4 +685,49 @@ public class ResourceManagerImplTest {
         resourceManager.destroyLocalStoragePoolVolumes(poolId);
         verify(volumeDao, never()).updateAndRemoveVolume(any(VolumeVO.class));
     }
+
+    private HostVO invokeGetNewHost(StartupCommand[] cmds) throws Exception {
+        Method method = 
ResourceManagerImpl.class.getDeclaredMethod("getNewHost", 
StartupCommand[].class);
+        method.setAccessible(true);
+        return (HostVO) method.invoke(resourceManager, (Object) cmds);
+    }
+
+    @Test
+    public void testGetNewHostRevivesRemovedHostOnGuidCollision() throws 
Exception {
+        String guid = "s-14-VM-PremiumSecondaryStorageResource";
+        String guidPrefix = "s-14-VM";
+        StartupCommand cmd = Mockito.mock(StartupCommand.class);
+        when(cmd.getGuid()).thenReturn(guid);
+        when(cmd.getGuidWithoutResource()).thenReturn(guidPrefix);
+
+        Mockito.doReturn(null).when(resourceManager).findHostByGuid(guid);
+        
Mockito.doReturn(null).when(resourceManager).findHostByGuidPrefix(guidPrefix);
+
+        HostVO removedHost = Mockito.mock(HostVO.class);
+        when(removedHost.getId()).thenReturn(99L);
+        when(removedHost.getRemoved()).thenReturn(new Date());
+        when(hostDao.findByGuidIncludingRemoved(guid)).thenReturn(removedHost);
+
+        HostVO result = invokeGetNewHost(new StartupCommand[]{cmd});
+
+        Assert.assertSame(removedHost, result);
+        Mockito.verify(removedHost).setRemoved(null);
+    }
+
+    @Test
+    public void testGetNewHostReturnsNullWhenNoHostAndNoRemovedMatch() throws 
Exception {
+        String guid = "unknown-guid";
+        String guidPrefix = "unknown";
+        StartupCommand cmd = Mockito.mock(StartupCommand.class);
+        when(cmd.getGuid()).thenReturn(guid);
+        when(cmd.getGuidWithoutResource()).thenReturn(guidPrefix);
+
+        Mockito.doReturn(null).when(resourceManager).findHostByGuid(guid);
+        
Mockito.doReturn(null).when(resourceManager).findHostByGuidPrefix(guidPrefix);
+        when(hostDao.findByGuidIncludingRemoved(guid)).thenReturn(null);
+
+        HostVO result = invokeGetNewHost(new StartupCommand[]{cmd});
+
+        Assert.assertNull(result);
+    }
 }

Reply via email to