Skip to content

Commit dfa2063

Browse files
QFREEEdsmiley
authored andcommitted
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]> (cherry picked from commit 52b5d10)
1 parent c5d2c46 commit dfa2063

File tree

5 files changed

+185
-2
lines changed

5 files changed

+185
-2
lines changed

solr/CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Other Changes
3838
---------------------
3939
(No changes)
4040

41+
* SOLR-17620: SolrCloud "live_node" now has metadata: version of Solr, roles (Yuntong Qu, David Smiley)
42+
4143
================== 9.9.1 ==================
4244
Bug Fixes
4345
---------------------

solr/core/src/java/org/apache/solr/cloud/ZkController.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_PROP;
2222
import static org.apache.solr.common.cloud.ZkStateReader.CORE_NODE_NAME_PROP;
2323
import static org.apache.solr.common.cloud.ZkStateReader.ELECTION_NODE_PROP;
24+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_NODE_NAME;
25+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_ROLES;
26+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_SOLR_VERSION;
2427
import static org.apache.solr.common.cloud.ZkStateReader.NODE_NAME_PROP;
2528
import static org.apache.solr.common.cloud.ZkStateReader.REJOIN_AT_HEAD_PROP;
2629
import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
@@ -39,6 +42,7 @@
3942
import java.util.Collections;
4043
import java.util.HashMap;
4144
import java.util.HashSet;
45+
import java.util.LinkedHashMap;
4246
import java.util.List;
4347
import java.util.Locale;
4448
import java.util.Map;
@@ -55,6 +59,7 @@
5559
import java.util.concurrent.TimeoutException;
5660
import java.util.concurrent.atomic.AtomicReference;
5761
import java.util.stream.Collectors;
62+
import org.apache.solr.client.api.util.SolrVersion;
5863
import org.apache.solr.client.solrj.SolrClient;
5964
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
6065
import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
@@ -1166,13 +1171,15 @@ private void createEphemeralLiveNode() throws KeeperException, InterruptedExcept
11661171

11671172
String nodeName = getNodeName();
11681173
String nodePath = ZkStateReader.LIVE_NODES_ZKNODE + "/" + nodeName;
1169-
log.info("Register node as live in ZooKeeper:{}", nodePath);
1174+
log.info("Register node as live in ZooKeeper: {}", nodePath);
11701175
Map<NodeRoles.Role, String> roles = cc.nodeRoles.getRoles();
1176+
11711177
List<Op> ops = new ArrayList<>(roles.size() + 1);
1178+
11721179
ops.add(
11731180
Op.create(
11741181
nodePath,
1175-
null,
1182+
buildLiveNodeData(),
11761183
zkClient.getZkACLProvider().getACLsToAdd(nodePath),
11771184
CreateMode.EPHEMERAL));
11781185

@@ -1189,6 +1196,17 @@ private void createEphemeralLiveNode() throws KeeperException, InterruptedExcept
11891196
zkClient.multi(ops, true);
11901197
}
11911198

