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

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


The following commit(s) were added to refs/heads/4.20 by this push:
     new b8a44e94951 lock only domains that have finite limits for checked 
reservation (#14169)
b8a44e94951 is described below

commit b8a44e949510d8fd76fbfaf9ceea235a548b57d6
Author: sb-abhish3k <[email protected]>
AuthorDate: Wed Oct 7 00:39:19 2026 +0530

    lock only domains that have finite limits for checked reservation (#14169)
    
    * lock on only domains that have finite limits for checked reservation
    
    * fix domain lock set to reflect tag->untagged fallback and limit 
inheritance
    
    listRowsToLockForLimitCheck previously only locked ancestor domains that
    owned an explicit finite resource_limit row for the exact tag, so it
    missed domains that inherit a finite limit from an ancestor's row, and
    domains that fall back to the untagged limit when no tag-specific limit
    is configured (per findCorrectResourceLimitForDomain). Either gap let
    concurrent reservations bypass domain-level serialization.
    
    Replace the set-based approximation with an in-memory walk of the
    account's domain chain that replicates findCorrectResourceLimitForDomain's
    nearest-row-with-fallback semantics, using bulk queries (covered by
    existing indexes) to fetch the chain's resource_limit rows and to resolve
    the final resource_count rows to lock.
    
    ---------
    
    Co-authored-by: Mark Armstrong <[email protected]>
---
 .../cloud/configuration/dao/ResourceLimitDao.java  |  11 +
 .../configuration/dao/ResourceLimitDaoImpl.java    |  31 +++
 .../resourcelimit/ResourceLimitManagerImpl.java    | 153 +++++++++++-
 .../ResourceLimitManagerImplTest.java              | 256 +++++++++++++++++++++
 4 files changed, 450 insertions(+), 1 deletion(-)

diff --git 
a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDao.java 
b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDao.java
index 7cdc2aacc3b..6cbfadf7ac5 100644
--- 
a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDao.java
+++ 
b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDao.java
@@ -17,6 +17,7 @@
 package com.cloud.configuration.dao;
 
 import java.util.List;
+import java.util.Set;
 
 import com.cloud.configuration.Resource;
 import com.cloud.configuration.Resource.ResourceOwnerType;
@@ -36,4 +37,14 @@ public interface ResourceLimitDao extends 
GenericDao<ResourceLimitVO, Long> {
 
     long removeEntriesByOwner(Long ownerId, ResourceOwnerType ownerType);
     void removeResourceLimitsForNonMatchingTags(Long ownerId, 
ResourceOwnerType ownerType, List<Resource.ResourceType> types, List<String> 
tags);
+
+    /**
+     * Returns the explicit {@code resource_limit} rows owned by any of
+     * {@code domainIds} for the supplied ({@code type}, {@code tag}),
+     * regardless of their {@code max} value. Domains with no matching row
+     * are simply absent from the result — the caller resolves inheritance
+     * (nearest-ancestor lookup) and the tag -&gt; untagged fallback itself,
+     * mirroring {@code 
ResourceLimitManagerImpl#findCorrectResourceLimitForDomain}.
+     */
+    List<ResourceLimitVO> listByDomainIdsAndTypeAndTag(Set<Long> domainIds, 
Resource.ResourceType type, String tag);
 }
diff --git 
a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDaoImpl.java
 
b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDaoImpl.java
index 96523ba9bea..c649b038d97 100644
--- 
a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDaoImpl.java
+++ 
b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceLimitDaoImpl.java
@@ -18,6 +18,7 @@ package com.cloud.configuration.dao;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 
 import org.apache.commons.collections.CollectionUtils;
@@ -38,6 +39,8 @@ public class ResourceLimitDaoImpl extends 
GenericDaoBase<ResourceLimitVO, Long>
     private SearchBuilder<ResourceLimitVO> IdTypeTagSearch;
     private SearchBuilder<ResourceLimitVO> IdTypeNullTagSearch;
     private SearchBuilder<ResourceLimitVO> NonMatchingTagsSearch;
+    private SearchBuilder<ResourceLimitVO> DomainsLimitTagSearch;
+    private SearchBuilder<ResourceLimitVO> DomainsLimitNullTagSearch;
 
     public ResourceLimitDaoImpl() {
         IdTypeTagSearch = createSearchBuilder();
@@ -60,6 +63,18 @@ public class ResourceLimitDaoImpl extends 
GenericDaoBase<ResourceLimitVO, Long>
         NonMatchingTagsSearch.and("tagNotNull", 
NonMatchingTagsSearch.entity().getTag(), SearchCriteria.Op.NNULL);
         NonMatchingTagsSearch.and("tags", 
NonMatchingTagsSearch.entity().getTag(), SearchCriteria.Op.NIN);
         NonMatchingTagsSearch.done();
+
+        DomainsLimitTagSearch = createSearchBuilder();
+        DomainsLimitTagSearch.and("type", 
DomainsLimitTagSearch.entity().getType(), SearchCriteria.Op.EQ);
+        DomainsLimitTagSearch.and("domainIds", 
DomainsLimitTagSearch.entity().getDomainId(), SearchCriteria.Op.IN);
+        DomainsLimitTagSearch.and("tag", 
DomainsLimitTagSearch.entity().getTag(), SearchCriteria.Op.EQ);
+        DomainsLimitTagSearch.done();
+
+        DomainsLimitNullTagSearch = createSearchBuilder();
+        DomainsLimitNullTagSearch.and("type", 
DomainsLimitNullTagSearch.entity().getType(), SearchCriteria.Op.EQ);
+        DomainsLimitNullTagSearch.and("domainIds", 
DomainsLimitNullTagSearch.entity().getDomainId(), SearchCriteria.Op.IN);
+        DomainsLimitNullTagSearch.and("tag", 
DomainsLimitNullTagSearch.entity().getTag(), SearchCriteria.Op.NULL);
+        DomainsLimitNullTagSearch.done();
     }
 
     @Override
@@ -150,4 +165,20 @@ public class ResourceLimitDaoImpl extends 
GenericDaoBase<ResourceLimitVO, Long>
         }
         remove(sc);
     }
