Skip to content

Commit ca1cfd5

Browse files
kvm: fix storage pool refcount race and misreported umount result
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>
1 parent 10037c8 commit ca1cfd5

2 files changed

Lines changed: 89 additions & 20 deletions

File tree

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java‎

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -700,35 +700,30 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) {
700700
* adjust refcount
701701
*/
702702
private int adjustStoragePoolRefCount(String uuid, int adjustment) {
703-
final String mutexKey = storagePoolRefCounts.keySet().stream()
704-
.filter(k -> k.equals(uuid))
705-
.findFirst()
706-
.orElse(uuid);
707-
synchronized (mutexKey) {
708-
// some access on the storagePoolRefCounts.key(mutexKey) element
709-
int refCount = storagePoolRefCounts.computeIfAbsent(mutexKey, k -> 0);
710-
refCount += adjustment;
711-
if (refCount < 1) {
712-
storagePoolRefCounts.remove(mutexKey);
713-
} else {
714-
storagePoolRefCounts.put(mutexKey, refCount);
715-
}
716-
return refCount;
717-
}
703+
/*
704+
* compute() is atomic for the key, so concurrent callers cannot lose an
705+
* update. Returning null from the remapping function removes the entry,
706+
* which keeps the map free of pools that are no longer in use.
707+
*/
708+
Integer refCount = storagePoolRefCounts.compute(uuid, (key, count) -> {
709+
int adjusted = (count == null ? 0 : count) + adjustment;
710+
return adjusted < 1 ? null : adjusted;
711+
});
712+
return refCount == null ? 0 : refCount;
718713
}
719714
/**
720715
* Thread-safe increment storage pool usage refcount
721716
* @param uuid UUID of the storage pool to increment the count
722717
*/
723-
private void incStoragePoolRefCount(String uuid) {
718+
protected void incStoragePoolRefCount(String uuid) {
724719
adjustStoragePoolRefCount(uuid, 1);
725720
}
726721
/**
727722
* Thread-safe decrement storage pool usage refcount for the given uuid and return if storage pool still in use.
728723
* @param uuid UUID of the storage pool to decrement the count
729724
* @return true if the storage pool is still used, else false.
730725
*/
731-
private boolean decStoragePoolRefCount(String uuid) {
726+
protected boolean decStoragePoolRefCount(String uuid) {
732727
return adjustStoragePoolRefCount(uuid, -1) > 0;
733728
}
734729

@@ -948,13 +943,29 @@ public boolean deleteStoragePool(String uuid) {
948943
String targetPath = _mountPoint + File.separator + uuid;
949944
logger.error("deleteStoragePool removed pool from libvirt, but libvirt had trouble unmounting the pool. Trying umount location " + targetPath +
950945
" again in a few seconds");
951-
String result = Script.runSimpleBashScript("sleep 5 && umount " + targetPath);
952-
if (result == null) {
946+
/*
947+
* runSimpleBashScript() returns null both when the command fails,
948+
* because runScript() discards the output on a non-zero exit, and
949+
* when it succeeds without printing anything. Its result therefore
950+
* cannot say whether the umount worked. It is still used to run the
951+
* umount, because it logs the failure reason, which is the useful
952+
* diagnostic, but the outcome is taken from whether the path is
953+
* still a mount point. That also covers the pool having been
954+
* unmounted by something else in the meantime.
955+
*/
956+
Script.runSimpleBashScript("sleep 5 && umount " + targetPath);
957+
if (Script.runSimpleBashScriptForExitValue("mountpoint -q " + targetPath) != 0) {
953958
logger.info("Succeeded in unmounting " + targetPath);
954959
destroyStoragePoolHandleException(conn, uuid);
955960
return true;
956961
}
957-
logger.error("Failed to unmount " + targetPath);
962+
/*
963+
* Do not throw here. deleteStoragePool() is called from finally
964+
* blocks, where a throw would discard the result of an operation
965+
* that has already succeeded.
966+
*/
967+
logger.error("Failed to unmount " + targetPath + ", it is still a mount point");
968+
return false;
958969
}
959970
throw new CloudRuntimeException(e.toString(), e);
960971
}

‎plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptorTest.java‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@
2222
import static org.mockito.ArgumentMatchers.anyString;
2323
import static org.mockito.Mockito.never;
2424

25+
import java.util.ArrayList;
2526
import java.util.HashMap;
27+
import java.util.List;
2628
import java.util.Map;
2729
import java.util.UUID;
30+
import java.util.concurrent.BrokenBarrierException;
31+
import java.util.concurrent.CyclicBarrier;
32+
import java.util.concurrent.ExecutorService;
33+
import java.util.concurrent.Executors;
34+
import java.util.concurrent.Future;
35+
import java.util.concurrent.TimeUnit;
2836

2937
import org.junit.After;
38+
import org.junit.Assert;
3039
import org.junit.Before;
3140
import org.junit.Test;
3241
import org.junit.runner.RunWith;
@@ -176,4 +185,53 @@ public void testUpdateLocalPoolIops_NullResultFromScript() {
176185

177186
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
178187
}
188+
189+
@Test(timeout = 120000)
190+
public void testStoragePoolRefCountCountsEveryConcurrentIncrement() throws Exception {
191+
final LibvirtStorageAdaptor adaptor = new LibvirtStorageAdaptor(null);
192+
final int threads = 16;
193+
final int rounds = 500;
194+
final CyclicBarrier barrier = new CyclicBarrier(threads);
195+
final ExecutorService executor = Executors.newFixedThreadPool(threads);
196+
197+
try {
198+
for (int round = 0; round < rounds; round++) {
199+
// A fresh uuid each round, so every round starts with no entry for the pool.
200+
final String uuid = String.valueOf(UUID.randomUUID());
201+
final List<Future<?>> futures = new ArrayList<>();
202+
203+
for (int i = 0; i < threads; i++) {
204+
futures.add(executor.submit(() -> {
205+
/*
206+
* Every caller arrives with its own String instance, the way the
207+
* agent does when the uuid is parsed out of a separate command
208+
* payload for each request. The instances are equal but they are
209+
* not the same object.
210+
*/
211+
final String ownInstance = new String(uuid);
212+
try {
213+
barrier.await();
214+
} catch (InterruptedException | BrokenBarrierException e) {
215+
Thread.currentThread().interrupt();
216+
throw new IllegalStateException(e);
217+
}
218+
adaptor.incStoragePoolRefCount(ownInstance);
219+
}));
220+
}
221+
for (Future<?> future : futures) {
222+
future.get(60, TimeUnit.SECONDS);
223+
}
224+
225+
// Every increment must be counted, so the pool stays in use until the last release.
226+
for (int i = 1; i < threads; i++) {
227+
Assert.assertTrue("Round " + round + ": pool should still be in use after " + i
228+
+ " of " + threads + " releases", adaptor.decStoragePoolRefCount(uuid));
229+
}
230+
Assert.assertFalse("Round " + round + ": pool should no longer be in use after the last release",
231+
adaptor.decStoragePoolRefCount(uuid));
232+
}
233+
} finally {
234+
executor.shutdownNow();
235+
}
236+
}
179237
}

0 commit comments

Comments
 (0)