Skip to content

DRS: honour anti-affinity when planning and executing migrations - #14108

Open
bhouse-nexthop wants to merge 6 commits into
apache:4.22from
bhouse-nexthop:drs-antiaffinity
Open

DRS: honour anti-affinity when planning and executing migrations#14108
bhouse-nexthop wants to merge 6 commits into
apache:4.22from
bhouse-nexthop:drs-antiaffinity

Conversation

@bhouse-nexthop

Copy link
Copy Markdown
Collaborator

Description

This PR fixes DRS generating and executing migration plans that violate host anti-affinity.

Four defects, one per commit:

# Defect
1 The reserved-capacity branch in HostAntiAffinityProcessor was chained to the wrong condition, making it unreachable for live VMs and throwing NullPointerException for a group mapping pointing at a deleted VM
2 The processor accepted a list of planned placements and ignored it, reading every group member's host from the database
3 Same gap in NonStrictHostAffinityProcessor, which NonStrictHostAntiAffinityProcessor extends
4 Nothing re-checked a plan before executing it, and non-strict groups were discarded entirely

Why the planned placements matter. DRS builds a plan of several migrations in memory and persists it only at the end. While the plan is being built, the database still shows the old host for every VM the plan has already moved, so anti-affinity was evaluated against stale placements and a plan could put two anti-affine VMs on the same host. HostAffinityProcessor already honours the same argument; the anti-affinity processors did not.

Why non-strict groups are honoured when rebalancing. Non-strict groups express themselves by lowering a host's priority on the deployment plan rather than by excluding it. ClusterDrsServiceImpl built a plan object, handed it to the processors, read only the exclude list, and discarded the plan - so the preference was written to an object nothing read.

Non-strict means the rule may be broken when there is nowhere else to put a VM. That cannot arise while rebalancing: the VM already runs somewhere that satisfies the group, and leaving it there is always available to DRS. Being better balanced is not a reason to break it.

Why execution is re-checked. A plan is generated once and executed later, and nothing downstream re-checks it - migrateVirtualMachine does not enforce affinity groups. By execution time the cluster may have changed. A queued migration also occupies both of its hosts until it completes, and may never complete, so a swap plan (A: host1 to host3, B: host2 to host1) could clear B for host1 while A was still on it.

A migration that is no longer valid is now skipped and recorded as CANCELLED rather than FAILED, with an event, since nothing went wrong - the plan went out of date.

Related to #12473, which was closed on the grounds that non-strict groups permit live migration. They do, but that is about migrations an operator asks for, not about DRS choosing to undo a placement that already satisfied the group. Strict groups were also affected, which that report did not isolate.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

How Has This Been Tested?

Unit tests, run against 4.22:

  • HostAntiAffinityProcessorTest - new, 10 cases. Covers the running host, the reserved-capacity last host in both Stopped and Starting states, expiry of the capacity release interval, removed and missing group members, and planned placements taking precedence over the database.
  • NonStrictHostAffinityProcessorTest - added a case asserting a planned placement wins and the database is not consulted.
  • ClusterDrsServiceImplTest - 33 cases. Added coverage for the pre-migration re-check, a VM that stopped between planning and execution, a destination still occupied by a queued migration, and non-strict preferences becoming exclusions.

The two commit-1 cases were confirmed to fail against unpatched code (one assertion failure, one NullPointerException).

Full mvn test on api, server and the four affinity processor plugins, checkstyle and license checks enabled: 0 failures.

How did you try to break this feature and the system with this change?

  • Non-DRS callers. Initial deployment and the find-hosts-for-migration API pass an empty placement list. Verified that path is unchanged by commit 2 and 3. Commit 1 does change it, and deliberately: a Stopped group member still holding reserved capacity now has its last host avoided, where previously the branch was unreachable. That widens the avoid set on the deployment path and restores the behaviour that predates the refactor which broke it. On a small cluster it can make a deployment fail where it previously succeeded, for the duration of capacity.skipcounting.hours. Calling it out explicitly since it affects more than DRS.
  • Removed VMs are no longer avoided at all, which is the other direction of the same fix - a removed VM is not running anywhere.
  • Swap plans, where two migrations exchange hosts, are the case the source-host tracking exists for. Covered by test.
  • Plans that go stale: VM stopped, VM already moved, destination filled by something else.
  • NPE hunting on null hosts, null VMs, and group mappings pointing at deleted VMs.
  • The affinity processors also cover dedicated resources and DPDK, so the skip message does not claim every refusal is about an affinity group.

The reserved-capacity branch was chained to the wrong condition, so it
only ran when the group VM was null or removed:

- a Stopped VM still holding reserved capacity never had its last host
  avoided - the branch was dead for every live VM
- a group mapping pointing at a deleted VM hit the branch with a null
  and threw NullPointerException

Restructured to match NonStrictHostAffinityProcessor, which already had
the intended shape: skip null/removed, then avoid the current host, else
the last host while capacity is still reserved.

Signed-off-by: Brad House <bhouse@nexthop.ai>
The processor already accepted a vmList of placements and ignored it,
reading every group member's host from the database instead.

DRS builds a multi-migration plan in memory and persists it only at the
end, so while the plan is being built the database still shows the old
host for every VM the plan has already moved. Anti-affinity was
therefore evaluated against stale placements, and a plan could put two
anti-affine VMs on the same host.

- resolve each group member from vmList first, fall back to the database
- mirrors what HostAffinityProcessor already does with the same argument
- no change when vmList is empty, which is every non-DRS caller

Signed-off-by: Brad House <bhouse@nexthop.ai>
Same gap as the strict processor: vmList was accepted and ignored, so
host priorities were adjusted from database placements even when the
caller supplied newer ones.