+
+    @Override
+    public List<ResourceLimitVO> listByDomainIdsAndTypeAndTag(Set<Long> 
domainIds, ResourceType type, String tag) {
+        if (CollectionUtils.isEmpty(domainIds)) {
+            return new ArrayList<>();
+        }
+        SearchCriteria<ResourceLimitVO> sc = (tag != null)
+                ? DomainsLimitTagSearch.create()
+                : DomainsLimitNullTagSearch.create();
+        sc.setParameters("type", type);
+        sc.setParameters("domainIds", domainIds.toArray());
+        if (tag != null) {
+            sc.setParameters("tag", tag);
+        }
+        return listBy(sc);
+    }
 }
diff --git 
a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java 
b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java
index 6d2ec103ca2..210c70e607a 100644
--- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java
+++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java
@@ -20,6 +20,7 @@ import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -577,12 +578,162 @@ public class ResourceLimitManagerImpl extends 
ManagerBase implements ResourceLim
     }
 
     protected List<ResourceCountVO> lockAccountAndOwnerDomainRows(long 
accountId, final ResourceType type, String tag) {
-        Set<Long> rowIdsToLock = 
_resourceCountDao.listAllRowsToUpdate(accountId, ResourceOwnerType.Account, 
type, tag);
+        Set<Long> rowIdsToLock = listRowsToLockForLimitCheck(accountId, type, 
tag);
+        if (rowIdsToLock.isEmpty()) {
+            return Collections.emptyList();
+        }
         SearchCriteria<ResourceCountVO> sc = ResourceCountSearch.create();
         sc.setParameters("id", rowIdsToLock.toArray());
         return _resourceCountDao.lockRows(sc, null, true);
     }
 