1199+
private byte[] buildLiveNodeData() {
1200+
Map<String, Object> props = new LinkedHashMap<>();
1201+
props.put(LIVE_NODE_SOLR_VERSION, SolrVersion.LATEST.toString());
1202+
props.put(LIVE_NODE_NODE_NAME, getNodeName());
1203+
1204+
Map<NodeRoles.Role, String> roles = cc.nodeRoles.getRoles();
1205+
props.put(LIVE_NODE_ROLES, roles);
1206+
1207+
return Utils.toJSON(props);
1208+
}
1209+
11921210
public void removeEphemeralLiveNode() throws KeeperException, InterruptedException {
11931211
if (zkRunOnly) {
11941212
return;

solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.apache.solr.cloud;
1818

1919
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
20+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_NODE_NAME;
21+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_SOLR_VERSION;
2022
import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
2123
import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
2224
import static org.mockito.Mockito.mock;
@@ -36,6 +38,7 @@
3638
import java.util.concurrent.atomic.AtomicInteger;
3739
import java.util.concurrent.atomic.AtomicReference;
3840
import org.apache.solr.SolrTestCaseJ4;
41+
import org.apache.solr.client.api.util.SolrVersion;
3942
import org.apache.solr.client.solrj.impl.Http2SolrClient;
4043
import org.apache.solr.common.MapWriter;
4144
import org.apache.solr.common.cloud.ClusterProperties;
@@ -215,6 +218,40 @@ public void testGetHostName() throws Exception {
215218
}
216219
}
217220

221+
@LogLevel(value = "org.apache.solr.cloud=DEBUG;org.apache.solr.cloud.overseer=DEBUG")
222+
@Test
223+
@SuppressWarnings("unchecked")
224+
public void testLiveNodeDataStored() throws Exception {
225+
Path zkDir = createTempDir("testLiveNodeDataStored");
226+
ZkTestServer server = new ZkTestServer(zkDir);
227+
try {
228+
server.run();
229+
CoreContainer cc = getCoreContainer();
230+
CloudConfig cloudConfig = new CloudConfig.CloudConfigBuilder("127.0.0.1", 8983).build();
231+
232+
ZkController zkController = new ZkController(cc, server.getZkAddress(), 10000, cloudConfig);
233+
234+
String nodeName = zkController.getNodeName();
235+
String liveNodePath = ZkStateReader.LIVE_NODES_ZKNODE + "/" + nodeName;
236+
SolrZkClient zkClient = zkController.getZkClient();
237+
byte[] actualData = zkClient.getData(liveNodePath, null, null, true);
238+
239+
Map<String, Object> liveProps = (Map<String, Object>) Utils.fromJSON(actualData);
240+
String expectedSolrVersion = SolrVersion.LATEST.toString();
241+
242+
assertEquals(
243+
"Live node solrVersion incorrect",
244+
expectedSolrVersion,
245+
liveProps.get(LIVE_NODE_SOLR_VERSION));
246+
assertEquals("Live node nodeName incorrect", nodeName, liveProps.get(LIVE_NODE_NODE_NAME));
247+
248+
zkController.close();
249+
cc.shutdown();
250+
} finally {
251+
server.shutdown();
252+
}
253+
}
254+
218255
@LogLevel(value = "org.apache.solr.cloud=DEBUG;org.apache.solr.cloud.overseer=DEBUG")
219256
@Test
220257
public void testPublishAndWaitForDownStates() throws Exception {

solr/core/src/test/org/apache/solr/cloud/overseer/ZkStateReaderTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package org.apache.solr.cloud.overseer;
1818

19+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_NODE_NAME;
20+
import static org.apache.solr.common.cloud.ZkStateReader.LIVE_NODE_SOLR_VERSION;
21+
1922
import java.io.Closeable;
2023
import java.io.IOException;
2124
import java.lang.invoke.MethodHandles;
@@ -24,6 +27,7 @@
2427
import java.util.Arrays;
2528
import java.util.Collections;
2629
import java.util.HashMap;
30+
import java.util.List;
2731
import java.util.Map;
2832
import java.util.Set;
2933
import java.util.concurrent.BrokenBarrierException;
@@ -38,6 +42,7 @@
3842
import java.util.concurrent.atomic.LongAdder;
3943
import org.apache.lucene.util.IOUtils;
4044
import org.apache.solr.SolrTestCaseJ4;
45+
import org.apache.solr.client.api.util.SolrVersion;
4146
import org.apache.solr.cloud.OverseerTest;
4247
import org.apache.solr.cloud.Stats;
4348
import org.apache.solr.cloud.ZkController;
@@ -61,6 +66,7 @@
6166
import org.apache.solr.handler.admin.ConfigSetsHandler;
6267
import org.apache.solr.util.LogLevel;
6368
import org.apache.solr.util.TimeOut;
69+
import org.apache.zookeeper.CreateMode;
6470
import org.apache.zookeeper.KeeperException;
6571
import org.apache.zookeeper.data.Stat;
6672
import org.junit.After;
@@ -888,4 +894,82 @@ public void testDeletePrsCollection() throws Exception {
888894
assertTrue(prsZkNodeNotFoundExceptionThrown.get());
889895
}
890896
}
897+
898+
/** Test when two live nodes have valid SemVer strings */
899+
public void testFetchLowestSolrVersion_validNodes() throws Exception {
900+
SolrZkClient zkClient = fixture.zkClient;
901+
ZkStateReader reader = fixture.reader;
902+
String livePath = ZkStateReader.LIVE_NODES_ZKNODE;
903+
904+
// Clear any existing live node children.
905+
List<String> nodes = zkClient.getChildren(livePath, null, true);
906+
for (String node : nodes) {
907+
zkClient.delete(livePath + "/" + node, -1, true);
908+
}
909+
910+
// Create two live nodes with valid SemVer strings.
911+
String node1 = "node1_solr";
912+
Map<String, Object> props1 = new HashMap<>();
913+
props1.put(LIVE_NODE_SOLR_VERSION, "9.1.2");
914+
props1.put(LIVE_NODE_NODE_NAME, node1);
915+
byte[] data1 = Utils.toJSON(props1);
916+
zkClient.create(livePath + "/" + node1, data1, CreateMode.EPHEMERAL, true);
917+
918+
String node2 = "node2_solr";
919+
Map<String, Object> props2 = new HashMap<>();
920+
props2.put(LIVE_NODE_SOLR_VERSION, "9.0.1");
921+
props2.put(LIVE_NODE_NODE_NAME, node2);
922+
byte[] data2 = Utils.toJSON(props2);
923+
zkClient.create(livePath + "/" + node2, data2, CreateMode.EPHEMERAL, true);
924+
925+
var lowestVersion = reader.fetchLowestSolrVersion();
926+
assertEquals(
927+
"Expected lowest version to be 9.0.1", SolrVersion.valueOf("9.0.1"), lowestVersion);
928+
}
929+
930+
/** Test when the only live node has empty data. */
931+
public void testFetchLowestSolrVersion_noData() throws Exception {
932+
SolrZkClient zkClient = fixture.zkClient;
933+
ZkStateReader reader = fixture.reader;
934+
String livePath = ZkStateReader.LIVE_NODES_ZKNODE;
935+
936+
// Clear any existing live node children.
937+
List<String> nodes = zkClient.getChildren(livePath, null, true);
938+
for (String node : nodes) {
939+
zkClient.delete(livePath + "/" + node, -1, true);
940+
}
941+
942+
// Create a live node with empty data.
943+
String emptyNode = "empty_node";
944+
zkClient.create(livePath + "/" + emptyNode, new byte[0], CreateMode.EPHEMERAL, true);
945+
946+
assertEquals("after empty node", SolrVersion.valueOf("9.9.0"), reader.fetchLowestSolrVersion());
947+
}
948+
949+
/** Test when two live nodes exist; one is blank and the other has a high version */
950+
public void testFetchLowestSolrVersion_blankAndHighVersion() throws Exception {
951+
SolrZkClient zkClient = fixture.zkClient;
952+
ZkStateReader reader = fixture.reader;
953+
String livePath = ZkStateReader.LIVE_NODES_ZKNODE;
954+
955+
// Clear any existing live node children.
956+
List<String> nodes = zkClient.getChildren(livePath, null, true);
957+
for (String node : nodes) {
958+
zkClient.delete(livePath + "/" + node, -1, true);
959+
}
960+
961+
String node1 = "node1_solr";
962+
zkClient.create(
963+
livePath + "/" + node1,
964+
Utils.toJSON(Map.<String, Object>of(LIVE_NODE_SOLR_VERSION, "888.0.0")),
965+
CreateMode.EPHEMERAL,
966+
true);
967+
968+
assertEquals("after high node", SolrVersion.LATEST, reader.fetchLowestSolrVersion());
969+
970+
String node2 = "node2_solr";
971+
zkClient.create(livePath + "/" + node2, new byte[0], CreateMode.EPHEMERAL, true);
972+
973+
assertEquals("after empty node", SolrVersion.valueOf("9.9.0"), reader.fetchLowestSolrVersion());
974+
}
891975
}

