This is an automated email from the ASF dual-hosted git repository.
harikrishna-patnala 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 cdbda0ae158 engine: key removeNic work job lookup on nic uuid to fix
concurrent removal race (#13700)
cdbda0ae158 is described below
commit cdbda0ae158fd9cdee6850f61f5b943bccdb1f26
Author: Ayush Sinha <[email protected]>
AuthorDate: Tue Sep 8 16:32:05 2026 +0530
engine: key removeNic work job lookup on nic uuid to fix concurrent removal
race (#13700)
removeNicFromVmThroughJobQueue looked up pending work jobs by
(vmType, vmId, commandName) only, so the nic was not part of the dedup
key. A second removeNicFromVirtualMachine request for a different nic on
the same vm matched the first still-pending VmWorkRemoveNicFromVm job and
joined it instead of submitting its own. That job removes only the nic it
was created for, yet both callers wait on the same job id and both receive
its success, leaving the second nic silently attached while its API call
reports success.
Make the nic uuid part of the lookup key, mirroring
addVmToNetworkThroughJobQueue which was fixed the same way in #5658:
- look up pending jobs with the 4-arg
listPendingWorkJobs(Instance, vmId, cmd, nic.getUuid())
- fail fast with CloudRuntimeException if more than one job matches
- stamp new jobs with setSecondaryObjectIdentifier(nic.getUuid())
before submitting
Genuine duplicates, two requests for the same nic, still dedup as before.
Adds three regression tests to VirtualMachineManagerImplTest covering the
cross-nic race, same-nic dedup, and the multiple-pending-jobs guard.
Fixes: #13699
Generated-by: Claude Code (Anthropic)
Signed-off-by: Ayush Sinha <[email protected]>
---
.../com/cloud/vm/VirtualMachineManagerImpl.java | 17 +++-
.../cloud/vm/VirtualMachineManagerImplTest.java | 92 ++++++++++++++++++++++
2 files changed, 106 insertions(+), 3 deletions(-)
diff --git
a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
index d322507fb4c..c98391a654d 100755
---
a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
+++
b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java
@@ -5957,14 +5957,25 @@ public class VirtualMachineManagerImpl extends
ManagerBase implements VirtualMac
final VirtualMachine vm, final Nic nic) {
Long vmId = vm.getId();
String commandName = VmWorkRemoveNicFromVm.class.getName();
- Pair<VmWorkJobVO, Long> pendingWorkJob = retrievePendingWorkJob(vmId,
commandName);
- VmWorkJobVO workJob = pendingWorkJob.first();
+ // The nic uuid must be part of the pending-job lookup key. Without
it, a concurrent request
+ // to remove a different nic from the same vm matches this
still-pending job and joins it
+ // instead of submitting its own, so only one nic is removed while
both callers wait on the
+ // single job and both receive its success. Mirrors the symmetric
addVmToNetworkThroughJobQueue.
+ final List<VmWorkJobVO> pendingWorkJobs =
_workJobDao.listPendingWorkJobs(
+ VirtualMachine.Type.Instance, vmId, commandName,
nic.getUuid());
- if (workJob == null) {
+ VmWorkJobVO workJob;
+ if (pendingWorkJobs != null && pendingWorkJobs.size() > 0) {
+ if (pendingWorkJobs.size() > 1) {
+ throw new CloudRuntimeException(String.format("The number of
jobs to remove nic %s from vm %s are %d", nic.getUuid(), vm.getInstanceName(),
pendingWorkJobs.size()));
+ }
+ workJob = pendingWorkJobs.get(0);
+ } else {
Pair<VmWorkJobVO, VmWork> newVmWorkJobAndInfo =
createWorkJobAndWorkInfo(commandName, vmId);
workJob = newVmWorkJobAndInfo.first();
+ workJob.setSecondaryObjectIdentifier(nic.getUuid());
VmWorkRemoveNicFromVm workInfo = new
VmWorkRemoveNicFromVm(newVmWorkJobAndInfo.second(), nic.getId());
setCmdInfoAndSubmitAsyncJob(workJob, workInfo, vmId);
diff --git
a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
index 7329b67b4be..c9a404f9c89 100644
---
a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
+++
b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java
@@ -70,6 +70,7 @@ import
org.apache.cloudstack.framework.config.impl.ConfigDepotImpl;
import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO;
+import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext;
import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao;
import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@@ -1868,6 +1869,97 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.unmanage(vmMockUuid, null);
}
+ /**
+ * A pending remove-nic job for a different nic on the same vm must not
swallow the removal of
+ * this nic: the pending-job lookup has to be keyed on the nic uuid so
that a new job is
+ * submitted for this nic instead of joining the other nic's job.
Regression test for the
+ * concurrent-removeNic collapse.
+ */
+ @Test
+ public void
removeNicFromVmThroughJobQueueDoesNotJoinAnotherNicsPendingJob() {
+ String commandName = VmWorkRemoveNicFromVm.class.getName();
+ String nicUuid = UUID.randomUUID().toString();
+
+ Nic nic = mock(Nic.class);
+ when(nic.getId()).thenReturn(42L);
+ when(nic.getUuid()).thenReturn(nicUuid);
+
+ // A pending remove-nic job exists for the vm (e.g. for another nic);
the nic-agnostic
+ // lookup would return it, but the nic-keyed lookup finds nothing for
this nic. This stub is
+ // lenient because the fixed code never consults the nic-agnostic
3-arg lookup - only the
+ // buggy code does, where this job is what it wrongly joins.
+
Mockito.lenient().when(_workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance,
vmInstanceVoMockId, commandName))
+
.thenReturn(Collections.singletonList(mock(VmWorkJobVO.class)));
+ when(_workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance,
vmInstanceVoMockId, commandName, nicUuid))
+ .thenReturn(Collections.emptyList());
+
+ VmWorkJobVO newJob = mock(VmWorkJobVO.class);
+ when(newJob.getId()).thenReturn(100L);
+ doReturn(new Pair<VmWorkJobVO, VmWork>(newJob, mock(VmWork.class)))
+
.when(virtualMachineManagerImpl).createWorkJobAndWorkInfo(commandName,
vmInstanceVoMockId);
+
doNothing().when(virtualMachineManagerImpl).setCmdInfoAndSubmitAsyncJob(any(),
any(), anyLong());
+
+ AsyncJobExecutionContext execContext =
mock(AsyncJobExecutionContext.class);
+ try (MockedStatic<AsyncJobExecutionContext> ignored =
Mockito.mockStatic(AsyncJobExecutionContext.class)) {
+
when(AsyncJobExecutionContext.getCurrentExecutionContext()).thenReturn(execContext);
+
+
virtualMachineManagerImpl.removeNicFromVmThroughJobQueue(vmInstanceMock, nic);
+
+ // A new job must be created for this nic, stamped with the nic
uuid, submitted and joined.
+ verify(virtualMachineManagerImpl,
times(1)).createWorkJobAndWorkInfo(commandName, vmInstanceVoMockId);
+ verify(newJob, times(1)).setSecondaryObjectIdentifier(nicUuid);
+ verify(virtualMachineManagerImpl,
times(1)).setCmdInfoAndSubmitAsyncJob(eq(newJob), any(),
eq(vmInstanceVoMockId));
+ verify(execContext, times(1)).joinJob(100L);
+ }
+ }
+
+ /**
+ * When a pending remove-nic job already exists for this same nic, the
request must join it
+ * rather than submit a duplicate: per-nic deduplication still holds.
+ */
+ @Test
+ public void removeNicFromVmThroughJobQueueJoinsExistingJobForSameNic() {
+ String commandName = VmWorkRemoveNicFromVm.class.getName();
+ String nicUuid = UUID.randomUUID().toString();
+
+ Nic nic = mock(Nic.class);
+ when(nic.getUuid()).thenReturn(nicUuid);
+
+ VmWorkJobVO existingJob = mock(VmWorkJobVO.class);
+ when(existingJob.getId()).thenReturn(77L);
+ when(_workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance,
vmInstanceVoMockId, commandName, nicUuid))
+ .thenReturn(Collections.singletonList(existingJob));
+
+ AsyncJobExecutionContext execContext =
mock(AsyncJobExecutionContext.class);
+ try (MockedStatic<AsyncJobExecutionContext> ignored =
Mockito.mockStatic(AsyncJobExecutionContext.class)) {
+
when(AsyncJobExecutionContext.getCurrentExecutionContext()).thenReturn(execContext);
+
+
virtualMachineManagerImpl.removeNicFromVmThroughJobQueue(vmInstanceMock, nic);
+
+ verify(virtualMachineManagerImpl,
never()).createWorkJobAndWorkInfo(anyString(), anyLong());
+ verify(virtualMachineManagerImpl,
never()).setCmdInfoAndSubmitAsyncJob(any(), any(), anyLong());
+ verify(execContext, times(1)).joinJob(77L);
+ }
+ }
+
+ /**
+ * More than one pending remove-nic job for the same nic is an
inconsistent state and must fail
+ * fast rather than pick one arbitrarily. Mirrors the guard in
addVmToNetworkThroughJobQueue.
+ */
+ @Test(expected = CloudRuntimeException.class)
+ public void
removeNicFromVmThroughJobQueueThrowsWhenMultiplePendingJobsForSameNic() {
+ String commandName = VmWorkRemoveNicFromVm.class.getName();
+ String nicUuid = UUID.randomUUID().toString();
+
+ Nic nic = mock(Nic.class);
+ when(nic.getUuid()).thenReturn(nicUuid);
+
+ when(_workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance,
vmInstanceVoMockId, commandName, nicUuid))
+ .thenReturn(Arrays.asList(mock(VmWorkJobVO.class),
mock(VmWorkJobVO.class)));
+
+
virtualMachineManagerImpl.removeNicFromVmThroughJobQueue(vmInstanceMock, nic);
+ }
+
@Test
public void testUnmanageHostNotFoundAfterTransaction() {
when(vmInstanceMock.getHostId()).thenReturn(hostMockId);