|
53 | 53 | import com.cloud.utils.exception.CloudRuntimeException; |
54 | 54 | import com.cloud.vm.VMInstanceDetailVO; |
55 | 55 | import com.cloud.vm.VMInstanceVO; |
| 56 | +import com.cloud.vm.VirtualMachineManager; |
| 57 | +import com.cloud.utils.concurrency.NamedThreadFactory; |
| 58 | +import org.apache.cloudstack.framework.messagebus.MessageBus; |
| 59 | +import org.apache.cloudstack.framework.messagebus.MessageDispatcher; |
| 60 | +import org.apache.cloudstack.framework.messagebus.MessageHandler; |
| 61 | +import java.util.concurrent.ConcurrentHashMap; |
| 62 | +import java.util.concurrent.ExecutorService; |
| 63 | +import java.util.concurrent.Executors; |
56 | 64 | import com.cloud.vm.VirtualMachine; |
57 | 65 | import com.cloud.vm.VirtualMachineProfile; |
58 | 66 | import com.cloud.vm.VirtualMachineProfileImpl; |
@@ -148,6 +156,13 @@ public class ClusterDrsServiceImpl extends ManagerBase implements ClusterDrsServ |
148 | 156 |
|
149 | 157 | Map<String, ClusterDrsAlgorithm> drsAlgorithmMap = new HashMap<>(); |
150 | 158 |
|
| 159 | + @Inject |
| 160 | + MessageBus messageBus; |
| 161 | + // Epoch-ms of the last event-triggered DRS run, per cluster; drives the cooldown. |
| 162 | + private final Map<Long, Long> lastEventDrsTriggerByCluster = new ConcurrentHashMap<>(); |
| 163 | + // Runs event-triggered DRS off the message-bus thread. |
| 164 | + private ExecutorService eventDrsExecutor; |
| 165 | + |
151 | 166 | public AsyncJobDispatcher getAsyncJobDispatcher() { |
152 | 167 | return asyncJobDispatcher; |
153 | 168 | } |
@@ -179,6 +194,11 @@ protected void runInContext() { |
179 | 194 | }; |
180 | 195 | Timer vmSchedulerTimer = new Timer("VMSchedulerPollTask"); |
181 | 196 | vmSchedulerTimer.schedule(schedulerPollTask, 5000L, 60 * 1000L); |
| 197 | + |
| 198 | + // Subscribe to VM power-state events for event-driven DRS (gated per cluster by drs.event.driven.enable). |
| 199 | + eventDrsExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("Event-Driven-DRS")); |
| 200 | + messageBus.subscribe(VirtualMachineManager.Topics.VM_POWER_STATE, MessageDispatcher.getDispatcher(this)); |
| 201 | + |
182 | 202 | return true; |
183 | 203 | } |
184 | 204 |
|
@@ -278,48 +298,127 @@ void generateDrsPlanForAllClusters() { |
278 | 298 | List<ClusterVO> clusterList = clusterDao.listAll(); |
279 | 299 |
|
280 | 300 | for (ClusterVO cluster : clusterList) { |
281 | | - if (cluster.getAllocationState() == Disabled || ClusterDrsEnabled.valueIn( |
282 | | - cluster.getId()).equals(Boolean.FALSE)) { |
283 | | - continue; |
284 | | - } |
| 301 | + generateDrsPlanForCluster(cluster, ClusterDrsInterval.valueIn(cluster.getId())); |
| 302 | + } |
| 303 | + } |
285 | 304 |
|
286 | | - ClusterDrsPlanVO lastPlan = drsPlanDao.listLatestPlanForClusterId(cluster.getId()); |
287 | | - |
288 | | - // If the last plan is ready or in progress or was executed within the last interval, skip this cluster. |
289 | | - // This is to avoid generating plans for clusters which are already being processed and to avoid |
290 | | - // generating plans for clusters which have been processed recently.This doesn't consider the type |
291 | | - // (manual or automated) of the last plan. |
292 | | - if (lastPlan != null && (lastPlan.getStatus() == ClusterDrsPlan.Status.READY || |
293 | | - lastPlan.getStatus() == ClusterDrsPlan.Status.IN_PROGRESS || |
294 | | - (lastPlan.getStatus() == ClusterDrsPlan.Status.COMPLETED && |
295 | | - lastPlan.getCreated().compareTo(DateUtils.addMinutes(new Date(), -1 * ClusterDrsInterval.valueIn(cluster.getId()))) > 0) |
296 | | - )) { |
297 | | - continue; |
| 305 | + /** |
| 306 | + * Generates a DRS plan for a single cluster, skipping if DRS is disabled, a plan is already |
| 307 | + * pending/running, or one completed within {@code debounceMinutes}. |
| 308 | + */ |
| 309 | + void generateDrsPlanForCluster(ClusterVO cluster, int debounceMinutes) { |
| 310 | + if (cluster.getAllocationState() == Disabled || ClusterDrsEnabled.valueIn(cluster.getId()).equals(Boolean.FALSE)) { |
| 311 | + return; |
| 312 | + } |
| 313 | + |
| 314 | + ClusterDrsPlanVO lastPlan = drsPlanDao.listLatestPlanForClusterId(cluster.getId()); |
| 315 | + |
| 316 | + // Skip if the last plan is ready, in progress, or completed within the debounce window. |
| 317 | + if (lastPlan != null && (lastPlan.getStatus() == ClusterDrsPlan.Status.READY || |
| 318 | + lastPlan.getStatus() == ClusterDrsPlan.Status.IN_PROGRESS || |
| 319 | + (lastPlan.getStatus() == ClusterDrsPlan.Status.COMPLETED && |
| 320 | + lastPlan.getCreated().compareTo(DateUtils.addMinutes(new Date(), -1 * debounceMinutes)) > 0) |
| 321 | + )) { |
| 322 | + return; |
| 323 | + } |
| 324 | + |
| 325 | + long eventId = ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, |
| 326 | + EventTypes.EVENT_CLUSTER_DRS, |
| 327 | + String.format("Generating DRS plan for cluster %s", cluster.getUuid()), cluster.getId(), |
| 328 | + ApiCommandResourceType.Cluster.toString(), true, 0); |
| 329 | + GlobalLock clusterLock = GlobalLock.getInternLock(String.format(CLUSTER_LOCK_STR, cluster.getId())); |
| 330 | + try { |
| 331 | + if (clusterLock.lock(30)) { |
| 332 | + try { |
| 333 | + List<Ternary<VirtualMachine, Host, Host>> plan = getDrsPlan(cluster, |
| 334 | + ClusterDrsMaxMigrations.valueIn(cluster.getId())); |
| 335 | + savePlan(cluster.getId(), plan, eventId, ClusterDrsPlan.Type.AUTOMATED, |
| 336 | + ClusterDrsPlan.Status.READY); |
| 337 | + logger.info("Generated DRS plan for cluster {}", cluster); |
| 338 | + } catch (Exception e) { |
| 339 | + logger.error("Unable to generate DRS plans for cluster {}", cluster, e); |
| 340 | + } finally { |
| 341 | + clusterLock.unlock(); |
| 342 | + } |
298 | 343 | } |
| 344 | + } finally { |
| 345 | + clusterLock.releaseRef(); |
| 346 | + } |
| 347 | + } |
299 | 348 |
|
300 | | - long eventId = ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, |
301 | | - EventTypes.EVENT_CLUSTER_DRS, |
302 | | - String.format("Generating DRS plan for cluster %s", cluster.getUuid()), cluster.getId(), |
303 | | - ApiCommandResourceType.Cluster.toString(), true, 0); |
304 | | - GlobalLock clusterLock = GlobalLock.getInternLock(String.format(CLUSTER_LOCK_STR, cluster.getId())); |
| 349 | + /** |
| 350 | + * Message-bus handler for VM power-state events; triggers event-driven DRS for the VM's cluster. |
| 351 | + */ |
| 352 | + @MessageHandler(topic = VirtualMachineManager.Topics.VM_POWER_STATE) |
| 353 | + protected void handleVmPowerStateEvent(String subject, String senderAddress, Object args) { |
| 354 | + if (!(args instanceof Long)) { |
| 355 | + return; |
| 356 | + } |
| 357 | + try { |
| 358 | + triggerEventDrivenDrsForVm((Long) args); |
| 359 | + } catch (Exception e) { |
| 360 | + logger.debug("Event-driven DRS: error handling VM power-state event for {}", args, e); |
| 361 | + } |
| 362 | + } |
| 363 | + |
| 364 | + /** |
| 365 | + * Resolves the VM's cluster and, subject to the enable flag and cooldown, schedules DRS plan generation. |
| 366 | + */ |
| 367 | + void triggerEventDrivenDrsForVm(Long vmId) { |
| 368 | + if (vmId == null) { |
| 369 | + return; |
| 370 | + } |
| 371 | + VMInstanceVO vm = vmInstanceDao.findById(vmId); |
| 372 | + if (vm == null || vm.getHostId() == null) { |
| 373 | + return; |
| 374 | + } |
| 375 | + HostVO host = hostDao.findById(vm.getHostId()); |
| 376 | + if (host == null || host.getClusterId() == null) { |
| 377 | + return; |
| 378 | + } |
| 379 | + Long clusterId = host.getClusterId(); |
| 380 | + if (!shouldTriggerEventDrivenDrs(clusterId)) { |
| 381 | + return; |
| 382 | + } |
| 383 | + final ClusterVO cluster = clusterDao.findById(clusterId); |
| 384 | + if (cluster == null) { |
| 385 | + return; |
| 386 | + } |
| 387 | + final int debounceMinutes = ClusterDrsEventDrivenInterval.valueIn(clusterId); |
| 388 | + logger.debug("Event-driven DRS: scheduling plan generation for cluster {} (triggered by VM {})", clusterId, vmId); |
| 389 | + submitEventDrivenDrs(cluster, debounceMinutes); |
| 390 | + } |
| 391 | + |
| 392 | + /** |
| 393 | + * Runs generateDrsPlanForCluster off the message-bus thread so event publishers are not blocked. |
| 394 | + */ |
| 395 | + protected void submitEventDrivenDrs(final ClusterVO cluster, final int debounceMinutes) { |
| 396 | + eventDrsExecutor.submit(() -> { |
305 | 397 | try { |
306 | | - if (clusterLock.lock(30)) { |
307 | | - try { |
308 | | - List<Ternary<VirtualMachine, Host, Host>> plan = getDrsPlan(cluster, |
309 | | - ClusterDrsMaxMigrations.valueIn(cluster.getId())); |
310 | | - savePlan(cluster.getId(), plan, eventId, ClusterDrsPlan.Type.AUTOMATED, |
311 | | - ClusterDrsPlan.Status.READY); |
312 | | - logger.info("Generated DRS plan for cluster {}", cluster); |
313 | | - } catch (Exception e) { |
314 | | - logger.error("Unable to generate DRS plans for cluster {}", cluster, e); |
315 | | - } finally { |
316 | | - clusterLock.unlock(); |
317 | | - } |
318 | | - } |
319 | | - } finally { |
320 | | - clusterLock.releaseRef(); |
| 398 | + generateDrsPlanForCluster(cluster, debounceMinutes); |
| 399 | + } catch (Exception e) { |
| 400 | + logger.warn("Event-driven DRS: plan generation failed for cluster {}", cluster, e); |
321 | 401 | } |
| 402 | + }); |
| 403 | + } |
| 404 | + |
| 405 | + /** |
| 406 | + * Returns true if automatic and event-driven DRS are enabled and the per-cluster cooldown has |
| 407 | + * elapsed, recording the trigger time when it does. |
| 408 | + */ |
| 409 | + boolean shouldTriggerEventDrivenDrs(Long clusterId) { |
| 410 | + if (ClusterDrsEnabled.valueIn(clusterId).equals(Boolean.FALSE) |
| 411 | + || ClusterDrsEventDrivenEnabled.valueIn(clusterId).equals(Boolean.FALSE)) { |
| 412 | + return false; |
322 | 413 | } |
| 414 | + long now = System.currentTimeMillis(); |
| 415 | + long cooldownMs = ClusterDrsEventDrivenInterval.valueIn(clusterId) * 60L * 1000L; |
| 416 | + Long last = lastEventDrsTriggerByCluster.get(clusterId); |
| 417 | + if (last != null && (now - last) < cooldownMs) { |
| 418 | + return false; |
| 419 | + } |
| 420 | + lastEventDrsTriggerByCluster.put(clusterId, now); |
| 421 | + return true; |
323 | 422 | } |
324 | 423 |
|
325 | 424 | /** |
@@ -855,7 +954,7 @@ public String getConfigComponentName() { |
855 | 954 | public ConfigKey<?>[] getConfigKeys() { |
856 | 955 | return new ConfigKey<?>[]{ClusterDrsPlanExpireInterval, ClusterDrsEnabled, ClusterDrsInterval, ClusterDrsMaxMigrations, |
857 | 956 | ClusterDrsAlgorithm, ClusterDrsImbalanceThreshold, ClusterDrsMetric, ClusterDrsMetricType, ClusterDrsMetricUseRatio, |
858 | | - ClusterDrsImbalanceSkipThreshold}; |
| 957 | + ClusterDrsImbalanceSkipThreshold, ClusterDrsEventDrivenEnabled, ClusterDrsEventDrivenInterval}; |
859 | 958 | } |
860 | 959 |
|
861 | 960 | @Override |
|
0 commit comments