kubevirt: keep domain bookkeeping until Cleanup - #6271
Conversation
12beebb to
431cc4d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #6271 +/- ##
==========================================
+ Coverage 24.27% 24.96% +0.68%
==========================================
Files 512 524 +12
Lines 93831 95836 +2005
==========================================
+ Hits 22782 23930 +1148
- Misses 69246 69873 +627
- Partials 1803 2033 +230 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Correcting the hardware section I posted earlier today: it claimed the guest data What the change does do. Restart-request to app-online went from 34–100 s Why that is not enough. The state never resolves, the 63 s backoff expires, and the replacement is created Data survival. Only the cycle where the restart is issued immediately after Net: the three commits are still correct in themselves — the bookkeeping and the Separately, both cycles log |
|
|
||
| // The vmiList entry must outlive Stop: Info() resolves a domain through | ||
| // it, and domainmgr polls Info() to learn when the guest actually | ||
| // stopped. Delete() and Cleanup() own the removal. |
There was a problem hiding this comment.
in the Info(), this will allow the function to find the 'vmis' struct, and move on, but later, in the scheduledOnMe():
onMe, _, err := ctx.scheduledOnMe(vmis.mtype, vmis.name)
if err != nil {
if isK3sUnreachable(err) {
return 0, types.UNKNOWN, nil
}
return 0, types.BROKEN, logError("Failed to determine scheduled node: %s", err)
}
if it is deleted in the kuberntes cluster, it is going to return here the types.BROKEN. which is also a problem.
Mayin in the 'vmis' struct, we can add a 'pendingOnDelete' bool, and set it here. Over in the Info(), checks on this bool on error, and return Unknown still.
There was a problem hiding this comment.
types.BROKEN is gone — #6257 rewrote Info() to confirm existence with its own Get first, so a deleted VMIRS returns HALTED/zero before scheduledOnMe runs. That is also why I didn't add pendingOnDelete: the live Get answers it better than a flag, which would be wrong if the delete itself failed.
The case underneath your comment is still real though. Info now Gets the same VMIRS up to three times, and a delete landing after the first is caught by none of them — the second's NotFound is dropped by the fall-through, the third ends at return id, types.SCHEDULING, err. waitForDomainGone returns true on any Info error before it looks at the state, so the race ends the graceful wait early.
Fixed in 5fafb53: errors.IsNotFound -> 0, HALTED, nil at both lookups, with a test that fails without it. Does that cover what you meant?
There was a problem hiding this comment.
I think this code added is fine. but I think there is still a problem on the ScheduledOnMe() in the deletion case:
the function checks the VMIRS first, if this is gone, it returns 'not found', I think the problem is that, even the VMIRS can be removed quickly, the VMI itself may still stay around fairly longer. So, this deletion case, I think we need the same check as confirmVMIRSGone() to verify.
431cc4d to
260c5a0
Compare
|
Heads-up on a force-push that changes what this PR contains. This branch is now based on #6257. Its two commits are the base here, included Consequences worth flagging rather than leaving to be discovered in the diff:
On testing: the two substantive commits have since been exercised in the 7-leg When #6257 merges I will rebase and its commits will drop out. It force-pushes often, so |
|
Correction to my comment above: it said the included #6257 commits are "at On why the base is not #6257's current tip: #6257's latest pillar commit Reproducible on @andrewd-zededa flagging the build break on your side. Nothing else about this PR changed: the branch head is still |
Replaces the three lf-edge#6271 kubevirt commits with lf-edge#6257 at df8ae7c plus the two of them that survive on top of it, since lf-edge#6257 fixes the VMIRS-delete logging the same way and rewrites Info to derive DomainId from a live Get rather than vmiList. Adds the proposed volumemgr readiness-publish follow-up to lf-edge#6240. Also records that lf-edge#5971 and rucoder#3 are held at the tips the 7/7 image used rather than caught up, so the bring-up baseline is not a variable in this run. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A failed VMIRS delete logged only the error, so an operator reading the log could not tell which app's replicaset failed to go away -- and this path fires during every teardown that races the Kubernetes API. Include the replicaset name, and drop the trailing newline that logrus adds itself. Also records lf-edge#6271's presence in the branch composition table, including which of its commits lf-edge#6257 supersedes, and corrects the lf-edge#6190 row: the fault-injection gates are compiled in whenever the build arg is non-empty, not only when it is y. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
260c5a0 to
fc0f374
Compare
|
Rebased onto #6257's The three Re-derived after the rebase, since #6257 reworked The description now names |
A purge must delete the previous generation of an app. In the kube path it can leave that generation in the cluster. No test found this, because no test looked at the cluster after a purge. These tests purge a container app. eve-k runs a container app in a shim VM, so the app has a VMIRS. - purge_baseline_test: purge a running app. Make sure that only the new VMIRS and one PVC stay. - purge_after_power_cycle_test: purge the app while the device is off, then power the device on. This is the failure that customers saw. - purge_during_failover_test: purge the app while it runs on a different node than the node that gets the purge. The power-cycle test runs two times, one time for each hypervisor. The HYPERVISOR parameter selects it, and the default is kubevirt. The kvm run is the control. A kvm domain is a local qemu process, and no cluster object stays alive after the node stops, so the duplicate generation cannot occur there. The kvm run must therefore pass, and a pass keeps the duplicate-generation defect on the kube path. The kvm run still tests the parts of a purge that are the same for each hypervisor: the purge must complete and must not stop for ever in zedmanager, and the old generation's disk must go away. It also shows that the kube fixes do not make kvm worse. The two runs use one test body, because only the last assertions are different: VMIRS and PVC objects for kubevirt, files on disk and the qemu domain state directory for kvm. The helpers go in files named for the state that they observe, and not for the test that needed them first. A catch-all helpers file gives a contributor no clue where to put the next helper, and it becomes large: - deviceaccess_test: the only file that runs a command on a device. - appstate_test: pillar's own view of the app, from pubsub and from persisted state, by app UUID. - appworkload_test: where the app runs, as the hypervisor sees it. VMIRS objects for kubevirt, qemu domain state directories for kvm. - appvolumes_test: the app's disk in each of its three forms, and the storage rules. These rules stay true after a delete or a restart, so they must not go in a purge file. - purge_assertions_test: the assertions that have no meaning if there is no purge. - fixtures_test: device requirements and app configuration. testsuite_test holds the file map and the three questions that tell you where a new helper goes. README.md points to this suite as the example. The disk assertions depend on a property of the framework, so a note in appvolumes_test records it. PurgeApplication increases the generationCount of each volume that the app uses, together with the app purge counter, the same as a controller does. Only that increase makes zedmanager replace the volume: it finds the volumes to replace with the key of the volume reference, which holds the volume UUID, the generation counters and the app UUID. An app purge counter alone changes none of them, so the reference still matches and nothing is removed or added. If the framework loses that increase, the assertions on the volume generation key and on the old artifact stop being possible to satisfy, not merely weaker. EdgeCluster.FindDeviceHostingApp could return a device that no longer runs the app. It reads the cluster info that each device published last, and that data can be older than the event that the caller waits for. The fault is clearest when a device is off: its own old data still names it as the host, and the device sends nothing more. The method now accepts excludeDevNames. It does not read cluster info from an excluded device, and it never returns one. The parameter is variadic, so the calls that exist do not change. The method also subscribes before it reads the cached data, because the old order could lose an update that arrived between the two steps. WaitUntilNodesAreReady already used the correct order. The power-cycle test flushes the device caches before it removes the power. EVE unpacks the container image layers without an fsync, and containerd marks the snapshots as committed. If the power stops in the writeback window, the layers keep the directory tree but each file becomes empty: approximately 150 MB of layers became 3 MB in a test. Nothing extracts them again, each later volume from that image is empty, and the app then starts and stops in a loop while EVE reports it as RUNNING. This is a durability fault in EVE. The sync keeps it out of a test that has a different subject. Each purge test has two phases, because the purge and the reclamation of the old disk use different clocks: - The purge end state: the counter increased by one, the purge phase is NotInprogress, the app is RUNNING, there is one volume with a new generation, and there is one VMIRS or one qemu domain. This settles in approximately one minute. - The removal of the old generation's storage: the old artifact is gone and no artifact stays that no VolumeStatus names, or, for kubevirt, no PVC stays that no VolumeStatus names. volumemgr does this on its own schedule, approximately five minutes after the purge in a test. This is much sooner than the one hour VdiskGCTime, but much later than the purge. A check for the second in the window of the first fails on time and not on substance. This was true for the PVC check also. On kubevirt each test waits for the node to become Ready before it waits for the app, and again after the power cycle, because the reboot stops k3s. Each other app test in this package does the first of these. Without the wait, the app stays in INITIAL while k3s and Longhorn start, which used six of the ten minutes of the app limit in a test, and the test then failed on time and not on substance. fixtures_test holds each timeout in one block with the reason for its value. Eventually stops as soon as the condition is true, so a large limit costs nothing when the system operates correctly, and it only delays a true failure. Each limit comes from a measured time with headroom. If a limit starts to expire, find what changed. Do not increase the number. If the app is not RUNNING, the failure message now includes BootFailed, TriedCount and the error from DomainStatus. The state alone cannot tell you that a guest started and then stopped itself. The failover test had a local helper to work around that fault. The helper is deleted, and findAppNodeName stays private, so this commit adds no new public API to the framework. EdgeDevice gets one method, Name, which the failover test needs to report which node now runs the app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Andrew Durbin <andrewd@zededa.com>
A purge must delete the previous generation of an app before it creates
the next one. In the kube path three defects stop this. The old VMIRS
then keeps running, and EVE reports the purge as complete.
1. Info() says a domain is gone when it only cannot see it.
Info() returns domainID 0 if it finds no replica on this node.
domainmgr copies that 0 into DomainStatus. zedmanager reads 0 as
"there is no domain", so it skips the teardown and releases the
purge. Info() now gets identity and existence from the cluster:
- domainID is workloadID(), a hash of the VMIRS metadata.uid. It was
rand.Uint32(), which has no relation to the cluster.
- Task() keeps the DomainStatus. The kube name now comes from the
status and not from the vmiList cache, which goes stale.
- Info() returns 0 only when the VMIRS is absent. If it cannot
confirm absence, it returns the last known id.
2. Nothing deletes an old generation after a reboot.
/run is tmpfs, so a reboot deletes the purge state. The VMIRS stays
in etcd. zedmanager finds no domain to halt and advances the counter,
so the new generation starts beside the old one. Start() now sweeps
first: it finds each older generation of the app in the cluster,
deletes it, and waits until the object and its pods are gone. The
sweep reads the cluster and does not need the old counter, so it
survives the reboot. If it cannot confirm that a generation is gone,
Start() fails. A failed start is safer than two generations that
share MACs, veth names and one RWO disk.
3. zedmanager waits for ever for a volume-ref removal.
On a purge, doInstall parks the old VolumeRefStatus and waits for
volumemgr to confirm the delete. If volumemgr has no live status for
that reference in this boot, the confirmation never comes. The app
stays in DownloadAndVerify and never asks for the new volume.
zedmanager now looks at volumemgr's live status first, and drops the
reference if it is already absent.
Each Kubernetes request that this change adds now has a deadline of
kubeapi.KubeAPITimeout, with the apiCtx helper. domainmgr calls Info on a
timer of 9 to 30 seconds and calls Start directly, and the watchdog of
zedbox restarts the device if that handler stops to report. A request
without a deadline on those paths can therefore stop the node. With a
deadline it fails, and Info reports UNKNOWN and keeps the id of the caller,
which is the contract on types.DomainStatus.DomainId. getVmirs already used
this same limit, so the new calls now match it.
Also in this change:
- StopReplicaVMI had an inverted IsNotFound test, so each successful
delete logged "Stop VMI Replicaset error <nil>" at error level. This
made a good teardown look like a bad one in the field. The return
values do not change.
- The kubevirt and k8s client constructors are now package-level vars.
The tests can then inject a fake client.
- Info() and sweepStaleGenerations() now call getConfig() themselves.
The receiver is a value, so a getConfig() call in a method they call
fills in a copy and leaves kubeConfig nil. A nil kubeConfig made
newKubevirtClient panic, which stopped zedbox and rebooted the node.
The client stubs in the tests now reject a nil config.
- docs/zedkube.md records what domainID means, what the sweep does, and
two gaps that stay open: the sweep does not delete the stale
generation's PVC, and it cannot run if a purge stops before Start().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrew Durbin <andrewd@zededa.com>
sweepStaleGenerations deletes a stale purge generation's VMIRS and pod, but not its PVC. The old disk stays in the cluster with nothing pointing at it. volumemgr already garbage-collects unused files and ZFS volumes on a timer. Add the same pass for PVCs: gcPVCs lists every PVC, and the existing gcVolumes deletes any with no live VolumeStatus. A node keeps a live VolumeStatus for a volume it does not run, when that volume's app can fail over to it. Checked on a live cluster: the non-hosting node publishes IsReplicated=true and RefCount>0 for such a volume, so gcVolumes already skips it. Split gcVolumes into two parts: volumesToReap decides which candidates are unused, gcVolumes destroys them. The decision is now unit tested; the Kubernetes delete call itself is not, and is unchanged. Also re-enables assertNoOrphanedPVCs in purge_after_power_cycle_test.go, disabled earlier in this branch because this fix did not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A PVC can stay Pending forever. Its ProvisioningFailed events alternate "volume not found" and "volume already exists" - the provisioner created the Longhorn backend volume once, lost track of that success in its own cache, and keeps retrying a name it no longer recognizes. This has hit TestVMAppPurgeDuringFailover at first-ever PVC creation, timing it out before it ever reaches the failover step it tests. Confirmed live, no EVE or pillar change involved: deleting the csi-provisioner pod forces a fresh leader election and cache, and the next retry succeeds. waitForAppRunningMitigatingPVCStall wraps WaitUntilAppIsRunning with a background watcher. It restarts csi-provisioner, once, only after a PVC has shown the failure signature continuously for two minutes. A PVC that clears on its own before then - a normal, if slow, retry - passes through untouched. It never changes the wait's own timeout or failure behavior. Wired into TestVMAppPurgeDuringFailover only, the one test this has actually been observed to fail. Restarting a cluster-wide Longhorn pod is not something to do from every test that creates a volume. This is marked REMOVE ME in longhorn_provisioner_workaround_test.go. It works around infra, not anything this suite tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rkers The lf-edge#6257 replay left conflict markers in four hypervisor files. Take the lf-edge#6257 side, matching how the same overlap was resolved on the newgo line. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: eriknordmark <erik@zededa.com>
The lf-edge#6257 replay resolved kubevirt_info_test.go to the lf-edge#6257 side, dropping this lf-edge#6271-only test. The newgo line kept it; restore it here so both lines cover the same behavior. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: eriknordmark <erik@zededa.com>
domainmgr asks the hypervisor to shut a domain down and then polls Info() until it reports the domain gone, before creating any replacement. Stop() dropped the vmiList entry Info() needs in order to read the guest's phase, so after Stop every poll could only answer SCHEDULING -- never the HALTED that ends the wait. domainmgr therefore polls out the entire graceful-shutdown budget and escalates to a forced shutdown even when KubeVirt stopped the guest promptly. Leave the entry in place in Stop and remove it in Cleanup, the unconditional tail of doInactivate. Delete cannot own the removal: it only runs while DomainId is still set, which is no longer true once the domain goes away during the graceful wait, and Cleanup's IsMetaReplicaVMI path did not remove it either. The removal is deferred so a Cleanup that fails partway still releases the entry. On EVE-kvm the same sequence polls real domain state throughout, because the containerd-backed Stop it inherits keeps no such bookkeeping. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delete asked scheduledOnMe() which node the domain is placed on and then discarded its error. A failed lookup -- a failed config load, a missing node name, an unreachable client, or a ReplicaSet Get that did not answer -- reports onMe=false, which is indistinguishable from the legitimate "scheduled on another node" case tested on the next line. Delete therefore returned nil, reporting a successful teardown to domainmgr while deleting nothing, whenever the Kubernetes API was briefly unavailable. Check the error and return it, as Stop already does with the same call. domainmgr logs a Delete failure and waits for the domain regardless, so the retry behaviour is unchanged. Note this also surfaces an already-deleted ReplicaSet as a Delete error rather than a silent success, since the lookup's Get reports NotFound through the same path. That is a louder log on a teardown that had nothing left to do; softening it would need the lookup to distinguish NotFound from a genuine API failure, which it currently does not. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A failed VMIRS delete logged only the error, so an operator reading the log could not tell which app's replicaset failed to go away -- and this path fires during every teardown that races the Kubernetes API. Include the replicaset name, and drop the trailing newline that logrus adds itself. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Info confirms the replicaset exists and then fetches it again to read the guest's phase. A workload deleted between those two calls made the second lookup fail with NotFound, which Info reported as an error alongside a still-scheduling state; domainmgr's wait for the domain to disappear treats any such error as "already gone" and stops waiting. That is the same early return the graceful wait exists to prevent, reached through a narrower window. Absence is now reported identically wherever it is observed, which also drops a redundant lookup of an object already known to be gone. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5fafb53 to
d66956b
Compare
The branch carried lf-edge#6271's pinned snapshot of these tests. lf-edge#6271 is now based on lf-edge#6257 rather than pinning a copy of it, so the tests move to the version lf-edge#6257 actually ships. Test-only; no image content changes. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch carried lf-edge#6271's pinned snapshot of these tests. lf-edge#6271 is now based on lf-edge#6257 rather than pinning a copy of it, so the tests move to the version lf-edge#6257 actually ships. Test-only; no image content changes. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch carried lf-edge#6271's pinned snapshot of these tests. lf-edge#6271 is now based on lf-edge#6257 rather than pinning a copy of it, so the tests move to the version lf-edge#6257 actually ships. Test-only; no image content changes. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch carried lf-edge#6271's pinned snapshot of these tests. lf-edge#6271 is now based on lf-edge#6257 rather than pinning a copy of it, so the tests move to the version lf-edge#6257 actually ships. Test-only; no image content changes. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch carried lf-edge#6271's pinned snapshot of these tests. lf-edge#6271 is now based on lf-edge#6257 rather than pinning a copy of it, so the tests move to the version lf-edge#6257 actually ships. Test-only; no image content changes. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lf-edge#6271 has moved to d66956b and is now stacked on lf-edge#6257, carrying two commits the branch already had under its own SHAs. Records that tip, refreshes the head and commit count, and states that all seven source PRs were checked by content against the working tree rather than by commit overlap. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict repair that brought TestInfoVmirsDeletedMidCall back onto this branch kept the test body but dropped the comment explaining why absence seen at the second Get has to answer HALTED rather than fall through to SCHEDULING-with-an-error, and why that error matters to waitForDomainGone. Takes the file from lf-edge#6271's current head, which also puts the test back at its original position, so the two are now byte-identical. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
Fixes to the KubeVirt hypervisor backend, found while investigating why an app
restart on EVE-k behaves differently from the same operation on EVE-kvm.
This branch is now based on #6257 — see "PR dependencies" below. Its first two
commits are Andrew Durbin's from that PR, included verbatim (authorship preserved)
because #6257 rewrites the same
Info/StopReplicaVMIcode this PR touches, and thetwo cannot be reviewed or tested independently of each other. Review the four
kubevirt:commits here.1.
kubevirt: keep domain bookkeeping until Cleanupdomainmgrasks the hypervisor to shut a domain down and then pollsInfo()until it reports the domain gone, before creating any replacement.
kubevirtContext.Stop()dropped thevmiListentry thatInfo()resolves adomain through, so every poll after
Stopfailed.waitForDomainGonetreats afailed poll as "the domain is gone", so it returned on its first one-second tick
and cleared
DomainId.The effect is that domainmgr requests deletion of a VMIRS and creates the
replacement roughly a second later, while KubeVirt is still shutting the old VMI
down — virt-handler signals a graceful shutdown and only kills the VMI once the
grace period expires (30 s by default). On a lab device two
virt-launcherpodsfor the same app were observed running at once as a result. Clearing
DomainIdalso made the forced-shutdown escalation in
doInactivateunreachable, since itis guarded on
DomainIdbeing set.The entry now stays in place in
Stopand is removed inCleanup, theunconditional tail of
doInactivate.Deletecannot own the removal: it onlyruns while
DomainIdis still set, which is no longer true once the domain goesaway during the graceful wait, and
Cleanup'sIsMetaReplicaVMIpath did notremove it either — so dropping the
Stopremoval on its own would leak an entryon every successful graceful shutdown. The removal is deferred, since
Cleanupcan fail after the domain is already on its way out.
For contrast, the same sequence on EVE-kvm polls real domain state, backs off
1/2/4/8/16/32 s, and escalates before anything replaces the domain, because the
containerd-backed
Stopit inherits keeps no such state.2.
kubevirt: name the replicaset in the delete errorA failed VMIRS delete logged only the error, so an operator could not tell which
app's replicaset failed to go away — and this path fires during every teardown that
races the Kubernetes API. The name is now included, and the trailing newline logrus
adds itself is dropped.
An earlier version of this PR also carried
kubevirt: log a successful VMIRS delete correctly, fixingerrors.IsNotFound(nil)being false so a successful deletelogged
Stop VMI Replicaset error <nil>. That commit is gone: #6257 fixes the samebug with a three-way
switchthat additionally logs the success case, so keepingboth would have double-applied it.
3.
kubevirt: don't ignore the scheduling lookup error on DeleteDeleteaskedscheduledOnMe()which node the domain is placed on and thendiscarded its error. A failed lookup reports
onMe=false, which is indistinguishablefrom the legitimate "scheduled on another node" case tested on the next line, so
Deletereturned nil — reporting a successful teardown while deleting nothing —whenever the Kubernetes API was briefly unavailable.
Stopalready checks the sameerror. (Not every failure path is
(false, false):replicaPodScheduledOnMe's"Unhandled scheduling state" returns
scheduledOnNone=truealongside its error, so thecheck has to key on the error rather than on
scheduledOnNone.)All five
Deletecall sites in domainmgr log the error and continue (three ofthem are behind
!ctx.hvTypeKubeand unreachable on EVE-k), so no caller'scontrol flow changes. Note this also surfaces an already-deleted ReplicaSet as a
Deleteerror rather than a silent success, since the lookup'sGetreportsNotFoundthrough the same path.4.
kubevirt: treat a mid-call NotFound as goneInfoconfirms the replicaset exists and then fetches it again to read the guest'sphase. A workload deleted between those two calls made the second lookup fail with
NotFound, whichInforeported as an error alongside a still-scheduling state —and
waitForDomainGonereturns true on anyInfoerror before it inspects thestate, so the graceful wait ended early. That is the same premature "domain is
gone" commit 1 exists to prevent, reached through a one-poll race rather than on
every shutdown.
NotFoundnow reports absence identically wherever it is observed,which also drops a redundant third
Getof an object already known to be gone.Raised by @naiming-zededa in review: the concern was that keeping the
vmiListentry past
Stopwould let a deleted replicaset reachscheduledOnMeand bereported
BROKEN. #6257'sInforework removed that specific outcome, but theerror path it named was still wrong for the reason above.
Known gaps (deliberately not addressed here)
Stop'sforceparameter is still ignored. This matchesKvmContext.Stop,which also discards it (
_ bool); in both backends the actual forcing is doneby the subsequent
Delete. Now that the escalation is reachable, a stuckdomain can spend an extra
maxDelayinforce-truebeforeDeleteruns.The KVM-analogous fix would be to make kubevirt's
Deletethe forcing step(delete with
GracePeriodSeconds: 0), not to act onforceinStop.Stopstill skips the graceful wait entirely, becausedoInactivateonly callswaitForDomainGoneon the success branch.PR dependencies
Depends on #6257 (
eve-k-purge-lost-delete, draft). Its two commits —evetest: add purge tests for the kube pathandpillar: keep purges from leaving the old app generation behind— are the base of this branch, included verbatim atbe7740c59(d36df5bbeandbe7740c59) with Andrew Durbin's authorship and sign-offintact. They are not proposed as new work here.
That base builds:
go build -tags k ./...and thehypervisorandzedmanagertests areclean on it, and on this branch on top of it. #6257 has force-pushed repeatedly, and one
of its earlier tips did not compile, so this PR pins a snapshot that does rather than
tracking its head continuously. Expect a rebase when #6257 settles.
Why stacked rather than independent: #6257 rewrites
Info()to deriveDomainIdfrom alive VMIRS
Getinstead of fromvmiList, and fixes theStopReplicaVMIlogging thisPR originally fixed. The two overlap in the same functions, so they conflict textually
and cannot be tested apart. When #6257 merges, this branch should be rebased and its two
commits will drop out. #6257 force-pushes frequently, so expect re-bases.
How to test and validate this PR
Automated:
go build -tags k ./hypervisor/,go vet -tags k ./hypervisor/andgo test -tags k ./hypervisor/underpkg/pillar. Note thekbuild tag isrequired —
hypervisor/kubevirt.gois behind//go:build k, so a build withoutit does not compile this file at all.
TestCreateReplicaPodConfigfails in thatbare host run, on
mastertoo — it mocks the kubeconfig at the real/run/.kube/k3s/k3s.yaml, which a non-root user cannot create; CI runs in acontainer as root and is unaffected (#6290).
On an EVE-k device, restart or deactivate an app instance and watch
waitForDomainGonein the device log:waiting for 1sfollowed byerror info domain <name>, thena replacement VMIRS created within about a second.
state still RUNNING waited …with the delay backing off, until theVMI is actually gone (or the budget expires and the forced path runs), and no
replacement until then.
Cross-check that
kubectl get pods -n eve-kube-appnever shows twovirt-launcherpods for the same app during a restart.Result on hardware (amd64 EVE-k)
Run on a Supermicro SYS-E300-8D EVE-k node with the patched image, using the
preceding build without these commits as the control. The exercise is an
integration test that writes ~128 KB inside a VM app's guest and then restarts
the app instance; each test run does one cycle where the restart is requested
90–240 s after the write and one where it is requested in the same millisecond.
The graceful wait now happens. Time from the restart request to the app being
back online:
The ranges do not overlap. That is the intended effect:
waitForDomainGonepollsreal domain state instead of accepting the first failed
Infoas "gone", so thereplacement is no longer created about a second after the delete request.
Data survival is improved but not fixed. Only the same-millisecond cycle ever
loses the guest's write:
Why, from the device log. The wait this change enables cannot currently
succeed, because
Stopdeletes the VMIRS and the wait then polls that sameVMIRS by name:
Every tick returns
NotFound, so the state never resolves, the full 1+2+4+8+16+32= 63 s backoff runs out, and the replacement is created with the old VMI still
present — by the code's own
VMI still availableon the preceding line. Theidentical sequence appears in a cycle that kept its data (
98ea9e82…, VMIRSztest-…-98ea9-1, give-up at 04:38:42.782,doActivate20 ms later), so theoverlap is now the steady state of every EVE-k app restart on this build, and
whether the guest's last write survives is a race inside that window.
So this change converts a 1-second false "domain is gone" into a 63-second wait
that ends in the same conclusion. It removes the misleading bookkeeping and the
misleading log, and it is a prerequisite for a correct wait, but on its own it
does not stop the replacement from overlapping the old VMI. A follow-up should
make the wait poll the VMI object (which outlives its ReplicaSet) rather than the
ReplicaSet that
Stophas just removed.Also visible in both cycles, and probably worth its own fix:
kubeapi.DetachOldWorkload: a node name is required— the old workload's volumedetach does not happen.
That analysis was derived against master's
Info, which returned an error whenevervmiListhad no entry for the domain. #6257, now the base of this branch, changesexactly that:
Inforesolves existence through a live VMIRSGetand reportsHALTEDonly when the object is confirmed absent. How the graceful wait behaves on the stacked
branch therefore needs re-deriving, and I have not re-measured it on hardware since the
rebase — the numbers above are from the standalone form of these commits.
The rest of the suite was unaffected: 46 passes, plus failures this node also
produces without the change (it has no assignable audio/COM hardware).
Result in the kvm→k conversion matrix (with #5971)
Separately from the hardware run above, the two substantive commits were exercised in the
7-leg EVE-kvm→EVE-k boot-disk conversion matrix, on integration image
0.0.0-newgo-allprs2-f40d7876. That build carries #5971 (the Gokube-initdaemon)and rucoder#3 on top of the conversion chain, plus #6257, #6197, #6240, #6259 and
#6190 — so it is the combination this PR will actually ship into, not a standalone build.
7/7. Six legs green in the batch (
ext4-shrink,twodisk-ext4,twodisk-zfs,zfs-grow,ext4-toofull,zfs-notail);ext4-growlost its emulator to a QEMU AHCIabort (
prdt_warnings=4 aborts=1) while another test shared the host, and passed onre-run with
prdt_warnings=0 aborts=0. Every leg came in faster than on the two previous7/7 images of that branch line, including
twodisk-zfs, which is the historicallyfragile leg.
Scope of that evidence, precisely:
keep domain bookkeeping until Cleanupanddon't ignore the scheduling lookup error on Delete— content-identical in that image to the commits here(patch-ids
7d1df4a12b16andc21b35662d3f).name the replicaset in the delete errorortreat a mid-call NotFound as gone, both committed after that image was built.not a targeted test of the graceful-stop behaviour itself; that evidence is the
hardware section above.
Changelog notes
On EVE-k, an app restart or deactivate no longer creates the replacement while
the previous instance is still shutting down.
PR Backports
behaviour it alters only matters for EVE-k.
Checklist
And the last but not least:
check them.
Reasons for the unchecked boxes: no documentation change — this alters
bookkeeping ownership inside one hypervisor backend, with no new knob or
user-visible surface. The patched image has been run on an amd64 EVE-k device
(see the hardware result above); arm64 is untested.