+    /**
+     * Returns the {@code resource_count} row IDs that need {@code FOR UPDATE}
+     * locks for a limit check: always the account row, plus ancestor-domain
+     * rows ONLY when their effective limit for ({@code type}, {@code tag})
+     * is finite. Ancestors with an UNLIMITED (or absent) effective limit are
+     * excluded — locking them would only create cross-tenant InnoDB row-lock
+     * contention with no effect on the check outcome (see
+     * {@link #checkDomainResourceLimit} which short-circuits when the
+     * domain's limit is {@link Resource#RESOURCE_UNLIMITED} and skips the
+     * ROOT domain entirely).
+     *
+     * <p>The account row is always included so two concurrent reservations
+     * against the same account still serialize at the InnoDB level.
+     *
+     * <p>"Effective limit" is resolved exactly like
+     * {@link #findCorrectResourceLimitForDomain}: a bulk query fetches every
+     * explicit tag-specific {@code resource_limit} row owned by any domain
+     * in the chain (a second bulk query for untagged rows only runs if some
+     * domain in the chain turns out to have no tag-specific row of its own
+     * or an ancestor's), then the chain is walked in memory from the
+     * account's domain up to (but excluding) ROOT, taking the nearest
+     * tag-specific row and falling back to the nearest untagged row only
+     * when no tag-specific row exists anywhere in the domain's own upward
+     * path. This correctly locks domains that inherit a finite limit from
+     * an ancestor's row (not just domains that own one), and domains that
+     * fall back to a finite untagged limit because no tag-specific limit is
+     * configured.
+     *
+     * <p>The final domain-row lookup is a single bulk
+     * {@link ResourceCountDao#findByOwnersAndTypeAndTag} call over exactly
+     * the domains found to have a finite effective limit — no more, no
+     * fewer, and no per-domain round trips.
+     *
+     * <p>If the global default for this type is itself finite (only possible
+     * for {@code primary_storage}/{@code secondary_storage} via
+     * {@code domainResourceLimitMap}), every ancestor inherits that finite
+     * default and we fall back to {@link ResourceCountDao#listAllRowsToUpdate}
+     * to lock the full chain. Same fallback applies when a tagged account
+     * row does not yet exist, so the create-on-miss materialization in
+     * {@code listAllRowsToUpdate} still fires.
+     *
+     * <p>This is a READ-PATH helper. The write path
+     * ({@link #updateResourceCountForAccount}) keeps using
+     * {@code listAllRowsToUpdate} directly so counts at unconstrained
+     * ancestors stay accurate for audit/aggregation.
+     */
+    protected Set<Long> listRowsToLockForLimitCheck(long accountId, 
ResourceType type, String tag) {
+        if (findDefaultResourceLimitForDomain(type) != 
Resource.RESOURCE_UNLIMITED) {
+            return _resourceCountDao.listAllRowsToUpdate(accountId, 
ResourceOwnerType.Account, type, tag);
+        }
+
+        Set<Long> rowIds = new HashSet<>();
+
+        ResourceCountVO accountRow = 
_resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
ResourceOwnerType.Account, type, tag);
+        if (accountRow != null) {
+            rowIds.add(accountRow.getId());
+        } else if (StringUtils.isNotEmpty(tag)) {
+            // Preserve the tagged-row create-on-miss side effect from
+            // ResourceCountDaoImpl.listAllRowsToUpdate.
+            return _resourceCountDao.listAllRowsToUpdate(accountId, 
ResourceOwnerType.Account, type, tag);
+        }
+
+        AccountVO account = _accountDao.findByIdIncludingRemoved(accountId);
+        if (account == null) {
+            return rowIds;
+        }
+
+        List<Long> ancestorChain = 
getOrderedAncestorDomainIds(account.getDomainId());
+        Set<Long> finiteLimitDomainIds = 
findAncestorDomainsWithFiniteLimit(ancestorChain, type, tag);
+        if (!finiteLimitDomainIds.isEmpty()) {
+            List<ResourceCountVO> domainRows = 
_resourceCountDao.findByOwnersAndTypeAndTag(
+                    new ArrayList<>(finiteLimitDomainIds), 
ResourceOwnerType.Domain, type, tag);
+            for (ResourceCountVO row : domainRows) {
+                rowIds.add(row.getId());
+            }
+        }
+
+        return rowIds;
+    }
+
+    /**
+     * Returns the account's domain plus every ancestor up to (but excluding)
+     * ROOT, ordered from the account's own domain outward — the same order
+     * {@link #findCorrectResourceLimitForDomain} and
+     * {@link #checkDomainResourceLimit} walk.
+     */
+    private List<Long> getOrderedAncestorDomainIds(long domainId) {
+        List<Long> chain = new ArrayList<>();
+        Long currentId = domainId;
+        while (currentId != null && currentId != Domain.ROOT_DOMAIN) {
+            chain.add(currentId);
+            DomainVO domain = _domainDao.findById(currentId);
+            currentId = (domain != null) ? domain.getParent() : null;
+        }
+        return chain;
+    }
+
+    /**
+     * Returns the subset of {@code ancestorChain} whose effective limit for
+     * ({@code type}, {@code tag}) is finite, per
+     * {@link #findCorrectResourceLimitForDomain}'s nearest-row-with-fallback
+     * semantics. The untagged fallback query only runs if at least one
+     * domain in the chain actually lacks a tag-specific row.
+     */
+    private Set<Long> findAncestorDomainsWithFiniteLimit(List<Long> 
ancestorChain, ResourceType type, String tag) {
+        if (ancestorChain.isEmpty()) {
+            return Collections.emptySet();
+        }
+
+        Set<Long> chainSet = new HashSet<>(ancestorChain);
+        Map<Long, ResourceLimitVO> tagRowsByDomain = 
indexByDomainId(_resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, type, 
tag));
+        Map<Long, ResourceLimitVO> untaggedRowsByDomain = null;
+
+        Set<Long> finiteLimitDomainIds = new HashSet<>();
+        for (int i = 0; i < ancestorChain.size(); i++) {
+            ResourceLimitVO nearestRow = findNearestRowFrom(ancestorChain, i, 
tagRowsByDomain);
+            if (nearestRow == null && StringUtils.isNotEmpty(tag)) {
+                if (untaggedRowsByDomain == null) {
+                    untaggedRowsByDomain = 
indexByDomainId(_resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, type, 
null));
+                }
+                nearestRow = findNearestRowFrom(ancestorChain, i, 
untaggedRowsByDomain);
+            }
+            if (nearestRow != null && nearestRow.getMax().longValue() != 
Resource.RESOURCE_UNLIMITED) {
+                finiteLimitDomainIds.add(ancestorChain.get(i));
+            }
+        }
+        return finiteLimitDomainIds;
+    }
+
+    private static ResourceLimitVO findNearestRowFrom(List<Long> chain, int 
startIndex, Map<Long, ResourceLimitVO> rowsByDomain) {
+        for (int i = startIndex; i < chain.size(); i++) {
+            ResourceLimitVO row = rowsByDomain.get(chain.get(i));
+            if (row != null) {
+                return row;
+            }
+        }
+        return null;
+    }
+
+    private static Map<Long, ResourceLimitVO> 
indexByDomainId(List<ResourceLimitVO> rows) {
+        Map<Long, ResourceLimitVO> map = new HashMap<>();
+        for (ResourceLimitVO row : rows) {
+            map.put(row.getDomainId(), row);
+        }
+        return map;
+    }
+
     @Override
     public long findDefaultResourceLimitForDomain(ResourceType resourceType) {
         Long resourceLimit;
diff --git 
a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java
 
b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java
index bdc6620c490..78d69365abb 100644
--- 
a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java
+++ 
b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java
@@ -19,9 +19,12 @@ package com.cloud.resourcelimit;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.cloudstack.api.response.AccountResponse;
 import org.apache.cloudstack.api.response.DomainResponse;
@@ -574,6 +577,259 @@ public class ResourceLimitManagerImplTest extends 
TestCase {
         }
     }
 
