Skip to content

Commit 291771d

Browse files
kvm: hold a per-pool lock across storage pool create and delete
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>
1 parent ac5e398 commit 291771d

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
8686
private String _mountPoint = "/mnt";
8787
private String _manageSnapshotPath;
8888
private static final ConcurrentHashMap<String, Integer> storagePoolRefCounts = new ConcurrentHashMap<>();
89+
/*
90+
* One monitor per pool, held across the whole of createStoragePool and deleteStoragePool. The
91+
* refcount alone cannot keep a pool alive: deleteStoragePool decides to tear the pool down when
92+
* the count reaches zero and then destroys and unmounts it, and a createStoragePool that finds the
93+
* still active pool and takes a reference in between would have it torn down underneath it.
94+
* Entries are never removed, as there is one per pool the host has ever used.
95+
*/
96+
private static final ConcurrentHashMap<String, Object> storagePoolLocks = new ConcurrentHashMap<>();
8997

9098
private String rbdTemplateSnapName = "cloudstack-base-snap";
9199
private static final int RBD_FEATURE_LAYERING = 1;
@@ -808,8 +816,18 @@ protected boolean decStoragePoolRefCount(String uuid) {
808816
return adjustStoragePoolRefCount(uuid, -1) > 0;
809817
}
810818

819+
private static Object getStoragePoolLock(String uuid) {
820+
return storagePoolLocks.computeIfAbsent(uuid, k -> new Object());
821+
}
822+
811823
@Override
812824
public KVMStoragePool createStoragePool(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map<String, String> details, boolean isPrimaryStorage) {
825+
synchronized (getStoragePoolLock(name)) {
826+
return createStoragePoolLocked(name, host, port, path, userInfo, type, details, isPrimaryStorage);
827+
}
828+
}
829+
830+
protected KVMStoragePool createStoragePoolLocked(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map<String, String> details, boolean isPrimaryStorage) {
813831
logger.info("Attempting to create storage pool {} ({}) in libvirt", name, type);
814832
StoragePool sp;
815833
Connect conn;
@@ -981,6 +999,12 @@ private boolean destroyStoragePoolHandleException(Connect conn, String uuid)
981999

9821000
@Override
9831001
public boolean deleteStoragePool(String uuid) {
1002+
synchronized (getStoragePoolLock(uuid)) {
1003+
return deleteStoragePoolLocked(uuid);
1004+
}
1005+
}
1006+
1007+
protected boolean deleteStoragePoolLocked(String uuid) {
9841008
logger.info("Attempting to remove storage pool " + uuid + " from libvirt");
9851009

9861010
// decrement and check if storage pool still in use

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
package com.cloud.hypervisor.kvm.storage;
1919

20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.ArgumentMatchers.anyBoolean;
22+
import static org.mockito.ArgumentMatchers.anyInt;
2023
import static org.mockito.ArgumentMatchers.anyList;
2124
import static org.mockito.ArgumentMatchers.anyLong;
2225
import static org.mockito.ArgumentMatchers.anyString;
@@ -28,11 +31,14 @@
2831
import java.util.Map;
2932
import java.util.UUID;
3033
import java.util.concurrent.BrokenBarrierException;
34+
import java.util.concurrent.CountDownLatch;
3135
import java.util.concurrent.CyclicBarrier;
3236
import java.util.concurrent.ExecutorService;
3337
import java.util.concurrent.Executors;
3438
import java.util.concurrent.Future;
3539
import java.util.concurrent.TimeUnit;
40+
import java.util.concurrent.TimeoutException;
41+
import java.util.concurrent.atomic.AtomicBoolean;
3642

3743
import org.junit.After;
3844
import org.junit.Assert;
@@ -234,4 +240,67 @@ public void testStoragePoolRefCountCountsEveryConcurrentIncrement() throws Excep
234240
executor.shutdownNow();
235241
}
236242
}
243+
244+
/**
245+
* Holds a delete of {@code deleteUuid} part way through its teardown, then starts a create of
246+
* {@code createUuid} and reports whether that create completed while the delete was still held.
247+
*/
248+
private boolean createRanDuringDelete(String deleteUuid, String createUuid) throws Exception {
249+
final LibvirtStorageAdaptor adaptor = Mockito.spy(new LibvirtStorageAdaptor(null));
250+
final CountDownLatch deleteEntered = new CountDownLatch(1);
251+
final CountDownLatch releaseDelete = new CountDownLatch(1);
252+
final AtomicBoolean deleteFinished = new AtomicBoolean();
253+
final AtomicBoolean createRanBeforeDeleteFinished = new AtomicBoolean();
254+
255+
Mockito.doAnswer(invocation -> {
256+
deleteEntered.countDown();
257+
releaseDelete.await();
258+
deleteFinished.set(true);
259+
return true;
260+
}).when(adaptor).deleteStoragePoolLocked(deleteUuid);
261+
Mockito.doAnswer(invocation -> {
262+
createRanBeforeDeleteFinished.set(!deleteFinished.get());
263+
return mockPool;
264+
}).when(adaptor).createStoragePoolLocked(Mockito.eq(createUuid), any(), anyInt(), any(), any(), any(), any(), anyBoolean());
265+
266+
final ExecutorService executor = Executors.newFixedThreadPool(2);
267+
try {
268+
final Future<Boolean> delete = executor.submit(() -> adaptor.deleteStoragePool(deleteUuid));
269+
Assert.assertTrue("delete never started", deleteEntered.await(30, TimeUnit.SECONDS));
270+
271+
final Future<KVMStoragePool> create = executor.submit(() -> adaptor.createStoragePool(createUuid, "127.0.0.1", 0,
272+
"/export/secondary", null, Storage.StoragePoolType.NetworkFilesystem, null, false));
273+
try {
274+
create.get(2, TimeUnit.SECONDS);
275+
} catch (TimeoutException e) {
276+
// still waiting on the delete, which is what a create of the same pool must do
277+
}
278+
279+
releaseDelete.countDown();
280+
Assert.assertTrue(delete.get(30, TimeUnit.SECONDS));
281+
Assert.assertSame(mockPool, create.get(30, TimeUnit.SECONDS));
282+
return createRanBeforeDeleteFinished.get();
283+
} finally {
284+
releaseDelete.countDown();
285+
executor.shutdownNow();
286+
}
287+
}
288+
289+
@Test(timeout = 120000)
290+
public void testCreateStoragePoolWaitsForDeleteOfSamePool() throws Exception {
291+
/*
292+
* A delete that has dropped the last reference goes on to destroy and unmount the pool. A
293+
* create that found the still active pool and took a reference in between would have it torn
294+
* down underneath it, so the create has to wait for the teardown to finish.
295+
*/
296+
final String uuid = String.valueOf(UUID.randomUUID());
297+
Assert.assertFalse("create of a pool ran while that pool was being deleted",
298+
createRanDuringDelete(uuid, new String(uuid)));
299+
}
300+
301+
@Test(timeout = 120000)
302+
public void testCreateStoragePoolDoesNotWaitForDeleteOfAnotherPool() throws Exception {
303+
Assert.assertTrue("create of one pool waited for the delete of another",
304+
createRanDuringDelete(String.valueOf(UUID.randomUUID()), String.valueOf(UUID.randomUUID())));
305+
}
237306
}

0 commit comments

Comments
 (0)