This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 52b5d10bd27 SOLR-17620: SolrCloud "liveNode" now has version & roles
(#3305)
52b5d10bd27 is described below
commit 52b5d10bd275470c159672d8c7c286d0ce37d794
Author: Yuntong Qu <[email protected]>
AuthorDate: Mon Aug 25 00:09:34 2025 -0400
SOLR-17620: SolrCloud "liveNode" now has version & roles (#3305)
And added ZkStateReader.fetchLowestSolrVersion for use in gating cluster
options.
---------
Co-authored-by: yqu63 <[email protected]>
Co-authored-by: David Smiley <[email protected]>
---
solr/CHANGES.txt | 2 +
.../java/org/apache/solr/cloud/ZkController.java | 23 +++++-
.../org/apache/solr/cloud/ZkControllerTest.java | 37 ++++++++++
.../solr/cloud/overseer/ZkStateReaderTest.java | 84 ++++++++++++++++++++++
.../apache/solr/common/cloud/ZkStateReader.java | 42 +++++++++++
5 files changed, 186 insertions(+), 2 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 1d36ccc138a..a4b6fb3cae4 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -248,6 +248,8 @@ Other Changes
* SOLR-17286: When proxying requests to another node, use Jetty HttpClient not
Apache HttpClient.
(David Smiley)
+* SOLR-17620: SolrCloud "live_node" now has metadata: version of Solr, roles
(Yuntong Qu, 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 511ee08ea48..0f4465cb867 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
@@ -19,6 +19,9 @@ package org.apache.solr.cloud;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_PROP;
import static org.apache.solr.common.cloud.ZkStateReader.CORE_NODE_NAME_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_NODE_NAME;
+import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_ROLES;
+import static
org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_SOLR_VERSION;
import static org.apache.solr.common.cloud.ZkStateReader.REJOIN_AT_HEAD_PROP;
import static org.apache.solr.common.cloud.ZkStateReader.UNSUPPORTED_SOLR_XML;
import static
org.apache.solr.common.params.CollectionParams.CollectionAction.ADDROLE;
@@ -35,6 +38,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -52,6 +56,7 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.curator.framework.api.ACLProvider;
+import org.apache.solr.client.api.util.SolrVersion;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
@@ -1185,10 +1190,13 @@ public class ZkController implements Closeable {
String nodeName = getNodeName();
String nodePath = ZkStateReader.LIVE_NODES_ZKNODE + "/" + nodeName;
- log.info("Register node as live in ZooKeeper:{}", nodePath);
+ log.info("Register node as live in ZooKeeper: {}", nodePath);
Map<NodeRoles.Role, String> roles = cc.nodeRoles.getRoles();
+
List<SolrZkClient.CuratorOpBuilder> ops = new ArrayList<>(roles.size() +
1);
- ops.add(op ->
op.create().withMode(CreateMode.EPHEMERAL).forPath(nodePath));
+
+ ops.add(
+ op -> op.create().withMode(CreateMode.EPHEMERAL).forPath(nodePath,
buildLiveNodeData()));
// Create the roles node as well
roles.forEach(
@@ -1202,6 +1210,17 @@ public class ZkController implements Closeable {
zkClient.multi(ops);
}
+ private byte[] buildLiveNodeData() {
+ Map<String, Object> props = new LinkedHashMap<>();
+ props.put(LIVE_NODE_SOLR_VERSION, SolrVersion.LATEST.toString());
+ props.put(LIVE_NODE_NODE_NAME, getNodeName());
+
+ Map<NodeRoles.Role, String> roles = cc.nodeRoles.getRoles();
+ props.put(LIVE_NODE_ROLES, roles);
+
+ return Utils.toJSON(props);
+ }
+
public void removeEphemeralLiveNode() throws KeeperException,
InterruptedException {
if (zkRunOnly) {
return;
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 fae3fd1ef07..ea1230774a4 100644
--- a/solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java
@@ -17,6 +17,8 @@
package org.apache.solr.cloud;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_NODE_NAME;
+import static
org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_SOLR_VERSION;
import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
import static
org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
import static org.mockito.Mockito.mock;
@@ -36,6 +38,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
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.cloud.ClusterProperties;
@@ -180,6 +183,40 @@ public class ZkControllerTest extends SolrCloudTestCase {
}
}
+ @LogLevel(value =
"org.apache.solr.cloud=DEBUG;org.apache.solr.cloud.overseer=DEBUG")
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testLiveNodeDataStored() throws Exception {
+ Path zkDir = createTempDir("testLiveNodeDataStored");
+ ZkTestServer server = new ZkTestServer(zkDir);
+ try {
+ server.run();
+ CoreContainer cc = getCoreContainer();
+ CloudConfig cloudConfig = new
CloudConfig.CloudConfigBuilder("127.0.0.1", 8983).build();
+
+ ZkController zkController = new ZkController(cc, server.getZkAddress(),
10000, cloudConfig);
+
+ String nodeName = zkController.getNodeName();
+ String liveNodePath = ZkStateReader.LIVE_NODES_ZKNODE + "/" + nodeName;
+ SolrZkClient zkClient = zkController.getZkClient();
+ byte[] actualData = zkClient.getData(liveNodePath, null, null, true);
+
+ Map<String, Object> liveProps = (Map<String, Object>)
Utils.fromJSON(actualData);
+ String expectedSolrVersion = SolrVersion.LATEST.toString();
+
+ assertEquals(
+ "Live node solrVersion incorrect",
+ expectedSolrVersion,
+ liveProps.get(LIVE_NODE_SOLR_VERSION));
+ assertEquals("Live node nodeName incorrect", nodeName,
liveProps.get(LIVE_NODE_NODE_NAME));
+
+ zkController.close();
+ cc.shutdown();
+ } finally {
+ server.shutdown();
+ }
+ }
+
@LogLevel(value =
"org.apache.solr.cloud=DEBUG;org.apache.solr.cloud.overseer=DEBUG")
@Test
public void testPublishAndWaitForDownStates() throws Exception {
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 4bd4524d009..b9523c9b2d9 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
@@ -16,6 +16,9 @@
*/
package org.apache.solr.cloud.overseer;
+import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_NODE_NAME;
+import static
org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_SOLR_VERSION;
+
import java.io.Closeable;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@@ -24,6 +27,7 @@ import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
@@ -38,6 +42,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.api.util.SolrVersion;
import org.apache.solr.cloud.OverseerTest;
import org.apache.solr.cloud.Stats;
import org.apache.solr.cloud.ZkController;
@@ -61,6 +66,7 @@ import org.apache.solr.common.util.ZLibCompressor;
import org.apache.solr.handler.admin.ConfigSetsHandler;
import org.apache.solr.util.LogLevel;
import org.apache.solr.util.TimeOut;
+import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
@@ -888,4 +894,82 @@ public class ZkStateReaderTest extends SolrTestCaseJ4 {
assertTrue(prsZkNodeNotFoundExceptionThrown.get());
}
}
+
+ /** Test when two live nodes have valid SemVer strings */
+ public void testFetchLowestSolrVersion_validNodes() 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);
+ }
+
+ // Create two live nodes with valid SemVer strings.
+ String node1 = "node1_solr";
+ Map<String, Object> props1 = new HashMap<>();
+ props1.put(LIVE_NODE_SOLR_VERSION, "9.1.2");
+ props1.put(LIVE_NODE_NODE_NAME, node1);
+ byte[] data1 = Utils.toJSON(props1);
+ zkClient.create(livePath + "/" + node1, data1, CreateMode.EPHEMERAL, true);
+
+ String node2 = "node2_solr";
+ Map<String, Object> props2 = new HashMap<>();
+ props2.put(LIVE_NODE_SOLR_VERSION, "9.0.1");
+ props2.put(LIVE_NODE_NODE_NAME, node2);
+ byte[] data2 = Utils.toJSON(props2);
+ zkClient.create(livePath + "/" + node2, data2, CreateMode.EPHEMERAL, true);
+
+ var lowestVersion = reader.fetchLowestSolrVersion();
+ assertEquals(
+ "Expected lowest version to be 9.0.1", SolrVersion.valueOf("9.0.1"),
lowestVersion);
+ }
+
+ /** Test when the only live node has empty data. */
+ public void testFetchLowestSolrVersion_noData() 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);
+ }
+
+ // Create a live node with empty data.
+ 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());
+ }
+
+ /** Test when two live nodes exist; one is blank and the other has a high
version */
+ public void testFetchLowestSolrVersion_blankAndHighVersion() 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);
+ }
+
+ String node1 = "node1_solr";
+ zkClient.create(
+ livePath + "/" + node1,
+ Utils.toJSON(Map.<String, Object>of(LIVE_NODE_SOLR_VERSION,
"888.0.0")),
+ CreateMode.EPHEMERAL,
+ true);
+
+ assertEquals("after high node", SolrVersion.LATEST,
reader.fetchLowestSolrVersion());
+
+ 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());
+ }
}
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 9a7e49f01e8..7d0f80379b2 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
@@ -46,6 +46,7 @@ import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
+import org.apache.solr.client.api.util.SolrVersion;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.ZkClientClusterStateProvider;
import org.apache.solr.common.AlreadyClosedException;
@@ -166,6 +167,12 @@ public class ZkStateReader implements SolrCloseable {
public static final String SHARD_LEADERS_ZKNODE = "leaders";
public static final String ELECTION_NODE = "election";
+ /** Live node JSON property keys. */
+ public static final String LIVE_NODE_SOLR_VERSION = "solrVersion";
+
+ public static final String LIVE_NODE_NODE_NAME = "nodeName";
+ public static final String LIVE_NODE_ROLES = "roles";
+
/** "Interesting" but not actively watched Collections. */
private final ConcurrentHashMap<String, LazyCollectionRef>
lazyCollectionStates =
new ConcurrentHashMap<>();
@@ -864,6 +871,41 @@ public class ZkStateReader implements SolrCloseable {
liveNodesListeners.remove(listener);
}
+ /**
+ * 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.
+ *
+ * @return the lowest Solr version of the cluster; not null
+ */
+ public SolrVersion fetchLowestSolrVersion() throws KeeperException,
InterruptedException {
+ List<String> liveNodeNames = zkClient.getChildren(LIVE_NODES_ZKNODE, null,
true);
+ SolrVersion lowest = SolrVersion.LATEST; // current software
+ // 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;
+ }
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> props = (Map<String, Object>) Utils.fromJSON(data);
+ String nodeVersionStr = (String) props.get(LIVE_NODE_SOLR_VERSION);
+ if (nodeVersionStr == null) { // weird
+ log.warn("No Solr version found: {}", props);
+ return UNSPECIFIED_VERSION;
+ }
+ SolrVersion nodeVersion = SolrVersion.valueOf(nodeVersionStr);
+ if (nodeVersion.compareTo(lowest) < 0) {
+ lowest = nodeVersion;
+ }
+ }
+ return lowest;
+ }
+
/**
* @return information about the cluster from ZooKeeper
*/