You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by no...@apache.org on 2022/04/13 08:47:43 UTC

[solr] branch jira/solr16146 created (now 02fa8e68021)

This is an automated email from the ASF dual-hosted git repository.

noble pushed a change to branch jira/solr16146
in repository https://gitbox.apache.org/repos/asf/solr.git


      at 02fa8e68021 Lazily load and cache node properties

This branch includes the following new commits:

     new 02fa8e68021 Lazily load and cache node properties

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[solr] 01/01: Lazily load and cache node properties

Posted by no...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

noble pushed a commit to branch jira/solr16146
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 02fa8e68021c738e4a85a622df74cda124f07f38
Author: Noble Paul <no...@gmail.com>
AuthorDate: Wed Apr 13 18:47:16 2022 +1000

    Lazily load and cache node properties
---
 .../java/org/apache/solr/cloud/ZkController.java   |  12 +-
 .../org/apache/solr/handler/StreamHandler.java     |   2 +-
 .../handler/component/HttpShardHandlerFactory.java |   2 +-
 .../routing/NodePreferenceRulesComparator.java     |  12 +-
 .../RequestReplicaListTransformerGenerator.java    |  17 +-
 .../solr/common/cloud/NodePropsProvider.java       | 106 +++++++++++
 .../solr/common/cloud/NodesSysPropsCacher.java     | 209 ---------------------
 .../solr/common/cloud/TestNodePropsProvider.java   |  56 ++++++
 8 files changed, 184 insertions(+), 232 deletions(-)

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 0778f7e9b66..3120cad4778 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
@@ -131,6 +131,7 @@ public class ZkController implements Closeable {
   private final DistributedMap overseerCompletedMap;
   private final DistributedMap overseerFailureMap;
   private final DistributedMap asyncIdsMap;
+  private final NodePropsProvider nodePropsProvider;
 
   public static final String COLLECTION_PARAM_PREFIX = "collection.";
   public static final String CONFIGNAME_PROP = "configName";
@@ -191,7 +192,6 @@ public class ZkController implements Closeable {
   private String baseURL; // example: http://127.0.0.1:54065/solr
 
   private final CloudConfig cloudConfig;
-  private final NodesSysPropsCacher sysPropsCacher;
 
   private final DistributedClusterStateUpdater distributedClusterStateUpdater;
 
@@ -501,9 +501,7 @@ public class ZkController implements Closeable {
     }
     this.overseerCollectionQueue = overseer.getCollectionQueue(zkClient);
     this.overseerConfigSetQueue = overseer.getConfigSetQueue(zkClient);
-    this.sysPropsCacher =
-        new NodesSysPropsCacher(
-            getSolrCloudManager().getNodeStateProvider(), getNodeName(), zkStateReader);
+    this.nodePropsProvider = new NodePropsProvider(cloudSolrClient, zkStateReader);
 
     assert ObjectReleaseTracker.track(this);
   }
@@ -628,8 +626,8 @@ public class ZkController implements Closeable {
     }
   }
 
-  public NodesSysPropsCacher getSysPropsCacher() {
-    return sysPropsCacher;
+  public NodePropsProvider getNodePropsProvider() {
+    return nodePropsProvider;
   }
 
   private void closeOutstandingElections(final Supplier<List<CoreDescriptor>> registerOnReconnect) {
@@ -729,7 +727,7 @@ public class ZkController implements Closeable {
 
     } finally {
 
-      sysPropsCacher.close();
+      nodePropsProvider.close();
       customThreadPool.submit(() -> IOUtils.closeQuietly(cloudSolrClient));
       customThreadPool.submit(() -> IOUtils.closeQuietly(cloudManager));
 
diff --git a/solr/core/src/java/org/apache/solr/handler/StreamHandler.java b/solr/core/src/java/org/apache/solr/handler/StreamHandler.java
index ff190ab0b40..50cd8c03fea 100644
--- a/solr/core/src/java/org/apache/solr/handler/StreamHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/StreamHandler.java
@@ -214,7 +214,7 @@ public class StreamHandler extends RequestHandlerBase
                   .toString(),
               zkController.getNodeName(),
               zkController.getBaseUrl(),
-              zkController.getSysPropsCacher());
+              zkController.getNodePropsProvider());
     } else {
       requestReplicaListTransformerGenerator = new RequestReplicaListTransformerGenerator();
     }
diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
index 275403426c5..63041bb2548 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
@@ -380,7 +380,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory
               .toString(),
           zkController.getNodeName(),
           zkController.getBaseUrl(),
-          zkController.getSysPropsCacher());
+          zkController.getNodePropsProvider());
     } else {
       return requestReplicaListTransformerGenerator.getReplicaListTransformer(params);
     }
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
index 11560817b70..6221a573787 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
@@ -23,7 +23,7 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.common.StringUtils;
-import org.apache.solr.common.cloud.NodesSysPropsCacher;
+import org.apache.solr.common.cloud.NodePropsProvider;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
@@ -40,7 +40,7 @@ import org.apache.solr.common.params.SolrParams;
  */
 public class NodePreferenceRulesComparator implements Comparator<Object> {
 
-  private final NodesSysPropsCacher sysPropsCache;
+  private final NodePropsProvider sysPropsCache;
   private final String nodeName;
   private final List<PreferenceRule> sortRules;
   private final List<PreferenceRule> preferenceRules;
@@ -60,10 +60,10 @@ public class NodePreferenceRulesComparator implements Comparator<Object> {
       final SolrParams requestParams,
       final String nodeName,
       final String localHostAddress,
-      final NodesSysPropsCacher sysPropsCache,
+      final NodePropsProvider nodePropsProvider,
       final ReplicaListTransformerFactory defaultRltFactory,
       final ReplicaListTransformerFactory stableRltFactory) {
-    this.sysPropsCache = sysPropsCache;
+    this.sysPropsCache = nodePropsProvider;
     this.preferenceRules = preferenceRules;
     this.nodeName = nodeName;
     this.localHostAddress = localHostAddress;
@@ -175,8 +175,8 @@ public class NodePreferenceRulesComparator implements Comparator<Object> {
 
     Collection<String> tags = Collections.singletonList(metricTag);
     String otherNodeName = ((Replica) o).getNodeName();
-    Map<String, Object> currentNodeMetric = sysPropsCache.getSysProps(nodeName, tags);
-    Map<String, Object> otherNodeMetric = sysPropsCache.getSysProps(otherNodeName, tags);
+    Map<String, Object> currentNodeMetric = sysPropsCache.getSystemProperties(nodeName, tags);
+    Map<String, Object> otherNodeMetric = sysPropsCache.getSystemProperties(otherNodeName, tags);
     return currentNodeMetric.equals(otherNodeMetric);
   }
 
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
index d7eb93ba245..94091bb83cc 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
@@ -22,9 +22,10 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
+
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.NodesSysPropsCacher;
+import org.apache.solr.common.cloud.NodePropsProvider;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
 import org.slf4j.Logger;
@@ -45,7 +46,7 @@ public class RequestReplicaListTransformerGenerator {
   private final String defaultShardPreferences;
   private final String nodeName;
   private final String localHostAddress;
-  private final NodesSysPropsCacher sysPropsCacher;
+  private final NodePropsProvider nodePropsProvider;
 
   public RequestReplicaListTransformerGenerator() {
     this(null);
@@ -65,8 +66,8 @@ public class RequestReplicaListTransformerGenerator {
       String defaultShardPreferences,
       String nodeName,
       String localHostAddress,
-      NodesSysPropsCacher sysPropsCacher) {
-    this(null, null, defaultShardPreferences, nodeName, localHostAddress, sysPropsCacher);
+      NodePropsProvider nodePropsProvider) {
+    this(null, null, defaultShardPreferences, nodeName, localHostAddress, nodePropsProvider);
   }
 
   public RequestReplicaListTransformerGenerator(
@@ -75,14 +76,14 @@ public class RequestReplicaListTransformerGenerator {
       String defaultShardPreferences,
       String nodeName,
       String localHostAddress,
-      NodesSysPropsCacher sysPropsCacher) {
+      NodePropsProvider nodePropsProvider) {
     this.defaultRltFactory = Objects.requireNonNullElse(defaultRltFactory, RANDOM_RLTF);
     this.stableRltFactory =
         Objects.requireNonNullElseGet(stableRltFactory, AffinityReplicaListTransformerFactory::new);
     this.defaultShardPreferences = Objects.requireNonNullElse(defaultShardPreferences, "");
     this.nodeName = nodeName;
     this.localHostAddress = localHostAddress;
-    this.sysPropsCacher = sysPropsCacher;
+    this.nodePropsProvider = nodePropsProvider;
   }
 
   public ReplicaListTransformer getReplicaListTransformer(final SolrParams requestParams) {
@@ -99,7 +100,7 @@ public class RequestReplicaListTransformerGenerator {
       String defaultShardPreferences,
       String nodeName,
       String localHostAddress,
-      NodesSysPropsCacher sysPropsCacher) {
+      NodePropsProvider nodePropsProvider) {
     defaultShardPreferences =
         Objects.requireNonNullElse(defaultShardPreferences, this.defaultShardPreferences);
     final String shardsPreferenceSpec =
@@ -115,7 +116,7 @@ public class RequestReplicaListTransformerGenerator {
               localHostAddress != null
                   ? localHostAddress
                   : this.localHostAddress, // could still be null
-              sysPropsCacher != null ? sysPropsCacher : this.sysPropsCacher, // could still be null
+                  nodePropsProvider != null ? nodePropsProvider : this.nodePropsProvider, // could still be null
               defaultRltFactory,
               stableRltFactory);
       ReplicaListTransformer baseReplicaListTransformer =
diff --git a/solr/solrj/src/java/org/apache/solr/common/cloud/NodePropsProvider.java b/solr/solrj/src/java/org/apache/solr/common/cloud/NodePropsProvider.java
new file mode 100644
index 00000000000..f7c8e1ca16f
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/NodePropsProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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.common.cloud;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.impl.CloudLegacySolrClient;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.Utils;
+
+import org.apache.solr.common.NavigableObject;
+
+/**
+ * Fetch lazily and cache a node's system properties
+ *
+ */
+public class NodePropsProvider  implements AutoCloseable {
+  private volatile boolean isClosed = false;
+  private final Map<String ,Map<String, Object>> nodeVsTagsCache = new ConcurrentHashMap<>();
+  private ZkStateReader zkStateReader;
+  private final CloudLegacySolrClient solrClient;
+
+  public NodePropsProvider(CloudLegacySolrClient solrClient, ZkStateReader zkStateReader) {
+    this.zkStateReader = zkStateReader;
+    this.solrClient = solrClient;
+    zkStateReader.registerLiveNodesListener((oldNodes, newNodes) -> {
+      for (String n : oldNodes) {
+        if(!newNodes.contains(n)) {
+          //this node has gone down, clear data
+          nodeVsTagsCache.remove(n);
+        }
+      }
+      return isClosed;
+    });
+
+  }
+
+  public Map<String, Object> getSystemProperties(String nodeName, Collection<String> tags) {
+    Map<String, Object> cached = nodeVsTagsCache.computeIfAbsent(nodeName, s -> new LinkedHashMap<>());
+    Map<String, Object> result = new LinkedHashMap<>();
+    for (String tag : tags) {
+      if (!cached.containsKey(tag)) {
+        //at least one property is missing. fetch properties from the node
+        Map<String, Object> props = fetchProps(nodeName, tags);
+        //make a copy
+        cached = new LinkedHashMap<>(cached);
+        //merge all properties
+        cached.putAll(props);
+        //update the cache with the new set of properties
+        nodeVsTagsCache.put(nodeName, cached);
+        return props;
+      } else {
+        result.put(tag, cached.get(tag));
+      }
+    }
+    return result;
+  }
+
+  private Map<String, Object> fetchProps(String nodeName, Collection<String> tags) {
+    SolrParams p = new ModifiableSolrParams();
+    StringBuilder sb = new StringBuilder(zkStateReader.getBaseUrlForNodeName(nodeName));
+    sb.append("/admin/metrics?omitHeader=true&wt=javabin");
+    LinkedHashMap<String,String> keys= new LinkedHashMap<>();
+    for (String tag : tags) {
+      String metricsKey = "solr.jvm:system.properties:"+tag;
+      keys.put(tag, metricsKey);
+      sb.append("&key=").append(metricsKey);
+    }
+
+    GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/metrics", p);
+
+    Map<String, Object> result = new LinkedHashMap<>();
+    NavigableObject response = (NavigableObject) Utils.executeGET(solrClient.getHttpClient(), sb.toString(), Utils.JAVABINCONSUMER);
+    NavigableObject metrics = (NavigableObject) response._get("metrics", Collections.emptyMap());
+    keys.forEach((tag, key) -> result.put(tag, metrics._get(key, null)));
+    return result;
+  }
+
+  @Override
+  public void close() {
+    isClosed = true;
+
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/common/cloud/NodesSysPropsCacher.java b/solr/solrj/src/java/org/apache/solr/common/cloud/NodesSysPropsCacher.java
deleted file mode 100644
index 26ef61fef86..00000000000
--- a/solr/solrj/src/java/org/apache/solr/common/cloud/NodesSysPropsCacher.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * 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.common.cloud;
-
-import static org.apache.solr.common.cloud.rule.ImplicitSnitch.SYSPROP;
-
-import java.lang.invoke.MethodHandles;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
-import org.apache.solr.client.solrj.cloud.NodeStateProvider;
-import org.apache.solr.client.solrj.routing.PreferenceRule;
-import org.apache.solr.common.SolrCloseable;
-import org.apache.solr.common.params.ShardParams;
-import org.apache.solr.common.util.CommonTestInjection;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Caching other nodes system properties. The properties that will be cached based on the value
- * define in {@link org.apache.solr.common.cloud.ZkStateReader#DEFAULT_SHARD_PREFERENCES } of {@link
- * org.apache.solr.common.cloud.ZkStateReader#CLUSTER_PROPS }. If that key does not present then
- * this cacher will do nothing.
- *
- * <p>The cache will be refresh whenever /live_nodes get changed.
- */
-public class NodesSysPropsCacher implements SolrCloseable {
-  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-  private static final int NUM_RETRY = 5;
-
-  private final AtomicBoolean isRunning = new AtomicBoolean(false);
-  private final NodeStateProvider nodeStateProvider;
-  private Map<String, String> additionalProps = CommonTestInjection.injectAdditionalProps();
-  private final String currentNode;
-  private final ConcurrentHashMap<String, Map<String, Object>> cache = new ConcurrentHashMap<>();
-  private final AtomicInteger fetchCounting = new AtomicInteger(0);
-
-  private volatile boolean isClosed;
-  private volatile Collection<String> tags = new ArrayList<>();
-
-  public NodesSysPropsCacher(
-      NodeStateProvider nodeStateProvider, String currentNode, ZkStateReader stateReader) {
-    this.nodeStateProvider = nodeStateProvider;
-    this.currentNode = currentNode;
-
-    stateReader.registerClusterPropertiesListener(
-        properties -> {
-          Collection<String> tags = new ArrayList<>();
-          String shardPreferences =
-              (String) properties.getOrDefault(ZkStateReader.DEFAULT_SHARD_PREFERENCES, "");
-          if (shardPreferences.contains(ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP)) {
-            try {
-              tags =
-                  PreferenceRule.from(shardPreferences).stream()
-                      .filter(
-                          r -> ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP.equals(r.name))
-                      .map(r -> r.value)
-                      .collect(Collectors.toSet());
-            } catch (Exception e) {
-              log.info("Error on parsing shards preference:{}", shardPreferences);
-            }
-          }
-
-          if (tags.isEmpty()) {
-            pause();
-          } else {
-            start(tags);
-            // start fetching now
-            fetchSysProps(stateReader.getClusterState().getLiveNodes());
-          }
-          return isClosed;
-        });
-
-    stateReader.registerLiveNodesListener(
-        (oldLiveNodes, newLiveNodes) -> {
-          fetchSysProps(newLiveNodes);
-          return isClosed;
-        });
-  }
-
-  private void start(Collection<String> tags) {
-    if (isClosed) return;
-    this.tags = tags;
-    isRunning.set(true);
-  }
-
-  private void fetchSysProps(Set<String> newLiveNodes) {
-    if (isRunning.get()) {
-      int fetchRound = fetchCounting.incrementAndGet();
-      // TODO smarter keeping caching entries by relying on Stat.cversion
-      cache.clear();
-      for (String node : newLiveNodes) {
-        // this might takes some times to finish, therefore if there are a latter change in listener
-        // triggering this method, skipping the old runner
-        if (isClosed && fetchRound != fetchCounting.get()) return;
-
-        if (currentNode.equals(node)) {
-          Map<String, String> props = new HashMap<>();
-          for (String tag : tags) {
-            String propName = tag.substring(SYSPROP.length());
-            if (additionalProps != null && additionalProps.containsKey(propName)) {
-              props.put(tag, additionalProps.get(propName));
-            } else {
-              props.put(tag, System.getProperty(propName));
-            }
-          }
-          cache.put(node, Collections.unmodifiableMap(props));
-        } else {
-          fetchRemoteProps(node, fetchRound);
-        }
-      }
-    }
-  }
-
-  private void fetchRemoteProps(String node, int fetchRound) {
-    for (int i = 0; i < NUM_RETRY; i++) {
-      if (isClosed && fetchRound != fetchCounting.get()) return;
-
-      try {
-        Map<String, Object> props = nodeStateProvider.getNodeValues(node, tags);
-        cache.put(node, Collections.unmodifiableMap(props));
-        break;
-      } catch (Exception e) {
-        try {
-          // 1, 4, 9
-          int backOffTime = 1000 * (i + 1);
-          backOffTime = backOffTime * backOffTime;
-          backOffTime = Math.min(10000, backOffTime);
-          Thread.sleep(backOffTime);
-        } catch (InterruptedException e1) {
-          Thread.currentThread().interrupt();
-          log.info(
-              "Exception on caching node:{} system.properties:{}, retry {}/{}",
-              node,
-              tags,
-              i + 1,
-              NUM_RETRY,
-              e); // nowarn
-          break;
-        }
-        log.info(
-            "Exception on caching node:{} system.properties:{}, retry {}/{}",
-            node,
-            tags,
-            i + 1,
-            NUM_RETRY,
-            e); // nowarn
-      }
-    }
-  }
-
-  public Map<String, Object> getSysProps(String node, Collection<String> tags) {
-    Map<String, Object> props = cache.get(node);
-    HashMap<String, Object> result = new HashMap<>();
-    if (props != null) {
-      for (String tag : tags) {
-        if (props.containsKey(tag)) {
-          result.put(tag, props.get(tag));
-        }
-      }
-    }
-    return result;
-  }
-
-  public int getCacheSize() {
-    return cache.size();
-  }
-
-  public boolean isRunning() {
-    return isRunning.get();
-  }
-
-  private void pause() {
-    isRunning.set(false);
-  }
-
-  @Override
-  public boolean isClosed() {
-    return isClosed;
-  }
-
-  @Override
-  public void close() {
-    isClosed = true;
-    pause();
-  }
-}
diff --git a/solr/solrj/src/test/org/apache/solr/common/cloud/TestNodePropsProvider.java b/solr/solrj/src/test/org/apache/solr/common/cloud/TestNodePropsProvider.java
new file mode 100644
index 00000000000..a823a1b23ab
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/common/cloud/TestNodePropsProvider.java
@@ -0,0 +1,56 @@
+/*
+ * 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.common.cloud;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.junit.Test;
+
+public class TestNodePropsProvider extends SolrCloudTestCase {
+
+  @Test
+  public void testSysProps() throws Exception {
+    System.setProperty("metricsEnabled", "true");
+    MiniSolrCloudCluster cluster =
+            configureCluster(4)
+                    .withJettyConfig(jetty -> jetty.enableV2(true))
+                    .addConfig("config", getFile("solrj/solr/collection1/conf").toPath())
+                    .configure();
+
+    System.clearProperty("metricsEnabled");
+    NodePropsProvider nodePropsProvider = cluster.getRandomJetty(random()).getCoreContainer().getZkController().getNodePropsProvider();
+
+    try {
+      for (JettySolrRunner j : cluster.getJettySolrRunners()) {
+        List<String> tags = Arrays.asList("file.encoding", "java.vm.version");
+        Map<String, Object> props = nodePropsProvider.getSystemProperties(j.getNodeName(), tags);
+        for (String tag : tags) assertNotNull(props.get(tag));
+        tags = Arrays.asList("file.encoding", "java.vm.version","os.arch" );
+        props = nodePropsProvider.getSystemProperties(j.getNodeName(), tags);
+        for (String tag : tags) assertNotNull(props.get(tag));
+      }
+    } finally {
+      cluster.shutdown();
+    }
+  }
+}