This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_9x in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push: new 3e676e5eef9 SOLR-17879: Fail to start if its major version is smaller than the cluster (#3510) 3e676e5eef9 is described below commit 3e676e5eef98527821b03bbe80ba45cab0d5c66a Author: David Smiley <dsmi...@apache.org> AuthorDate: Fri Sep 5 20:41:49 2025 -0400 SOLR-17879: Fail to start if its major version is smaller than the cluster (#3510) A Solr node will now fail to start if its major.minor version (e.g. 9.10) is *lower* than that of any existing Solr node in a SolrCloud cluster (as reported by info in "live_node"). (cherry picked from commit bd767e3f744e540530abd86cba8f4a6a8a17035c) --- solr/CHANGES.txt | 3 + .../java/org/apache/solr/cloud/ZkController.java | 49 +++++++ .../org/apache/solr/cloud/ZkControllerTest.java | 151 +++++++++++++++++++++ .../solr/cloud/overseer/ZkStateReaderTest.java | 32 ++++- .../pages/major-changes-in-solr-9.adoc | 8 ++ .../apache/solr/common/cloud/ZkStateReader.java | 25 ++-- 6 files changed, 253 insertions(+), 15 deletions(-) diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 123bbfb63d3..7977e711944 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -42,6 +42,9 @@ Other Changes * SOLR-17620: SolrCloud "live_node" now has metadata: version of Solr, roles (Yuntong Qu, David Smiley) +* SOLR-17879: A Solr node will now fail to start if it's major.minor version (e.g. 9.10) is *lower* than that of any existing + Solr node in a SolrCloud cluster (as reported by info in "live_node"). (David Smiley) + ================== 9.9.1 ================== Bug Fixes --------------------- diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkController.java b/solr/core/src/java/org/apache/solr/cloud/ZkController.java index da81cc405c7..e75c6b64202 100644 --- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java +++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java @@ -107,6 +107,7 @@ import org.apache.solr.common.params.CollectionAdminParams; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.Compressor; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.ExecutorUtil; import org.apache.solr.common.util.IOUtils; import org.apache.solr.common.util.ObjectReleaseTracker; @@ -596,6 +597,53 @@ public class ZkController implements Closeable { } } + /** + * Checks version compatibility with other nodes in the cluster. Refuses to start if there's a + * major.minor version difference between our Solr version and other nodes in the cluster. Note: + * uses live nodes. + */ + private void checkClusterVersionCompatibility() throws InterruptedException, KeeperException { + Optional<SolrVersion> lowestVersion = zkStateReader.fetchLowestSolrVersion(); + if (lowestVersion.isPresent()) { + SolrVersion ourVersion = SolrVersion.LATEST; + SolrVersion clusterVersion = lowestVersion.get(); + + if (ourVersion.lessThan(clusterVersion)) { + log.warn( + "Our Solr version {} is older than cluster version {}", ourVersion, clusterVersion); + + if (EnvUtils.getPropertyAsBool("solr.cloud.downgrade.enabled", false)) { + return; + } + + // Check major version compatibility + if (ourVersion.getMajorVersion() < clusterVersion.getMajorVersion()) { + String message = + String.format( + Locale.ROOT, + "Refusing to start Solr, since our version is lower than the lowest version currently running in the cluster. " + + "Our version: %s, lowest version in cluster: %s.", + ourVersion, + clusterVersion); + throw new SolrException(ErrorCode.INVALID_STATE, message); + } + + // Check minor version compatibility within the same major version + if (ourVersion.getMajorVersion() == clusterVersion.getMajorVersion() + && ourVersion.getMinorVersion() < clusterVersion.getMinorVersion()) { + String message = + String.format( + Locale.ROOT, + "Refusing to start Solr, since our version is lower than the lowest version currently running in the cluster. " + + "Our version: %s, lowest version in cluster: %s.", + ourVersion, + clusterVersion); + throw new SolrException(ErrorCode.INVALID_STATE, message); + } + } + } + } + public CloudSolrClient getSolrClient() { return getSolrCloudManager().getSolrClient(); } @@ -983,6 +1031,7 @@ public class ZkController implements Closeable { checkForExistingEphemeralNode(); registerLiveNodesListener(); + checkClusterVersionCompatibility(); // start the overseer first as following code may need it's processing if (!zkRunOnly) { diff --git a/solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java b/solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java index cf78c8ff47b..2d45de8ebb3 100644 --- a/solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java @@ -41,6 +41,7 @@ import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.client.api.util.SolrVersion; import org.apache.solr.client.solrj.impl.Http2SolrClient; import org.apache.solr.common.MapWriter; +import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.ClusterProperties; import org.apache.solr.common.cloud.ClusterState; import org.apache.solr.common.cloud.DocCollection; @@ -66,6 +67,7 @@ import org.apache.solr.util.LogLevel; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.data.Stat; import org.hamcrest.Matchers; +import org.junit.Ignore; import org.junit.Test; @SolrTestCaseJ4.SuppressSSL @@ -516,6 +518,155 @@ public class ZkControllerTest extends SolrCloudTestCase { } } + @Test + @Ignore("Would need to disable ObjectReleaseTracker") + public void testVersionCompatibilityFailsStartup() throws Exception { + Path zkDir = createTempDir("testVersionCompatibilityFailsStartup"); + ZkTestServer server = new ZkTestServer(zkDir); + try { + server.run(); + + // Manually create a live node with a high version (99.0.0) to simulate + // a newer cluster that the current node (SolrVersion.LATEST=10.0.0) cannot join + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(server.getZkAddress()) + .withTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + + // Create cluster nodes first + ZkController.createClusterZkNodes(zkClient); + + String liveNodeName = "test_node:8983_solr"; + String liveNodePath = ZkStateReader.LIVE_NODES_ZKNODE + "/" + liveNodeName; + + // Create live node data with version 99.0.0 + Map<String, Object> liveNodeData = + Map.of(LIVE_NODE_SOLR_VERSION, "99.0.0", LIVE_NODE_NODE_NAME, liveNodeName); + byte[] data = Utils.toJSON(liveNodeData); + + // persistent since we're about to close this zkClient + zkClient.create(liveNodePath, data, CreateMode.PERSISTENT, true); + } + + // Now try to create a ZkController - this should fail due to version incompatibility + CoreContainer cc = getCoreContainer(); + try { + CloudConfig cloudConfig = new CloudConfig.CloudConfigBuilder("127.0.0.1", 8984).build(); + + SolrException exception = + expectThrows( + SolrException.class, + () -> { + var zc = new ZkController(cc, server.getZkAddress(), TIMEOUT, cloudConfig); + zc.close(); + }); + + // Verify the exception is due to version incompatibility + assertEquals( + "Expected INVALID_STATE error code", + SolrException.ErrorCode.INVALID_STATE.code, + exception.code()); + assertTrue( + "Exception message should mention refusing to start: " + exception.getMessage(), + exception.getMessage().contains("Refusing to start Solr")); + assertTrue( + "Exception message should mention minor version: " + exception.getMessage(), + exception.getMessage().contains("minor version")); + assertTrue( + "Exception message should mention our version: " + exception.getMessage(), + exception.getMessage().contains("10.0.0")); + assertTrue( + "Exception message should mention cluster version: " + exception.getMessage(), + exception.getMessage().contains("99.0.0")); + } finally { + cc.shutdown(); + } + } finally { + server.shutdown(); + } + } + + @Ignore("Would need to disable ObjectReleaseTracker") + public void testMinorVersionCompatibilityFailsStartup() throws Exception { + Path zkDir = createTempDir("testMinorVersionCompatibilityFailsStartup"); + ZkTestServer server = new ZkTestServer(zkDir); + try { + server.run(); + + // Create a higher minor version based on SolrVersion.LATEST for cluster simulation + SolrVersion currentVersion = SolrVersion.LATEST; + SolrVersion higherMinorVersion = + SolrVersion.forIntegers( + currentVersion.getMajorVersion(), + currentVersion.getMinorVersion() + 1, + currentVersion.getPatchVersion()); + + // Manually create a live node with a higher minor version to simulate + // a newer cluster that the current node (SolrVersion.LATEST) cannot join + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(server.getZkAddress()) + .withTimeout(TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + + // Create cluster nodes first + ZkController.createClusterZkNodes(zkClient); + + String liveNodeName = "test_node:8983_solr"; + String liveNodePath = ZkStateReader.LIVE_NODES_ZKNODE + "/" + liveNodeName; + + // Create live node data with higher minor version (same major, higher minor than LATEST) + Map<String, Object> liveNodeData = + Map.of( + LIVE_NODE_SOLR_VERSION, + higherMinorVersion.toString(), + LIVE_NODE_NODE_NAME, + liveNodeName); + byte[] data = Utils.toJSON(liveNodeData); + + // persistent since we're about to close this zkClient + zkClient.create(liveNodePath, data, CreateMode.PERSISTENT, true); + } + + // Now try to create a ZkController - this should fail due to minor version incompatibility + CoreContainer cc = getCoreContainer(); + try { + CloudConfig cloudConfig = new CloudConfig.CloudConfigBuilder("127.0.0.1", 8984).build(); + + SolrException exception = + expectThrows( + SolrException.class, + () -> { + var zc = new ZkController(cc, server.getZkAddress(), TIMEOUT, cloudConfig); + zc.close(); + }); + + // Verify the exception is due to minor version incompatibility + assertEquals( + "Expected INVALID_STATE error code", + SolrException.ErrorCode.INVALID_STATE.code, + exception.code()); + assertTrue( + "Exception message should mention refusing to start: " + exception.getMessage(), + exception.getMessage().contains("Refusing to start Solr")); + assertTrue( + "Exception message should mention minor version: " + exception.getMessage(), + exception.getMessage().contains("minor version")); + assertTrue( + "Exception message should mention our version: " + exception.getMessage(), + exception.getMessage().contains(currentVersion.toString())); + assertTrue( + "Exception message should mention cluster version: " + exception.getMessage(), + exception.getMessage().contains(higherMinorVersion.toString())); + } finally { + cc.shutdown(); + } + } finally { + server.shutdown(); + } + } + public void testCheckNoOldClusterstate() throws Exception { Path zkDir = createTempDir("testCheckNoOldClusterstate"); ZkTestServer server = new ZkTestServer(zkDir); diff --git a/solr/core/src/test/org/apache/solr/cloud/overseer/ZkStateReaderTest.java b/solr/core/src/test/org/apache/solr/cloud/overseer/ZkStateReaderTest.java index b9523c9b2d9..cb07e84f5b5 100644 --- a/solr/core/src/test/org/apache/solr/cloud/overseer/ZkStateReaderTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/overseer/ZkStateReaderTest.java @@ -923,8 +923,9 @@ public class ZkStateReaderTest extends SolrTestCaseJ4 { zkClient.create(livePath + "/" + node2, data2, CreateMode.EPHEMERAL, true); var lowestVersion = reader.fetchLowestSolrVersion(); + assertTrue("Expected lowest version to be present", lowestVersion.isPresent()); assertEquals( - "Expected lowest version to be 9.0.1", SolrVersion.valueOf("9.0.1"), lowestVersion); + "Expected lowest version to be 9.0.1", SolrVersion.valueOf("9.0.1"), lowestVersion.get()); } /** Test when the only live node has empty data. */ @@ -943,7 +944,9 @@ public class ZkStateReaderTest extends SolrTestCaseJ4 { String emptyNode = "empty_node"; zkClient.create(livePath + "/" + emptyNode, new byte[0], CreateMode.EPHEMERAL, true); - assertEquals("after empty node", SolrVersion.valueOf("9.9.0"), reader.fetchLowestSolrVersion()); + var lowestVersion = reader.fetchLowestSolrVersion(); + assertTrue("Expected lowest version to be present for empty node", lowestVersion.isPresent()); + assertEquals("after empty node", SolrVersion.valueOf("9.9.0"), lowestVersion.get()); } /** Test when two live nodes exist; one is blank and the other has a high version */ @@ -965,11 +968,32 @@ public class ZkStateReaderTest extends SolrTestCaseJ4 { CreateMode.EPHEMERAL, true); - assertEquals("after high node", SolrVersion.LATEST, reader.fetchLowestSolrVersion()); + var lowestVersion1 = reader.fetchLowestSolrVersion(); + assertTrue( + "Expected lowest version to be present for high version node", lowestVersion1.isPresent()); + assertEquals("after high node", SolrVersion.valueOf("888.0.0"), lowestVersion1.get()); String node2 = "node2_solr"; zkClient.create(livePath + "/" + node2, new byte[0], CreateMode.EPHEMERAL, true); - assertEquals("after empty node", SolrVersion.valueOf("9.9.0"), reader.fetchLowestSolrVersion()); + var lowestVersion2 = reader.fetchLowestSolrVersion(); + assertTrue("Expected lowest version to be present for empty node", lowestVersion2.isPresent()); + assertEquals("after empty node", SolrVersion.valueOf("9.9.0"), lowestVersion2.get()); + } + + /** Test when no live nodes exist - should return empty Optional */ + public void testFetchLowestSolrVersion_noLiveNodes() throws Exception { + SolrZkClient zkClient = fixture.zkClient; + ZkStateReader reader = fixture.reader; + String livePath = ZkStateReader.LIVE_NODES_ZKNODE; + + // Clear any existing live node children. + List<String> nodes = zkClient.getChildren(livePath, null, true); + for (String node : nodes) { + zkClient.delete(livePath + "/" + node, -1, true); + } + + var lowestVersion = reader.fetchLowestSolrVersion(); + assertFalse("Expected no lowest version when no live nodes exist", lowestVersion.isPresent()); } } diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc index a220a63b8f8..9f0e1d127c8 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc @@ -28,6 +28,14 @@ You should also consider all changes that have been made to Solr in any version A thorough review of the list in xref:major-changes-in-earlier-8-x-versions[Major Changes in Earlier 8.x Versions] as well as the {solr-javadocs}/changes/Changes.html[CHANGES.txt] in your Solr instance will help you plan your migration to Solr 9. +There is a new limitation introduced in Solr 9.10, and that which is especially relevant to Solr 10 +and beyond. +A Solr node will now fail to start if it's major.minor version (e.g. 9.10) is *lower* than that of any existing Solr node in a SolrCloud cluster (as reported by info in "live_node"). +What this +means is that Solr supports rolling _upgrades_ but not rolling _downgrades_ spanning a major version. +This compatibility safeguard can be toggled with "SOLR_CLOUD_DOWNGRADE_ENABLED". + + === Upgrade Prerequisites *Solr 9 requires Java 11 as minimum Java version and is also tested with Java 17.* diff --git a/solr/solrj-zookeeper/src/java/org/apache/solr/common/cloud/ZkStateReader.java b/solr/solrj-zookeeper/src/java/org/apache/solr/common/cloud/ZkStateReader.java index 3bff3ea21ca..24b9b1d537c 100644 --- a/solr/solrj-zookeeper/src/java/org/apache/solr/common/cloud/ZkStateReader.java +++ b/solr/solrj-zookeeper/src/java/org/apache/solr/common/cloud/ZkStateReader.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -866,23 +867,25 @@ public class ZkStateReader implements SolrCloseable { } /** - * Returns the lowest Solr version among all live nodes in the cluster. It's not greater than - * {@link SolrVersion#LATEST_STRING}. Will not return null. If older Solr nodes have joined that - * don't declare their version, the result won't be accurate, but it's at least an upper bound on - * the possible version it might be. + * Returns the lowest Solr version among all live nodes in the cluster. If older Solr nodes have + * joined that don't declare their version, the result won't be accurate, but it's at least an + * upper bound on the possible version it might be. * - * @return the lowest Solr version of the cluster; not null + * @return an Optional containing the lowest Solr version of nodes in the cluster, or empty if no + * live nodes exist or all nodes return 9.9.0 for unspecified versions */ - public SolrVersion fetchLowestSolrVersion() throws KeeperException, InterruptedException { + public Optional<SolrVersion> fetchLowestSolrVersion() + throws KeeperException, InterruptedException { List<String> liveNodeNames = zkClient.getChildren(LIVE_NODES_ZKNODE, null, true); - SolrVersion lowest = SolrVersion.LATEST; // current software + SolrVersion lowest = null; // the last version to not specify its version in live nodes final SolrVersion UNSPECIFIED_VERSION = SolrVersion.valueOf("9.9.0"); + for (String nodeName : liveNodeNames) { String path = LIVE_NODES_ZKNODE + "/" + nodeName; byte[] data = zkClient.getData(path, null, null, true); if (data == null || data.length == 0) { - return UNSPECIFIED_VERSION; + return Optional.of(UNSPECIFIED_VERSION); } @SuppressWarnings("unchecked") @@ -890,14 +893,14 @@ public class ZkStateReader implements SolrCloseable { String nodeVersionStr = (String) props.get(LIVE_NODE_SOLR_VERSION); if (nodeVersionStr == null) { // weird log.warn("No Solr version found: {}", props); - return UNSPECIFIED_VERSION; + return Optional.of(UNSPECIFIED_VERSION); } SolrVersion nodeVersion = SolrVersion.valueOf(nodeVersionStr); - if (nodeVersion.compareTo(lowest) < 0) { + if (lowest == null || nodeVersion.compareTo(lowest) < 0) { lowest = nodeVersion; } } - return lowest; + return Optional.ofNullable(lowest); } /**