You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ab...@apache.org on 2020/01/30 20:07:21 UTC

[lucene-solr] branch jira/solr-13579 updated: SOLR-13579: Fix the v2 API paths. Fix some metrics / scope refactoring bugs.

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

ab pushed a commit to branch jira/solr-13579
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/jira/solr-13579 by this push:
     new 159d5ac  SOLR-13579: Fix the v2 API paths. Fix some metrics / scope refactoring bugs.
159d5ac is described below

commit 159d5ac164e031e7d1cabd858a781c079bbf184f
Author: Andrzej Bialecki <ab...@apache.org>
AuthorDate: Thu Jan 30 21:06:42 2020 +0100

    SOLR-13579: Fix the v2 API paths. Fix some metrics / scope refactoring bugs.
---
 .../java/org/apache/solr/core/CoreContainer.java   |   2 +-
 .../apache/solr/managed/ResourceManagerAPI.java    |  46 +--
 .../apache/solr/managed/ResourceManagerPool.java   |  11 +
 .../solr/managed/types/CacheManagerPool.java       |   8 -
 .../apache/solr/metrics/SolrCoreMetricManager.java |   2 +-
 .../solr/handler/admin/MetricsHandlerTest.java     |   2 +-
 .../managed/TestResourceManagerIntegration.java    | 328 +++++++++++++++++----
 .../solr/client/solrj/impl/BaseHttpSolrClient.java |   4 +-
 8 files changed, 309 insertions(+), 94 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 6b7b0e8..55f13f2 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -629,7 +629,7 @@ public class CoreContainer {
 
     metricManager = new SolrMetricManager(loader, cfg.getMetricsConfig());
     String registryName = SolrMetricManager.getRegistryName(SolrInfoBean.Group.node);
-    solrMetricsContext = new SolrMetricsContext(metricManager, registryName, "container", metricTag);
+    solrMetricsContext = new SolrMetricsContext(metricManager, registryName, metricTag);
 
     coreContainerWorkExecutor = MetricUtils.instrumentedExecutorService(
         coreContainerWorkExecutor, null,
diff --git a/solr/core/src/java/org/apache/solr/managed/ResourceManagerAPI.java b/solr/core/src/java/org/apache/solr/managed/ResourceManagerAPI.java
index 079368f..5f18c03 100644
--- a/solr/core/src/java/org/apache/solr/managed/ResourceManagerAPI.java
+++ b/solr/core/src/java/org/apache/solr/managed/ResourceManagerAPI.java
@@ -291,7 +291,7 @@ public class ResourceManagerAPI implements SolrInfoBean {
 
   // ============= helper methods ==============
 
-  private List<ManagedComponent> getComponents(SolrQueryRequest req, SolrQueryResponse rsp, AtomicReference<ResourceManagerPool> poolRef) {
+  private List<ManagedComponent> getComponents(SolrQueryRequest req, SolrQueryResponse rsp, String componentPrefix, AtomicReference<ResourceManagerPool> poolRef) {
     String poolName = req.getPathTemplateValues().get("poolName");
     if (poolName == null || poolName.isBlank()) {
       rsp.setException(new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required pool name."));
@@ -303,7 +303,7 @@ public class ResourceManagerAPI implements SolrInfoBean {
       return null;
     }
     poolRef.set(pool);
-    String componentName = req.getPathTemplateValues().get("componentName");
+    String componentName = componentPrefix != null ? componentPrefix : req.getPathTemplateValues().get("componentName");
     NamedList<Object> result = new SimpleOrderedMap<>();
     // we support a prefix of component names because eg. searcher caches will have a quickly
     // changing unique suffix
@@ -350,10 +350,10 @@ public class ResourceManagerAPI implements SolrInfoBean {
         perPool.add("type", pool.getType());
         perPool.add("size", pool.getComponents().size());
         if (withLimits) {
-          perPool.add("poolLimits", pool.getPoolLimits());
+          perPool.add("limits", pool.getPoolLimits());
         }
         if (withParams) {
-          perPool.add("poolParams", pool.getPoolParams());
+          perPool.add("params", pool.getPoolParams());
         }
         if (withComponents) {
           perPool.add("components", new TreeSet<>(pool.getComponents().keySet()));
@@ -361,7 +361,7 @@ public class ResourceManagerAPI implements SolrInfoBean {
         if (withValues) {
           try {
             Map<String, Map<String, Object>> values = pool.getCurrentValues();
-            perPool.add("totalValues", new TreeMap<>(pool.aggregateTotalValues(values)));
+            perPool.add("values", new TreeMap<>(pool.aggregateTotalValues(values)));
           } catch (Exception e) {
             log.warn("Error getting current values from pool " + name, e);
             perPool.add("error", "Error getting current values: " + e.toString());
@@ -419,7 +419,7 @@ public class ResourceManagerAPI implements SolrInfoBean {
       NamedList<Object> result = new SimpleOrderedMap<>();
       rsp.add("result", result);
       Set<String> toDelete = new TreeSet<>();
-      if (payload.get() != null && !payload.get().isBlank()) {
+      if (payload != null && payload.get() != null && !payload.get().isBlank()) {
         toDelete.add(payload.get());
       }
       if (req.getPathTemplateValues().get("poolName") != null) {
@@ -534,8 +534,8 @@ public class ResourceManagerAPI implements SolrInfoBean {
   @EndPoint(
       method = SolrRequest.METHOD.GET,
       path = {
-          "/cluster/resources/{poolName}/components",
-          "/cluster/resources/{poolName}/components/{componentName}"
+          "/node/resources/{poolName}/components",
+          "/node/resources/{poolName}/components/{componentName}"
       },
       permission = PermissionNameProvider.Name.RESOURCE_READ_PERM
   )
@@ -544,13 +544,14 @@ public class ResourceManagerAPI implements SolrInfoBean {
     @Command()
     public void list(SolrQueryRequest req, SolrQueryResponse rsp) {
       AtomicReference<ResourceManagerPool> poolRef = new AtomicReference<>();
-      List<ManagedComponent> components = getComponents(req, rsp, poolRef);
+      List<ManagedComponent> components = getComponents(req, rsp, null, poolRef);
       if (components == null) {
         return;
       }
       ResourceManagerPool pool = poolRef.get();
       boolean withValues = req.getParams().getBool("values", false);
       boolean withLimits = req.getParams().getBool("limits", false);
+      boolean withInitialLimits = req.getParams().getBool("initialLimits", false);
       NamedList<Object> result = new SimpleOrderedMap<>();
       rsp.add("result", result);
       components.forEach(component -> {
@@ -559,15 +560,18 @@ public class ResourceManagerAPI implements SolrInfoBean {
         perComponent.add("class", component.getClass().getName());
         if (withLimits) {
           try {
-            perComponent.add("resourceLimits", new TreeMap<>(pool.getResourceLimits(component)));
+            perComponent.add("limits", new TreeMap<>(pool.getResourceLimits(component)));
           } catch (Exception e) {
             log.warn("Error getting resource limits of " + component.getManagedComponentId(), e);
             result.add("error", "Error getting resource limits of " + component.getManagedComponentId() + ": " + e.toString());
           }
         }
+        if (withInitialLimits) {
+          perComponent.add("initialLimits", new TreeMap<>(pool.getInitialResourceLimits(component.getManagedComponentId().toString())));
+        }
         if (withValues) {
           try {
-            perComponent.add("monitoredValues", new TreeMap<>(pool.getMonitoredValues(component)));
+            perComponent.add("values", new TreeMap<>(pool.getMonitoredValues(component)));
           } catch (Exception e) {
             log.warn("Error getting monitored values of " + component.getManagedComponentId() + "/" + pool.getName() + " : " + e.toString(), e);
             perComponent.add("error", "Error getting monitored values of " + component.getManagedComponentId() + ": " + e.toString());
@@ -581,26 +585,28 @@ public class ResourceManagerAPI implements SolrInfoBean {
   @EndPoint(
       method = SolrRequest.METHOD.POST,
       path = {
-          "/cluster/resources/{poolName}/components",
-          "/cluster/resources/{poolName}/components/{componentName}"
+          "/node/resources/{poolName}/components",
+          "/node/resources/{poolName}/components/{componentName}"
       },
       permission = PermissionNameProvider.Name.RESOURCE_WRITE_PERM
   )
   public class EditComponentAPI {
 
     @Command(name = "setlimits")
-    public void setLimits(SolrQueryRequest req, SolrQueryResponse rsp, PayloadObj<String> payload) {
+    public void setLimits(SolrQueryRequest req, SolrQueryResponse rsp, PayloadObj<Map<String, Object>> payload) {
       if (!checkEnabled(payload)) return;
       AtomicReference<ResourceManagerPool> poolRef = new AtomicReference<>();
-      List<ManagedComponent> components = getComponents(req, rsp, poolRef);
+      List<ManagedComponent> components = getComponents(req, rsp, null, poolRef);
       if (components == null) {
         return;
       }
       NamedList<Object> result = new SimpleOrderedMap<>();
       rsp.add("result", result);
       ResourceManagerPool pool = poolRef.get();
-      Map<String, Object> newLimits = (Map<String, Object>)Utils.fromJSONString(payload.get());
+      Map<String, Object> newLimits = payload.get();
       for (ManagedComponent cmp : components) {
+        NamedList<Object> perComponent = new SimpleOrderedMap<>();
+        result.add(cmp.getManagedComponentId().toString(), perComponent);
         try {
           Map<String, Object> currentLimits = new HashMap<>(pool.getResourceLimits(cmp));
           newLimits.forEach((k, v) -> {
@@ -612,14 +618,14 @@ public class ResourceManagerAPI implements SolrInfoBean {
           });
           try {
             pool.setResourceLimits(cmp, newLimits, ChangeListener.Reason.USER);
-            result.add("success", newLimits);
+            perComponent.addAll(newLimits);
           } catch (Exception e) {
             log.warn("Error setting resource limits of " + cmp.getManagedComponentId() + "/" + pool.getName() + " : " + e.toString(), e);
-            result.add("error", "Error setting resource limits of " + cmp.getManagedComponentId() + ": " + e.toString());
+            perComponent.add("error", "Error setting resource limits of " + cmp.getManagedComponentId() + ": " + e.toString());
           }
         } catch (Exception e) {
           log.warn("Error getting resource limits of " + cmp.getManagedComponentId() + "/" + pool.getName() + " : " + e.toString(), e);
-          result.add("error", "Error getting resource limits of " + cmp.getManagedComponentId() + ": " + e.toString());
+          perComponent.add("error", "Error getting resource limits of " + cmp.getManagedComponentId() + ": " + e.toString());
         }
       }
     }
@@ -632,7 +638,7 @@ public class ResourceManagerAPI implements SolrInfoBean {
       NamedList<Object> result = new SimpleOrderedMap<>();
       rsp.add("result", result);
       AtomicReference<ResourceManagerPool> poolRef = new AtomicReference<>();
-      List<ManagedComponent> components = getComponents(req, rsp, poolRef);
+      List<ManagedComponent> components = getComponents(req, rsp, payload.get(), poolRef);
       if (components == null) {
         return;
       }
diff --git a/solr/core/src/java/org/apache/solr/managed/ResourceManagerPool.java b/solr/core/src/java/org/apache/solr/managed/ResourceManagerPool.java
index dc3ac6a..cdcf304 100644
--- a/solr/core/src/java/org/apache/solr/managed/ResourceManagerPool.java
+++ b/solr/core/src/java/org/apache/solr/managed/ResourceManagerPool.java
@@ -55,6 +55,7 @@ public abstract class ResourceManagerPool<T extends ManagedComponent> implements
   protected final String type;
   protected final Map<String, Object> poolLimits = new ConcurrentHashMap<>();
   protected final Map<String, T> components = new ConcurrentHashMap<>();
+  protected final Map<String, Map<String, Object>> initialComponentLimits = new ConcurrentHashMap<>();
   protected final ResourceManager resourceManager;
   protected final Class<? extends ManagedComponent> componentClass;
   protected final Map<String, Object> poolParams = new ConcurrentHashMap<>();
@@ -141,13 +142,23 @@ public abstract class ResourceManagerPool<T extends ManagedComponent> implements
     if (existing != null) {
       throw new IllegalArgumentException("Component '" + managedComponent.getManagedComponentId() + "' already exists in pool '" + name + "' !");
     }
+    try {
+      initialComponentLimits.put(managedComponent.getManagedComponentId().toString(), getResourceLimits(managedComponent));
+    } catch (Exception e) {
+      log.warn("Could not retrieve initial component limits for {} / {}: {}", managedComponent.getManagedComponentId(), getName(), e);
+    }
   }
 
   /** Remove named component from this pool. */
   public boolean unregisterComponent(String componentId) {
+    initialComponentLimits.remove(componentId);
     return components.remove(componentId) != null;
   }
 
+  public Map<String, Object> getInitialResourceLimits(String componentId) {
+    return initialComponentLimits.getOrDefault(componentId, Collections.emptyMap());
+  }
+
   /**
    * Check whether a named component is registered in this pool.
    * @param componentId component id
diff --git a/solr/core/src/java/org/apache/solr/managed/types/CacheManagerPool.java b/solr/core/src/java/org/apache/solr/managed/types/CacheManagerPool.java
index e070488..9c76d71 100644
--- a/solr/core/src/java/org/apache/solr/managed/types/CacheManagerPool.java
+++ b/solr/core/src/java/org/apache/solr/managed/types/CacheManagerPool.java
@@ -124,7 +124,6 @@ public class CacheManagerPool extends ResourceManagerPool<SolrCache> {
   protected boolean optimize_expand = true;
   protected Map<String, Long> lookups = new HashMap<>();
   protected Map<String, Long> hits = new HashMap<>();
-  protected Map<String, Map<String, Object>> initialComponentLimits = new HashMap<>();
   protected int reportExceededCounter = 0;
   protected int reportExceededThreshold = DEFAULT_REPORT_EXCEEDED_THRESHOLD;
 
@@ -175,16 +174,9 @@ public class CacheManagerPool extends ResourceManagerPool<SolrCache> {
   }
 
   @Override
-  public void registerComponent(SolrCache component) {
-    super.registerComponent(component);
-    initialComponentLimits.put(component.getManagedComponentId().toString(), getResourceLimits(component));
-  }
-
-  @Override
   public boolean unregisterComponent(String componentId) {
     lookups.remove(componentId);
     hits.remove(componentId);
-    initialComponentLimits.remove(componentId);
     return super.unregisterComponent(componentId);
   }
 
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java b/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
index 7fb0944..39c672a 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
@@ -53,7 +53,7 @@ public class SolrCoreMetricManager implements Closeable {
     initCloudMode();
     metricManager = core.getCoreContainer().getMetricManager();
     String registryName = createRegistryName(cloudMode, collectionName, shardName, replicaName, core.getName());
-    solrMetricsContext = new SolrMetricsContext(metricManager, registryName, "SolrCore", core.getMetricTag());
+    solrMetricsContext = new SolrMetricsContext(metricManager, registryName, core.getMetricTag());
     leaderRegistryName = createLeaderRegistryName(cloudMode, collectionName, shardName);
   }
 
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
index 18b4efe..53db494 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
@@ -444,7 +444,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
       super.initializeMetrics(parentContext, scope);
       MetricsMap metrics = new MetricsMap((detailed, map) -> map.putAll(gaugevals));
       solrMetricsContext.gauge(
-           metrics,  true, "dumphandlergauge", getCategory().toString(), scope);
+           metrics,  true, "dumphandlergauge", getCategory().toString());
 
     }
 
diff --git a/solr/core/src/test/org/apache/solr/managed/TestResourceManagerIntegration.java b/solr/core/src/test/org/apache/solr/managed/TestResourceManagerIntegration.java
index 730d1b4..cfdd465 100644
--- a/solr/core/src/test/org/apache/solr/managed/TestResourceManagerIntegration.java
+++ b/solr/core/src/test/org/apache/solr/managed/TestResourceManagerIntegration.java
@@ -1,8 +1,9 @@
 package org.apache.solr.managed;
 
-import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -19,18 +20,15 @@ import org.apache.solr.cloud.CloudUtil;
 import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.Utils;
+import org.apache.solr.search.CaffeineCache;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  *
  */
 public class TestResourceManagerIntegration extends SolrCloudTestCase {
-  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   private static ResourceManager resourceManager;
   private static CloudSolrClient solrClient;
@@ -66,69 +64,275 @@ public class TestResourceManagerIntegration extends SolrCloudTestCase {
   public void testPoolAPI() throws Exception {
 
     // READ API
-
+    Set<String> expectedPools = resourceManager.getDefaultPoolConfigs().keySet();
     // read all
-    V2Request req = new V2Request.Builder("/cluster/resources")
-        .forceV2(true)
-        .withMethod(SolrRequest.METHOD.GET)
-        .withParams(params("values", "true", "components", "true", "limits", "true", "params", "true"))
-        .build();
+    {
+      V2Request req = new V2Request.Builder("/cluster/resources")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.GET)
+          .withParams(params("values", "true", "components", "true", "limits", "true", "params", "true"))
+          .build();
 
-    V2Response rsp = req.process(cluster.getSolrClient());
-    Set<String> expectedPools = resourceManager.getDefaultPoolConfigs().keySet();
-    NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
-    assertEquals(rsp.toString(), expectedPools.size(), result.size());
-    expectedPools.forEach(p -> {
-      assertNotNull("default pool " + p + " is missing!", result.get(p));
-      NamedList<Object> pool = (NamedList<Object>) result.get(p);
-      assertTrue(pool.toString(), pool.get("type") != null && pool.get("type").toString().equals("cache"));
-      assertTrue(pool.toString(), pool.get("size") != null && ((Number)pool.get("size")).intValue() > 0);
-      assertTrue(pool.toString(), pool.get("poolLimits") != null);
-      assertTrue(pool.toString(), pool.get("poolParams") != null);
-      assertTrue(pool.toString(), pool.get("components") != null);
-      assertTrue(pool.toString(), pool.get("totalValues") != null);
-    });
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals(rsp.toString(), expectedPools.size(), result.size());
+      expectedPools.forEach(p -> {
+        assertNotNull("default pool " + p + " is missing!", result.get(p));
+        NamedList<Object> pool = (NamedList<Object>) result.get(p);
+        assertTrue(pool.toString(), pool.get("type") != null && pool.get("type").toString().equals("cache"));
+        assertTrue(pool.toString(), pool.get("size") != null && ((Number)pool.get("size")).intValue() > 0);
+        assertTrue(pool.toString(), pool.get("limits") != null);
+        assertTrue(pool.toString(), pool.get("params") != null);
+        assertTrue(pool.toString(), pool.get("components") != null);
+        assertTrue(pool.toString(), pool.get("values") != null);
+      });
+    }
 
     // read one
-    String poolName = expectedPools.iterator().next();
-
-    req = new V2Request.Builder("/cluster/resources/" + poolName)
-        .forceV2(true)
-        .withMethod(SolrRequest.METHOD.GET)
-        .build();
-    rsp = req.process(cluster.getSolrClient());
-    NamedList<Object> result1 = (NamedList<Object>)rsp.getResponse().get("result");
-    assertEquals(rsp.toString(), 1, result1.size());
-    assertNotNull(rsp.toString(), result1.get(poolName));
-
-    // create one
-    ResourcePoolConfig config = new ResourcePoolConfig();
-    config.name = "foo";
-    config.type = "cache";
-    config.poolLimits.put("foo", "bar");
-    config.poolLimits.put("baz", 10);
-    config.poolParams.put("foo", "bar");
-
-    req = new V2Request.Builder("/cluster/resources")
-        .forceV2(true)
-        .withMethod(SolrRequest.METHOD.POST)
-        .withPayload(Collections.singletonMap("create", config))
-        .build();
-    rsp = req.process(cluster.getSolrClient());
-    NamedList<Object> result2 = (NamedList<Object>)rsp.getResponse().get("result");
-    assertNotNull(result2.toString(), result2.get("success"));
+    {
+      String poolName = expectedPools.iterator().next();
+
+      V2Request req = new V2Request.Builder("/cluster/resources/" + poolName)
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.GET)
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals(rsp.toString(), 1, result.size());
+      assertNotNull(rsp.toString(), result.get(poolName));
+    }
+
+    // WRITE API
+
+    // create
+    {
+      ResourcePoolConfig config = new ResourcePoolConfig();
+      config.name = "testPool";
+      config.type = "cache";
+      config.poolLimits.put("foo", "bar");
+      config.poolLimits.put("baz", 10);
+      config.poolParams.put("foo", "bar");
+
+      V2Request req = new V2Request.Builder("/cluster/resources")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("create", config))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertNotNull(result.toString(), result.get("success"));
+
+      config.name = "testPool2";
+      req = new V2Request.Builder("/cluster/resources")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("create", config))
+          .build();
+      rsp = req.process(cluster.getSolrClient());
+      result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertNotNull(result.toString(), result.get("success"));
+    }
 
     // set params
-    Map<String, Object> map = new HashMap<>();
-    map.put("foo", null);
-    map.put("bar", "xyz");
-    req = new V2Request.Builder("/cluster/resources/foo")
-        .forceV2(true)
-        .withMethod(SolrRequest.METHOD.POST)
-        .withPayload(Collections.singletonMap("setparams", map))
-        .build();
-    rsp = req.process(cluster.getSolrClient());
-    NamedList<Object> result3 = (NamedList<Object>)rsp.getResponse().get("result");
+    {
+      Map<String, Object> map = new HashMap<>();
+      map.put("foo", null);
+      map.put("bar", "xyz");
+      map.put("baz", 10);
+      V2Request req = new V2Request.Builder("/cluster/resources/testPool")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("setparams", map))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertNotNull("missing pool testPool params: " + result, result.get("testPool"));
+      Map<String, Object> newParams = (Map<String, Object>) result.get("testPool");
+      assertNull("foo param should be removed: " + newParams, newParams.get("foo"));
+      assertEquals(newParams.toString(), "xyz", newParams.get("bar"));
+      assertTrue("baz should be a number: " + newParams, newParams.get("baz") instanceof Number);
+      assertEquals(newParams.toString(), 10, ((Number)newParams.get("baz")).intValue());
+    }
+
+    // set limits
+    {
+      Map<String, Object> map = new HashMap<>();
+      map.put("foo", null);
+      map.put("maxSize", -10);
+      map.put("maxRamMB", 100);
+      map.put("baz", 10);
+      V2Request req = new V2Request.Builder("/cluster/resources/testPool")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("setlimits", map))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertNotNull("missing pool testPool limits: " + result, result.get("testPool"));
+      Map<String, Object> newLimits = (Map<String, Object>) result.get("testPool");
+      assertNull("foo limit should be removed: " + newLimits, newLimits.get("foo"));
+      assertEquals(newLimits.toString(), "xyz", newLimits.get("bar"));
+      assertTrue("baz should be a number: " + newLimits, newLimits.get("baz") instanceof Number);
+      assertEquals(newLimits.toString(), 10, ((Number)newLimits.get("baz")).intValue());
+      assertTrue("maxSize should be a number: " + newLimits, newLimits.get("maxSize") instanceof Number);
+      assertEquals(newLimits.toString(), -10, ((Number)newLimits.get("maxSize")).intValue());
+      assertTrue("maxRamMB should be a number: " + newLimits, newLimits.get("maxRamMB") instanceof Number);
+      assertEquals(newLimits.toString(), 100, ((Number)newLimits.get("maxRamMB")).intValue());
+    }
+
+    // delete
+
+    // delete by path
+    {
+      V2Request req = new V2Request.Builder("/cluster/resources/testPool")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("delete", null))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals(result.toString(), "success", result.get("testPool"));
+    }
+    // delete by payload
+    {
+      V2Request req = new V2Request.Builder("/cluster/resources")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("delete", "testPool2"))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals(result.toString(), "success", result.get("testPool2"));
+    }
+    // attempt deleting a predefined pool
+    {
+      String poolName = expectedPools.iterator().next();
+      V2Request req = new V2Request.Builder("/cluster/resources")
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("delete", poolName))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals(result.toString(), "ignored - cannot delete default pool", result.get(poolName));
+    }
+  }
+
+  @Test
+  public void testComponentAPI() throws Exception {
+
+    // READ API
+    Set<String> expectedPools = resourceManager.getDefaultPoolConfigs().keySet();
+    List<String> randomPools = new ArrayList<>(expectedPools);
+    Collections.shuffle(randomPools, random());
+    String componentsPath = "/node/resources/" + randomPools.get(0) + "/components";
+    List<String> componentIds = new ArrayList<>();
+    // list
+    {
+      V2Request req = new V2Request.Builder(componentsPath)
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.GET)
+          .withParams(params("values", "true", "limits", "true", "initialLimits", "true"))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      // 4 replicas, 2 of each shard
+      assertEquals(result.toString(), 4, result.size());
+      result.asMap(10).forEach((cmp, obj) -> {
+        // capture component id-s
+        componentIds.add(String.valueOf(cmp));
+
+        Map<String, Object> status = (Map<String, Object>) obj;
+        assertEquals(status.toString(), CaffeineCache.class.getName(), status.get("class"));
+        assertNotNull("limits are missing: " + status.toString(), status.get("limits"));
+        Map<String, Object> limits = (Map<String, Object>) status.get("limits");
+        assertNotNull("maxSize limit is missing: " + status.toString(), limits.get("maxSize"));
+        assertNotNull("maxRamMB limit is missing: " + status.toString(), limits.get("maxRamMB"));
+        assertNotNull("initialLimits are missing: " + status.toString(), status.get("initialLimits"));
+        assertNotNull("values are missing: " + status.toString(), status.get("values"));
+      });
+    }
+
+    Collections.shuffle(componentIds, random());
+
+    // WRITE API
+
+    // setlimits for all
+    {
+      Map<String, Object> limits = new HashMap<>();
+      limits.put("maxSize", 11);
+      limits.put("maxRamMB", 111);
+      V2Request req = new V2Request.Builder(componentsPath)
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("setlimits", limits))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals("exactly 4 components should be present: " + result.toString(), 4, result.size());
+      result.asMap(10).forEach((cmp, obj) -> {
+        Map<String, Object> newLimits = (Map<String, Object>)obj;
+        assertEquals(newLimits.toString(), 11, ((Number) newLimits.get("maxSize")).intValue());
+        assertEquals(newLimits.toString(), 111, ((Number) newLimits.get("maxRamMB")).intValue());
+      });
+    }
+
+    String componentId = componentIds.get(0);
+    String prefix = componentId.substring(0, componentId.indexOf("replica"));
+
+    // setlimits for prefix
+    {
+      Map<String, Object> limits = new HashMap<>();
+      limits.put("maxSize", 10);
+      limits.put("maxRamMB", 110);
+      V2Request req = new V2Request.Builder(componentsPath + "/" + prefix)
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("setlimits", limits))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals("exactly 2 components should be present: " + result.toString(), 2, result.size());
+      result.asMap(10).forEach((cmp, obj) -> {
+        Map<String, Object> newLimits = (Map<String, Object>)obj;
+        assertEquals(newLimits.toString(), 10, ((Number) newLimits.get("maxSize")).intValue());
+        assertEquals(newLimits.toString(), 110, ((Number) newLimits.get("maxRamMB")).intValue());
+      });
+    }
+
+    // delete by prefix
+    List<String> removed = new ArrayList<>();
+    {
+      V2Request req = new V2Request.Builder(componentsPath + "/" + prefix)
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("delete", null))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals("exactly 2 components should be removed: " + result.toString(), 2, result.size());
+      result.asMap(10).forEach((cmp, obj) -> {
+        assertEquals(result.toString(), "removed", obj);
+        removed.add(String.valueOf(cmp));
+      });
+    }
+
+    // delete by id
+    componentIds.removeAll(removed);
+    componentId = componentIds.get(0);
+    {
+      V2Request req = new V2Request.Builder(componentsPath)
+          .forceV2(true)
+          .withMethod(SolrRequest.METHOD.POST)
+          .withPayload(Collections.singletonMap("delete", componentId))
+          .build();
+      V2Response rsp = req.process(cluster.getSolrClient());
+      NamedList<Object> result = (NamedList<Object>)rsp.getResponse().get("result");
+      assertEquals("exactly 1 component should be removed: " + result.toString(), 1, result.size());
+      result.asMap(10).forEach((cmp, obj) -> {
+        assertEquals(result.toString(), "removed", obj);
+      });
+    }
   }
 
   @AfterClass
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpSolrClient.java
index dceb13c..2db6442 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpSolrClient.java
@@ -52,7 +52,7 @@ public abstract class BaseHttpSolrClient extends SolrClient {
     private NamedList meta;
 
     public RemoteExecutionException(String remoteHost, int code, String msg, NamedList meta) {
-      super(remoteHost, code, msg, null);
+      super(remoteHost, code, msg + (meta != null ? ": " + meta : ""), null);
       this.meta = meta;
     }
 
@@ -71,6 +71,8 @@ public abstract class BaseHttpSolrClient extends SolrClient {
 
     }
 
+
+
     public NamedList getMetaData() {
 
       return meta;