NonStrictHostAntiAffinityProcessor extends this class and only overrides
the priority direction, so it is fixed by the same change.

Signed-off-by: Brad House <bhouse@nexthop.ai>
A DRS plan is generated once and executed later, and nothing downstream
re-checks it - migrateVirtualMachine does not enforce affinity groups.
By execution time the cluster may have changed, so a plan that was valid
when generated can violate anti-affinity when it runs.

- validate each migration against current state before queueing it
- skip and mark failed instead of migrating into a violation
- track destinations already queued in this run, since the jobs are
  asynchronous and the database does not reflect them yet

Signed-off-by: Brad House <bhouse@nexthop.ai>
Three defects found reviewing the previous commits, all in how DRS
decides whether a planned migration is still allowed.

Non-strict anti-affinity was discarded entirely. Non-strict groups
express themselves by lowering a host's priority on the deployment plan
rather than by excluding it, and DRS built a plan, handed it to the
processors, read only the exclude list and threw the plan away.

  Non-strict means the rule may be broken when there is nowhere else to
  put a VM. That cannot arise while rebalancing: the VM already runs
  somewhere that satisfies the group and leaving it there is always an
  option. Being better balanced is not a reason to break it.

A host was treated as free the moment a migration away from it was
queued. The jobs are asynchronous and can fail, so a queued migration
occupies both ends until it completes. In a swap - A from host1 to
host3, B from host2 to host1 - B was cleared for host1 while A was
still on it.

  - track the hosts queued migrations have not actually left
  - refuse a destination that is one of them

A VM that stopped between planning and execution threw NPE inside the
affinity check, which was then logged without a stack trace.

  - treat a VM that is no longer running as an out of date plan
  - log the exception rather than its message

Signed-off-by: Brad House <bhouse@nexthop.ai>
- a skipped migration is CANCELLED, not FAILED. Nothing went wrong; the
  plan went out of date. FAILED was indistinguishable from a migration
  that genuinely broke, and left no record of why
- record an event when one is skipped, so it is visible rather than a
  silent no-op in an otherwise successful plan
- the processors also cover dedicated resources and DPDK, so the refusal
  message no longer claims every skip is about an affinity group
- hoist the third copy of getVmIdVmMap into AffinityProcessorBase

Signed-off-by: Brad House <bhouse@nexthop.ai>
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.02151% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 17.93%. Comparing base (10d3e11) to head (192bab5).

Files with missing lines Patch % Lines
...che/cloudstack/affinity/AffinityProcessorBase.java 55.55% 3 Missing and 1 partial ⚠️
...ache/cloudstack/cluster/ClusterDrsServiceImpl.java 91.30% 2 Missing and 2 partials ⚠️
...stack/affinity/NonStrictHostAffinityProcessor.java 72.72% 2 Missing and 1 partial ⚠️
...cloudstack/affinity/HostAntiAffinityProcessor.java 92.59% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               4.22   #14108      +/-   ##
============================================
+ Coverage     17.91%   17.93%   +0.01%     
- Complexity    16109    16132      +23     
============================================
  Files          5928     5928              
  Lines        534964   535050      +86     
  Branches      65463    65471       +8     
============================================
+ Hits          95865    95973     +108     
+ Misses       428191   428156      -35     
- Partials      10908    10921      +13     
Flag Coverage Δ
uitests 4.02% <ø> (ø)
unittests 19.00% <86.02%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new execution-time validation currently skips re-checking DPDK/dedicated-resource exclusions for VMs without affinity groups (and there is also an orphaned/misleading Javadoc block after the refactor).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR addresses DRS migration planning/execution violating host (anti-)affinity by ensuring affinity processors honor in-memory “planned placements”, by preserving non-strict affinity preferences through to DRS decision-making, and by re-validating queued migrations against current placement rules at execution time.

Changes:

  • Update host anti-affinity and non-strict host affinity processors to prefer caller-supplied planned placements over stale DB placement.
  • Preserve non-strict affinity preferences by converting lowered host priorities into DRS exclusions.
  • Re-check each planned migration at execution time and cancel (not fail) migrations that are no longer valid.
File summaries
File Description
server/src/main/java/org/apache/cloudstack/cluster/ClusterDrsServiceImpl.java Converts non-strict priority adjustments to exclusions and adds execution-time migration re-checking.
server/src/test/java/org/apache/cloudstack/cluster/ClusterDrsServiceImplTest.java Adds coverage for execution-time re-checking, queued-migration occupancy, and non-strict preference handling.
api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java Adds helper to index caller-supplied planned placements by VM id.
plugins/affinity-group-processors/host-anti-affinity/src/main/java/org/apache/cloudstack/affinity/HostAntiAffinityProcessor.java Honors planned placements when building avoid sets; refactors per-VM host avoidance logic.
plugins/affinity-group-processors/host-anti-affinity/src/test/java/org/apache/cloudstack/affinity/HostAntiAffinityProcessorTest.java New unit tests covering planned placements, reserved-capacity behavior, and removed/missing members.
plugins/affinity-group-processors/non-strict-host-affinity/src/main/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessor.java Honors planned placements when adjusting host priorities for non-strict affinity.
plugins/affinity-group-processors/non-strict-host-affinity/src/test/java/org/apache/cloudstack/affinity/NonStrictHostAffinityProcessorTest.java Adds a test ensuring planned placement wins over DB placement.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +824 to +826
if (CollectionUtils.isEmpty(affinityGroupVMMapDao.listByInstanceId(vm.getId()))) {
return false;
}
Comment on lines 790 to +797
/**
* Executes the DRS plan by migrating virtual machines to their destination hosts.
* If there are no migrations to be executed, the plan is marked as completed.
*
* @param plan
* the DRS plan to be executed
*/
/**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants