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

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


The following commit(s) were added to refs/heads/main by this push:
     new 80bbb29abfb CleanUp Async Jobs after mgmt server maintenance (#8394)
80bbb29abfb is described below

commit 80bbb29abfb90476cec155c2f831e626710bf420
Author: kishankavala <[email protected]>
AuthorDate: Fri Jan 19 13:26:25 2024 +0530

    CleanUp Async Jobs after mgmt server maintenance (#8394)
    
    This PR fixes moves resources stuck in transition state during async job 
cleanup
    
    Problem:
    During maintenance of the management server, other servers in the cluster 
or the same server after a restart initiate async job cleanup. However, this 
process leaves resources in a transitional state. The only recovery option 
currently available is to make direct database changes.
    
    Solution:
    This PR introduces a resolution by changing Volume, Virtual Machine, and 
Network resources from their transitional states. This adjustment enables the 
reattempt of failed operations without the need for manual database 
modifications.
---
 api/src/main/java/com/cloud/storage/Volume.java    |  54 +++++---
 .../service/NetworkOrchestrationService.java       |   3 +
 .../subsystem/api/storage/VolumeService.java       |   5 +-
 .../engine/orchestration/NetworkOrchestrator.java  |   3 +-
 .../cloudstack/storage/volume/VolumeObject.java    |   2 +-
 .../storage/volume/VolumeServiceImpl.java          |  12 +-
 .../framework/jobs/impl/AsyncJobManagerImpl.java   | 143 ++++++++++++++++++---
 .../jobs/impl/AsyncJobManagerImplTest.java         |  96 ++++++++++++++
 .../java/com/cloud/vpc/MockNetworkManagerImpl.java |   6 +
 9 files changed, 274 insertions(+), 50 deletions(-)

diff --git a/api/src/main/java/com/cloud/storage/Volume.java 
b/api/src/main/java/com/cloud/storage/Volume.java
index 4a14197bd30..308ed2544ed 100644
--- a/api/src/main/java/com/cloud/storage/Volume.java
+++ b/api/src/main/java/com/cloud/storage/Volume.java
@@ -39,30 +39,38 @@ public interface Volume extends ControlledEntity, Identity, 
InternalIdentity, Ba
     };
 
     enum State {
-        Allocated("The volume is allocated but has not been created yet."),
-        Creating("The volume is being created.  getPoolId() should reflect the 
pool where it is being created."),
-        Ready("The volume is ready to be used."),
-        Migrating("The volume is migrating to other storage pool"),
-        Snapshotting("There is a snapshot created on this volume, not backed 
up to secondary storage yet"),
-        RevertSnapshotting("There is a snapshot created on this volume, the 
volume is being reverting from snapshot"),
-        Resizing("The volume is being resized"),
-        Expunging("The volume is being expunging"),
-        Expunged("The volume has been expunged, and can no longer be 
recovered"),
-        Destroy("The volume is destroyed, and can be recovered."),
-        Destroying("The volume is destroying, and can't be recovered."),
-        UploadOp("The volume upload operation is in progress or in short the 
volume is on secondary storage"),
-        Copying("Volume is copying from image store to primary, in case it's 
an uploaded volume"),
-        Uploaded("Volume is uploaded"),
-        NotUploaded("The volume entry is just created in DB, not yet 
uploaded"),
-        UploadInProgress("Volume upload is in progress"),
-        UploadError("Volume upload encountered some error"),
-        UploadAbandoned("Volume upload is abandoned since the upload was never 
initiated within a specified time"),
-        Attaching("The volume is attaching to a VM from Ready state."),
-        Restoring("The volume is being restored from backup.");
+        Allocated(false, "The volume is allocated but has not been created 
yet."),
+        Creating(true, "The volume is being created.  getPoolId() should 
reflect the pool where it is being created."),
+        Ready(false, "The volume is ready to be used."),
+        Migrating(true, "The volume is migrating to other storage pool"),
+        Snapshotting(true, "There is a snapshot created on this volume, not 
backed up to secondary storage yet"),
+        RevertSnapshotting(true, "There is a snapshot created on this volume, 
the volume is being reverting from snapshot"),
+        Resizing(true, "The volume is being resized"),
+        Expunging(true, "The volume is being expunging"),
+        Expunged(false, "The volume has been expunged, and can no longer be 
recovered"),
+        Destroy(false, "The volume is destroyed, and can be recovered."),
+        Destroying(false, "The volume is destroying, and can't be recovered."),
+        UploadOp(true, "The volume upload operation is in progress or in short 
the volume is on secondary storage"),
+        Copying(true, "Volume is copying from image store to primary, in case 
it's an uploaded volume"),
+        Uploaded(false, "Volume is uploaded"),
+        NotUploaded(true, "The volume entry is just created in DB, not yet 
uploaded"),
+        UploadInProgress(true, "Volume upload is in progress"),
+        UploadError(false, "Volume upload encountered some error"),
+        UploadAbandoned(false, "Volume upload is abandoned since the upload 
was never initiated within a specified time"),
+        Attaching(true, "The volume is attaching to a VM from Ready state."),
+        Restoring(true, "The volume is being restored from backup.");
+
+        boolean _transitional;
 
         String _description;
 
-        private State(String description) {
+        /**
+         * Volume State
+         * @param transitional true for transition/non-final state, otherwise 
false
+         * @param description description of the state
+         */
+        private State(boolean transitional, String description) {
+            _transitional = transitional;
             _description = description;
         }
 
@@ -70,6 +78,10 @@ public interface Volume extends ControlledEntity, Identity, 
InternalIdentity, Ba
             return s_fsm;
         }
 
+        public boolean isTransitional() {
+            return _transitional;
+        }
+
         public String getDescription() {
             return _description;
         }
diff --git 
a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java
 
b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java
index c691b8b0942..2005b70b439 100644
--- 
a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java
+++ 
b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java
@@ -50,6 +50,7 @@ import com.cloud.network.rules.LoadBalancerContainer.Scheme;
 import com.cloud.offering.NetworkOffering;
 import com.cloud.user.Account;
 import com.cloud.user.User;
+import com.cloud.utils.fsm.NoTransitionException;
 import com.cloud.utils.Pair;
 import com.cloud.vm.Nic;
 import com.cloud.vm.NicProfile;
@@ -268,6 +269,8 @@ public interface NetworkOrchestrationService {
 
     Map<String, String> finalizeServicesAndProvidersForNetwork(NetworkOffering 
offering, Long physicalNetworkId);
 
+    boolean stateTransitTo(Network network, Network.Event e) throws 
NoTransitionException;
+
     List<Provider> getProvidersForServiceInNetwork(Network network, Service 
service);
 
     StaticNatServiceProvider getStaticNatProviderForNetwork(Network network);
diff --git 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
index 2c12b70e9eb..50aee83f497 100644
--- 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
+++ 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
@@ -18,13 +18,13 @@
  */
 package org.apache.cloudstack.engine.subsystem.api.storage;
 
-import com.cloud.agent.api.Answer;
 import java.util.Map;
 
 import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity;
 import org.apache.cloudstack.framework.async.AsyncCallFuture;
 import org.apache.cloudstack.storage.command.CommandResult;
 
+import com.cloud.agent.api.Answer;
 import com.cloud.agent.api.to.VirtualMachineTO;
 import com.cloud.exception.StorageAccessException;
 import com.cloud.host.Host;
@@ -35,6 +35,9 @@ import com.cloud.user.Account;
 import com.cloud.utils.Pair;
 
 public interface VolumeService {
+
+    String SNAPSHOT_ID = "SNAPSHOT_ID";
+
     class VolumeApiResult extends CommandResult {
         private final VolumeInfo volume;
 
diff --git 
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
 
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
index 0f6dfe60866..6c10a02abba 100644
--- 
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
+++ 
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
@@ -4424,7 +4424,8 @@ public class NetworkOrchestrator extends ManagerBase 
implements NetworkOrchestra
         return accessDetails;
     }
 
-    protected boolean stateTransitTo(final NetworkVO network, final 
Network.Event e) throws NoTransitionException {
+    @Override
+    public boolean stateTransitTo(final Network network, final Network.Event 
e) throws NoTransitionException {
         return _stateMachine.transitTo(network, e, null, _networksDao);
     }
 
diff --git 
a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
 
b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
index 5ebee87acd4..9e41e0d4d0e 100644
--- 
a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
+++ 
b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
@@ -851,7 +851,7 @@ public class VolumeObject implements VolumeInfo {
 
     @Override
     public boolean delete() {
-        return dataStore == null ? true : dataStore.delete(this);
+        return dataStore == null || dataStore.delete(this);
     }
 
     @Override
diff --git 
a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java
 
b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java
index c0ef227251c..8a3cd39ecbf 100644
--- 
a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java
+++ 
b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java
@@ -32,10 +32,6 @@ import java.util.concurrent.ExecutionException;
 
 import javax.inject.Inject;
 
-import org.apache.cloudstack.secret.dao.PassphraseDao;
-import com.cloud.storage.VMTemplateVO;
-import com.cloud.storage.dao.VMTemplateDao;
-import com.cloud.storage.resource.StorageProcessor;
 import org.apache.cloudstack.annotation.AnnotationService;
 import org.apache.cloudstack.annotation.dao.AnnotationDao;
 import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity;
@@ -66,6 +62,7 @@ import 
org.apache.cloudstack.framework.async.AsyncCallbackDispatcher;
 import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
 import org.apache.cloudstack.framework.async.AsyncRpcContext;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import org.apache.cloudstack.secret.dao.PassphraseDao;
 import org.apache.cloudstack.storage.RemoteHostEndPoint;
 import org.apache.cloudstack.storage.command.CommandResult;
 import org.apache.cloudstack.storage.command.CopyCmdAnswer;
@@ -83,6 +80,7 @@ import 
org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
 import org.apache.cloudstack.storage.image.store.TemplateObject;
 import org.apache.cloudstack.storage.to.TemplateObjectTO;
 import org.apache.cloudstack.storage.to.VolumeObjectTO;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
@@ -122,13 +120,16 @@ import com.cloud.storage.StoragePool;
 import com.cloud.storage.VMTemplateStoragePoolVO;
 import com.cloud.storage.VMTemplateStorageResourceAssoc;
 import com.cloud.storage.VMTemplateStorageResourceAssoc.Status;
+import com.cloud.storage.VMTemplateVO;
 import com.cloud.storage.Volume;
 import com.cloud.storage.Volume.State;
 import com.cloud.storage.VolumeDetailVO;
 import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.VMTemplateDao;
 import com.cloud.storage.dao.VMTemplatePoolDao;
 import com.cloud.storage.dao.VolumeDao;
 import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.storage.resource.StorageProcessor;
 import com.cloud.storage.snapshot.SnapshotApiService;
 import com.cloud.storage.snapshot.SnapshotManager;
 import com.cloud.storage.template.TemplateConstants;
@@ -142,7 +143,6 @@ import com.cloud.utils.db.DB;
 import com.cloud.utils.db.GlobalLock;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.vm.VirtualMachine;
-import org.apache.commons.lang3.StringUtils;
 
 @Component
 public class VolumeServiceImpl implements VolumeService {
@@ -206,8 +206,6 @@ public class VolumeServiceImpl implements VolumeService {
     @Inject
     private PassphraseDao passphraseDao;
 
-    private final static String SNAPSHOT_ID = "SNAPSHOT_ID";
-
     public VolumeServiceImpl() {
     }
 
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 9100ee5e34b..3c0f81d0bc1 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
@@ -38,9 +38,13 @@ import javax.naming.ConfigurationException;
 import org.apache.cloudstack.api.ApiCommandResourceType;
 import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.context.CallContext;
+import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.apache.cloudstack.framework.config.Configurable;
 import org.apache.cloudstack.framework.jobs.AsyncJob;
@@ -65,7 +69,12 @@ import org.apache.log4j.MDC;
 import org.apache.log4j.NDC;
 
 import com.cloud.cluster.ClusterManagerListener;
+import com.cloud.network.Network;
+import com.cloud.network.dao.NetworkDao;
+import com.cloud.network.dao.NetworkVO;
 import com.cloud.storage.Snapshot;
+import com.cloud.storage.Volume;
+import com.cloud.storage.VolumeDetailVO;
 import com.cloud.storage.dao.SnapshotDao;
 import com.cloud.storage.dao.SnapshotDetailsDao;
 import com.cloud.storage.dao.SnapshotDetailsVO;
@@ -93,7 +102,11 @@ import com.cloud.utils.db.TransactionCallbackNoReturn;
 import com.cloud.utils.db.TransactionStatus;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.exception.ExceptionUtil;
+import com.cloud.utils.fsm.NoTransitionException;
 import com.cloud.utils.mgmt.JmxUtil;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineManager;
 import com.cloud.vm.dao.VMInstanceDao;
 
 public class AsyncJobManagerImpl extends ManagerBase implements 
AsyncJobManager, ClusterManagerListener, Configurable {
@@ -148,6 +161,15 @@ public class AsyncJobManagerImpl extends ManagerBase 
implements AsyncJobManager,
     @Inject
     private SnapshotDetailsDao _snapshotDetailsDao;
 
+    @Inject
+    private VolumeDataFactory volFactory;
+    @Inject
+    private VirtualMachineManager virtualMachineManager;
+    @Inject
+    private NetworkDao networkDao;
+    @Inject
+    private NetworkOrchestrationService networkOrchestrationService;
+
     private volatile long _executionRunNumber = 1;
 
     private final ScheduledExecutorService _heartbeatScheduler = 
Executors.newScheduledThreadPool(1, new 
NamedThreadFactory("AsyncJobMgr-Heartbeat"));
@@ -1089,6 +1111,7 @@ public class AsyncJobManagerImpl extends ManagerBase 
implements AsyncJobManager,
                         if (s_logger.isDebugEnabled()) {
                             s_logger.debug("Cancel left-over job-" + 
job.getId());
                         }
+                        cleanupResources(job);
                         job.setStatus(JobInfo.Status.FAILED);
                         
job.setResultCode(ApiErrorCode.INTERNAL_ERROR.getHttpCode());
                         job.setResult("job cancelled because of management 
server restart or shutdown");
@@ -1101,26 +1124,8 @@ public class AsyncJobManagerImpl extends ManagerBase 
implements AsyncJobManager,
                             s_logger.debug("Purge queue item for cancelled 
job-" + job.getId());
                         }
                         _queueMgr.purgeAsyncJobQueueItemId(job.getId());
-                        if 
(ApiCommandResourceType.Volume.toString().equals(job.getInstanceType())) {
-
-                            try {
-                                
_volumeDetailsDao.removeDetail(job.getInstanceId(), "SNAPSHOT_ID");
-                                _volsDao.remove(job.getInstanceId());
-                            } catch (Exception e) {
-                                s_logger.error("Unexpected exception while 
removing concurrent request meta data :" + e.getLocalizedMessage());
-                            }
-                        }
-                    }
-                    final List<SnapshotDetailsVO> snapshotList = 
_snapshotDetailsDao.findDetails(AsyncJob.Constants.MS_ID, Long.toString(msid), 
false);
-                    for (final SnapshotDetailsVO snapshotDetailsVO : 
snapshotList) {
-                        SnapshotInfo snapshot = 
snapshotFactory.getSnapshotOnPrimaryStore(snapshotDetailsVO.getResourceId());
-                        if (snapshot == null) {
-                            
_snapshotDetailsDao.remove(snapshotDetailsVO.getId());
-                            continue;
-                        }
-                        snapshotSrv.processEventOnSnapshotObject(snapshot, 
Snapshot.Event.OperationFailed);
-                        
_snapshotDetailsDao.removeDetail(snapshotDetailsVO.getResourceId(), 
AsyncJob.Constants.MS_ID);
                     }
+                    cleanupFailedSnapshotsCreatedWithDefaultStrategy(msid);
                 }
             });
         } catch (Throwable e) {
@@ -1128,6 +1133,106 @@ public class AsyncJobManagerImpl extends ManagerBase 
implements AsyncJobManager,
         }
     }
 
+    /*
+    Cleanup Resources in transition state and move them to appropriate state
+    This will allow other operation on the resource, instead of being stuck in 
transition state
+     */
+    protected boolean cleanupResources(AsyncJobVO job) {
+        try {
+            ApiCommandResourceType resourceType = 
ApiCommandResourceType.fromString(job.getInstanceType());
+            if (resourceType == null) {
+                s_logger.warn("Unknown ResourceType. Skip Cleanup: " + 
job.getInstanceType());
+                return true;
+            }
+            switch (resourceType) {
+                case Volume:
+                    return cleanupVolume(job.getInstanceId());
+                case VirtualMachine:
+                    return cleanupVirtualMachine(job.getInstanceId());
+                case Network:
+                    return cleanupNetwork(job.getInstanceId());
+            }
+        } catch (Exception e) {
+            s_logger.warn("Error while cleaning up resource: [" + 
job.getInstanceType().toString()  + "] with Id: " + job.getInstanceId(), e);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean cleanupVolume(final long volumeId) {
+        VolumeInfo vol = volFactory.getVolume(volumeId);
+        if (vol == null) {
+            s_logger.warn("Volume not found. Skip Cleanup. VolumeId: " + 
volumeId);
+            return true;
+        }
+        if (vol.getState().isTransitional()) {
+            s_logger.debug("Cleaning up volume with Id: " + volumeId);
+            boolean status = vol.stateTransit(Volume.Event.OperationFailed);
+            cleanupFailedVolumesCreatedFromSnapshots(volumeId);
+            return status;
+        }
+        s_logger.debug("Volume not in transition state. Skip cleanup. 
VolumeId: " + volumeId);
+        return true;
+    }
+
+    private boolean cleanupVirtualMachine(final long vmId) throws Exception {
+        VMInstanceVO vmInstanceVO = _vmInstanceDao.findById(vmId);
+        if (vmInstanceVO == null) {
+            s_logger.warn("Instance not found. Skip Cleanup. InstanceId: " + 
vmId);
+            return true;
+        }
+        if (vmInstanceVO.getState().isTransitional()) {
+            s_logger.debug("Cleaning up Instance with Id: " + vmId);
+            return virtualMachineManager.stateTransitTo(vmInstanceVO, 
VirtualMachine.Event.OperationFailed, vmInstanceVO.getHostId());
+        }
+        s_logger.debug("Instance not in transition state. Skip cleanup. 
InstanceId: " + vmId);
+        return true;
+    }
+
+    private boolean cleanupNetwork(final long networkId) throws Exception {
+        NetworkVO networkVO = networkDao.findById(networkId);
+        if (networkVO == null) {
+            s_logger.warn("Network not found. Skip Cleanup. NetworkId: " + 
networkId);
+            return true;
+        }
+        if (Network.State.Implementing.equals(networkVO.getState())) {
+            try {
+                s_logger.debug("Cleaning up Network with Id: " + networkId);
+                return networkOrchestrationService.stateTransitTo(networkVO, 
Network.Event.OperationFailed);
+            } catch (final NoTransitionException e) {
+                networkVO.setState(Network.State.Shutdown);
+                networkDao.update(networkVO.getId(), networkVO);
+            }
+        }
+        s_logger.debug("Network not in transition state. Skip cleanup. 
NetworkId: " + networkId);
+        return true;
+    }
+
+    private void cleanupFailedVolumesCreatedFromSnapshots(final long volumeId) 
{
+        try {
+            VolumeDetailVO volumeDetail = 
_volumeDetailsDao.findDetail(volumeId, VolumeService.SNAPSHOT_ID);
+            if (volumeDetail != null) {
+                _volumeDetailsDao.removeDetail(volumeId, 
VolumeService.SNAPSHOT_ID);
+                _volsDao.remove(volumeId);
+            }
+        } catch (Exception e) {
+            s_logger.error("Unexpected exception while removing concurrent 
request meta data :" + e.getLocalizedMessage());
+        }
+    }
+
+    private void cleanupFailedSnapshotsCreatedWithDefaultStrategy(final long 
msid) {
+        final List<SnapshotDetailsVO> snapshotList = 
_snapshotDetailsDao.findDetails(AsyncJob.Constants.MS_ID, Long.toString(msid), 
false);
+        for (final SnapshotDetailsVO snapshotDetailsVO : snapshotList) {
+            SnapshotInfo snapshot = 
snapshotFactory.getSnapshotOnPrimaryStore(snapshotDetailsVO.getResourceId());
+            if (snapshot == null) {
+                _snapshotDetailsDao.remove(snapshotDetailsVO.getId());
+                continue;
+            }
+            snapshotSrv.processEventOnSnapshotObject(snapshot, 
Snapshot.Event.OperationFailed);
+            
_snapshotDetailsDao.removeDetail(snapshotDetailsVO.getResourceId(), 
AsyncJob.Constants.MS_ID);
+        }
+    }
+
     @Override
     public void onManagementNodeJoined(List<? extends ManagementServerHost> 
nodeList, long selfNodeId) {
     }
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
new file mode 100644
index 00000000000..0be5dbc01cb
--- /dev/null
+++ 
b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImplTest.java
@@ -0,0 +1,96 @@
+// 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 org.apache.cloudstack.framework.jobs.impl;
+
+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.fsm.NoTransitionException;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.dao.VMInstanceDao;
+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.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AsyncJobManagerImplTest {
+    @Spy
+    @InjectMocks
+    AsyncJobManagerImpl asyncJobManager;
+    @Mock
+    VolumeDataFactory volFactory;
+    @Mock
+    VMInstanceDao vmInstanceDao;
+    @Mock
+    VirtualMachineManager virtualMachineManager;
+    @Mock
+    NetworkDao networkDao;
+    @Mock
+    NetworkOrchestrationService networkOrchestrationService;
+
+    @Test
+    public void testCleanupVolumeResource() {
+        AsyncJobVO job = new AsyncJobVO();
+        job.setInstanceType(ApiCommandResourceType.Volume.toString());
+        job.setInstanceId(1L);
+        VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
+        when(volFactory.getVolume(Mockito.anyLong())).thenReturn(volumeInfo);
+        when(volumeInfo.getState()).thenReturn(Volume.State.Attaching);
+        asyncJobManager.cleanupResources(job);
+        Mockito.verify(volumeInfo, 
Mockito.times(1)).stateTransit(Volume.Event.OperationFailed);
+    }
+
+    @Test
+    public void testCleanupVmResource() throws NoTransitionException {
+        AsyncJobVO job = new AsyncJobVO();
+        job.setInstanceType(ApiCommandResourceType.VirtualMachine.toString());
+        job.setInstanceId(1L);
+        VMInstanceVO vmInstanceVO = Mockito.mock(VMInstanceVO.class);
+        
when(vmInstanceDao.findById(Mockito.anyLong())).thenReturn(vmInstanceVO);
+        
when(vmInstanceVO.getState()).thenReturn(VirtualMachine.State.Starting);
+        when(vmInstanceVO.getHostId()).thenReturn(1L);
+        asyncJobManager.cleanupResources(job);
+        Mockito.verify(virtualMachineManager, 
Mockito.times(1)).stateTransitTo(vmInstanceVO, 
VirtualMachine.Event.OperationFailed, 1L);
+    }
+
+    @Test
+    public void testCleanupNetworkResource() throws NoTransitionException {
+        AsyncJobVO job = new AsyncJobVO();
+        job.setInstanceType(ApiCommandResourceType.Network.toString());
+        job.setInstanceId(1L);
+        NetworkVO networkVO = Mockito.mock(NetworkVO.class);
+        when(networkDao.findById(Mockito.anyLong())).thenReturn(networkVO);
+        when(networkVO.getState()).thenReturn(Network.State.Implementing);
+        asyncJobManager.cleanupResources(job);
+        Mockito.verify(networkOrchestrationService, 
Mockito.times(1)).stateTransitTo(networkVO,
+                Network.Event.OperationFailed);
+    }
+}
diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java 
b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java
index 04556c49ab2..288211c4330 100644
--- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java
+++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java
@@ -26,6 +26,7 @@ import javax.naming.ConfigurationException;
 
 import com.cloud.dc.DataCenter;
 import com.cloud.network.PublicIpQuarantine;
+import com.cloud.utils.fsm.NoTransitionException;
 import org.apache.cloudstack.acl.ControlledEntity.ACLType;
 import org.apache.cloudstack.api.command.admin.address.ReleasePodIpCmdByAdmin;
 import 
org.apache.cloudstack.api.command.admin.network.DedicateGuestVlanRangeCmd;
@@ -824,6 +825,11 @@ public class MockNetworkManagerImpl extends ManagerBase 
implements NetworkOrches
         return null;
     }
 
+    @Override
+    public boolean stateTransitTo(Network network, Network.Event e) throws 
NoTransitionException {
+        return true;
+    }
+
     @Override
     public boolean isNetworkInlineMode(Network network) {
         // TODO Auto-generated method stub

Reply via email to