You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2026/10/01 14:04:35 UTC
(cloudstack) 01/01: considder stopped VMs on autoscale
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 <da...@apache.org>
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)) {