This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 6518c15fbb5e372cf0e636b331699eb67248e833 Author: sarkaramrit1993 <[email protected]> AuthorDate: Mon Oct 26 06:00:13 2026 -0700 SOLR-17895: When Overseer is disabled: Remove unused lock nodes (#5161) by using modern Zookeeper "container znodes" (cherry picked from commit 0d48fb5b1afdc985f1577576d32deb3d649a418e) --- .../unreleased/SOLR-17895-lock-znode-leak.yml | 8 + .../cloud/ZkDistributedCollectionLockFactory.java | 19 +- .../cloud/ZkDistributedConfigSetLockFactory.java | 9 +- .../solr/cloud/ZkDistributedLockFactory.java | 66 ++++-- ...istributedCollectionConfigSetCommandRunner.java | 30 ++- .../apache/solr/cloud/ZkDistributedLockTest.java | 248 +++++++++++++++++++++ .../DeleteCollectionLockCleanupTest.java | 79 +++++++ .../pages/major-changes-in-solr-10.adoc | 2 + 8 files changed, 428 insertions(+), 33 deletions(-) diff --git a/changelog/unreleased/SOLR-17895-lock-znode-leak.yml b/changelog/unreleased/SOLR-17895-lock-znode-leak.yml new file mode 100644 index 00000000000..01e047ab4cd --- /dev/null +++ b/changelog/unreleased/SOLR-17895-lock-znode-leak.yml @@ -0,0 +1,8 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: With the Overseer disabled, lock nodes in ZooKeeper are now removed once no longer used. +type: fixed # added, changed, fixed, deprecated, removed, dependency_update, security, other +authors: + - name: Amrit Sarkar +links: + - name: SOLR-17895 + url: https://issues.apache.org/jira/browse/SOLR-17895 diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkDistributedCollectionLockFactory.java b/solr/core/src/java/org/apache/solr/cloud/ZkDistributedCollectionLockFactory.java index 098545b6afc..54a17b919ca 100644 --- a/solr/core/src/java/org/apache/solr/cloud/ZkDistributedCollectionLockFactory.java +++ b/solr/core/src/java/org/apache/solr/cloud/ZkDistributedCollectionLockFactory.java @@ -22,11 +22,13 @@ import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetComma import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.params.CollectionParams; +import org.apache.zookeeper.KeeperException; /** * A distributed lock implementation using Zookeeper "directory" nodes created within a collection * znode hierarchy, for use with the distributed Collection API implementation. The locks are - * implemented using ephemeral nodes placed below the "directory" nodes. + * implemented using ephemeral nodes placed below the "directory" nodes, which are container nodes + * so ZooKeeper removes them once empty. * * @see <a href="https://zookeeper.apache.org/doc/current/recipes.html#sc_recipes_Locks">Zookeeper * lock recipe</a> @@ -62,8 +64,16 @@ public class ZkDistributedCollectionLockFactory extends ZkDistributedLockFactory } /** - * Returns the Zookeeper path to the lock, creating missing nodes if needed. Note that the - * complete lock hierarchy (/locks and below) can be deleted if SolrCloud is stopped. + * Deletes the empty lock dirs left for {@code collName}. Clusters that ran older versions have + * these as persistent nodes, which ZooKeeper never removes on its own. + */ + public void deleteEmptyLockDirs(String collName) throws KeeperException, InterruptedException { + deleteEmptyDirs(getPathPrefix().append(collName).toString()); + } + + /** + * Returns the Zookeeper path to the lock. Note that the complete lock hierarchy (/locks and + * below) can be deleted if SolrCloud is stopped. * * <p>The tree of lock directories for a given collection {@code collName} is as follows: * @@ -81,8 +91,7 @@ public class ZkDistributedCollectionLockFactory extends ZkDistributedLockFactory * _replicaNameS2R2 <-- EPHEMERAL replica level locks go here * }</pre> * - * This method will create the path where the {@code EPHEMERAL} lock nodes should go. That path - * is: + * The path where the {@code EPHEMERAL} lock nodes should go is: * * <ul> * <li>For {@link org.apache.solr.common.params.CollectionParams.LockLevel#COLLECTION} - {@code diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkDistributedConfigSetLockFactory.java b/solr/core/src/java/org/apache/solr/cloud/ZkDistributedConfigSetLockFactory.java index 3458ea32215..c3da79044e7 100644 --- a/solr/core/src/java/org/apache/solr/cloud/ZkDistributedConfigSetLockFactory.java +++ b/solr/core/src/java/org/apache/solr/cloud/ZkDistributedConfigSetLockFactory.java @@ -23,7 +23,8 @@ import org.apache.solr.common.cloud.SolrZkClient; /** * A distributed lock implementation using Zookeeper "directory" nodes created within a collection * znode hierarchy, for use with the distributed Collection API implementation. The locks are - * implemented using ephemeral nodes placed below the "directory" nodes. + * implemented using ephemeral nodes placed below the "directory" nodes, which are container nodes + * so ZooKeeper removes them once empty. * * @see <a href="https://zookeeper.apache.org/doc/current/recipes.html#sc_recipes_Locks">Zookeeper * lock recipe</a> @@ -44,8 +45,8 @@ public class ZkDistributedConfigSetLockFactory extends ZkDistributedLockFactory } /** - * Returns the Zookeeper path to the lock, creating missing nodes if needed. Note that the - * complete lock hierarchy (/rootPath and below) can be deleted if SolrCloud is stopped. + * Returns the Zookeeper path to the lock. Note that the complete lock hierarchy (/rootPath and + * below) can be deleted if SolrCloud is stopped. * * <p>The tree of lock directories is very flat, given there's no real structure to what's being * locked in a config set: @@ -57,8 +58,6 @@ public class ZkDistributedConfigSetLockFactory extends ZkDistributedLockFactory * etc... * }</pre> * - * This method will create the path where the {@code EPHEMERAL} lock nodes should go. - * * <p>The returned path does not contain the separator ({@code "/"}) at the end. */ private String getLockPath(String configSetName) { diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkDistributedLockFactory.java b/solr/core/src/java/org/apache/solr/cloud/ZkDistributedLockFactory.java index 969e65a6aaa..90d475faac2 100644 --- a/solr/core/src/java/org/apache/solr/cloud/ZkDistributedLockFactory.java +++ b/solr/core/src/java/org/apache/solr/cloud/ZkDistributedLockFactory.java @@ -20,12 +20,14 @@ package org.apache.solr.cloud; import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner; import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.SolrZkClient; -import org.apache.solr.common.util.StrUtils; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.data.Stat; abstract class ZkDistributedLockFactory { + static final int MAX_CREATE_ATTEMPTS = 3; + private final SolrZkClient zkClient; private final String rootPath; @@ -37,16 +39,19 @@ abstract class ZkDistributedLockFactory { protected DistributedLock doCreateLock( boolean isWriteLock, String lockPath, String lockIdToMirror) { try { - if (StrUtils.isBlank(lockIdToMirror)) { - // TODO optimize by first attempting to create the ZkDistributedLock without calling - // makeLockPath() and only call it if the lock creation fails. This will be less costly on - // high contention (and slightly more on low contention) - makeLockPath(lockPath); + for (int attempt = 1; ; attempt++) { + try { + return isWriteLock + ? new ZkDistributedLock.Write(zkClient, lockPath, lockIdToMirror) + : new ZkDistributedLock.Read(zkClient, lockPath, lockIdToMirror); + } catch (KeeperException.NoNodeException e) { + if (attempt == MAX_CREATE_ATTEMPTS) { + throw e; + } + // Lock dirs are containers, so ZK drops them when empty. Recreate and try again. + makeLockPath(lockPath); + } } - - return isWriteLock - ? new ZkDistributedLock.Write(zkClient, lockPath, lockIdToMirror) - : new ZkDistributedLock.Read(zkClient, lockPath, lockIdToMirror); } catch (KeeperException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e); } catch (InterruptedException e) { @@ -61,13 +66,44 @@ abstract class ZkDistributedLockFactory { return path; } - private void makeLockPath(String lockNodePath) throws KeeperException, InterruptedException { + private void makeLockPath(String lockPath) throws KeeperException, InterruptedException { + if (!zkClient.exists(rootPath)) { + zkClient.makePath(rootPath, false); + } + StringBuilder path = new StringBuilder(rootPath); + for (String node : + lockPath + .substring(rootPath.length() + 1) + .split(DistributedCollectionConfigSetCommandRunner.ZK_PATH_SEPARATOR)) { + path.append(DistributedCollectionConfigSetCommandRunner.ZK_PATH_SEPARATOR).append(node); + try { + zkClient.create(path.toString(), null, CreateMode.CONTAINER); + } catch (KeeperException.NodeExistsException nee) { + // Some other thread (on this or another JVM) beat us to create the node, that's ok. + } catch (KeeperException.NoNodeException nne) { + // A parent got removed while we were here. The caller retries. + return; + } + } + } + + /** + * Deletes the empty lock dirs under {@code path}, deepest first. Lock nodes and dirs still + * holding them are left alone. + */ + protected void deleteEmptyDirs(String path) throws KeeperException, InterruptedException { + Stat stat = zkClient.exists(path, null); + if (stat == null || stat.getEphemeralOwner() != 0) { + return; + } try { - if (!zkClient.exists(lockNodePath)) { - zkClient.makePath(lockNodePath, new byte[0], CreateMode.PERSISTENT); + for (String child : zkClient.getChildren(path, null)) { + deleteEmptyDirs( + path + DistributedCollectionConfigSetCommandRunner.ZK_PATH_SEPARATOR + child); } - } catch (KeeperException.NodeExistsException nee) { - // Some other thread (on this or another JVM) beat us to create the node, that's ok. + zkClient.delete(path, -1); + } catch (KeeperException.NoNodeException | KeeperException.NotEmptyException e) { + // Already gone, or a lock is in use underneath. Either way, nothing to do. } } } diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java index 77f9481b196..b9cf8bb7eda 100644 --- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java +++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java @@ -47,6 +47,7 @@ import org.apache.solr.cloud.ZkDistributedConfigSetLockFactory; import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkNodeProps; +import org.apache.solr.common.params.CollectionParams.CollectionAction; import org.apache.solr.common.params.ConfigSetParams; import org.apache.solr.common.params.CoreAdminParams; import org.apache.solr.common.util.ExecutorUtil; @@ -75,7 +76,8 @@ public class DistributedCollectionConfigSetCommandRunner { private static final String ZK_DISTRIBUTED_API_ROOT = "/distributedapi"; /** Zookeeper node below which the locking hierarchy is anchored */ - private static final String ZK_COLLECTION_LOCKS = ZK_DISTRIBUTED_API_ROOT + "/collectionlocks"; + @VisibleForTesting + static final String ZK_COLLECTION_LOCKS = ZK_DISTRIBUTED_API_ROOT + "/collectionlocks"; private static final String ZK_CONFIG_SET_LOCKS = ZK_DISTRIBUTED_API_ROOT + "/configsetlocks"; @@ -400,10 +402,11 @@ public class DistributedCollectionConfigSetCommandRunner { // creations is low, and these classes do not hold state but only serve as an interface to // Zookeeper. Note that after this call, we MUST execute the lock.release(); in the finally // below + ZkDistributedCollectionLockFactory lockFactory = + new ZkDistributedCollectionLockFactory( + ccc.getZkStateReader().getZkClient(), ZK_COLLECTION_LOCKS); DistributedMultiLock lock = - new CollectionApiLockFactory( - new ZkDistributedCollectionLockFactory( - ccc.getZkStateReader().getZkClient(), ZK_COLLECTION_LOCKS)) + new CollectionApiLockFactory(lockFactory) .createCollectionApiLock(adminCmdContext, collName, shardId, replicaName); try { @@ -448,10 +451,6 @@ public class DistributedCollectionConfigSetCommandRunner { } } finally { try { - // TODO If the Collection API command failed because the collection does not exist, - // we've just created some lock directory structure for a non existent collection... - // Maybe try to remove it here? No big deal for now as leftover nodes in the lock - // hierarchy do no harm, and there shouldn't be too many of those. lock.release(); } catch (SolrException se) { if (log.isErrorEnabled()) { @@ -461,6 +460,9 @@ public class DistributedCollectionConfigSetCommandRunner { se); } } + if (adminCmdContext.getAction() == CollectionAction.DELETE) { + deleteEmptyLockDirs(lockFactory, collName); + } } } catch (Exception e) { if (e instanceof InterruptedException) { @@ -477,6 +479,18 @@ public class DistributedCollectionConfigSetCommandRunner { } } + private static void deleteEmptyLockDirs( + ZkDistributedCollectionLockFactory lockFactory, String collName) { + try { + lockFactory.deleteEmptyLockDirs(collName); + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + log.warn("Could not delete lock dirs of deleted collection {}", collName, e); + } + } + /** * All Config Set API commands are executed in separate threads so that the command can run to * completion even if the client request times out.<br> diff --git a/solr/core/src/test/org/apache/solr/cloud/ZkDistributedLockTest.java b/solr/core/src/test/org/apache/solr/cloud/ZkDistributedLockTest.java index e31f042bd09..8a538ccc11a 100644 --- a/solr/core/src/test/org/apache/solr/cloud/ZkDistributedLockTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/ZkDistributedLockTest.java @@ -17,11 +17,18 @@ package org.apache.solr.cloud; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.params.CollectionParams; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; import org.junit.Test; public class ZkDistributedLockTest extends SolrTestCaseJ4 { @@ -71,6 +78,247 @@ public class ZkDistributedLockTest extends SolrTestCaseJ4 { } } + /** + * Lock directories must be ZooKeeper container nodes so they get removed once empty, and a lock + * must still be obtainable after its directory was removed. + */ + @Test + public void testLockDirsAreContainers() throws Exception { + ZkTestServer server = new ZkTestServer(createTempDir("zkData")); + try { + server.run(); + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(server.getZkHost()) + .withTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + String collRoot = "/lockTestCollectionRoot"; + DistributedCollectionLockFactory collLockFactory = + new ZkDistributedCollectionLockFactory(zkClient, collRoot); + DistributedLock replicaLock = + collLockFactory.createLock( + true, + CollectionParams.LockLevel.REPLICA, + COLLECTION_NAME, + SHARD_NAME, + REPLICA_NAME, + null); + replicaLock.release(); + DistributedLock shardLock = + collLockFactory.createLock( + true, CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD_NAME, null, null); + shardLock.release(); + DistributedLock collLock = + collLockFactory.createLock( + true, CollectionParams.LockLevel.COLLECTION, COLLECTION_NAME, null, null, null); + collLock.release(); + assertLockDirsAreContainers(server, replicaLock, collRoot); + assertLockDirsAreContainers(server, shardLock, collRoot); + assertLockDirsAreContainers(server, collLock, collRoot); + + String configSetRoot = "/lockTestConfigSetRoot"; + DistributedConfigSetLockFactory configSetLockFactory = + new ZkDistributedConfigSetLockFactory(zkClient, configSetRoot); + DistributedLock configSetLock = configSetLockFactory.createLock(true, CONFIG_SET_NAME); + configSetLock.release(); + assertLockDirsAreContainers(server, configSetLock, configSetRoot); + + // Simulate ZooKeeper removing the empty containers + zkClient.clean(parent(parent(collLock.getLockId()))); + DistributedLock lock = + collLockFactory.createLock( + true, CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD_NAME, null, null); + assertTrue(lock.isAcquired()); + lock.release(); + } + } finally { + server.shutdown(); + } + } + + /** Lock dirs left as persistent nodes by older versions get removed, live locks are kept. */ + @Test + public void testDeleteEmptyLockDirs() throws Exception { + ZkTestServer server = new ZkTestServer(createTempDir("zkData")); + try { + server.run(); + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(server.getZkAddress()) + .withTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + String root = "/lockTestCollectionRoot"; + ZkDistributedCollectionLockFactory factory = + new ZkDistributedCollectionLockFactory(zkClient, root); + String collLockDir = + lockDir( + factory.createLock( + true, + CollectionParams.LockLevel.COLLECTION, + COLLECTION_NAME, + null, + null, + null)); + String replicaLockDir = + lockDir( + factory.createLock( + true, + CollectionParams.LockLevel.REPLICA, + COLLECTION_NAME, + SHARD_NAME, + REPLICA_NAME, + null)); + String shardLockDir = + lockDir( + factory.createLock( + true, + CollectionParams.LockLevel.SHARD, + COLLECTION_NAME, + SHARD_NAME, + null, + null)); + String otherCollLockDir = + lockDir( + factory.createLock( + true, CollectionParams.LockLevel.COLLECTION, "otherColl", null, null, null)); + String coll = parent(collLockDir); + zkClient.clean(root); + for (String dir : List.of(collLockDir, shardLockDir, replicaLockDir, otherCollLockDir)) { + zkClient.makePath(dir, false); + } + + DistributedLock shardLock = + factory.createLock( + true, CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD_NAME, null, null); + + factory.deleteEmptyLockDirs(COLLECTION_NAME); + assertFalse(zkClient.exists(collLockDir)); + assertFalse(zkClient.exists(replicaLockDir)); + assertTrue(zkClient.exists(shardLock.getLockId())); + assertTrue(shardLock.isAcquired()); + + shardLock.release(); + factory.deleteEmptyLockDirs(COLLECTION_NAME); + assertFalse(zkClient.exists(coll)); + assertTrue(zkClient.exists(otherCollLockDir)); + } + } finally { + server.shutdown(); + } + } + + /** A parent lock dir removed while the lock path is being created must not fail the lock. */ + @Test + public void testParentRemovedWhileCreatingLockPath() throws Exception { + ZkTestServer server = new ZkTestServer(createTempDir("zkData")); + try { + server.run(); + String root = "/lockTestCollectionRoot"; + AtomicReference<String> shardLockDir = new AtomicReference<>(); + AtomicBoolean removed = new AtomicBoolean(); + try (SolrZkClient zkClient = + new SolrZkClient( + new SolrZkClient.Builder() + .withUrl(server.getZkAddress()) + .withTimeout(TIMEOUT, TimeUnit.MILLISECONDS)) { + @Override + public String create(String path, byte[] data, CreateMode createMode) + throws KeeperException, InterruptedException { + if (path.equals(shardLockDir.get()) && removed.compareAndSet(false, true)) { + delete(parent(path), -1); + } + return super.create(path, data, createMode); + } + }) { + ZkDistributedCollectionLockFactory factory = + new ZkDistributedCollectionLockFactory(zkClient, root); + shardLockDir.set( + lockDir( + factory.createLock( + true, + CollectionParams.LockLevel.SHARD, + COLLECTION_NAME, + SHARD_NAME, + null, + null))); + zkClient.clean(root); + + DistributedLock lock = + factory.createLock( + true, CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD_NAME, null, null); + assertTrue(removed.get()); + assertTrue(lock.isAcquired()); + lock.release(); + } + } finally { + server.shutdown(); + } + } + + /** Lock creation gives up once the lock dir keeps disappearing on every attempt. */ + @Test + public void testLockCreationGivesUpAfterMaxAttempts() throws Exception { + ZkTestServer server = new ZkTestServer(createTempDir("zkData")); + try { + server.run(); + AtomicInteger lockNodeCreates = new AtomicInteger(); + try (SolrZkClient zkClient = + new SolrZkClient( + new SolrZkClient.Builder() + .withUrl(server.getZkAddress()) + .withTimeout(TIMEOUT, TimeUnit.MILLISECONDS)) { + @Override + public String create(String path, byte[] data, CreateMode createMode) + throws KeeperException, InterruptedException { + if (createMode == CreateMode.EPHEMERAL_SEQUENTIAL + && lockNodeCreates.incrementAndGet() + <= ZkDistributedLockFactory.MAX_CREATE_ATTEMPTS) { + throw new KeeperException.NoNodeException(path); + } + return super.create(path, data, createMode); + } + }) { + ZkDistributedCollectionLockFactory factory = + new ZkDistributedCollectionLockFactory(zkClient, "/lockTestCollectionRoot"); + SolrException e = + expectThrows( + SolrException.class, + () -> + factory.createLock( + true, + CollectionParams.LockLevel.COLLECTION, + COLLECTION_NAME, + null, + null, + null)); + assertTrue(e.getCause() instanceof KeeperException.NoNodeException); + assertEquals(ZkDistributedLockFactory.MAX_CREATE_ATTEMPTS, lockNodeCreates.get()); + } + } finally { + server.shutdown(); + } + } + + /** Asserts every node between the lock and {@code root} is a container. */ + private static void assertLockDirsAreContainers( + ZkTestServer server, DistributedLock lock, String root) { + for (String dir = parent(lock.getLockId()); !dir.equals(root); dir = parent(dir)) { + assertTrue( + dir + " should be a container node", + server.getZKDatabase().getDataTree().getContainers().contains(dir)); + } + } + + /** Releases the lock and returns the dir it was created in. */ + private static String lockDir(DistributedLock lock) { + lock.release(); + return parent(lock.getLockId()); + } + + private static String parent(String path) { + return path.substring(0, path.lastIndexOf('/')); + } + private void monothreadedCollectionTests(DistributedCollectionLockFactory factory) { // Collection level locks DistributedLock collRL1 = diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/DeleteCollectionLockCleanupTest.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/DeleteCollectionLockCleanupTest.java new file mode 100644 index 00000000000..63977657a8e --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/DeleteCollectionLockCleanupTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.solr.cloud.api.collections; + +import java.util.List; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.cloud.DistributedLock; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.cloud.ZkDistributedCollectionLockFactory; +import org.apache.solr.common.cloud.SolrZkClient; +import org.apache.solr.common.params.CollectionParams.LockLevel; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * With the Overseer disabled, deleting a collection removes the persistent lock dirs that older + * versions left for it. + */ +public class DeleteCollectionLockCleanupTest extends SolrCloudTestCase { + + @BeforeClass + public static void setupCluster() throws Exception { + configureCluster(1) + .addConfig("conf", configset("cloud-minimal")) + .withOverseer(false) + .configure(); + } + + @Test + public void testDeleteRemovesLegacyLockDirs() throws Exception { + String collection = "lockCleanup"; + CollectionAdminRequest.createCollection(collection, "conf", 1, 1) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(collection, 1, 1); + String shard = getCollectionState(collection).getSlices().iterator().next().getName(); + + SolrZkClient zkClient = cluster.getZkClient(); + ZkDistributedCollectionLockFactory factory = + new ZkDistributedCollectionLockFactory( + zkClient, DistributedCollectionConfigSetCommandRunner.ZK_COLLECTION_LOCKS); + String collLockDir = + lockDir(factory.createLock(true, LockLevel.COLLECTION, collection, null, null, null)); + String shardLockDir = + lockDir(factory.createLock(true, LockLevel.SHARD, collection, shard, null, null)); + String collDir = parent(collLockDir); + + zkClient.clean(collDir); + for (String dir : List.of(collLockDir, shardLockDir)) { + zkClient.makePath(dir, false); + } + + CollectionAdminRequest.deleteCollection(collection).process(cluster.getSolrClient()); + assertFalse(collDir + " should have been removed", zkClient.exists(collDir)); + } + + private static String lockDir(DistributedLock lock) { + lock.release(); + return parent(lock.getLockId()); + } + + private static String parent(String path) { + return path.substring(0, path.lastIndexOf('/')); + } +} diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc index 37a82e00720..a98410083a7 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc @@ -237,6 +237,8 @@ All nodes in the cluster must always have a consistent understanding of the over If using the cluster property toggle, use the xref:deployment-guide:zookeeper-utilities.adoc#set-a-cluster-property[bin/solr cluster] CLI utility to set it while the cluster is offline. If using the env var; ensure each Solr node is configured to start with the setting set consistently. +NOTE: Without the Overseer, Solr keeps its Collection and ConfigSet API locks in ZooKeeper container znodes, so an external ZooKeeper ensemble must be version 3.5.1 or higher. + When the Overseer is disabled, it is nonetheless still elected, which can be influenced by node roles. If you are using any xref:configuration-guide:cluster-singleton-plugins.adoc[cluster singleton plugins], they execute on the node elected to be the Overseer.