solr/solrj-zookeeper/src/java/org/apache/solr/common/cloud/ZkStateReader.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.util.function.Predicate;
4747
import java.util.function.UnaryOperator;
4848
import java.util.stream.Collectors;
49+
import org.apache.solr.client.api.util.SolrVersion;
4950
import org.apache.solr.client.solrj.impl.CloudSolrClient;
5051
import org.apache.solr.client.solrj.impl.ZkClientClusterStateProvider;
5152
import org.apache.solr.common.AlreadyClosedException;
@@ -165,6 +166,12 @@ public class ZkStateReader implements SolrCloseable {
165166
public static final String SHARD_LEADERS_ZKNODE = "leaders";
166167
public static final String ELECTION_NODE = "election";
167168

169+
/** Live node JSON property keys. */
170+
public static final String LIVE_NODE_SOLR_VERSION = "solrVersion";
171+
172+
public static final String LIVE_NODE_NODE_NAME = "nodeName";
173+
public static final String LIVE_NODE_ROLES = "roles";
174+
168175
/** "Interesting" but not actively watched Collections. */
169176
private final ConcurrentHashMap<String, LazyCollectionRef> lazyCollectionStates =
170177
new ConcurrentHashMap<>();
@@ -857,6 +864,41 @@ public void removeLiveNodesListener(LiveNodesListener listener) {
857864
liveNodesListeners.remove(listener);
858865
}
859866

867+
/**
868+
* Returns the lowest Solr version among all live nodes in the cluster. It's not greater than
869+
* {@link SolrVersion#LATEST_STRING}. Will not return null. If older Solr nodes have joined that
870+
* don't declare their version, the result won't be accurate, but it's at least an upper bound on
871+
* the possible version it might be.
872+
*
873+
* @return the lowest Solr version of the cluster; not null
874+
*/
875+
public SolrVersion fetchLowestSolrVersion() throws KeeperException, InterruptedException {
876+
List<String> liveNodeNames = zkClient.getChildren(LIVE_NODES_ZKNODE, null, true);
877+
SolrVersion lowest = SolrVersion.LATEST; // current software
878+
// the last version to not specify its version in live nodes
879+
final SolrVersion UNSPECIFIED_VERSION = SolrVersion.valueOf("9.9.0");
880+
for (String nodeName : liveNodeNames) {
881+
String path = LIVE_NODES_ZKNODE + "/" + nodeName;
882+
byte[] data = zkClient.getData(path, null, null, true);
883+
if (data == null || data.length == 0) {
884+
return UNSPECIFIED_VERSION;
885+
}
886+
887+
@SuppressWarnings("unchecked")
888+
Map<String, Object> props = (Map<String, Object>) Utils.fromJSON(data);
889+
String nodeVersionStr = (String) props.get(LIVE_NODE_SOLR_VERSION);
890+
if (nodeVersionStr == null) { // weird
891+
log.warn("No Solr version found: {}", props);
892+
return UNSPECIFIED_VERSION;
893+
}
894+
SolrVersion nodeVersion = SolrVersion.valueOf(nodeVersionStr);
895+
if (nodeVersion.compareTo(lowest) < 0) {
896+
lowest = nodeVersion;
897+
}
898+
}
899+
return lowest;
900+
}
901+
860902
/**
861903
* @return information about the cluster from ZooKeeper
862904
*/

0 commit comments

Comments
 (0)