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

nvazquez pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.22 by this push:
     new 768298a2111 Batch VM and host lookups in StorageManagerImpl to 
eliminate N+1 queries during cleanup and pool connect (#13353)
768298a2111 is described below

commit 768298a21116c27a38f98db797896a322cf832e7
Author: Vishesh <[email protected]>
AuthorDate: Mon Oct 19 04:00:06 2026 +0530

    Batch VM and host lookups in StorageManagerImpl to eliminate N+1 queries 
during cleanup and pool connect (#13353)
    
    * Batch VM and host lookups in StorageManagerImpl to eliminate N+1 queries 
during cleanup and pool connect
    
    cleanupStorage: replaced per-volume 
_vmInstanceDao.findById(vol.getInstanceId())
    with a single batch SELECT ... WHERE id IN (...) before the loop, using a
    HashMap for O(1) lookups. Added null guard for volumes with null instanceId.
    
    connectHostsToPool: replaced per-thread _hostDao.findById(hostId) with a
    batch load before thread pool submission. Each thread now reads from a
    pre-built HashMap instead of hitting the DB. Added null guard with warning
    log for hosts removed between list assembly and batch read.
    
    Added VMInstanceDao.listByIds(List<Long>) with empty-list guard and 
IN-clause
    SearchBuilder, matching the established pattern in HostDao.
    
    * Fix test failures
    
    * Address comments
    
    ---------
    
    Co-authored-by: Aaron Chung <[email protected]>
---
 .../main/java/com/cloud/vm/dao/VMInstanceDao.java   |  2 ++
 .../java/com/cloud/vm/dao/VMInstanceDaoImpl.java    | 16 ++++++++++++++++
 ...CloudStackPrimaryDataStoreLifeCycleImplTest.java |  2 ++
 .../ScaleIOPrimaryDataStoreLifeCycleTest.java       |  3 +++
 .../java/com/cloud/storage/StorageManagerImpl.java  | 21 +++++++++++++++++++--
 5 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java
index 4fd3e729e0d..fbc4ea669e5 100755
--- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java
@@ -197,4 +197,6 @@ public interface VMInstanceDao extends 
GenericDao<VMInstanceVO, Long>, StateDao<
     List<VMInstanceVO> listDeleteProtectedVmsByAccountId(long accountId);
 
     List<VMInstanceVO> listDeleteProtectedVmsByDomainIds(Set<Long> domainIds);
+
+    List<VMInstanceVO> listByIds(List<Long> ids);
 }
diff --git 
a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java
index d8c9b9253c8..b22d7e0274a 100755
--- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java
@@ -21,6 +21,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -106,6 +107,7 @@ public class VMInstanceDaoImpl extends 
GenericDaoBase<VMInstanceVO, Long> implem
     protected SearchBuilder<VMInstanceVO> LastHostAndStatesSearch;
     protected SearchBuilder<VMInstanceVO> VmsNotInClusterUsingPool;
     protected SearchBuilder<VMInstanceVO> IdsPowerStateSelectSearch;
+    protected SearchBuilder<VMInstanceVO> IdsSearch;
     GenericSearchBuilder<VMInstanceVO, Integer> CountByOfferingId;
     GenericSearchBuilder<VMInstanceVO, Integer> CountUserVmNotInDomain;
     SearchBuilder<VMInstanceVO> DeleteProtectedVmSearchByAccount;
@@ -386,6 +388,10 @@ public class VMInstanceDaoImpl extends 
GenericDaoBase<VMInstanceVO, Long> implem
         DeleteProtectedVmSearchByDomainIds.and(ApiConstants.DELETE_PROTECTION, 
DeleteProtectedVmSearchByDomainIds.entity().isDeleteProtection(), Op.EQ);
         DeleteProtectedVmSearchByDomainIds.and(ApiConstants.REMOVED, 
DeleteProtectedVmSearchByDomainIds.entity().getRemoved(), Op.NULL);
         DeleteProtectedVmSearchByDomainIds.done();
+
+        IdsSearch = createSearchBuilder();
+        IdsSearch.and("ids", IdsSearch.entity().getId(), Op.IN);
+        IdsSearch.done();
     }
 
     @Override
@@ -1336,4 +1342,14 @@ public class VMInstanceDaoImpl extends 
GenericDaoBase<VMInstanceVO, Long> implem
         Filter filter = new Filter(VMInstanceVO.class, null, false, 0L, 10L);
         return listBy(sc, filter);
     }
+
+    @Override
+    public List<VMInstanceVO> listByIds(List<Long> ids) {
+        if (CollectionUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+        SearchCriteria<VMInstanceVO> sc = IdsSearch.create();
+        sc.setParameters("ids", ids.toArray());
+        return listBy(sc);
+    }
 }
diff --git 
a/plugins/storage/volume/default/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImplTest.java
 
b/plugins/storage/volume/default/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImplTest.java
index 3b533d588d3..5d765267178 100644
--- 
a/plugins/storage/volume/default/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImplTest.java
+++ 
b/plugins/storage/volume/default/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImplTest.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
+import java.util.List;
 
 import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@@ -153,6 +154,7 @@ public class CloudStackPrimaryDataStoreLifeCycleImplTest 
