Skip to content

Commit 7e093df

Browse files
kvm: take a pool's lock before the storage pool manager's
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>
1 parent d40010d commit 7e093df

4 files changed

Lines changed: 136 additions & 15 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.hypervisor.kvm.storage;
18+
19+
import java.util.concurrent.ConcurrentHashMap;
20+
21+
/**
22+
* One monitor per storage pool uuid, shared by {@link KVMStoragePoolManager} and
23+
* {@link LibvirtStorageAdaptor} so that both take the same lock for a pool.
24+
*
25+
* The adaptor holds it across the whole of createStoragePool and deleteStoragePool. The refcount
26+
* alone cannot keep a pool alive: deleteStoragePool decides to tear the pool down when the count
27+
* reaches zero and then destroys and unmounts it, and a createStoragePool that finds the still
28+
* active pool and takes a reference in between would have it torn down underneath it.
29+
*
30+
* Whoever also takes the manager wide lock must take this one first. A teardown can sit in an
31+
* umount for as long as the storage takes to answer, and a create of that pool that waited for it
32+
* while holding the manager wide lock would hold up the creates of every other pool on the host.
33+
*
34+
* Entries are never removed, as there is one per pool the host has ever used.
35+
*/
36+
final class KVMStoragePoolLocks {
37+
private static final ConcurrentHashMap<String, Object> LOCKS = new ConcurrentHashMap<>();
38+
39+
private KVMStoragePoolLocks() {
40+
}
41+
42+
static Object get(String uuid) {
43+
return LOCKS.computeIfAbsent(uuid, k -> new Object());
44+
}
45+
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,19 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri
387387
return createStoragePool(name, host, port, path, userInfo, type, details, true);
388388
}
389389

390+
private KVMStoragePool createStoragePool(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map<String, String> details, boolean primaryStorage) {
391+
/*
392+
* Take the pool's own lock before the manager wide one. A create of a pool that is being torn
393+
* down waits here for the teardown, which can sit in an umount, and must not hold up the
394+
* creates of every other pool on the host while it does. See KVMStoragePoolLocks.
395+
*/
396+
synchronized (KVMStoragePoolLocks.get(name)) {
397+
return createStoragePoolSynchronized(name, host, port, path, userInfo, type, details, primaryStorage);
398+
}
399+
}
400+
390401
//Note: due to bug CLOUDSTACK-4459, createStoragepool can be called in parallel, so need to be synced.
391-
private synchronized KVMStoragePool createStoragePool(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map<String, String> details, boolean primaryStorage) {
402+
protected synchronized KVMStoragePool createStoragePoolSynchronized(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map<String, String> details, boolean primaryStorage) {
392403
StorageAdaptor adaptor = getStorageAdaptor(type);
393404
KVMStoragePool pool = adaptor.createStoragePool(name, host, port, path, userInfo, type, details, primaryStorage);
394405
if (pool instanceof LibvirtStoragePool) {

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

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,6 @@ 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<>();
9789

9890
private String rbdTemplateSnapName = "cloudstack-base-snap";
9991
private static final int RBD_FEATURE_LAYERING = 1;
@@ -735,13 +727,9 @@ protected boolean decStoragePoolRefCount(String uuid) {
735727
return adjustStoragePoolRefCount(uuid, -1) > 0;
736728
}
737729

738-
private static Object getStoragePoolLock(String uuid) {
739-
return storagePoolLocks.computeIfAbsent(uuid, k -> new Object());
740-
}
741-
742730
@Override
743731
public KVMStoragePool createStoragePool(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map<String, String> details, boolean isPrimaryStorage) {
744-
synchronized (getStoragePoolLock(name)) {
732+
synchronized (KVMStoragePoolLocks.get(name)) {
745733
return createStoragePoolLocked(name, host, port, path, userInfo, type, details, isPrimaryStorage);
746734
}
747735
}
@@ -918,7 +906,7 @@ private boolean destroyStoragePoolHandleException(Connect conn, String uuid)
918906

919907
@Override
920908
public boolean deleteStoragePool(String uuid) {
921-
synchronized (getStoragePoolLock(uuid)) {
909+
synchronized (KVMStoragePoolLocks.get(uuid)) {
922910
return deleteStoragePoolLocked(uuid);
923911
}
924912
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.hypervisor.kvm.storage;
18+
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.ArgumentMatchers.anyBoolean;
21+
import static org.mockito.ArgumentMatchers.anyInt;
22+
import static org.mockito.ArgumentMatchers.eq;
23+
24+
import java.util.UUID;
25+
import java.util.concurrent.CountDownLatch;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
29+
import org.junit.Assert;
30+
import org.junit.Test;
31+
import org.mockito.Mockito;
32+
33+
import com.cloud.storage.Storage.StoragePoolType;
34+
35+
public class KVMStoragePoolManagerTest {
36+
37+
@Test(timeout = 60000)
38+
public void testCreateWaitingForPoolLockDoesNotHoldManagerLock() throws Exception {
39+
/*
40+
* A teardown of a pool holds that pool's lock, and can sit in an umount for as long as the
41+
* storage takes to answer. A create of the same pool has to wait for it, but must do so
42+
* without holding the manager wide lock, or the creates of every other pool on the host wait
43+
* behind the umount too.
44+
*/
45+
final KVMStoragePoolManager manager = Mockito.mock(KVMStoragePoolManager.class, Mockito.CALLS_REAL_METHODS);
46+
final KVMStoragePool pool = Mockito.mock(KVMStoragePool.class);
47+
final String uuid = String.valueOf(UUID.randomUUID());
48+
Mockito.doReturn(pool).when(manager).createStoragePoolSynchronized(eq(uuid), any(), anyInt(), any(), any(), any(), any(), anyBoolean());
49+
50+
final AtomicReference<KVMStoragePool> created = new AtomicReference<>();
51+
final Thread create;
52+
synchronized (KVMStoragePoolLocks.get(uuid)) {
53+
// the teardown in progress
54+
create = new Thread(() -> created.set(manager.createStoragePool(uuid, "127.0.0.1", 0, "/export/primary",
55+
null, StoragePoolType.NetworkFilesystem)));
56+
create.start();
57+
while (create.getState() != Thread.State.BLOCKED) {
58+
Assert.assertTrue("create finished while the pool's lock was held", create.isAlive());
59+
Thread.sleep(10);
60+
}
61+
62+
final CountDownLatch managerLockTaken = new CountDownLatch(1);
63+
final Thread otherPool = new Thread(() -> {
64+
synchronized (manager) {
65+
managerLockTaken.countDown();
66+
}
67+
});
68+
otherPool.start();
69+
Assert.assertTrue("the manager wide lock was held by a create waiting for its pool's lock",
70+
managerLockTaken.await(30, TimeUnit.SECONDS));
71+
Assert.assertNull(created.get());
72+
}
73+
74+
create.join(30000);
75+
Assert.assertSame(pool, created.get());
76+
}
77+
}

0 commit comments

Comments
 (0)