diff --git a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java index 368487c2b9b0..fe0e13af5db4 100644 --- a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java +++ b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java @@ -96,9 +96,19 @@ Ternary getMetrics(Cluster cluster, VirtualMachine vm, S * @return the cluster imbalance after migration */ default Double getImbalancePostMigration(VirtualMachine vm, - Host destHost, Long clusterId, long vmMetric, double[] baseMetricsArray, + Host destHost, Long clusterId, ServiceOffering serviceOffering, double[] baseMetricsArray, Map hostIdToIndexMap, Map> hostCpuMap, - Map> hostMemoryMap) { + Map> hostMemoryMap) throws ConfigurationException { + // Metric "both": return the worse of the cpu and memory post-migration imbalance. The + // baseMetricsArray fast path is single-metric, so evaluate both maps directly instead. + if ("both".equals(getClusterDrsMetric(clusterId))) { + long vmCpuMetric = (long) serviceOffering.getCpu() * serviceOffering.getSpeed(); + long vmMemMetric = serviceOffering.getRamSize() * 1024L * 1024L; + return Math.max( + imbalancePostMigrationForMap(vm, destHost, clusterId, vmCpuMetric, hostCpuMap), + imbalancePostMigrationForMap(vm, destHost, clusterId, vmMemMetric, hostMemoryMap)); + } + long vmMetric = getVmMetric(serviceOffering, clusterId); // Create a copy of the base array and adjust only the two affected hosts double[] adjustedMetrics = new double[baseMetricsArray.length]; System.arraycopy(baseMetricsArray, 0, adjustedMetrics, 0, baseMetricsArray.length); @@ -129,6 +139,23 @@ default Double getImbalancePostMigration(VirtualMachine vm, return calculateImbalance(adjustedMetrics); } + /** + * Cluster imbalance for a single resource map after hypothetically migrating vm to destHost. + */ + private Double imbalancePostMigrationForMap(VirtualMachine vm, Host destHost, Long clusterId, + long vmMetric, Map> metricMap) { + long destHostId = destHost.getId(); + long vmHostId = vm.getHostId(); + List list = new ArrayList<>(); + for (Map.Entry> entry : metricMap.entrySet()) { + Double value = getMetricValuePostMigration(clusterId, entry.getValue(), vmMetric, entry.getKey(), destHostId, vmHostId); + if (value != null) { + list.add(value); + } + } + return getImbalance(list); + } + /** * Calculate imbalance from an array of metric values. * Imbalance is defined as standard deviation divided by mean. @@ -272,6 +299,10 @@ static Double getClusterImbalance(Long clusterId, List case "memory": list = getMetricList(clusterId, memoryList, skipThreshold); break; + case "both": + return Math.max( + getImbalance(getMetricList(clusterId, cpuList, skipThreshold)), + getImbalance(getMetricList(clusterId, memoryList, skipThreshold))); default: throw new ConfigurationException( String.format("Invalid metric: %s for cluster: %d", metric, clusterId)); diff --git a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java index ba6a6464fc20..4f851c41dce1 100644 --- a/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java +++ b/api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsService.java @@ -45,6 +45,20 @@ public interface ClusterDrsService extends Manager, Configurable, Scheduler { "The interval in minutes after which a periodic background thread will schedule DRS for a cluster.", true, ConfigKey.Scope.Cluster, null, "Interval for Automatic DRS ", null, null, null); + ConfigKey ClusterDrsEventDrivenEnabled = new ConfigKey<>(Boolean.class, "drs.event.driven.enable", + ConfigKey.CATEGORY_ADVANCED, "false", + "In addition to the periodic drs.automatic.interval timer, trigger DRS for a cluster on VM " + + "power-state events (deploy, start, stop, migrate) so it reacts to imbalance in near-real-time " + + "rather than waiting a whole interval. Requires drs.automatic.enable; rate-limited per cluster " + + "by drs.event.driven.interval.", true, + ConfigKey.Scope.Cluster, null, "Enable event-driven DRS", null, null, null); + + ConfigKey ClusterDrsEventDrivenInterval = new ConfigKey<>(Integer.class, "drs.event.driven.interval", + ConfigKey.CATEGORY_ADVANCED, "5", + "Minimum minutes between event-triggered DRS runs for a cluster (debounce). " + + "Only applies when drs.event.driven.enable is true.", true, + ConfigKey.Scope.Cluster, null, "Event-driven DRS min interval", null, null, null); + ConfigKey ClusterDrsMaxMigrations = new ConfigKey<>(Integer.class, "drs.max.migrations", ConfigKey.CATEGORY_ADVANCED, "50", "Maximum number of live migrations in a DRS execution.", @@ -62,9 +76,10 @@ public interface ClusterDrsService extends Manager, Configurable, Scheduler { ConfigKey ClusterDrsMetric = new ConfigKey<>(String.class, "drs.metric", ConfigKey.CATEGORY_ADVANCED, "memory", - "The allocated resource metric used to measure imbalance in a cluster. Possible values are memory, cpu.", + "The allocated resource metric used to measure imbalance in a cluster. Possible values are memory, cpu, both. " + + "'both' balances on the worse of the cpu and memory imbalance so neither resource is left contended.", true, ConfigKey.Scope.Cluster, null, "DRS metric", null, null, null, ConfigKey.Kind.Select, - "memory,cpu"); + "memory,cpu,both"); ConfigKey ClusterDrsMetricType = new ConfigKey<>(String.class, "drs.metric.type", ConfigKey.CATEGORY_ADVANCED, "used", diff --git a/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java b/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java index 902ab0900bd7..8770c81a3479 100644 --- a/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java +++ b/plugins/drs/cluster/balanced/src/main/java/org/apache/cloudstack/cluster/Balanced.java @@ -82,7 +82,7 @@ public Ternary getMetrics(Cluster cluster, VirtualMachin // Use optimized post-imbalance calculation that adjusts only affected hosts Double postImbalance = getImbalancePostMigration(vm, destHost, - cluster.getId(), ClusterDrsAlgorithm.getVmMetric(serviceOffering, cluster.getId()), + cluster.getId(), serviceOffering, baseMetricsArray, hostIdToIndexMap, hostCpuMap, hostMemoryMap); logger.trace("Cluster {} pre-imbalance: {} post-imbalance: {} Algorithm: {} VM: {} srcHost ID: {} destHost: {}", diff --git a/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java b/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java index d672ddfda615..7657dd0f458e 100644 --- a/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java +++ b/plugins/drs/cluster/condensed/src/main/java/org/apache/cloudstack/cluster/Condensed.java @@ -85,7 +85,7 @@ public Ternary getMetrics(Cluster cluster, VirtualMachin // Use optimized post-imbalance calculation that adjusts only affected hosts Double postImbalance = getImbalancePostMigration(vm, destHost, - cluster.getId(), ClusterDrsAlgorithm.getVmMetric(serviceOffering, cluster.getId()), + cluster.getId(), serviceOffering, baseMetricsArray, hostIdToIndexMap, hostCpuMap, hostMemoryMap); logger.trace("Cluster {} pre-imbalance: {} post-imbalance: {} Algorithm: {} VM: {} srcHost ID: {} destHost: {}", diff --git a/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java b/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java index 62075aae596e..d60580e646d9 100644 --- a/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java @@ -53,6 +53,14 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.utils.concurrency.NamedThreadFactory; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.apache.cloudstack.framework.messagebus.MessageDispatcher; +import org.apache.cloudstack.framework.messagebus.MessageHandler; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachineProfileImpl; @@ -148,6 +156,13 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ Map drsAlgorithmMap = new HashMap<>(); + @Inject + MessageBus messageBus; + // Epoch-ms of the last event-triggered DRS run, per cluster; drives the cooldown. + private final Map lastEventDrsTriggerByCluster = new ConcurrentHashMap<>(); + // Runs event-triggered DRS off the message-bus thread. + private ExecutorService eventDrsExecutor; + public AsyncJobDispatcher getAsyncJobDispatcher() { return asyncJobDispatcher; } @@ -179,6 +194,11 @@ protected void runInContext() { }; Timer vmSchedulerTimer = new Timer("VMSchedulerPollTask"); vmSchedulerTimer.schedule(schedulerPollTask, 5000L, 60 * 1000L); + + // Subscribe to VM power-state events for event-driven DRS (gated per cluster by drs.event.driven.enable). + eventDrsExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("Event-Driven-DRS")); + messageBus.subscribe(VirtualMachineManager.Topics.VM_POWER_STATE, MessageDispatcher.getDispatcher(this)); + return true; } @@ -278,48 +298,127 @@ void generateDrsPlanForAllClusters() { List clusterList = clusterDao.listAll(); for (ClusterVO cluster : clusterList) { - if (cluster.getAllocationState() == Disabled || ClusterDrsEnabled.valueIn( - cluster.getId()).equals(Boolean.FALSE)) { - continue; - } + generateDrsPlanForCluster(cluster, ClusterDrsInterval.valueIn(cluster.getId())); + } + } - ClusterDrsPlanVO lastPlan = drsPlanDao.listLatestPlanForClusterId(cluster.getId()); - - // If the last plan is ready or in progress or was executed within the last interval, skip this cluster. - // This is to avoid generating plans for clusters which are already being processed and to avoid - // generating plans for clusters which have been processed recently.This doesn't consider the type - // (manual or automated) of the last plan. - if (lastPlan != null && (lastPlan.getStatus() == ClusterDrsPlan.Status.READY || - lastPlan.getStatus() == ClusterDrsPlan.Status.IN_PROGRESS || - (lastPlan.getStatus() == ClusterDrsPlan.Status.COMPLETED && - lastPlan.getCreated().compareTo(DateUtils.addMinutes(new Date(), -1 * ClusterDrsInterval.valueIn(cluster.getId()))) > 0) - )) { - continue; + /** + * Generates a DRS plan for a single cluster, skipping if DRS is disabled, a plan is already + * pending/running, or one completed within {@code debounceMinutes}. + */ + void generateDrsPlanForCluster(ClusterVO cluster, int debounceMinutes) { + if (cluster.getAllocationState() == Disabled || ClusterDrsEnabled.valueIn(cluster.getId()).equals(Boolean.FALSE)) { + return; + } + + ClusterDrsPlanVO lastPlan = drsPlanDao.listLatestPlanForClusterId(cluster.getId()); + + // Skip if the last plan is ready, in progress, or completed within the debounce window. + if (lastPlan != null && (lastPlan.getStatus() == ClusterDrsPlan.Status.READY || + lastPlan.getStatus() == ClusterDrsPlan.Status.IN_PROGRESS || + (lastPlan.getStatus() == ClusterDrsPlan.Status.COMPLETED && + lastPlan.getCreated().compareTo(DateUtils.addMinutes(new Date(), -1 * debounceMinutes)) > 0) + )) { + return; + } + + long eventId = ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, + EventTypes.EVENT_CLUSTER_DRS, + String.format("Generating DRS plan for cluster %s", cluster.getUuid()), cluster.getId(), + ApiCommandResourceType.Cluster.toString(), true, 0); + GlobalLock clusterLock = GlobalLock.getInternLock(String.format(CLUSTER_LOCK_STR, cluster.getId())); + try { + if (clusterLock.lock(30)) { + try { + List> plan = getDrsPlan(cluster, + ClusterDrsMaxMigrations.valueIn(cluster.getId())); + savePlan(cluster.getId(), plan, eventId, ClusterDrsPlan.Type.AUTOMATED, + ClusterDrsPlan.Status.READY); + logger.info("Generated DRS plan for cluster {}", cluster); + } catch (Exception e) { + logger.error("Unable to generate DRS plans for cluster {}", cluster, e); + } finally { + clusterLock.unlock(); + } } + } finally { + clusterLock.releaseRef(); + } + } - long eventId = ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, - EventTypes.EVENT_CLUSTER_DRS, - String.format("Generating DRS plan for cluster %s", cluster.getUuid()), cluster.getId(), - ApiCommandResourceType.Cluster.toString(), true, 0); - GlobalLock clusterLock = GlobalLock.getInternLock(String.format(CLUSTER_LOCK_STR, cluster.getId())); + /** + * Message-bus handler for VM power-state events; triggers event-driven DRS for the VM's cluster. + */ + @MessageHandler(topic = VirtualMachineManager.Topics.VM_POWER_STATE) + protected void handleVmPowerStateEvent(String subject, String senderAddress, Object args) { + if (!(args instanceof Long)) { + return; + } + try { + triggerEventDrivenDrsForVm((Long) args); + } catch (Exception e) { + logger.debug("Event-driven DRS: error handling VM power-state event for {}", args, e); + } + } + + /** + * Resolves the VM's cluster and, subject to the enable flag and cooldown, schedules DRS plan generation. + */ + void triggerEventDrivenDrsForVm(Long vmId) { + if (vmId == null) { + return; + } + VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null || vm.getHostId() == null) { + return; + } + HostVO host = hostDao.findById(vm.getHostId()); + if (host == null || host.getClusterId() == null) { + return; + } + Long clusterId = host.getClusterId(); + if (!shouldTriggerEventDrivenDrs(clusterId)) { + return; + } + final ClusterVO cluster = clusterDao.findById(clusterId); + if (cluster == null) { + return; + } + final int debounceMinutes = ClusterDrsEventDrivenInterval.valueIn(clusterId); + logger.debug("Event-driven DRS: scheduling plan generation for cluster {} (triggered by VM {})", clusterId, vmId); + submitEventDrivenDrs(cluster, debounceMinutes); + } + + /** + * Runs generateDrsPlanForCluster off the message-bus thread so event publishers are not blocked. + */ + protected void submitEventDrivenDrs(final ClusterVO cluster, final int debounceMinutes) { + eventDrsExecutor.submit(() -> { try { - if (clusterLock.lock(30)) { - try { - List> plan = getDrsPlan(cluster, - ClusterDrsMaxMigrations.valueIn(cluster.getId())); - savePlan(cluster.getId(), plan, eventId, ClusterDrsPlan.Type.AUTOMATED, - ClusterDrsPlan.Status.READY); - logger.info("Generated DRS plan for cluster {}", cluster); - } catch (Exception e) { - logger.error("Unable to generate DRS plans for cluster {}", cluster, e); - } finally { - clusterLock.unlock(); - } - } - } finally { - clusterLock.releaseRef(); + generateDrsPlanForCluster(cluster, debounceMinutes); + } catch (Exception e) { + logger.warn("Event-driven DRS: plan generation failed for cluster {}", cluster, e); } + }); + } + + /** + * Returns true if automatic and event-driven DRS are enabled and the per-cluster cooldown has + * elapsed, recording the trigger time when it does. + */ + boolean shouldTriggerEventDrivenDrs(Long clusterId) { + if (ClusterDrsEnabled.valueIn(clusterId).equals(Boolean.FALSE) + || ClusterDrsEventDrivenEnabled.valueIn(clusterId).equals(Boolean.FALSE)) { + return false; } + long now = System.currentTimeMillis(); + long cooldownMs = ClusterDrsEventDrivenInterval.valueIn(clusterId) * 60L * 1000L; + Long last = lastEventDrsTriggerByCluster.get(clusterId); + if (last != null && (now - last) < cooldownMs) { + return false; + } + lastEventDrsTriggerByCluster.put(clusterId, now); + return true; } /** @@ -855,7 +954,7 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ClusterDrsPlanExpireInterval, ClusterDrsEnabled, ClusterDrsInterval, ClusterDrsMaxMigrations, ClusterDrsAlgorithm, ClusterDrsImbalanceThreshold, ClusterDrsMetric, ClusterDrsMetricType, ClusterDrsMetricUseRatio, - ClusterDrsImbalanceSkipThreshold}; + ClusterDrsImbalanceSkipThreshold, ClusterDrsEventDrivenEnabled, ClusterDrsEventDrivenInterval}; } @Override diff --git a/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java index 6390b29097b5..f4aa6d5df9ba 100644 --- a/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java @@ -78,6 +78,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; @RunWith(MockitoJUnitRunner.class) public class ClusterDrsServiceImplTest { @@ -951,4 +952,114 @@ public void testProcessPlans() { Mockito.verify(clusterDrsService, Mockito.times(2)).executeDrsPlan(Mockito.any(ClusterDrsPlanVO.class)); } + + // ---- event-driven DRS ---- + // The ConfigKeys are shared interface constants, so each test that overrides a default restores it + // in a finally block to avoid leaking into other tests. + + private static String getConfigDefault(ConfigKey key) throws Exception { + Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + return (String) f.get(key); + } + + private static void setConfigDefault(ConfigKey key, String value) throws Exception { + Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(key, value); + } + + @Test + public void testShouldTriggerEventDrivenDrsDisabledByDefault() throws Exception { + // Automatic DRS enabled but event-driven off -> must not trigger. + String origDrs = getConfigDefault(clusterDrsService.ClusterDrsEnabled); + String origEvt = getConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled); + try { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, "true"); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, "false"); + assertFalse(clusterDrsService.shouldTriggerEventDrivenDrs(1L)); + } finally { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, origDrs); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, origEvt); + } + } + + @Test + public void testShouldTriggerEventDrivenDrsRequiresAutomaticDrs() throws Exception { + // Event-driven on but automatic DRS off -> must not trigger (event-driven depends on drs.automatic.enable). + String origDrs = getConfigDefault(clusterDrsService.ClusterDrsEnabled); + String origEvt = getConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled); + try { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, "false"); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, "true"); + assertFalse(clusterDrsService.shouldTriggerEventDrivenDrs(1L)); + } finally { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, origDrs); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, origEvt); + } + } + + @Test + public void testShouldTriggerEventDrivenDrsEnabledThenDebounced() throws Exception { + String origDrs = getConfigDefault(clusterDrsService.ClusterDrsEnabled); + String origEvt = getConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled); + String origInt = getConfigDefault(clusterDrsService.ClusterDrsEventDrivenInterval); + try { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, "true"); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, "true"); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenInterval, "5"); + // First event fires; the per-cluster cooldown then suppresses an immediate second event. + assertTrue(clusterDrsService.shouldTriggerEventDrivenDrs(1L)); + assertFalse(clusterDrsService.shouldTriggerEventDrivenDrs(1L)); + // A different cluster has an independent cooldown and still fires. + assertTrue(clusterDrsService.shouldTriggerEventDrivenDrs(2L)); + } finally { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, origDrs); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, origEvt); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenInterval, origInt); + } + } + + @Test + public void testTriggerEventDrivenDrsForVmSchedulesWhenEnabled() throws Exception { + String origDrs = getConfigDefault(clusterDrsService.ClusterDrsEnabled); + String origEvt = getConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled); + try { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, "true"); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, "true"); + + VMInstanceVO vm = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm.getHostId()).thenReturn(10L); + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getClusterId()).thenReturn(1L); + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(vmInstanceDao.findById(100L)).thenReturn(vm); + Mockito.when(hostDao.findById(10L)).thenReturn(host); + Mockito.when(clusterDao.findById(1L)).thenReturn(cluster); + // Don't actually run DRS on a background thread in the test. + Mockito.doNothing().when(clusterDrsService).submitEventDrivenDrs(Mockito.any(ClusterVO.class), Mockito.anyInt()); + + clusterDrsService.triggerEventDrivenDrsForVm(100L); + + Mockito.verify(clusterDrsService, Mockito.times(1)).submitEventDrivenDrs(Mockito.eq(cluster), Mockito.anyInt()); + } finally { + setConfigDefault(clusterDrsService.ClusterDrsEnabled, origDrs); + setConfigDefault(clusterDrsService.ClusterDrsEventDrivenEnabled, origEvt); + } + } + + @Test + public void testTriggerEventDrivenDrsForVmDoesNotScheduleWhenDisabled() { + // Defaults: both flags false -> must not schedule, even though the VM resolves to a cluster. + VMInstanceVO vm = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm.getHostId()).thenReturn(10L); + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getClusterId()).thenReturn(1L); + Mockito.when(vmInstanceDao.findById(100L)).thenReturn(vm); + Mockito.when(hostDao.findById(10L)).thenReturn(host); + + clusterDrsService.triggerEventDrivenDrsForVm(100L); + + Mockito.verify(clusterDrsService, Mockito.never()).submitEventDrivenDrs(Mockito.any(ClusterVO.class), Mockito.anyInt()); + } }