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:44 UTC

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

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();
+    }
+  }
+}