+    private ResourceCountVO mockCountRow(long id) {
+        ResourceCountVO row = Mockito.mock(ResourceCountVO.class);
+        Mockito.when(row.getId()).thenReturn(id);
+        return row;
+    }
+
+    private ResourceLimitVO mockLimitRow(long domainId, long max) {
+        ResourceLimitVO row = Mockito.mock(ResourceLimitVO.class);
+        Mockito.when(row.getDomainId()).thenReturn(domainId);
+        Mockito.when(row.getMax()).thenReturn(max);
+        return row;
+    }
+
+    /**
+     * Mocks domainDao.findById() so that walking getParent() from
+     * chainWithRootLast[0] visits chainWithRootLast in order, ending at
+     * Domain.ROOT_DOMAIN (the last element).
+     */
+    private void mockDomainParentChain(long... chainWithRootLast) {
+        for (int i = 0; i < chainWithRootLast.length - 1; i++) {
+            DomainVO domain = Mockito.mock(DomainVO.class);
+            Mockito.when(domain.getParent()).thenReturn(chainWithRootLast[i + 
1]);
+            
Mockito.when(domainDao.findById(chainWithRootLast[i])).thenReturn(domain);
+        }
+    }
+
+    @Test
+    public void 
testListRowsToLockForLimitCheckOnlyAccountDomainHasFiniteLimit() {
+        // Account-domain has a finite limit; zone-domain (parent) has none 
(unlimited).
+        // Expected lock set = {account row, account-domain row}.
+        long accountId = 1L;
+        long accountDomainId = 7L;
+        long zoneDomainId = 3L;
+        Resource.ResourceType type = Resource.ResourceType.volume;
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getDomainId()).thenReturn(accountDomainId);
+        
Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account);
+        mockDomainParentChain(accountDomainId, zoneDomainId, 
Domain.ROOT_DOMAIN);
+
+        ResourceCountVO accountRow = mockCountRow(100L);
+        ResourceCountVO accountDomainRow = mockCountRow(200L);
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, null))
+                .thenReturn(accountRow);
+
+        Set<Long> chainSet = new HashSet<>(Arrays.asList(accountDomainId, 
zoneDomainId));
+        ResourceLimitVO accountDomainLimitRow = mockLimitRow(accountDomainId, 
10L);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, null))
+                .thenReturn(Collections.singletonList(accountDomainLimitRow));
+        
Mockito.when(resourceCountDao.findByOwnersAndTypeAndTag(Mockito.anyList(), 
Mockito.eq(Resource.ResourceOwnerType.Domain), Mockito.eq(type), 
Mockito.isNull()))
+                .thenReturn(Collections.singletonList(accountDomainRow));
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, null);
+
+        Assert.assertEquals(new HashSet<>(Arrays.asList(100L, 200L)), rowIds);
+        Mockito.verify(resourceCountDao, 
Mockito.never()).listAllRowsToUpdate(Mockito.anyLong(), Mockito.any(), 
Mockito.any(), Mockito.any());
+        Mockito.verify(resourceCountDao, Mockito.times(1))
+                
.findByOwnersAndTypeAndTag(Mockito.eq(Collections.singletonList(accountDomainId)),
 Mockito.eq(Resource.ResourceOwnerType.Domain), Mockito.eq(type), 
Mockito.isNull());
+    }
+
+    @Test
+    public void testListRowsToLockForLimitCheckMultipleFiniteAncestors() {
+        // Both ancestor-domain rows have finite limits; both must be locked.
+        long accountId = 1L;
+        long accountDomainId = 7L;
+        long zoneDomainId = 3L;
+        Resource.ResourceType type = Resource.ResourceType.cpu;
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getDomainId()).thenReturn(accountDomainId);
+        
Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account);
+        mockDomainParentChain(accountDomainId, zoneDomainId, 
Domain.ROOT_DOMAIN);
+
+        ResourceCountVO accountRow = mockCountRow(100L);
+        ResourceCountVO accountDomainRow = mockCountRow(200L);
+        ResourceCountVO zoneDomainRow = mockCountRow(300L);
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, null))
+                .thenReturn(accountRow);
+
+        Set<Long> chainSet = new HashSet<>(Arrays.asList(accountDomainId, 
zoneDomainId));
+        ResourceLimitVO accountDomainLimitRow = mockLimitRow(accountDomainId, 
10L);
+        ResourceLimitVO zoneDomainLimitRow = mockLimitRow(zoneDomainId, 20L);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, null))
+                .thenReturn(Arrays.asList(accountDomainLimitRow, 
zoneDomainLimitRow));
+        
Mockito.when(resourceCountDao.findByOwnersAndTypeAndTag(Mockito.anyList(), 
Mockito.eq(Resource.ResourceOwnerType.Domain), Mockito.eq(type), 
Mockito.isNull()))
+                .thenReturn(Arrays.asList(accountDomainRow, zoneDomainRow));
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, null);
+
+        Assert.assertEquals(new HashSet<>(Arrays.asList(100L, 200L, 300L)), 
rowIds);
+    }
+
+    @Test
+    public void 
testListRowsToLockForLimitCheckInheritsFiniteLimitFromAncestorRow() {
+        // Account-domain has NO explicit row of its own, but inherits a 
finite limit from
+        // its parent (zone-domain)'s explicit row. Both must be locked: the 
zone-domain
+        // because it owns the finite row, and the account-domain because its 
own effective
+        // limit (via inheritance) is also finite.
+        long accountId = 1L;
+        long accountDomainId = 7L;
+        long zoneDomainId = 3L;
+        Resource.ResourceType type = Resource.ResourceType.cpu;
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getDomainId()).thenReturn(accountDomainId);
+        
Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account);
+        mockDomainParentChain(accountDomainId, zoneDomainId, 
Domain.ROOT_DOMAIN);
+
+        ResourceCountVO accountRow = mockCountRow(100L);
+        ResourceCountVO accountDomainRow = mockCountRow(200L);
+        ResourceCountVO zoneDomainRow = mockCountRow(300L);
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, null))
+                .thenReturn(accountRow);
+
+        Set<Long> chainSet = new HashSet<>(Arrays.asList(accountDomainId, 
zoneDomainId));
+        // Only the zone-domain owns an explicit row; the account-domain has 
none.
+        ResourceLimitVO zoneDomainLimitRow = mockLimitRow(zoneDomainId, 20L);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, null))
+                .thenReturn(Collections.singletonList(zoneDomainLimitRow));
+        
Mockito.when(resourceCountDao.findByOwnersAndTypeAndTag(Mockito.anyList(), 
Mockito.eq(Resource.ResourceOwnerType.Domain), Mockito.eq(type), 
Mockito.isNull()))
+                .thenReturn(Arrays.asList(accountDomainRow, zoneDomainRow));
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, null);
+
+        Assert.assertEquals(new HashSet<>(Arrays.asList(100L, 200L, 300L)), 
rowIds);
+    }
+
+    @Test
+    public void testListRowsToLockForLimitCheckAllUnlimited() {
+        // No ancestor has a finite limit; only the account row is locked.
+        long accountId = 1L;
+        long accountDomainId = 7L;
+        Resource.ResourceType type = Resource.ResourceType.cpu;
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getDomainId()).thenReturn(accountDomainId);
+        
Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account);
+        mockDomainParentChain(accountDomainId, Domain.ROOT_DOMAIN);
+
+        ResourceCountVO accountRow = mockCountRow(100L);
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, null))
+                .thenReturn(accountRow);
+
+        Set<Long> chainSet = Collections.singleton(accountDomainId);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, null))
+                .thenReturn(Collections.emptyList());
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, null);
+
+        Assert.assertEquals(Collections.singleton(100L), rowIds);
+        Mockito.verify(resourceCountDao, 
Mockito.never()).findByOwnersAndTypeAndTag(Mockito.any(), Mockito.any(), 
Mockito.any(), Mockito.any());
+    }
+
+    @Test
+    public void testListRowsToLockForLimitCheckFiniteGlobalDefaultFallsBack() {
+        // primary_storage with a finite global default → fall back to 
listAllRowsToUpdate
+        // to preserve upstream behavior (every ancestor inherits the finite 
default).
+        long accountId = 1L;
+        Resource.ResourceType type = Resource.ResourceType.primary_storage;
+        Set<Long> fullChain = new HashSet<>(Arrays.asList(100L, 200L, 300L, 
400L));
+
+        
Mockito.doReturn(1024L).when(resourceLimitManager).findDefaultResourceLimitForDomain(type);
+        Mockito.when(resourceCountDao.listAllRowsToUpdate(accountId, 
Resource.ResourceOwnerType.Account, type, null))
+                .thenReturn(fullChain);
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, null);
+
+        Assert.assertEquals(fullChain, rowIds);
+        Mockito.verify(resourceLimitDao, 
Mockito.never()).listByDomainIdsAndTypeAndTag(Mockito.anySet(), Mockito.any(), 
Mockito.any());
+    }
+
+    @Test
+    public void 
testListRowsToLockForLimitCheckTaggedAccountRowMissingFallsBack() {
+        // Tagged limit, account row not yet materialized → fall back to 
listAllRowsToUpdate
+        // so the create-on-miss tagged-row materialization still fires.
+        long accountId = 1L;
+        Resource.ResourceType type = Resource.ResourceType.cpu;
+        String tag = hostTags.get(0);
+        Set<Long> fullChain = new HashSet<>(Arrays.asList(101L, 201L));
+
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, tag))
+                .thenReturn(null);
+        Mockito.when(resourceCountDao.listAllRowsToUpdate(accountId, 
Resource.ResourceOwnerType.Account, type, tag))
+                .thenReturn(fullChain);
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, tag);
+
+        Assert.assertEquals(fullChain, rowIds);
+        Mockito.verify(resourceCountDao, 
Mockito.times(1)).listAllRowsToUpdate(accountId, 
Resource.ResourceOwnerType.Account, type, tag);
+        Mockito.verify(resourceLimitDao, 
Mockito.never()).listByDomainIdsAndTypeAndTag(Mockito.anySet(), Mockito.any(), 
Mockito.any());
+    }
+
+    @Test
+    public void 
testListRowsToLockForLimitCheckTagFallsBackToUntaggedWhenNoTagRowExists() {
+        // No domain in the chain has ANY explicit tag-specific row, but the 
account-domain
+        // has a finite UNTAGGED limit. findCorrectResourceLimitForDomain 
falls back to that
+        // untagged limit for this domain, so it must still get its (tagged) 
resource_count
+        // row locked.
+        long accountId = 1L;
+        long accountDomainId = 7L;
+        Resource.ResourceType type = Resource.ResourceType.cpu;
+        String tag = hostTags.get(0);
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getDomainId()).thenReturn(accountDomainId);
+        
Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account);
+        mockDomainParentChain(accountDomainId, Domain.ROOT_DOMAIN);
+
+        ResourceCountVO accountRow = mockCountRow(100L);
+        ResourceCountVO accountDomainRow = mockCountRow(200L);
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, tag))
+                .thenReturn(accountRow);
+
+        Set<Long> chainSet = Collections.singleton(accountDomainId);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, tag))
+                .thenReturn(Collections.emptyList());
+        ResourceLimitVO accountDomainLimitRow = mockLimitRow(accountDomainId, 
10L);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, null))
+                .thenReturn(Collections.singletonList(accountDomainLimitRow));
+        
Mockito.when(resourceCountDao.findByOwnersAndTypeAndTag(Mockito.eq(Collections.singletonList(accountDomainId)),
 Mockito.eq(Resource.ResourceOwnerType.Domain), Mockito.eq(type), 
Mockito.eq(tag)))
+                .thenReturn(Collections.singletonList(accountDomainRow));
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, tag);
+
+        Assert.assertEquals(new HashSet<>(Arrays.asList(100L, 200L)), rowIds);
+    }
+
+    @Test
+    public void 
testListRowsToLockForLimitCheckExplicitUnlimitedTagRowSkipsUntaggedFallback() {
+        // An explicit tag-specific row exists (even though it's UNLIMITED), so
+        // findCorrectResourceLimitForDomain does NOT fall back to the 
untagged limit for
+        // this domain. The untagged finite-limit lookup must never be 
consulted, and the
+        // domain must not be locked.
+        long accountId = 1L;
+        long accountDomainId = 7L;
+        Resource.ResourceType type = Resource.ResourceType.cpu;
+        String tag = hostTags.get(0);
+        AccountVO account = Mockito.mock(AccountVO.class);
+        Mockito.when(account.getDomainId()).thenReturn(accountDomainId);
+        
Mockito.when(accountDao.findByIdIncludingRemoved(accountId)).thenReturn(account);
+        mockDomainParentChain(accountDomainId, Domain.ROOT_DOMAIN);
+
+        ResourceCountVO accountRow = mockCountRow(100L);
+        Mockito.when(resourceCountDao.findByOwnerAndTypeAndTag(accountId, 
Resource.ResourceOwnerType.Account, type, tag))
+                .thenReturn(accountRow);
+
+        Set<Long> chainSet = Collections.singleton(accountDomainId);
+        ResourceLimitVO unlimitedTagRow = mockLimitRow(accountDomainId, 
Resource.RESOURCE_UNLIMITED);
+        Mockito.when(resourceLimitDao.listByDomainIdsAndTypeAndTag(chainSet, 
type, tag))
+                .thenReturn(Collections.singletonList(unlimitedTagRow));
+
+        Set<Long> rowIds = 
resourceLimitManager.listRowsToLockForLimitCheck(accountId, type, tag);
+
+        Assert.assertEquals(Collections.singleton(100L), rowIds);
+        Mockito.verify(resourceLimitDao, 
Mockito.never()).listByDomainIdsAndTypeAndTag(chainSet, type, null);
+        Mockito.verify(resourceCountDao, 
Mockito.never()).findByOwnersAndTypeAndTag(Mockito.any(), Mockito.any(), 
Mockito.any(), Mockito.any());
+    }
+
     @Test
     public void testRemoveResourceLimitAndCountForNonMatchingTags() {
         resourceLimitManager.removeResourceLimitAndCountForNonMatchingTags(1L, 
Resource.ResourceOwnerType.Account, hostTags, storageTags);

Reply via email to