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)) {
