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

DaanHoogland 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 2ebcaf626cb Do not shut down networks that are being implemented in 
the orphaned-network cleanup (#14527)
2ebcaf626cb is described below

commit 2ebcaf626cba519ad50a829aa130cdb8ca916394
Author: Andrija Panic <[email protected]>
AuthorDate: Tue Nov 3 12:23:48 2026 +0100

    Do not shut down networks that are being implemented in the 
orphaned-network cleanup (#14527)
---
 .../main/java/com/cloud/utils/db/Merovingian2.java |  10 ++
 .../cloud/utils/db/Merovingian2AcquireTest.java    |  82 ++++++++++++++
 .../framework/jobs/impl/AsyncJobManagerImpl.java   |  66 +++++++----
 .../jobs/impl/AsyncJobManagerImplTest.java         | 125 +++++++++++++++++++++
 4 files changed, 262 insertions(+), 21 deletions(-)

diff --git a/framework/db/src/main/java/com/cloud/utils/db/Merovingian2.java 
b/framework/db/src/main/java/com/cloud/utils/db/Merovingian2.java
index a9a1ce4272e..3ef2456f6ac 100644
--- a/framework/db/src/main/java/com/cloud/utils/db/Merovingian2.java
+++ b/framework/db/src/main/java/com/cloud/utils/db/Merovingian2.java
@@ -127,6 +127,16 @@ public class Merovingian2 extends StandardMBean implements 
MerovingianMBean {
         if (LOGGER.isTraceEnabled()) {
             LOGGER.trace("Acquiring lck-" + key + " with wait time of " + 
timeInSeconds);
         }
+
+        if (timeInSeconds <= 0) {
+            // no wait: try exactly once (the loop below would not try at all)
+            int count = owns(key);
+            if (count >= 1) {
+                return increment(key, threadName, threadId);
+            }
+            return count == 0 && doAcquire(key, threadName, threadId);
+        }
+
         long startTime = InaccurateClock.getTime();
 
         while ((InaccurateClock.getTime() - startTime) < (timeInSeconds * 
1000l)) {
diff --git 
a/framework/db/src/test/java/com/cloud/utils/db/Merovingian2AcquireTest.java 
b/framework/db/src/test/java/com/cloud/utils/db/Merovingian2AcquireTest.java
new file mode 100644
index 00000000000..44f87cde902
--- /dev/null
+++ b/framework/db/src/test/java/com/cloud/utils/db/Merovingian2AcquireTest.java
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.utils.db;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class Merovingian2AcquireTest {
+    private static final String KEY = "networks42";
+
+    // the lock table calls are stubbed, so no database is needed
+    private final Merovingian2 lockController = 
Mockito.mock(Merovingian2.class, Mockito.CALLS_REAL_METHODS);
+
+    @Test
+    public void acquireWithoutWaitTakesFreeLock() {
+        Mockito.doReturn(0).when(lockController).owns(KEY);
+        Mockito.doReturn(true).when(lockController).doAcquire(Mockito.eq(KEY), 
Mockito.anyString(), Mockito.anyInt());
+
+        Assert.assertTrue(lockController.acquire(KEY, 0));
+        Mockito.verify(lockController).doAcquire(Mockito.eq(KEY), 
Mockito.anyString(), Mockito.anyInt());
+    }
+
+    @Test
+    public void acquireWithoutWaitTriesOnceWhenLockIsHeldElsewhere() {
+        Mockito.doReturn(-1).when(lockController).owns(KEY);
+
+        Assert.assertFalse(lockController.acquire(KEY, 0));
+        Mockito.verify(lockController, Mockito.times(1)).owns(KEY);
+        Mockito.verify(lockController, 
Mockito.never()).doAcquire(Mockito.anyString(), Mockito.anyString(), 
Mockito.anyInt());
+    }
+
+    @Test
+    public void acquireWithoutWaitReentersLockHeldByThisThread() {
+        Mockito.doReturn(1).when(lockController).owns(KEY);
+        Mockito.doReturn(true).when(lockController).increment(Mockito.eq(KEY), 
Mockito.anyString(), Mockito.anyInt());
+
+        Assert.assertTrue(lockController.acquire(KEY, 0));
+        Mockito.verify(lockController, 
Mockito.never()).doAcquire(Mockito.anyString(), Mockito.anyString(), 
Mockito.anyInt());
+    }
+
+    @Test
+    public void acquireWithoutWaitFailsWhenInsertLosesRace() {
+        Mockito.doReturn(0).when(lockController).owns(KEY);
+        
Mockito.doReturn(false).when(lockController).doAcquire(Mockito.eq(KEY), 
Mockito.anyString(), Mockito.anyInt());
+
+        Assert.assertFalse(lockController.acquire(KEY, 0));
+        Mockito.verify(lockController, 
Mockito.times(1)).doAcquire(Mockito.eq(KEY), Mockito.anyString(), 
Mockito.anyInt());
+    }
+
+    @Test
+    public void acquireWithTimeoutStillWaitsForHeldLock() {
+        // held on the first try, free on the second; the timeout is long 
enough for the 5 s retry interval
+        Mockito.doReturn(-1).doReturn(0).when(lockController).owns(KEY);
+        Mockito.doReturn(true).when(lockController).doAcquire(Mockito.eq(KEY), 
Mockito.anyString(), Mockito.anyInt());
+
+        long start = System.currentTimeMillis();
+        Assert.assertTrue(lockController.acquire(KEY, 30));
+        long waited = System.currentTimeMillis() - start;
+
+        Assert.assertTrue("waited only " + waited + " ms", waited >= 4900);
+        Mockito.verify(lockController, Mockito.times(2)).owns(KEY);
+        Mockito.verify(lockController, 
Mockito.times(1)).doAcquire(Mockito.eq(KEY), Mockito.anyString(), 
Mockito.anyInt());
+    }
+}
diff --git 
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
 
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
index f9fdc075132..9f21971ab8d 100644
--- 
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
+++ 
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
@@ -1288,7 +1288,7 @@ public class AsyncJobManagerImpl extends ManagerBase 
implements AsyncJobManager,
      * Cleanup networks that are stuck in Implementing state without 
associated async jobs.
      * This only processes networks that have been stuck for longer than the 
job expiration threshold.
      */
-    private void cleanupOrphanedNetworks() {
+    protected void cleanupOrphanedNetworks() {
         try {
             SearchCriteria<NetworkVO> sc = networkDao.createSearchCriteria();
             sc.addAnd("state", SearchCriteria.Op.EQ, 
Network.State.Implementing);
@@ -1313,30 +1313,54 @@ public class AsyncJobManagerImpl extends ManagerBase 
implements AsyncJobManager,
                     continue;
                 }
 
-                List<AsyncJobVO> jobs = 
_jobDao.findInstancePendingAsyncJobs("Network", network.getAccountId());
-                boolean hasActiveJob = false;
-                for (AsyncJobVO job : jobs) {
-                    if (job.getInstanceId() != null && 
job.getInstanceId().equals(network.getId())) {
-                        hasActiveJob = true;
-                        break;
-                    }
-                }
+                cleanupNetworkIfOrphaned(network.getId(), expireMinutes);
+            }
+        } catch (Exception e) {
+            logger.error("Error while cleaning up orphaned networks", e);
+        }
+    }
 
-                if (hasActiveJob) {
-                    logger.debug("Network {} in Implementing state has active 
async job, skipping cleanup", network.getId());
-                    continue;
-                }
+    /**
+     * The thread implementing a network holds the network's lock for the 
whole implementation, and that work can run
+     * inside a job of another type (a VM start, for example), so a network in 
Implementing is orphaned only if its lock
+     * is free. The lock is tried once without waiting, so the GC task does 
not block behind a running implementation,
+     * and the network is re-read under the lock before it is changed.
+     */
+    private void cleanupNetworkIfOrphaned(long networkId, long expireMinutes) {
+        NetworkVO network = networkDao.acquireInLockTable(networkId, 0);
+        if (network == null) {
+            logger.debug("Network {} in Implementing state is locked (being 
implemented) or was removed, skipping cleanup", networkId);
+            return;
+        }
 
-                logger.warn("Found orphaned network {} in Implementing state 
without async job. " +
-                           "Network created: {}, age: {} minutes, expiration 
threshold: {} minutes. Transitioning to Shutdown state.",
-                           network.getId(), network.getCreated(),
-                           (System.currentTimeMillis() - 
network.getCreated().getTime()) / 60000,
-                           expireMinutes);
-                updateNetworkState(network);
+        try {
+            if (!Network.State.Implementing.equals(network.getState())) {
+                logger.debug("Network {} is no longer in Implementing state 
(now {}), skipping cleanup", networkId, network.getState());
+                return;
+            }
 
+            List<AsyncJobVO> jobs = 
_jobDao.findInstancePendingAsyncJobs("Network", network.getAccountId());
+            boolean hasActiveJob = false;
+            for (AsyncJobVO job : jobs) {
+                if (job.getInstanceId() != null && 
job.getInstanceId().equals(network.getId())) {
+                    hasActiveJob = true;
+                    break;
+                }
             }
-        } catch (Exception e) {
-            logger.error("Error while cleaning up orphaned networks", e);
+
+            if (hasActiveJob) {
+                logger.debug("Network {} in Implementing state has active 
async job, skipping cleanup", network.getId());
+                return;
+            }
+
+            logger.warn("Found orphaned network {} in Implementing state 
without async job. " +
+                       "Network created: {}, age: {} minutes, expiration 
threshold: {} minutes. Transitioning to Shutdown state.",
+                       network.getId(), network.getCreated(),
+                       (System.currentTimeMillis() - 
network.getCreated().getTime()) / 60000,
+                       expireMinutes);
+            updateNetworkState(network);
+        } finally {
+            networkDao.releaseFromLockTable(networkId);
         }
     }
 
diff --git 
a/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImplTest.java
 
b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImplTest.java
index 0be5dbc01cb..364cc7d7a00 100644
--- 
a/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImplTest.java
+++ 
b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImplTest.java
@@ -21,6 +21,7 @@ import com.cloud.network.Network;
 import com.cloud.network.dao.NetworkDao;
 import com.cloud.network.dao.NetworkVO;
 import com.cloud.storage.Volume;
+import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.fsm.NoTransitionException;
 import com.cloud.vm.VMInstanceVO;
 import com.cloud.vm.VirtualMachine;
@@ -30,14 +31,20 @@ import org.apache.cloudstack.api.ApiCommandResourceType;
 import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
 import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.framework.jobs.dao.AsyncJobDao;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
 import static org.mockito.Mockito.when;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -55,6 +62,29 @@ public class AsyncJobManagerImplTest {
     NetworkDao networkDao;
     @Mock
     NetworkOrchestrationService networkOrchestrationService;
+    @Mock
+    AsyncJobDao asyncJobDao;
+
+    private static final long NETWORK_ID = 42L;
+    private static final long ACCOUNT_ID = 2L;
+    // job.expire.minutes defaults to one day; these networks were created two 
days ago
+    private static final Date OLD_NETWORK_CREATED = new 
Date(System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000L);
+
+    private NetworkVO mockImplementingNetworkListed(Date created) {
+        NetworkVO listed = Mockito.mock(NetworkVO.class);
+        when(listed.getCreated()).thenReturn(created);
+        SearchCriteria<NetworkVO> sc = Mockito.mock(SearchCriteria.class);
+        when(networkDao.createSearchCriteria()).thenReturn(sc);
+        when(networkDao.search(sc, null)).thenReturn(List.of(listed));
+        return listed;
+    }
+
+    private NetworkVO mockNetworkReadUnderLock(Network.State state) {
+        NetworkVO locked = Mockito.mock(NetworkVO.class);
+        when(locked.getState()).thenReturn(state);
+        when(networkDao.acquireInLockTable(NETWORK_ID, 0)).thenReturn(locked);
+        return locked;
+    }
 
     @Test
     public void testCleanupVolumeResource() {
@@ -93,4 +123,99 @@ public class AsyncJobManagerImplTest {
         Mockito.verify(networkOrchestrationService, 
Mockito.times(1)).stateTransitTo(networkVO,
                 Network.Event.OperationFailed);
     }
+
+    @Test
+    public void testCleanupOrphanedNetworksSkipsNetworkWhoseLockIsHeld() 
throws NoTransitionException {
+        NetworkVO listed = mockImplementingNetworkListed(OLD_NETWORK_CREATED);
+        when(listed.getId()).thenReturn(NETWORK_ID);
+        // a VM start implementing the network holds its lock, so a try 
without waiting fails
+        when(networkDao.acquireInLockTable(NETWORK_ID, 0)).thenReturn(null);
+
+        asyncJobManager.cleanupOrphanedNetworks();
+
+        Mockito.verify(networkOrchestrationService, 
Mockito.never()).stateTransitTo(Mockito.any(), Mockito.any());
+        Mockito.verify(networkDao, Mockito.never()).update(Mockito.anyLong(), 
Mockito.any());
+        Mockito.verify(networkDao, 
Mockito.never()).releaseFromLockTable(Mockito.anyLong());
+        Mockito.verifyNoInteractions(asyncJobDao);
+    }
+
+    @Test
+    public void testCleanupOrphanedNetworksShutsDownOrphanedNetworkUnderLock() 
throws NoTransitionException {
+        NetworkVO listed = mockImplementingNetworkListed(OLD_NETWORK_CREATED);
+        when(listed.getId()).thenReturn(NETWORK_ID);
+        NetworkVO locked = 
mockNetworkReadUnderLock(Network.State.Implementing);
+        when(locked.getId()).thenReturn(NETWORK_ID);
+        when(locked.getAccountId()).thenReturn(ACCOUNT_ID);
+        when(locked.getCreated()).thenReturn(OLD_NETWORK_CREATED);
+        when(asyncJobDao.findInstancePendingAsyncJobs("Network", 
ACCOUNT_ID)).thenReturn(Collections.emptyList());
+
+        asyncJobManager.cleanupOrphanedNetworks();
+
+        InOrder inOrder = Mockito.inOrder(networkDao, 
networkOrchestrationService);
+        inOrder.verify(networkDao).acquireInLockTable(NETWORK_ID, 0);
+        inOrder.verify(networkOrchestrationService).stateTransitTo(locked, 
Network.Event.OperationFailed);
+        inOrder.verify(networkDao).releaseFromLockTable(NETWORK_ID);
+        Mockito.verify(networkOrchestrationService, 
Mockito.never()).stateTransitTo(Mockito.eq(listed), Mockito.any());
+    }
+
+    @Test
+    public void 
testCleanupOrphanedNetworksSkipsNetworkImplementedBeforeItWasLocked() throws 
NoTransitionException {
+        NetworkVO listed = mockImplementingNetworkListed(OLD_NETWORK_CREATED);
+        when(listed.getId()).thenReturn(NETWORK_ID);
+        // the implementation finished between the search and the lock
+        mockNetworkReadUnderLock(Network.State.Implemented);
+
+        asyncJobManager.cleanupOrphanedNetworks();
+
+        Mockito.verify(networkOrchestrationService, 
Mockito.never()).stateTransitTo(Mockito.any(), Mockito.any());
+        Mockito.verify(networkDao, Mockito.never()).update(Mockito.anyLong(), 
Mockito.any());
+        Mockito.verify(networkDao).releaseFromLockTable(NETWORK_ID);
+    }
+
+    @Test
+    public void testCleanupOrphanedNetworksSkipsNetworkWithPendingNetworkJob() 
throws NoTransitionException {
+        NetworkVO listed = mockImplementingNetworkListed(OLD_NETWORK_CREATED);
+        when(listed.getId()).thenReturn(NETWORK_ID);
+        NetworkVO locked = 
mockNetworkReadUnderLock(Network.State.Implementing);
+        when(locked.getId()).thenReturn(NETWORK_ID);
+        when(locked.getAccountId()).thenReturn(ACCOUNT_ID);
+        AsyncJobVO job = new AsyncJobVO();
+        job.setInstanceId(NETWORK_ID);
+        when(asyncJobDao.findInstancePendingAsyncJobs("Network", 
ACCOUNT_ID)).thenReturn(List.of(job));
+
+        asyncJobManager.cleanupOrphanedNetworks();
+
+        Mockito.verify(networkOrchestrationService, 
Mockito.never()).stateTransitTo(Mockito.any(), Mockito.any());
+        Mockito.verify(networkDao).releaseFromLockTable(NETWORK_ID);
+    }
+
+    @Test
+    public void testCleanupOrphanedNetworksSkipsYoungNetwork() throws 
NoTransitionException {
+        mockImplementingNetworkListed(new Date());
+
+        asyncJobManager.cleanupOrphanedNetworks();
+
+        Mockito.verify(networkDao, 
Mockito.never()).acquireInLockTable(Mockito.anyLong(), Mockito.anyInt());
+        Mockito.verify(networkOrchestrationService, 
Mockito.never()).stateTransitTo(Mockito.any(), Mockito.any());
+        Mockito.verifyNoInteractions(asyncJobDao);
+    }
+
+    @Test
+    public void testCleanupOrphanedNetworksReleasesLockWhenForcingShutdown() 
throws NoTransitionException {
+        NetworkVO listed = mockImplementingNetworkListed(OLD_NETWORK_CREATED);
+        when(listed.getId()).thenReturn(NETWORK_ID);
+        NetworkVO locked = 
mockNetworkReadUnderLock(Network.State.Implementing);
+        when(locked.getId()).thenReturn(NETWORK_ID);
+        when(locked.getAccountId()).thenReturn(ACCOUNT_ID);
+        when(locked.getCreated()).thenReturn(OLD_NETWORK_CREATED);
+        when(asyncJobDao.findInstancePendingAsyncJobs("Network", 
ACCOUNT_ID)).thenReturn(Collections.emptyList());
+        when(networkOrchestrationService.stateTransitTo(locked, 
Network.Event.OperationFailed)).thenThrow(new NoTransitionException("no 
transition"));
+
+        asyncJobManager.cleanupOrphanedNetworks();
+
+        InOrder inOrder = Mockito.inOrder(locked, networkDao);
+        inOrder.verify(locked).setState(Network.State.Shutdown);
+        inOrder.verify(networkDao).update(NETWORK_ID, locked);
+        inOrder.verify(networkDao).releaseFromLockTable(NETWORK_ID);
+    }
 }

Reply via email to