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

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

commit 9c2bf245c69c973693966dad51462d76e867ea18
Author: Daan Hoogland <[email protected]>
AuthorDate: Thu Oct 1 16:03:20 2026 +0200

    considder stopped VMs on autoscale
---
 .../as/dao/AutoScaleVmGroupVmMapDaoImpl.java       |  2 +-
 .../as/dao/AutoScaleVmGroupVmMapDaoImplTest.java   | 14 ++++++++
 .../com/cloud/network/as/AutoScaleManagerImpl.java |  2 +-
 .../cloud/network/as/AutoScaleManagerImplTest.java | 37 ++++++++++++++++++++++
 4 files changed, 53 insertions(+), 2 deletions(-)

diff --git 
a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java
 
b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java
index b2f4e578a82..576dac14bed 100644
--- 
a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java
+++ 
b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java
@@ -132,7 +132,7 @@ public class AutoScaleVmGroupVmMapDaoImpl extends 
GenericDaoBase<AutoScaleVmGrou
     public int getErroredInstanceCount(long vmGroupId) {
         SearchCriteria<Integer> sc = CountBy.create();
         sc.setParameters("vmGroupId", vmGroupId);
-        sc.setJoinParameters("vmSearch", "states", State.Error);
+        sc.setJoinParameters("vmSearch", "states", State.Error, State.Stopped);
         final List<Integer> results = customSearch(sc, null);
         return results.get(0);
     }
diff --git 
a/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java
 
b/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java
index 6de8960ae74..8aa51b926b2 100644
--- 
a/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java
+++ 
b/engine/schema/src/test/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImplTest.java
@@ -82,6 +82,20 @@ public class AutoScaleVmGroupVmMapDaoImplTest {
         
Mockito.verify(searchCriteriaCountAvailableVmsByGroup).setJoinParameters("vmSearch",
 "states", new Object[] {VirtualMachine.State.Starting, 
VirtualMachine.State.Running, VirtualMachine.State.Stopping, 
VirtualMachine.State.Migrating});
     }
 