extends TestCase {
                 .thenReturn(Arrays.asList(host1, host2));
 
         when(hostDao.findById(anyLong())).thenReturn(mock(HostVO.class));
+        when(hostDao.listByIds(List.of(1L, 2L))).thenReturn(List.of(host1, 
host2));
         when(agentMgr.easySend(anyLong(), 
Mockito.any(ModifyStoragePoolCommand.class))).thenReturn(answer);
         when(answer.getResult()).thenReturn(true);
 
diff --git 
a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java
 
b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java
index 324f3c08cb8..ac7075ee462 100644
--- 
a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java
+++ 
b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java
@@ -151,6 +151,7 @@ public class ScaleIOPrimaryDataStoreLifeCycleTest {
 
         
when(resourceManager.getEligibleUpAndEnabledHostsInZoneForStorageConnection(dataStore,
 scope.getScopeId(), Hypervisor.HypervisorType.KVM))
                 .thenReturn(Arrays.asList(host1, host2));
+        when(hostDao.listByIds(List.of(1L, 2L))).thenReturn(List.of(host1, 
host2));
 
         when(dataStoreMgr.getDataStore(anyLong(), 
eq(DataStoreRole.Primary))).thenReturn(store);
         when(store.isShared()).thenReturn(true);
@@ -164,6 +165,8 @@ public class ScaleIOPrimaryDataStoreLifeCycleTest {
 
         boolean result = 
scaleIOPrimaryDataStoreLifeCycleTest.attachZone(dataStore, scope, 
Hypervisor.HypervisorType.KVM);
         assertThat(result).isTrue();
+        Mockito.verify(hostListener).hostConnect(host1, store);
+        Mockito.verify(hostListener).hostConnect(host2, store);
     }
 
     @Test(expected = CloudRuntimeException.class)
diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java 
b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
index 0b2aca08e08..587a5269cba 100644
--- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
+++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
@@ -1846,6 +1846,10 @@ public class StorageManagerImpl extends ManagerBase 
implements StorageManager, C
         if (CollectionUtils.isEmpty(hostIds)) {
             return;
         }
+        Map<Long, HostVO> hostMap = new HashMap<>();
+        for (HostVO h : _hostDao.listByIds(hostIds)) {
+            hostMap.put(h.getId(), h);
+        }
         CopyOnWriteArrayList<Long> poolHostIds = new CopyOnWriteArrayList<>();
         ExecutorService executorService = 
Executors.newFixedThreadPool(Math.max(1, Math.min(hostIds.size(),
                 StoragePoolHostConnectWorkers.value())));
@@ -1856,10 +1860,14 @@ public class StorageManagerImpl extends ManagerBase 
implements StorageManager, C
                 if (exceptionOccurred.get()) {
                     return null;
                 }
+                HostVO host = hostMap.get(hostId);
+                if (host == null) {
+                    logger.warn("Host [{}] not found, skipping pool connection 
for {}", hostId, primaryStore);
+                    return null;
+                }
                 Transaction.execute(new 
TransactionCallbackWithExceptionNoReturn<Exception>() {
                     @Override
                     public void doInTransactionWithoutResult(TransactionStatus 
status) throws Exception {
-                        HostVO host = _hostDao.findById(hostId);
                         try {
                             connectHostToSharedPool(host, 
primaryStore.getId());
                             poolHostIds.add(hostId);
@@ -2126,9 +2134,18 @@ public class StorageManagerImpl extends ManagerBase 
implements StorageManager, C
                     cleanupSecondaryStorage(recurring);
 
                     List<VolumeVO> vols = 
volumeDao.listVolumesToBeDestroyed(new Date(System.currentTimeMillis() - 
((long)StorageCleanupDelay.value() << 10)));
+                    List<Long> rootVolumeVmIds = vols.stream()
+                            .filter(v -> Type.ROOT.equals(v.getVolumeType()) 
&& v.getInstanceId() != null)
+                            .map(VolumeVO::getInstanceId)
+                            .distinct()
+                            .collect(Collectors.toList());
+                    Map<Long, VMInstanceVO> vmMap = new HashMap<>();
+                    for (VMInstanceVO vm : 
_vmInstanceDao.listByIds(rootVolumeVmIds)) {
+                        vmMap.put(vm.getId(), vm);
+                    }
                     for (VolumeVO vol : vols) {
                         if (Type.ROOT.equals(vol.getVolumeType())) {
-                             VMInstanceVO vmInstanceVO = 
_vmInstanceDao.findById(vol.getInstanceId());
+                            VMInstanceVO vmInstanceVO = vol.getInstanceId() != 
null ? vmMap.get(vol.getInstanceId()) : null;
                              if (vmInstanceVO != null && 
vmInstanceVO.getState() == State.Destroyed) {
                                  logger.debug("ROOT volume [{}] will not be 
expunged because the VM is [{}], therefore this volume will be expunged with 
the VM"
                                          + " cleanup job.", vol, 
vmInstanceVO.getState());

Reply via email to