Skip to content

Commit f734aa0

Browse files
Serialize NIC add/remove on the target VM's work job queue
addVmToNetwork() runs directly (no queue) whenever the calling thread is inside any VM work job: jobContext.isJobDispatchedBy(VM_WORK_JOB_DISPATCHER) That check does not care whose work job it is. When several VMs deploy in parallel into different tiers of a VPC, each VmWorkStart job implements its tier and calls addVpcRouterToGuestNetwork() -> addVmToNetwork(router, ...). The check is true (we are in the user VM's work job), so every tier attach runs orchestrateAddVmToNetwork() on the router directly. Nothing serializes them: - concurrent getFreeDeviceId() calls hand the same device id to several NICs - the colliding PlugNicCommands partially fail, then cleanup and retries reshuffle device ids differently on each router - on a redundant VPC the two routers' keepalived configs diverge and VRRP breaks: FAULT or both PRIMARY Fix: run directly only when the current work job belongs to the VM being modified (new helper isRunningVmWorkJobForVm()); otherwise dispatch through that VM's own work job queue. Apply the same rule to removeNicFromVm() and removeVmFromNetwork() — the latter always ran directly. Scope removeVmFromNetworkThroughJobQueue()'s pending-job lookup by network uuid, like the add path, so removals of different networks do not collapse into one job. Drop an unused retrievePendingWorkJob() call from addVmToNetworkThroughJobQueue(). Fixes: #11710 Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent b9a5977 commit f734aa0

2 files changed

Lines changed: 63 additions & 25 deletions

File tree

engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4549,12 +4549,29 @@ public boolean upgradeVmDb(final long vmId, final ServiceOffering newServiceOffe
45494549
return _vmDao.update(vmId, vmForUpdate);
45504550
}
45514551

4552+
/**
4553+
* A VM operation may run directly only when the current thread is already
4554+
* executing the work job of that same VM, as that VM's work queue is then
4555+
* held and provides the serialization. Running inside some other VM's work
4556+
* job (for example a router NIC operation performed while a user VM's
4557+
* start job implements a VPC tier) provides no such guarantee, so the
4558+
* operation must be dispatched through the target VM's own work job queue
4559+
* instead.
4560+
*/
4561+
protected boolean isRunningVmWorkJobForVm(final AsyncJobExecutionContext jobContext, final long vmId) {
4562+
if (!jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) {
4563+
return false;
4564+
}
4565+
final VmWorkJobVO workJob = _workJobDao.findById(jobContext.getJob().getId());
4566+
return workJob != null && workJob.getVmInstanceId() == vmId;
4567+
}
4568+
45524569
@Override
45534570
public NicProfile addVmToNetwork(final VirtualMachine vm, final Network network, final NicProfile requested)
45544571
throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException {
45554572

45564573
final AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext();
4557-
if (jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) {
4574+
if (isRunningVmWorkJobForVm(jobContext, vm.getId())) {
45584575
VmWorkJobVO placeHolder = createPlaceHolderWork(vm.getId(), network.getUuid());
45594576
try {
45604577
return orchestrateAddVmToNetwork(vm, network, requested);
@@ -4661,7 +4678,7 @@ public boolean removeNicFromVm(final VirtualMachine vm, final Nic nic)
46614678
throws ConcurrentOperationException, ResourceUnavailableException {
46624679

46634680
final AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext();
4664-
if (jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) {
4681+
if (isRunningVmWorkJobForVm(jobContext, vm.getId())) {
46654682
VmWorkJobVO placeHolder = createPlaceHolderWork(vm.getId());
46664683
try {
46674684
return orchestrateRemoveNicFromVm(vm, nic);
@@ -4737,7 +4754,25 @@ private boolean orchestrateRemoveNicFromVm(final VirtualMachine vm, final Nic ni
47374754
@Override
47384755
@DB
47394756
public boolean removeVmFromNetwork(final VirtualMachine vm, final Network network, final URI broadcastUri) throws ConcurrentOperationException, ResourceUnavailableException {
4740-
return orchestrateRemoveVmFromNetwork(vm, network, broadcastUri);
4757+
final AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext();
4758+
if (isRunningVmWorkJobForVm(jobContext, vm.getId())) {
4759+
return orchestrateRemoveVmFromNetwork(vm, network, broadcastUri);
4760+
}
4761+
4762+
final Outcome<VirtualMachine> outcome = removeVmFromNetworkThroughJobQueue(vm, network, broadcastUri);
4763+
4764+
retrieveVmFromJobOutcome(outcome, vm.getUuid(), "removeVmFromNetwork");
4765+
4766+
try {
4767+
Object jobResult = retrieveResultFromJobOutcomeAndThrowExceptionIfNeeded(outcome);
4768+
if (jobResult != null && jobResult instanceof Boolean) {
4769+
return (Boolean)jobResult;
4770+
}
4771+
} catch (InsufficientCapacityException ex) {
4772+
throw new RuntimeException("Unexpected exception", ex);
4773+
}
4774+
4775+
throw new RuntimeException("Job failed with un-handled exception");
47414776
}
47424777

47434778
@DB
@@ -5887,10 +5922,6 @@ public Outcome<VirtualMachine> migrateVmStorageThroughJobQueue(final String vmUu
58875922

58885923
public Outcome<VirtualMachine> addVmToNetworkThroughJobQueue(
58895924
final VirtualMachine vm, final Network network, final NicProfile requested) {
5890-
Long vmId = vm.getId();
5891-
String commandName = VmWorkAddVmToNetwork.class.getName();
5892-
Pair<VmWorkJobVO, Long> pendingWorkJob = retrievePendingWorkJob(vmId, commandName);
5893-
58945925
final CallContext context = CallContext.current();
58955926
final User user = context.getCallingUser();
58965927
final Account account = context.getCallingAccount();
@@ -5989,15 +6020,21 @@ public Outcome<VirtualMachine> removeVmFromNetworkThroughJobQueue(
59896020
final VirtualMachine vm, final Network network, final URI broadcastUri) {
59906021
Long vmId = vm.getId();
59916022
String commandName = VmWorkRemoveVmFromNetwork.class.getName();
5992-
Pair<VmWorkJobVO, Long> pendingWorkJob = retrievePendingWorkJob(vmId, commandName);
59936023

5994-
VmWorkJobVO workJob = pendingWorkJob.first();
6024+
// pending-job lookup must be scoped by network, otherwise concurrent
6025+
// removals for different networks on the same VM collapse into one job
6026+
final List<VmWorkJobVO> pendingWorkJobs = _workJobDao.listPendingWorkJobs(
6027+
VirtualMachine.Type.Instance, vmId, commandName, network.getUuid());
59956028

5996-
if (workJob == null) {
6029+
VmWorkJobVO workJob;
6030+
if (CollectionUtils.isNotEmpty(pendingWorkJobs)) {
6031+
workJob = pendingWorkJobs.get(0);
6032+
} else {
59976033
Pair<VmWorkJobVO, VmWork> newVmWorkJobAndInfo = createWorkJobAndWorkInfo(commandName, vmId);
59986034

59996035
workJob = newVmWorkJobAndInfo.first();
6000-
VmWorkRemoveVmFromNetwork workInfo = new VmWorkRemoveVmFromNetwork(newVmWorkJobAndInfo.second(), network, broadcastUri);
6036+
workJob.setSecondaryObjectIdentifier(network.getUuid());
6037+
VmWorkRemoveVmFromNetwork workInfo = new VmWorkRemoveVmFromNetwork(newVmWorkJobAndInfo.second(), network.getId(), broadcastUri);
60016038

60026039
setCmdInfoAndSubmitAsyncJob(workJob, workInfo, vmId);
60036040
}
@@ -6132,7 +6169,7 @@ private Pair<JobInfo.Status, String> orchestrateRemoveNicFromVm(final VmWorkRemo
61326169
private Pair<JobInfo.Status, String> orchestrateRemoveVmFromNetwork(final VmWorkRemoveVmFromNetwork work) throws Exception {
61336170
VMInstanceVO vm = findVmById(work.getVmId());
61346171
final boolean result = orchestrateRemoveVmFromNetwork(vm,
6135-
work.getNetwork(), work.getBroadcastUri());
6172+
_networkDao.findById(work.getNetworkId()), work.getBroadcastUri());
61366173
return new Pair<>(JobInfo.Status.SUCCEEDED,
61376174
_jobMgr.marshallResultObject(result));
61386175
}

engine/orchestration/src/main/java/com/cloud/vm/VmWorkRemoveVmFromNetwork.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,33 @@
1818

1919
import java.net.URI;
2020

21-
import com.cloud.network.Network;
22-
2321
public class VmWorkRemoveVmFromNetwork extends VmWork {
2422
private static final long serialVersionUID = -5070392905642149925L;
2523

26-
Network network;
27-
URI broadcastUri;
24+
// only the network id and the broadcast uri as text are stored: the work
25+
// info is serialized with Gson, and entity or URI object graphs do not
26+
// survive that (see VmWorkAddVmToNetwork, which stores the id the same way)
27+
Long networkId;
28+
String broadcastUri;
2829

29-
public VmWorkRemoveVmFromNetwork(long userId, long accountId, long vmId, String handlerName, Network network, URI broadcastUri) {
30+
public VmWorkRemoveVmFromNetwork(long userId, long accountId, long vmId, String handlerName, long networkId, URI broadcastUri) {
3031
super(userId, accountId, vmId, handlerName);
3132

32-
this.network = network;
33-
this.broadcastUri = broadcastUri;
33+
this.networkId = networkId;
34+
this.broadcastUri = broadcastUri != null ? broadcastUri.toString() : null;
3435
}
3536

36-
public VmWorkRemoveVmFromNetwork(VmWork vmWork, Network network, URI broadcastUri) {
37+
public VmWorkRemoveVmFromNetwork(VmWork vmWork, long networkId, URI broadcastUri) {
3738
super(vmWork);
38-
this.network = network;
39-
this.broadcastUri = broadcastUri;
39+
this.networkId = networkId;
40+
this.broadcastUri = broadcastUri != null ? broadcastUri.toString() : null;
4041
}
4142

42-
public Network getNetwork() {
43-
return network;
43+
public Long getNetworkId() {
44+
return networkId;
4445
}
4546

4647
public URI getBroadcastUri() {
47-
return broadcastUri;
48+
return broadcastUri != null ? URI.create(broadcastUri) : null;
4849
}
4950
}

0 commit comments

Comments
 (0)