+    @Test
+    public void testGetErroredInstanceCount() throws Exception {
+        
Mockito.doReturn(Arrays.asList(3)).when(AutoScaleVmGroupVmMapDaoImplSpy).customSearch(Mockito.any(SearchCriteria.class),
 Mockito.eq(null));
+
+        long groupId = 4L;
+
+        int result = 
AutoScaleVmGroupVmMapDaoImplSpy.getErroredInstanceCount(groupId);
+
+        Assert.assertEquals(3, result);
+
+        
Mockito.verify(searchCriteriaCountAvailableVmsByGroup).setParameters("vmGroupId",
 groupId);
+        
Mockito.verify(searchCriteriaCountAvailableVmsByGroup).setJoinParameters("vmSearch",
 "states", new Object[] {VirtualMachine.State.Error, 
VirtualMachine.State.Stopped});
+    }
+
     @Test
     public void testCountByGroup() throws Exception {
         
Mockito.doNothing().when(searchCriteriaAutoScaleVmGroupVmMapVOMock).setParameters(Mockito.anyString(),
 Mockito.any());
diff --git 
a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java 
b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
index 8e93c681764..43b63dd4b08 100644
--- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java
@@ -2112,7 +2112,7 @@ public class AutoScaleManagerImpl extends ManagerBase 
implements AutoScaleManage
                                 String.format("Failed to assign LB rule for VM 
%s in AutoScale VM group %s", vm, asGroup), groupId, 
ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                         break;
                     }
-                } catch (ServerApiException e) {
+                } catch (CloudRuntimeException e) {
                     logger.error("Can not deploy new VM for scaling up in the 
group {}. Waiting for next round", asGroup);
                     ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, 
asGroup.getAccountId(), EventVO.LEVEL_ERROR, 
EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEUP,
                             String.format("Failed to start VM %s in AutoScale 
VM group %s", vm, asGroup), groupId, 
ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
diff --git 
a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java 
b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java
index 4dc815cafd0..2553fc3c426 100644
--- a/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java
+++ b/server/src/test/java/com/cloud/network/as/AutoScaleManagerImplTest.java
@@ -1526,6 +1526,43 @@ public class AutoScaleManagerImplTest {
         }
     }
 
+    /**
+     * Regression test for #14185: VirtualMachineManagerImpl.start() wraps a 
failed start into an unchecked
+     * CloudRuntimeException rather than the checked exceptions startNewVM 
converts to ServerApiException.
+     * Before the fix, doScaleUp's catch(ServerApiException) missed it, the VM 
was never destroyed, and its
+     * autoscale_vmgroup_vm_map row leaked forever (the VM stays in 
State.Stopped, invisible to both
+     * getErroredInstanceCount() and countAvailableVmsByGroup(), so the group 
scales up again next interval).
+     */
+    @Test
+    public void testDoScaleUpDestroysVmWhenStartThrowsCloudRuntimeException() 
throws ResourceUnavailableException, InsufficientCapacityException, 
ResourceAllocationException {
+        try (MockedStatic<ActionEventUtils> ignored = 
Mockito.mockStatic(ActionEventUtils.class)) {
+            
when(autoScaleVmGroupDao.findById(vmGroupId)).thenReturn(asVmGroupMock);
+            when(asVmGroupMock.getId()).thenReturn(vmGroupId);
+            when(asVmGroupMock.getAccountId()).thenReturn(accountId);
+            when(asVmGroupMock.getMaxMembers()).thenReturn(maxMembers);
+            
when(autoScaleVmGroupVmMapDao.countAvailableVmsByGroup(vmGroupId)).thenReturn(maxMembers
 - 1);
+            
when(autoScaleVmGroupVmMapDao.getErroredInstanceCount(vmGroupId)).thenReturn(0);
+            
when(asVmGroupMock.getState()).thenReturn(AutoScaleVmGroup.State.ENABLED);
+
+            when(autoScaleVmGroupDao.updateState(vmGroupId, 
AutoScaleVmGroup.State.ENABLED, 
AutoScaleVmGroup.State.SCALING)).thenReturn(true);
+            when(autoScaleVmGroupDao.updateState(vmGroupId, 
AutoScaleVmGroup.State.SCALING, 
AutoScaleVmGroup.State.ENABLED)).thenReturn(true);
+            
Mockito.doReturn(userVmMock).when(autoScaleManagerImplSpy).createNewVM(asVmGroupMock);
+            when(userVmMock.getId()).thenReturn(virtualMachineId);
+
+            Mockito.doThrow(new CloudRuntimeException(String.format("Unable to 
start a VM [%s] due to [Resource unavailable].", virtualMachineId)))
+                    .when(userVmMgr).startVirtualMachine(virtualMachineId, 
null, new HashMap<>(), null);
+            
Mockito.doReturn(true).when(autoScaleManagerImplSpy).destroyVm(virtualMachineId);
+
+            autoScaleManagerImplSpy.doScaleUp(vmGroupId, 1);
+
+            
Mockito.verify(autoScaleVmGroupVmMapDao).persist(any(AutoScaleVmGroupVmMapVO.class));
+            
Mockito.verify(autoScaleManagerImplSpy).destroyVm(virtualMachineId);
+            Mockito.verify(loadBalancingRulesService, 
Mockito.never()).assignToLoadBalancer(anyLong(), any(), any(), eq(true));
+            // the group must leave SCALING even though the start failed, so 
the next monitor interval can retry
+            Mockito.verify(autoScaleVmGroupDao).updateState(vmGroupId, 
AutoScaleVmGroup.State.SCALING, AutoScaleVmGroup.State.ENABLED);
+        }
+    }
+
     @Test
     public void testDoScaleDown() {
         try (MockedStatic<ActionEventUtils> ignored = 
Mockito.mockStatic(ActionEventUtils.class)) {

Reply via email to