engine: do not orphan an instance on a forced power report during start - #14207
bhouse-nexthop wants to merge 11 commits into
Conversation
A host VM state report lists only the instances running on the host at the moment the report was collected. An out-of-band report (sent by the agent on a libvirt lifecycle event) is processed with `force`, which skips both the graceful period and the outdated-report filter. Any instance absent from that report is immediately marked PowerReportMissing. That is wrong when the report was collected before a state change the management server made a moment later. Example: | time | event | |----------|----------------------------------------------------------| | 20:25:14 | another instance crashes; agent collects and sends report | | 20:25:16 | the starting instance's domain is created | | 20:25:17 | StartAnswer arrives, instance -> Running | | 20:25:18 | the 20:25:14 report is processed, instance not in it | | | -> PowerReportMissing, instance -> Stopped | The instance is running but is now recorded as stopped. On a busy host these forced reports are frequent, so the graceful period almost never applies. Fix: skip the missing-report verdict for an instance whose state changed within the graceful period, forced or not. A later report decides instead. An instance that has been Running for a while and then shuts itself down is unaffected, so out-of-band stop detection keeps working. Also make the log line state whether the verdict was forced. It previously said "has passed graceful period" even when `force` short-circuited the check, which is misleading when diagnosing this. Signed-off-by: Brad House <bhouse@nexthop.ai>
… missing report When a power report for a Running instance is missing, the instance is synced to Stopped and `releaseVmResources` frees its NICs and IP addresses. Unlike the PowerOff branch above it, no StopCommand is sent. A missing report only means the host did not list the instance. It is not proof the instance is gone. When it is still running: - its NIC and IP are marked free while it keeps using them - the IP is later assigned to another instance - two instances answer for the same address Fix: use the same path as PowerOff. Send the StopCommand first and release resources only if it succeeds. If the stop fails, keep the resources and let a later report retry, rather than freeing an address that is still in use. Signed-off-by: Brad House <bhouse@nexthop.ai>
When a host reports an instance as powered on and the database has it as Destroyed or Expunging, the report is only logged. The instance keeps running unmanaged: - CloudStack knows the host and the instance name at that moment - the instance's IP addresses and volumes have already been handed back - nothing else ever looks at it again Changes: | state | before | after | |----------------------|--------|---------------------------| | Destroyed, Expunging | log | send StopCommand to host | | Error | log | log and raise an alert | Error is only alerted, not stopped, because an instance in that state may still be wanted for inspection. Destroyed and Expunging are being deleted, so there is no case for leaving them running. Signed-off-by: Brad House <bhouse@nexthop.ai>
`advanceStop()` returns immediately, without sending anything to the host,
when the database already has the instance in one of these states:
if (state == State.Stopped) return;
if (state == State.Destroyed || state == State.Expunging || state == State.Error) return;
Expunge calls `advanceStop()` and then releases NICs and deletes volumes. So
for an instance recorded as Stopped or Error, expunge never contacts the host
at all. If the host is still running it, the instance keeps its addresses and
its disks are deleted underneath it.
`vm.destroy.forcestop` does not help: the early return happens before the
host id is even looked at.
Fix: after `advanceStop()`, send a StopCommand to the instance's host, or its
last known host, regardless of the database state. It is a no-op when no
domain is there.
Signed-off-by: Brad House <bhouse@nexthop.ai>
…bout
A host report naming an instance with no record in the database means
something is running there unmanaged, usually left behind by a deploy or an
expunge that never reached the host. Today that produces one debug line per
instance per report and nothing else, so it can go unnoticed indefinitely:
Unable to find matched VM in CloudStack DB. name: i-2-3-VM powerstate: PowerOn
Change: log the unknown instances for a host at warn level, and only when the
set changes, so a standing condition is visible once instead of on every
report. A host that stops reporting unknown instances logs one info line.
Nothing is stopped automatically here. An unknown instance may be a domain an
operator put on the host deliberately, so this only makes the condition
visible; `listUnmanagedInstances` and import remain the way to act on it.
Signed-off-by: Brad House <bhouse@nexthop.ai>
Covers: | test | checks | |-----------------------------------------------|---------------------------------------------| | hasRecentStateChange, within/outside/null | the graceful period guard on state changes | | convertVmStateReport, known and unknown names | unknown instances are skipped, not mapped | | convertVmStateReport, empty report | no DB lookup for an empty report | | reportUnknownInstances, only reports changes | one report per change, quiet while unchanged| Signed-off-by: Brad House <bhouse@nexthop.ai>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 4.22 #14207 +/- ##
============================================
+ Coverage 17.97% 18.00% +0.03%
- Complexity 16184 16219 +35
============================================
Files 5930 5930
Lines 535615 535695 +80
Branches 65582 65594 +12
============================================
+ Hits 96271 96465 +194
+ Misses 428377 428241 -136
- Partials 10967 10989 +22
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The two helpers added earlier in this branch called `new StopCommand(...)` directly. `sendStop()` does more than that, and skipping it has consequences: | not set | effect | |------------------------------|---------------------------------------------------------------| | `vlanToPersistenceMap` | `shouldDeleteBridge()` returns true for an empty map, so the host deletes bridges belonging to persistent networks | | external hypervisor details | `ExternalPathPayloadProvisioner` dereferences `cmd.getVirtualMachine()`, which is null -> NPE | | `controlIp` | the cmdline backup for system VMs does not happen | | `volumesToDisconnect` | volumes stay connected on the host | Fix: pull the command construction out of `sendStop()` into `buildStopCommand()` and use it in both helpers, so there is one place that knows how to build a StopCommand. `sendStop()` behaviour is unchanged. External instances are skipped in both helpers. Their teardown is done by their extension in `finalizeExpunge`, and a StopCommand issued from here would not carry what that path needs. Also removes a duplicated Javadoc block, which left `stopUnmanagedInstanceOnReportingHost` documented as the other method. Signed-off-by: Brad House <bhouse@nexthop.ai>
The tests added earlier stubbed `findByHostInStatesExcluding` with a single
`Mockito.any()` for a varargs parameter that receives three states:
findByHostInStatesExcluding(Long hostId, Collection<Long> excludingIds, State... states)
The matcher never matched, so the mock returned its default empty list,
`processMissingVmReport()` returned at the `isEmpty()` check, and the
`never()` assertions passed without exercising anything.
Fix: match the varargs explicitly. Verified by mutation - disabling the
`hasRecentStateChange()` guard now fails
`test_processMissingVmReport_forcedReportDoesNotOverrideRecentStateChange`
with NeverWantedButInvoked. It did not before.
Tests now cover:
| test | checks |
|-----------------------------------------------|--------------------------------------------------|
| forcedReportDoesNotOverrideRecentStateChange | a forced report does not mark a just-changed instance missing |
| forcedReportStillReportsSettledInstance | a settled instance is still reported, so out-of-band stop detection works |
| unforcedReportHonoursGracefulPeriod | the graceful period still applies without force |
| recordsUnknownInstances | an unknown name is recorded, not just logged |
Also moves the "Detected missing VM" line below the guard, so a skipped
instance is no longer logged as detected.
Signed-off-by: Brad House <bhouse@nexthop.ai>
|
Self-review follow-up. Two defects found in the new stop paths, both fixed. 1. The new StopCommands were built by hand and missed what
Fixed by extracting 2. The new tests did not test the fix. The Fixed by matching the varargs explicitly. Verified by mutation: disabling the Also in this push:
Known limitations, deliberately not addressed here:
|
`sendStop()` swallows `AgentUnavailableException` and `OperationTimedoutException`
and reports success when `force` is true:
} catch (final AgentUnavailableException | OperationTimedoutException e) {
if (!force) { return new Pair<>(false, errorMsg); }
}
return new Pair<>(true, null);
So a host that is unreachable or too slow to answer counts as a successful
stop, and the caller goes on to release the NICs and IP addresses. A host too
busy to answer is exactly the condition that produces the stale report this
branch is reacting to, so that is the worst case to guess in.
| report | force | on an unreachable host |
|---------------------|-------|-------------------------------|
| PowerOff | true | unchanged, host said it is down |
| PowerReportMissing | false | back off, keep the addresses, let a later report decide |
A PowerOff report is the host stating the instance is down. A missing report
only means the host did not list it, which is not the same thing.
Also makes `handlePowerOffReportWithNoPendingJobsOnVM()` protected so this
branch can be tested.
Signed-off-by: Brad House <bhouse@nexthop.ai>
…about A warn line only helps someone already reading the log. Raise an ALERT_TYPE_SYNC alert as well, so the condition reaches whoever watches alerts. The alert follows the same rule as the log: raised when the set of unknown instances on a host changes, not on every report. Nothing is stopped automatically. An unknown instance may be a domain an operator put on the host deliberately. Signed-off-by: Brad House <bhouse@nexthop.ai>
These are the branches that decide whether a still-running instance keeps its addresses, and they had no tests. `VirtualMachineManagerImplTest`: | test | checks | |-----------------------------------------------|-----------------------------------------------| | missingReportUsesUnforcedStopAndKeepsResources | a missing report stops unforced, and a failed stop does not release resources | | powerOffKeepsForcedStop | a PowerOff report still stops forced | | ensure...skipsExternal | External instances are left to their extension | | ensure...noHostIdDoesNothing | nothing is sent when there is no host to send to | | ensure...fallsBackToLastHostId | the last known host is used when `host_id` is cleared | | ensure...agentUnavailableIsSwallowed | an unreachable host does not break the expunge | `VirtualMachinePowerStateSyncImplTest` gains the AlertManager mock the alert needs. Signed-off-by: Brad House <bhouse@nexthop.ai>
|
Three further changes pushed. 1. The stop for a missing report was forced. } catch (final AgentUnavailableException | OperationTimedoutException e) {
if (!force) { return new Pair<>(false, errorMsg); }
}
return new Pair<>(true, null);So an unreachable or slow host counted as a successful stop and the addresses were released. A host too busy to answer is the condition that produces the stale report in the first place.
2. Unknown instances now raise an 3. Tests for the branches that decide whether an address is freed: the Not changed: aborting the expunge when the stop fails. |
Description
An instance can end up running on a KVM host with no record in CloudStack. Its IP address is released and later handed to another instance, so two instances answer for the same address. Its root volume stays in
Destroyand cannot be deleted while the domain holds it.Fixes: #14206
The trigger is the out-of-band ping the KVM agent sends when another instance on the same host shuts down or crashes. That ping is processed with
force, which skips the graceful period. The KVM report lists only powered-on domains, so an instance that is still starting is absent from it and is markedPowerReportMissing.Sequence:
StartCommandsentforceStartAnswerarrives, instance -> RunningPowerReportMissingadvanceStop()returns at once because state is ErrorOne commit per finding, so they can be reviewed independently.
sendStop()does, so the host gets the VLAN persistence map, external details, control IP and volumes to disconnectCommit 1 is the root cause. Commits 2 to 4 are independent backstops, each of which would have prevented the end result on its own.
Behaviour that does not change:
Known limitations
2 x ping.interval. The exact invariant is "this report was collected before the last state change". Stamping the report with its collection time and comparing againstupdate_timewould be precise; that needs a field onPingRoutingCommandand is better done separately.UserVmManagerImpl.expunge()calls_networkMgr.release()beforeadvanceExpunge(), so by the time the expunge-time StopCommand is sent the addresses have already been released. Moving the stop ahead of that release would close the remaining window, in a separate change.Types of changes
Feature/Enhancement Scale or Bug Severity
Bug Severity
How Has This Been Tested?
VirtualMachinePowerStateSyncImplTestpasses (11 tests)How did you try to break this feature and the system with this change?
checkBeforeCleanupis set correctly for each new StopCommand. It makes the host refuse to stop a running domain, so it is false where a running domain is what must be removed (commits 3 and 4) and true where the point is to back off if the instance turns out to be alive (commit 2)sendStop(..., force=true, ...)swallowsAgentUnavailableExceptionandOperationTimedoutExceptionand answers success, so an unreachable host would have had its addresses released. Commit 9 passesforce=falsefor a missing report;PowerOffkeepsforce=true, which is its existing behaviour