You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
4.22.0.0 (observed)
Also present in 4.22.1.0, 4.23.0.0 and main (verified by source inspection)
CONFIGURATION
Advanced zone, KVM, Ceph/RBD-only primary storage. One AutoScale VM group
(min_members=1, max_members=2, interval=30) on an isolated network with a
/24 guest CIDR.
OS / ENVIRONMENT
Linux (Debian 12), KVM hosts.
SUMMARY
The infinite-autoscaling guard added in #11244 (fixing #9318) only counts
instances in State.Error. When scale-up VMs fail to start — as opposed to
failing to be created — they land in State.Stopped, not State.Error. The
guard therefore never trips, and the group scales up on every interval
indefinitely.
In our incident this produced 2,296 VMs from a group whose max_members is 2,
over roughly 21 hours, until the guest subnet was exhausted.
Two independent counters are involved and both exclude Stopped:
AutoScaleVmGroupVmMapDaoImpl.countAvailableVmsByGroup() — used by every
scaling decision in AutoScaleManagerImpl (checkConditionUp, checkConditionDown, checkAutoScaleVmGroup, and the group-state handlers) —
counts only Starting, Running, Stopping, Migrating:
sc.setJoinParameters("vmSearch", "states",
State.Starting, State.Running, State.Stopping, State.Migrating); // Stopped not counted
So with N leaked Stopped members, currentVM == 0:
checkAutoScaleVmGroup: if (currentVM < minMembers) -> 0 < 1 -> scale up, every interval
checkAutoScaleVmGroup: if (currentVM > maxMembers) -> 0 > 2 -> scale-down never fires
A third defect prevents the failed VM from being cleaned up, which is what allows
the leak to accumulate in the first place. doScaleUp persists the group map row before attempting the start, and its cleanup is guarded on ServerApiException:
// AutoScaleManagerImpl.doScaleUpautoScaleVmGroupVmMapDao.persist(groupVmMapVO); // persisted BEFORE the start attempttry {
startNewVM(vm.getId());
...
} catch (ServerApiExceptione) {
...
destroyVm(vm.getId()); // never reached, see belowbreak;
}
startNewVM does convert InsufficientCapacityException into ServerApiException,
but it never sees that exception, because VirtualMachineManagerImpl.start()
has already wrapped it into an unchecked CloudRuntimeException:
try {
advanceStart(vmUuid, params, planToDeploy, planner);
} catch (ConcurrentOperationException | InsufficientCapacityExceptione) {
thrownewCloudRuntimeException(String.format("Unable to start a VM [%s] due to [%s].", vmUuid, e.getMessage()), e);
}
CloudRuntimeException matches none of startNewVM's typed catches and is not a ServerApiException, so it propagates past doScaleUp's handler to AutoScaleManagerImpl$MonitorTask, and destroyVm() is never called. The
observed log line is exactly this:
WARN [c.c.n.a.A.MonitorTask] Caught the following exception on monitoring AutoScale Vm Group
com.cloud.utils.exception.CloudRuntimeException: Unable to start a VM [...]
Note that PR #9574 ("Prevent infinite retries of autoscaling"), which proposed a
one-line change to AutoScaleVmGroupVmMapDaoImpl, was closed unmerged; the merged #11244 took the threshold approach instead, which is what leaves this variant
uncovered.
STEPS TO REPRODUCE
Create an AutoScale VM group (min_members=1, max_members=2, short interval).
Let it stabilise at 1 running VM.
Break VM start (not creation) in a way that returns an InsufficientCapacityException or ResourceUnavailableException from advanceStart.
The trigger we actually hit was the group network's Virtual Router becoming
unreachable, so VirtualRouterElement.applyDhcpEntries failed with: ResourceUnavailableException: Resource [DataCenter:1] is unreachable: Unable to apply dhcp entry on router.
That was an observed failure rather than a deliberate test, so I have not
confirmed that stopping the VR is a minimal reproducer — any start-path failure
that surfaces as CloudRuntimeException out of VirtualMachineManagerImpl.start()
should exhibit the same leak.
Observe: one new VM per interval, each landing in Stopped, each retaining its autoscale_vmgroup_vm_map row, indefinitely.
EXPECTED RESULTS
Scale-up stops after a bounded number of consecutive failed starts, and/or failed
instances are cleaned up, and/or Stopped members count toward max_members.
ACTUAL RESULTS
Unbounded VM creation. In our case, one VM per 30s for ~21 hours:
2,043 VMs in Error (created after the guest subnet was exhausted — these fail
at IP allocation, before a NIC is assigned)
253 VMs in Stopped, each holding a NIC and therefore a guest IP
The /24 guest network reached 254/254 NICs and all subsequent VM deployments —
including unrelated, non-autoscale ones — failed with InsufficientVirtualNetworkCapacityException: Unable to acquire Guest IP address
autoscale.errored.instance.threshold was at its default of 10 throughout, and getErroredInstanceCount() returned 0 the entire time, because none of the leaked
instances were in Error — they were in Stopped.
SUGGESTED FIX
Any one of these would break the loop; the first two seem most direct:
Include State.Stopped in getErroredInstanceCount() (or add a separate
"failed instance" counter covering both Error and Stopped).
Broaden doScaleUp's catch from ServerApiException to also handle CloudRuntimeException, so destroyVm() runs and the member is not leaked.
Count Stopped members in countAvailableVmsByGroup() so that leaked members
contribute to max_members and become eligible for scale-down.
ISSUE TYPE
COMPONENT NAME
CLOUDSTACK VERSION
CONFIGURATION
Advanced zone, KVM, Ceph/RBD-only primary storage. One AutoScale VM group
(
min_members=1,max_members=2,interval=30) on an isolated network with a/24 guest CIDR.
OS / ENVIRONMENT
Linux (Debian 12), KVM hosts.
SUMMARY
The infinite-autoscaling guard added in #11244 (fixing #9318) only counts
instances in
State.Error. When scale-up VMs fail to start — as opposed tofailing to be created — they land in
State.Stopped, notState.Error. Theguard therefore never trips, and the group scales up on every interval
indefinitely.
In our incident this produced 2,296 VMs from a group whose
max_membersis 2,over roughly 21 hours, until the guest subnet was exhausted.
Two independent counters are involved and both exclude
Stopped:AutoScaleVmGroupVmMapDaoImpl.getErroredInstanceCount()— the Prevent infinite autoscaling #11244 guard —counts
State.Erroronly:AutoScaleVmGroupVmMapDaoImpl.countAvailableVmsByGroup()— used by everyscaling decision in
AutoScaleManagerImpl(checkConditionUp,checkConditionDown,checkAutoScaleVmGroup, and the group-state handlers) —counts only
Starting,Running,Stopping,Migrating:So with N leaked
Stoppedmembers,currentVM == 0:checkAutoScaleVmGroup:if (currentVM < minMembers)->0 < 1-> scale up, every intervalcheckAutoScaleVmGroup:if (currentVM > maxMembers)->0 > 2-> scale-down never firescheckConditionDown:if (currentVM - 1 < minVm)->-1 < 1-> scale-down additionally blockedcheckConditionUp: errored-instance guard ->0 > 10false -> guard never tripsA third defect prevents the failed VM from being cleaned up, which is what allows
the leak to accumulate in the first place.
doScaleUppersists the group map rowbefore attempting the start, and its cleanup is guarded on
ServerApiException:startNewVMdoes convertInsufficientCapacityExceptionintoServerApiException,but it never sees that exception, because
VirtualMachineManagerImpl.start()has already wrapped it into an unchecked
CloudRuntimeException:CloudRuntimeExceptionmatches none ofstartNewVM's typed catches and is not aServerApiException, so it propagates pastdoScaleUp's handler toAutoScaleManagerImpl$MonitorTask, anddestroyVm()is never called. Theobserved log line is exactly this:
Note that PR #9574 ("Prevent infinite retries of autoscaling"), which proposed a
one-line change to
AutoScaleVmGroupVmMapDaoImpl, was closed unmerged; the merged#11244 took the threshold approach instead, which is what leaves this variant
uncovered.
STEPS TO REPRODUCE
Create an AutoScale VM group (
min_members=1,max_members=2, short interval).Let it stabilise at 1 running VM.
Break VM start (not creation) in a way that returns an
InsufficientCapacityExceptionorResourceUnavailableExceptionfromadvanceStart.The trigger we actually hit was the group network's Virtual Router becoming
unreachable, so
VirtualRouterElement.applyDhcpEntriesfailed with:ResourceUnavailableException: Resource [DataCenter:1] is unreachable: Unable to apply dhcp entry on router.That was an observed failure rather than a deliberate test, so I have not
confirmed that stopping the VR is a minimal reproducer — any start-path failure
that surfaces as
CloudRuntimeExceptionout ofVirtualMachineManagerImpl.start()should exhibit the same leak.
Observe: one new VM per interval, each landing in
Stopped, each retaining itsautoscale_vmgroup_vm_maprow, indefinitely.EXPECTED RESULTS
Scale-up stops after a bounded number of consecutive failed starts, and/or failed
instances are cleaned up, and/or
Stoppedmembers count towardmax_members.ACTUAL RESULTS
Unbounded VM creation. In our case, one VM per 30s for ~21 hours:
Error(created after the guest subnet was exhausted — these failat IP allocation, before a NIC is assigned)
Stopped, each holding a NIC and therefore a guest IPincluding unrelated, non-autoscale ones — failed with
InsufficientVirtualNetworkCapacityException: Unable to acquire Guest IP addressautoscale.errored.instance.thresholdwas at its default of 10 throughout, andgetErroredInstanceCount()returned 0 the entire time, because none of the leakedinstances were in
Error— they were inStopped.SUGGESTED FIX
Any one of these would break the loop; the first two seem most direct:
State.StoppedingetErroredInstanceCount()(or add a separate"failed instance" counter covering both
ErrorandStopped).doScaleUp's catch fromServerApiExceptionto also handleCloudRuntimeException, sodestroyVm()runs and the member is not leaked.Stoppedmembers incountAvailableVmsByGroup()so that leaked memberscontribute to
max_membersand become eligible for scale-down.