| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Codecov Report❌ Patch coverage is 70.00000% with 9 lines in your changes missing coverage. Please review. @@ Coverage Diff @@
## 4.22 #14189 +/- ##
=========================================
Coverage 18.13% 18.13%
- Complexity 16445 16455 +10
=========================================
Files 5939 5940 +1
Lines 537306 537319 +13
Branches 65867 65868 +1
=========================================
+ Hits 97439 97469 +30
+ Misses 428749 428729 -20
- Partials 11118 11121 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness.
|
Sorry, something went wrong.
|
@vladimirpetrov @sureshanaparti could you take a look at this one? We'd like this fix to make it into the upcoming 4.22.2 release. |
Sorry, something went wrong.
There was a problem hiding this comment.
thanks @bhouse-nexthop, fix looks right. CLGTM
Sorry, something went wrong.
Two bugs in the storage pool teardown path. adjustStoragePoolRefCount() means to lock on the single String instance held as the map key, so that all callers share a monitor. When the map has no entry for the pool, orElse(uuid) returns the caller's own instance instead and the synchronized block guards nothing. Increments cannot race each other: they happen inside createStoragePool, which is only reached through KVMStoragePoolManager.createStoragePool, and that is synchronized. Decrements are not covered, because KVMStoragePoolManager.deleteStoragePool is not. So an increment and a decrement can run at the same time, and while the map has no entry for the pool they take different monitors and the decrement's remove() can erase the increment. The count then reaches zero while the pool is still in use. Use ConcurrentHashMap.compute(), which is atomic for the key. deleteStoragePool() decided whether the retried umount had worked from the return of runSimpleBashScript(), which is null both when the command fails, because runScript() discards the output on a non-zero exit, and when it succeeds without printing anything. A failed umount was therefore logged and returned as a success. Take the outcome from whether the path is still a mount point, which also covers the pool having been unmounted by something else in the meantime, and return false rather than throwing when it is still mounted: deleteStoragePool() is called from finally blocks, where a throw would discard the result of an operation that has already succeeded. Signed-off-by: Brad House <bhouse@nexthop.ai>
The refcount is atomic, but it cannot keep a pool alive on its own. deleteStoragePool() drops the last reference and then goes on to destroy and unmount the pool without holding anything. createStoragePool() is serialised by KVMStoragePoolManager, but deleteStoragePool() is not, so a create can find the still active pool and take a reference after the delete has seen the count reach zero, and the delete then tears the pool down underneath it. Take a monitor per pool uuid for the whole of createStoragePool() and deleteStoragePool(). Every delete path ends in deleteStoragePool(String), including LibvirtStoragePool.delete(), so locking there covers callers that bypass the manager. Pools other than the one being deleted are not held up, which matters because a delete can sit in the five second umount retry. Signed-off-by: Brad House <bhouse@nexthop.ai>
The previous commit took the per-pool lock inside LibvirtStorageAdaptor.createStoragePool(), which is only reached through KVMStoragePoolManager.createStoragePool(), and that is synchronized on the manager. A create of a pool that is being torn down therefore waited for the teardown while holding the manager wide lock. The umount retry in deleteStoragePool() runs with no timeout, so a umount hung on storage that has gone away held up the creates of every pool on the host, not just the one being deleted. Take the pool's lock in the manager first, before its own monitor, so a create waiting for a teardown holds nothing that other pools need. The locks move to KVMStoragePoolLocks so the manager and the adaptor share them; the adaptor still takes the same lock, reentrantly on the create path and on its own for deletes, including LibvirtStoragePool.delete(), which does not go through the manager. Signed-off-by: Brad House <bhouse@nexthop.ai>
|
@DaanHoogland @weizhouapache could one of you review this for 4.22.2? @Damans227 approved it on 10-07 and I rebased it onto current 4.22 today; it still needs a committer review and a test run. I understand 4.22.2 is due out late this week or early next. |
Sorry, something went wrong.
|
@DaanHoogland a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress. |
Sorry, something went wrong.
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19686 |
Sorry, something went wrong.
|
@bhouse-nexthop , your findings are valueable and you fixes look sound; my only worry is the use of mountpoint as system executable. I might be seeing bears on the road here, but is it available on all supported platforms? |
Sorry, something went wrong.
|
@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests |
Sorry, something went wrong.
|
@DaanHoogland thanks. It's a fair thing to check, but this doesn't add a new dependency. LibvirtStorageAdaptor already runs mountpoint -q in two places on 4.22. checkNetfsStoragePoolMounted() and the stale-mount check in createNetfsStoragePool() both came in with #8887 in 4.19.1, and this change reuses the same call. mountpoint is part of util-linux, which is a base package on every distro the KVM agent supports (EL8/9/10, Ubuntu/Debian, SUSE). It's the same package that provides umount itself. One thing to note: if the binary were ever missing, the shell would return 127 and the new check would read the pool as unmounted. That failure would be easy to spot, though. With mountpoint missing, checkNetfsStoragePoolMounted() would already fail every NFS pool create on that host, so the agent couldn't use NFS primary storage at all. |
Sorry, something went wrong.
There was a problem hiding this comment.
clgtm
Sorry, something went wrong.
|
[SF] Trillian test result (tid-17224)
|
Sorry, something went wrong.
|
Hi @bhouse-nexthop , I went through PR #14189 and noticed the two follow-up issues you mentioned:
I'd be interested in investigating the second issue first, particularly adding regression tests for the exception-handling and reference count cleanup paths. Has this issue already been tracked or addressed separately? If not, would you be comfortable with me investigating it and submitting a separate fix? Thanks! |
Sorry, something went wrong.
Test report: storage pool refcount and the false umount success, on a KVM zoneEnvironment
1. A busy mount is reported as a failed unmount
2. Parallel first copies: no busy unmounts, no leaked mounts Two rounds of parallel deploys, each from its own fresh template: 8 in the first round and 7 in the second. With nothing holding the mounts:
Not covered: the lock-stall check. The jstack snapshots I took during the rounds are unusable because of a mistake in my capture script, so I make no claim about cross-pool lock waits. The PR's unit tests cover the locking. Result: pass.
|
Sorry, something went wrong.
|
Hi @bhouse-nexthop , @DaanHoogland I investigated the separate storage pool refcount leak mentioned in your PR description and have submitted a follow-up PR to address it: #14694. The fix ensures that the reference count is decremented if storage pool creation fails after the increment, including failures that throw CloudRuntimeException. I've also added a regression test covering the getStoragePool() failure case. This change is intentionally scoped to the refcount leak and does not modify the race or unmount fixes in your PR. Thanks for highlighting this issue! |
Sorry, something went wrong.
|
@dheeraj12347 thanks for following up on the refcount leak. I left some notes on #14694: the fix looks right, but as written it depends on this PR's createStoragePoolLocked and the now-protected ref count methods, so it doesn't compile against 4.22 on its own. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Description
Two bugs in the KVM agent's storage pool teardown path. They are independent but they show up together, so they are fixed together.
Bug 1 - the storage pool refcount is not thread safe
The lock is meant to be the one String instance held as the map key, so that all callers share a monitor. When the map has no entry for the pool, orElse(uuid) hands back the caller's own String instead. Two threads in that state synchronize on two different objects and the block guards nothing.
Which callers can actually collide:
So two increments never race. The reachable losing interleaving is an increment against a decrement while the map has no entry for the pool: they take different monitors, and the decrement's remove() erases the increment. The count then reaches zero while the pool is still in use.
Observed on one KVM host over one day:
All 22 failed. If the count were right, reaching zero would mean nothing holds the mount and the unmount would succeed. They also arrive in bursts, four threads tearing down the same pool inside 20 seconds:
Bug 2 - a failed umount is logged and returned as success
Script.runScript() returns null in two different situations:
umount prints nothing on success, so result is always null and this branch always reported success. From an agent log, 28 ms apart:
Fix
The umount is still run through runSimpleBashScript(), because that is what logs the reason the umount failed (device is busy and so on) and that line is the useful diagnostic. Only the decision moved: mountpoint -q says whether the pool is actually unmounted. That also makes "something else already unmounted it" a success rather than a failure.
deleteStoragePool() returns false on a genuine failure rather than throwing. It is called from finally blocks in LibvirtCopyVolumeCommandWrapper, LibvirtPrimaryStorageDownloadCommandWrapper and LibvirtComputingResource.templateToPrimaryDownload, and a throw from a finally discards the result of the operation that just succeeded. The only caller that reads the returned boolean is LibvirtModifyStoragePoolCommandWrapper, which uses the overload that takes pool details and is not on this path.
compute() also removes the entry by returning null, so the map still drops pools that are no longer in use. The behaviour seen by callers is unchanged: decStoragePoolRefCount() still reports whether the pool is still in use.
incStoragePoolRefCount and decStoragePoolRefCount become protected so the behaviour can be tested. They already are protected on main.
What this does not fix
Two related problems in the same path are left alone, to keep this change small. Both are worth fixing separately.
The decrement is not atomic with the teardown. compute() makes each adjustment atomic; it does not make "decrement reached zero" atomic with the unmount that follows it. Between decStoragePoolRefCount() returning false and destroyStoragePool() running, there is a libvirt connect and a secret lookup, and another thread can take the pool again in that window. That still ends in device is busy.
A refcount leak in createStoragePool. incStoragePoolRefCount is inside a try whose only catch is LibvirtException, but checkNetfsStoragePoolMounted and getStoragePool both throw CloudRuntimeException. On that path the compensating decrement is skipped and the count leaks by one, permanently, so the pool is never unmounted. This is the opposite failure to the one fixed here.
Types of changes
How Has This Been Tested?
Added testStoragePoolRefCountCountsEveryConcurrentIncrement to LibvirtStorageAdaptorTest.
What it does, per round:
It runs 500 rounds, because the window only exists while the map has no entry for the pool. Once an entry is there, the key set lookup does find a shared instance and the lock works.
Results:
It is a probabilistic detector, not a deterministic one. It needs real parallelism, so its power depends on the runner: it caught the bug on every run on 6 or more cores, around 6 runs in 10 on 4 cores, and not at all on 2 cores. It never fails with the fix applied.
The test uses a plain LibvirtStorageAdaptor rather than the class's shared Mockito @Spy, because routing 16,000 concurrent calls through Mockito's invocation recorder adds a lock of its own that could mask the race being tested.
Full KVM plugin test suite on this branch: 677 tests, 0 failures, 1 skipped.