lucene-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a.@apache.org
Subject [lucene-solr] 36/36: SOLR-13579: Soft optimization, unit tests.
Date Wed, 18 Dec 2019 16:39:14 GMT
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

commit 27ef7cc6ba2a7e39b3dd99c1f5b35c341fbbc0f1
Author: Andrzej Bialecki <ab@apache.org>
AuthorDate: Wed Dec 18 15:59:00 2019 +0100

    SOLR-13579: Soft optimization, unit tests.
---
 .../solr/handler/admin/ResourceManagerHandler.java |   3 +-
 .../org/apache/solr/managed/ChangeListener.java    |  23 ++-
 .../apache/solr/managed/ResourceManagerPool.java   |   8 +-
 .../solr/managed/types/CacheManagerPool.java       | 189 +++++++++++++++++++--
 .../java/org/apache/solr/search/CaffeineCache.java |   1 +
 .../src/java/org/apache/solr/search/SolrCache.java |   1 +
 .../managed/types/TestCacheManagerPluginCloud.java |   9 -
 .../solr/managed/types/TestCacheManagerPool.java   | 157 +++++++++++++++--
 8 files changed, 343 insertions(+), 48 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ResourceManagerHandler.java
b/solr/core/src/java/org/apache/solr/handler/admin/ResourceManagerHandler.java
index dd587ca..7cd1733 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ResourceManagerHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ResourceManagerHandler.java
@@ -27,6 +27,7 @@ import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.managed.ChangeListener;
 import org.apache.solr.managed.ManagedComponent;
 import org.apache.solr.managed.ResourceManager;
 import org.apache.solr.managed.ResourceManagerPool;
@@ -295,7 +296,7 @@ public class ResourceManagerHandler extends RequestHandlerBase implements
Permis
             }
           });
           try {
-            pool.setResourceLimits(managedComponent2, newLimits);
+            pool.setResourceLimits(managedComponent2, newLimits, ChangeListener.Reason.USER);
             result.add("success", newLimits);
           } catch (Exception e) {
             log.warn("Error setting resource limits of " + resName + "/" + poolName + " :
" + e.toString(), e);
diff --git a/solr/core/src/java/org/apache/solr/managed/ChangeListener.java b/solr/core/src/java/org/apache/solr/managed/ChangeListener.java
index 49fbad6..42201d5 100644
--- a/solr/core/src/java/org/apache/solr/managed/ChangeListener.java
+++ b/solr/core/src/java/org/apache/solr/managed/ChangeListener.java
@@ -17,10 +17,28 @@
 package org.apache.solr.managed;
 
 /**
- *
+ * Listen to changes in resource limit settings caused by resource management framework
+ * (or by users via resource management API).
  */
 public interface ChangeListener {
 
+  enum Reason {
+    /** Administrative user action. */
+    USER,
+    /** Adjustment made to optimize the resource behavior. */
+    OPTIMIZATION,
+    /** Adjustment made due to total limit exceeded. */
+    ABOVE_TOTAL_LIMIT,
+    /** Adjustment made due to total limit underuse. */
+    BELOW_TOTAL_LIMIT,
+    /** Adjustment made due to individual resource limit exceeded. */
+    ABOVE_LIMIT,
+    /** Adjustment made due to individual resource limit underuse. */
+    BELOW_LIMIT,
+    /** Other unspecified reason. */
+    OTHER
+  }
+
   /**
    * Notify about changing a limit of a resource.
    * @param poolName pool name where resource is managed.
@@ -29,6 +47,7 @@ public interface ChangeListener {
    * @param newRequestedVal requested new value of the resource limit.
    * @param newActualVal actual value applied to the resource configuration. Note: this may
differ from the
    *                     value requested due to internal logic of the component.
+   * @param reason reason of the change
    */
-  void changedLimit(String poolName, ManagedComponent component, String limitName, Object
newRequestedVal, Object newActualVal);
+  void changedLimit(String poolName, ManagedComponent component, String limitName, Object
newRequestedVal, Object newActualVal, Reason reason);
 }
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 dfa822f..d9aa346 100644
--- a/solr/core/src/java/org/apache/solr/managed/ResourceManagerPool.java
+++ b/solr/core/src/java/org/apache/solr/managed/ResourceManagerPool.java
@@ -124,19 +124,19 @@ public abstract class ResourceManagerPool<T extends ManagedComponent>
implements
 
   public abstract Map<String, Object> getMonitoredValues(T component) throws Exception;
 
-  public void setResourceLimits(T component, Map<String, Object> limits) throws Exception
{
+  public void setResourceLimits(T component, Map<String, Object> limits, ChangeListener.Reason
reason) throws Exception {
     if (limits == null || limits.isEmpty()) {
       return;
     }
     for (Map.Entry<String, Object> entry : limits.entrySet()) {
-      setResourceLimit(component, entry.getKey(), entry.getValue());
+      setResourceLimit(component, entry.getKey(), entry.getValue(), reason);
     }
   }
 
-  public Object setResourceLimit(T component, String limitName, Object value) throws Exception
{
+  public Object setResourceLimit(T component, String limitName, Object value, ChangeListener.Reason
reason) throws Exception {
     Object newActualLimit = doSetResourceLimit(component, limitName, value);
     for (ChangeListener listener : listeners) {
-      listener.changedLimit(getName(), component, limitName, value, newActualLimit);
+      listener.changedLimit(getName(), component, limitName, value, newActualLimit, reason);
     }
     return newActualLimit;
   }
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 cee4fe7..483b1f4 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
@@ -17,10 +17,14 @@
 package org.apache.solr.managed.types;
 
 import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 
+import org.apache.solr.managed.ChangeListener;
 import org.apache.solr.managed.ResourceManager;
 import org.apache.solr.managed.ResourceManagerPool;
 import org.apache.solr.metrics.SolrMetricsContext;
@@ -34,16 +38,53 @@ import org.slf4j.LoggerFactory;
  * <p>This plugin calculates the total size and maxRamMB of all registered cache instances
  * and adjusts each cache's limits so that the aggregated values again fit within the pool
limits.</p>
  * <p>In order to avoid thrashing the plugin uses a dead band (by default {@link #DEFAULT_DEAD_BAND}),
- * which can be adjusted using configuration parameter {@link #DEAD_BAND}. If monitored values
don't
- * exceed the limits +/- the dead band then no action is taken.</p>
+ * which can be adjusted using configuration parameter {@link #DEAD_BAND_PARAM}. If monitored
values don't
+ * exceed the limits +/- the dead band then no forcible adjustment takes place.</p>
+ * <p>The management strategy consists of two distinct phases: soft optimization phase
and then hard limit phase.</p>
+ * <p><b>Soft optimization</b> tries to adjust the resource consumption
based on the cache hit ratio.
+ * This phase is executed only if there's no total limit exceeded. Also, hit ratio is considered
a valid monitored
+ * variable only when at least N lookups occurred since the last adjustment (default value
is {@link #DEFAULT_LOOKUP_DELTA}).
+ * If the hit ratio is higher than a threshold (default value is {@link #DEFAULT_TARGET_HITRATIO})
then the size
+ * of the cache can be reduced so that the resource consumption is minimized while still
keeping acceptable hit
+ * ratio - and vice versa.</p>
+ * <p>This optimization phase can only adjust the limits within a {@link #DEFAULT_MAX_ADJUST_RATIO},
i.e. increased
+ * or decreased values may not be larger / smaller than this multiple / fraction of the initially
configured limit.</p>
+ * <p><b>Hard limit</b> phase follows the soft optimization phase and it
forcibly reduces resource consumption of all components
+ * if the total usage is still above the pool limit after the first phase has completed.
Each component's limit is reduced
+ * by the same factor, regardless of the actual population or hit ratio.</p>
  */
 public class CacheManagerPool extends ResourceManagerPool<SolrCache> {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   public static String TYPE = "cache";
 
-  public static final String DEAD_BAND = "deadBand";
+  /** Controller dead-band - changes smaller than this ratio will be ignored. */
+  public static final String DEAD_BAND_PARAM = "deadBand";
+  /** Target hit ratio - high enough to be useful, low enough to avoid excessive cache size.
*/
+  public static final String TARGET_HIT_RATIO_PARAM = "targetHitRatio";
+  /**
+   * Maximum allowed adjustment ratio from the initial configuration value. Adjusted value
may not be
+   * higher than multiple of this factor, and not lower than divided by this factor.
+   */
+  public static final String MAX_ADJUST_RATIO_PARAM = "maxAdjustRatio";
+  /**
+   * Minimum number of lookups since last adjustment to consider the reported hitRatio
+   *  to be statistically valid.
+   */
+  public static final String MIN_LOOKUP_DELTA_PARAM = "minLookupDelta";
+  /** Default value of dead band (10%). */
   public static final double DEFAULT_DEAD_BAND = 0.1;
+  /** Default target hit ratio - a compromise between usefulness and limited resource usage.
*/
+  public static final double DEFAULT_TARGET_HITRATIO = 0.8;
+  /**
+   * Default minimum number of lookups since the last adjustment. This can be treated as
Bernoulli trials
+   * that give a 5% confidence about the statistical validity of hit ratio (<code>0.5
/ sqrt(lookups)</code>).
+   */
+  public static final long DEFAULT_LOOKUP_DELTA = 100;
+  /**
+   * Default maximum adjustment ratio from the initially configured values.
+   */
+  public static final double DEFAULT_MAX_ADJUST_RATIO = 2.0;
 
   protected static final Map<String, Function<Map<String, Object>, Double>>
controlledToMonitored = new HashMap<>();
 
@@ -53,22 +94,40 @@ public class CacheManagerPool extends ResourceManagerPool<SolrCache>
{
       return ramBytes != null ? ramBytes.doubleValue() / SolrCache.MB : 0.0;
     });
     controlledToMonitored.put(SolrCache.MAX_SIZE_PARAM, values ->
-        ((Number)values.getOrDefault(SolrCache.MAX_SIZE_PARAM, -1.0)).doubleValue());
+        ((Number)values.getOrDefault(SolrCache.SIZE_PARAM, -1.0)).doubleValue());
   }
 
   protected double deadBand = DEFAULT_DEAD_BAND;
+  protected double targetHitRatio = DEFAULT_TARGET_HITRATIO;
+  protected long lookupDelta = DEFAULT_LOOKUP_DELTA;
+  protected double maxAdjustRatio = DEFAULT_MAX_ADJUST_RATIO;
+  protected Map<String, Long> lookups = new HashMap<>();
+  protected Map<String, Map<String, Object>> initialComponentLimits = new HashMap<>();
 
   public CacheManagerPool(String name, String type, ResourceManager resourceManager, Map<String,
Object> poolLimits, Map<String, Object> poolParams) {
     super(name, type, resourceManager, poolLimits, poolParams);
-    String deadBandStr = String.valueOf(poolParams.getOrDefault(DEAD_BAND, DEFAULT_DEAD_BAND));
+    String str = String.valueOf(poolParams.getOrDefault(DEAD_BAND_PARAM, DEFAULT_DEAD_BAND));
     try {
-      deadBand = Double.parseDouble(deadBandStr);
+      deadBand = Double.parseDouble(str);
     } catch (Exception e) {
-      log.warn("Invalid deadBand parameter value '" + deadBandStr + "', using default " +
DEFAULT_DEAD_BAND);
+      log.warn("Invalid deadBand parameter value '" + str + "', using default " + DEFAULT_DEAD_BAND);
     }
   }
 
   @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);
+    initialComponentLimits.remove(componentId);
+    return super.unregisterComponent(componentId);
+  }
+
+  @Override
   public Object doSetResourceLimit(SolrCache component, String limitName, Object val) {
     if (!(val instanceof Number)) {
       try {
@@ -110,8 +169,10 @@ public class CacheManagerPool extends ResourceManagerPool<SolrCache>
{
     SolrMetricsContext metricsContext = component.getSolrMetricsContext();
     if (metricsContext != null) {
       Map<String, Object> metrics = metricsContext.getMetricsSnapshot();
-      String hitRatioKey = component.getCategory().toString() + "." + metricsContext.getScope()
+ "." + SolrCache.HIT_RATIO_PARAM;
-      values.put(SolrCache.HIT_RATIO_PARAM, metrics.get(hitRatioKey));
+      String key = component.getCategory().toString() + "." + metricsContext.getScope() +
"." + SolrCache.HIT_RATIO_PARAM;
+      values.put(SolrCache.HIT_RATIO_PARAM, metrics.get(key));
+      key = component.getCategory().toString() + "." + metricsContext.getScope() + "." +
SolrCache.LOOKUPS_PARAM;
+      values.put(SolrCache.LOOKUPS_PARAM, metrics.get(key));
     }
     return values;
   }
@@ -145,8 +206,7 @@ public class CacheManagerPool extends ResourceManagerPool<SolrCache>
{
         return;
       }
 
-      double changeRatio = poolLimitValue / totalValue.doubleValue();
-      // modify evenly every component's current limits by the changeRatio
+      List<SolrCache> adjustableComponents = new ArrayList<>();
       components.forEach((name, component) -> {
         Map<String, Object> resourceLimits = getResourceLimits((SolrCache) component);
         Object limit = resourceLimits.get(poolLimitName);
@@ -159,14 +219,111 @@ public class CacheManagerPool extends ResourceManagerPool<SolrCache>
{
         if (currentResourceLimit <= 0) { // undefined or unsupported
           return;
         }
-        double newLimit = currentResourceLimit * changeRatio;
+        adjustableComponents.add(component);
+      });
+      optimize(adjustableComponents, currentValues, poolLimitName, poolLimitValue, totalValue.doubleValue());
+    });
+  }
+
+  /**
+   * Manage all eligible components that support this pool limit.
+   */
+  private void optimize(List<SolrCache> components, Map<String, Map<String, Object>>
currentValues, String limitName,
+                        double poolLimitValue, double totalValue) {
+    // changeRatio > 1.0 means there are available free resources
+    // changeRatio < 1.0 means there's shortage of resources
+    final AtomicReference<Double> changeRatio = new AtomicReference<>(poolLimitValue
/ totalValue);
+
+    // ========================== OPTIMIZATION ==============================
+    // if the situation is not critical (ie. total consumption is less than max)
+    // try to proactively optimize by reducing the size of caches with too high hitRatio
+    // (because a lower hit ratio is still acceptable if it means saving resources) and
+    // expand the size of caches with too low hitRatio
+    final AtomicReference<Double> newTotalValue = new AtomicReference<>(totalValue);
+    components.forEach(component -> {
+      long currentLookups = ((Number)currentValues.get(component.getManagedComponentId().toString()).get(SolrCache.LOOKUPS_PARAM)).longValue();
+      long lastLookups = lookups.computeIfAbsent(component.getManagedComponentId().toString(),
k -> 0L);
+      if (currentLookups < lastLookups + lookupDelta) {
+        // too little data, skip the optimization
+        return;
+      }
+      Map<String, Object> resourceLimits = getResourceLimits(component);
+      double currentLimit = ((Number)resourceLimits.get(limitName)).doubleValue();
+      double currentHitRatio = ((Number)currentValues.get(component.getManagedComponentId().toString()).get(SolrCache.HIT_RATIO_PARAM)).doubleValue();
+      Number initialLimit = (Number)initialComponentLimits.get(component.getManagedComponentId().toString()).get(limitName);
+      if (initialLimit == null) {
+        // can't optimize because we don't know how far off we are from the initial setting
+        return;
+      }
+      if (currentHitRatio < targetHitRatio) {
+        if (changeRatio.get() < 1.0) {
+          // don't expand if we're already short on resources
+          return;
+        }
+        // expand to increase the hitRatio, but not more than maxAdjustRatio from the initialLimit
+        double newLimit = currentLimit * changeRatio.get();
+        if (newLimit > initialLimit.doubleValue() * maxAdjustRatio) {
+          // don't expand ad infinitum
+          newLimit = initialLimit.doubleValue() * maxAdjustRatio;
+        }
+        if (newLimit > poolLimitValue) {
+          // don't expand above the total pool limit
+          newLimit = poolLimitValue;
+        }
+        if (newLimit <= currentLimit) {
+          return;
+        }
+        lookups.put(component.getManagedComponentId().toString(), currentLookups);
         try {
-          setResourceLimit((SolrCache) component, poolLimitName, newLimit);
+          Number actualNewLimit = (Number)setResourceLimit(component, limitName, newLimit,
ChangeListener.Reason.OPTIMIZATION);
+          newTotalValue.getAndUpdate(v -> v - currentLimit + actualNewLimit.doubleValue());
         } catch (Exception e) {
-          log.warn("Failed to set managed limit " + poolLimitName +
-              " from " + currentResourceLimit + " to " + newLimit + " on " + component.getManagedComponentId(),
e);
+          log.warn("Failed to set managed limit " + limitName +
+              " from " + currentLimit + " to " + newLimit + " on " + component.getManagedComponentId(),
e);
         }
-      });
+      } else {
+        // shrink to release some resources but not more than maxAdjustRatio from the initialLimit
+        double newLimit = targetHitRatio / currentHitRatio * currentLimit;
+        if (newLimit * maxAdjustRatio < initialLimit.doubleValue()) {
+          // don't shrink ad infinitum
+          return;
+        }
+        lookups.put(component.getManagedComponentId().toString(), currentLookups);
+        try {
+          Number actualNewLimit = (Number)setResourceLimit(component, limitName, newLimit,
ChangeListener.Reason.OPTIMIZATION);
+          newTotalValue.getAndUpdate(v -> v - currentLimit + actualNewLimit.doubleValue());
+        } catch (Exception e) {
+          log.warn("Failed to set managed limit " + limitName +
+              " from " + currentLimit + " to " + newLimit + " on " + component.getManagedComponentId(),
e);
+        }
+      }
+    });
+
+    // ======================== HARD LIMIT ================
+    // now re-calculate the new changeRatio based on possible
+    // optimizations made above
+    double totalDelta = poolLimitValue - newTotalValue.get();
+
+    // dead band to avoid thrashing
+    if (Math.abs(totalDelta / poolLimitValue) < deadBand) {
+      return;
+    }
+
+    changeRatio.set(poolLimitValue / newTotalValue.get());
+    if (changeRatio.get() >= 1.0) { // there's no resource shortage
+      return;
+    }
+    // forcibly trim each resource limit (evenly) to fit within the total pool limit
+    components.forEach(component -> {
+      Map<String, Object> resourceLimits = getResourceLimits(component);
+      double currentLimit = ((Number)resourceLimits.get(limitName)).doubleValue();
+      double newLimit = currentLimit * changeRatio.get();
+      try {
+        setResourceLimit(component, limitName, newLimit, ChangeListener.Reason.ABOVE_TOTAL_LIMIT);
+      } catch (Exception e) {
+        log.warn("Failed to set managed limit " + limitName +
+            " from " + currentLimit + " to " + newLimit + " on " + component.getManagedComponentId(),
e);
+      }
     });
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/search/CaffeineCache.java b/solr/core/src/java/org/apache/solr/search/CaffeineCache.java
index d40b319..451f72b 100644
--- a/solr/core/src/java/org/apache/solr/search/CaffeineCache.java
+++ b/solr/core/src/java/org/apache/solr/search/CaffeineCache.java
@@ -378,6 +378,7 @@ public class CaffeineCache<K, V> extends SolrCacheBase implements
SolrCache<K, V
         map.put("warmupTime", warmupTime);
         map.put(RAM_BYTES_USED_PARAM, ramBytesUsed());
         map.put(MAX_RAM_MB_PARAM, getMaxRamMB());
+        map.put(MAX_SIZE_PARAM, getMaxSize());
 
         CacheStats cumulativeStats = priorStats.plus(stats);
         map.put("cumulative_lookups", cumulativeStats.requestCount());
diff --git a/solr/core/src/java/org/apache/solr/search/SolrCache.java b/solr/core/src/java/org/apache/solr/search/SolrCache.java
index ecc0308..ebe5079 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrCache.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrCache.java
@@ -55,6 +55,7 @@ public interface SolrCache<K,V> extends SolrInfoBean, ManagedComponent,
Accounta
   /** Use a background thread for cache evictions and cleanup. */
   String CLEANUP_THREAD_PARAM = "cleanupThread";
 
+  long KB = 1024L;
   long MB = 1024L * 1024L;
 
   /**
diff --git a/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPluginCloud.java
b/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPluginCloud.java
deleted file mode 100644
index 2abcca6..0000000
--- a/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPluginCloud.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.apache.solr.managed.types;
-
-import org.apache.solr.cloud.SolrCloudTestCase;
-
-/**
- *
- */
-public class TestCacheManagerPluginCloud extends SolrCloudTestCase {
-}
diff --git a/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPool.java b/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPool.java
index 9e22158..cba581c 100644
--- a/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPool.java
+++ b/solr/core/src/test/org/apache/solr/managed/types/TestCacheManagerPool.java
@@ -1,5 +1,6 @@
 package org.apache.solr.managed.types;
 
+import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -7,8 +8,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import org.apache.commons.math3.distribution.ZipfDistribution;
 import org.apache.lucene.util.Accountable;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.managed.ChangeListener;
 import org.apache.solr.managed.DefaultResourceManager;
 import org.apache.solr.managed.ManagedComponent;
 import org.apache.solr.managed.ResourceManager;
@@ -21,11 +24,14 @@ import org.apache.solr.search.SolrCache;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  *
  */
 public class TestCacheManagerPool extends SolrTestCaseJ4 {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   ResourceManager resourceManager;
 
@@ -37,16 +43,14 @@ public class TestCacheManagerPool extends SolrTestCaseJ4 {
     resourceManager.init(null);
   }
 
-  private static final long KB = 1024;
-  private static final long MB = 1024 * KB;
-
-  private static class ChangeListener implements org.apache.solr.managed.ChangeListener {
+  private static class ChangeTestListener implements ChangeListener {
     Map<String, Map<String, Object>> changedValues = new ConcurrentHashMap<>();
 
     @Override
-    public void changedLimit(String poolName, ManagedComponent component, String limitName,
Object newRequestedVal, Object newActualVal) {
+    public void changedLimit(String poolName, ManagedComponent component, String limitName,
Object newRequestedVal, Object newActualVal, Reason reason) {
       Map<String, Object> perComponent = changedValues.computeIfAbsent(component.getManagedComponentId().toString(),
id -> new ConcurrentHashMap<>());
       perComponent.put(limitName, newActualVal);
+      perComponent.put("reason", reason);
     }
 
     public void clear() {
@@ -56,7 +60,7 @@ public class TestCacheManagerPool extends SolrTestCaseJ4 {
 
   @Test
   public void testPoolLimits() throws Exception {
-    ResourceManagerPool pool = resourceManager.createPool("test", CacheManagerPool.TYPE,
Collections.singletonMap("maxRamMB", 200), Collections.emptyMap());
+    ResourceManagerPool pool = resourceManager.createPool("testPoolLimits", CacheManagerPool.TYPE,
Collections.singletonMap("maxRamMB", 200), Collections.emptyMap());
     SolrMetricManager metricManager = new SolrMetricManager();
     SolrMetricsContext solrMetricsContext = new SolrMetricsContext(metricManager, "fooRegistry",
"barScope", "bazTag");
     List<SolrCache> caches = new ArrayList<>();
@@ -66,10 +70,10 @@ public class TestCacheManagerPool extends SolrTestCaseJ4 {
       params.put("maxRamMB", "50");
       cache.init(params, null, new NoOpRegenerator());
       cache.initializeMetrics(solrMetricsContext, "child-" + i);
-      cache.initializeManagedComponent(resourceManager, "test");
+      cache.initializeManagedComponent(resourceManager, "testPoolLimits");
       caches.add(cache);
     }
-    ChangeListener listener = new ChangeListener();
+    ChangeTestListener listener = new ChangeTestListener();
     pool.addChangeListener(listener);
     // fill up all caches just below the global limit, evenly with small values
     for (int i = 0; i < 202; i++) {
@@ -77,11 +81,17 @@ public class TestCacheManagerPool extends SolrTestCaseJ4 {
         cache.put("id-" + i, new Accountable() {
           @Override
           public long ramBytesUsed() {
-            return 100 * KB;
+            return 100 * SolrCache.KB;
           }
         });
       }
     }
+    // generate lookups to trigger the optimization
+    for (SolrCache<String, Accountable> cache : caches) {
+      for (int i = 0; i < CacheManagerPool.DEFAULT_LOOKUP_DELTA * 2; i++) {
+        cache.get("id-" + i);
+      }
+    }
     pool.manage();
     Map<String, Object> totalValues = pool.aggregateTotalValues(pool.getCurrentValues());
 
@@ -92,22 +102,137 @@ public class TestCacheManagerPool extends SolrTestCaseJ4 {
       caches.get(0).put("large-" + i, new Accountable() {
         @Override
         public long ramBytesUsed() {
-          return 2560 * KB;
+          return 2560 * SolrCache.KB;
         }
       });
     }
     pool.manage();
     totalValues = pool.aggregateTotalValues(pool.getCurrentValues());
-
+    // OPTIMIZATION should have handled this, due to abnormally high hit ratios
     assertEquals("should adjust all: " + listener.changedValues.toString(), 10, listener.changedValues.size());
-    listener.clear();
+    listener.changedValues.values().forEach(map -> assertEquals(ChangeListener.Reason.OPTIMIZATION,
map.get("reason")));
+    // add more large values
+    for (int i = 0; i < 10; i++) {
+      caches.get(i).put("large1-" + i, new Accountable() {
+        @Override
+        public long ramBytesUsed() {
+          return 2560 * SolrCache.KB;
+        }
+      });
+    }
+    // don't generate any new lookups - prevents OPTIMIZATION
+
+    //
+
+    // NOTE: this takes a few rounds to adjust because we modify the original maxRamMB which
+    // may have been very different for each cache.
+    int cnt = 0;
+    do {
+      cnt++;
+      listener.clear();
+      pool.manage();
+      if (listener.changedValues.isEmpty()) {
+        break;
+      }
+      totalValues = pool.aggregateTotalValues(pool.getCurrentValues());
+      assertEquals("should adjust all again: " + listener.changedValues.toString(), 10, listener.changedValues.size());
+      listener.changedValues.values().forEach(map -> assertEquals(ChangeListener.Reason.ABOVE_TOTAL_LIMIT,
map.get("reason")));
+      log.info(" - step " + cnt + ": " + listener.changedValues);
+    } while (cnt < 10);
+    if (cnt == 0) {
+      fail("failed to adjust to fit in 10 steps: " + listener.changedValues);
+    }
+  }
+
+  private static final Accountable LARGE_ITEM = new Accountable() {
+    @Override
+    public long ramBytesUsed() {
+      return SolrCache.MB;
+    }
+  };
+
+  private static final Accountable SMALL_ITEM = new Accountable() {
+    @Override
+    public long ramBytesUsed() {
+      return SolrCache.KB;
+    }
+  };
+
+  @Test
+  public void testHitRatioOptimization() throws Exception {
+    ResourceManagerPool pool = resourceManager.createPool("testHitRatio", CacheManagerPool.TYPE,
Collections.singletonMap("maxSize", 200), Collections.emptyMap());
+    SolrMetricManager metricManager = new SolrMetricManager();
+    SolrMetricsContext solrMetricsContext = new SolrMetricsContext(metricManager, "fooRegistry",
"barScope", "bazTag");
+    SolrCache<Integer, Accountable> cache = new CaffeineCache<>();
+    Map<String, String> params = new HashMap<>();
+    int initialSize = 100;
+    params.put("size", "" + initialSize);
+    cache.init(params, null, new NoOpRegenerator());
+    cache.initializeMetrics(solrMetricsContext, "testHitRatio");
+    cache.initializeManagedComponent(resourceManager, "testHitRatio");
+
+    ChangeTestListener listener = new ChangeTestListener();
+    pool.addChangeListener(listener);
+
+    // ===== test shrinking =====
+    // populate / lookup with a small set of items -> high hit ratio.
+    // Optimization should kick in and shrink the cache
+    ZipfDistribution zipf = new ZipfDistribution(100, 0.99);
+    int NUM_LOOKUPS = 3000;
+    for (int i = 0; i < NUM_LOOKUPS; i++) {
+      cache.computeIfAbsent(zipf.sample(), k -> SMALL_ITEM);
+    }
     pool.manage();
-    totalValues = pool.aggregateTotalValues(pool.getCurrentValues());
-    assertEquals("should adjust all again: " + listener.changedValues.toString(), 10, listener.changedValues.size());
+    assertTrue(cache.getMaxSize() < initialSize);
+    assertEquals(listener.changedValues.toString(), 1, listener.changedValues.size());
+    listener.changedValues.values().forEach(map -> assertEquals(ChangeListener.Reason.OPTIMIZATION,
map.get("reason")));
+    // iterate until it's small enough to affect the hit ratio
+    int cnt = 0;
+    do {
+      listener.clear();
+      cnt++;
+      for (int i = 0; i < NUM_LOOKUPS; i++) {
+        cache.computeIfAbsent(zipf.sample(), k -> SMALL_ITEM);
+      }
+      pool.manage();
+      if (listener.changedValues.isEmpty()) {
+        break;
+      }
+      log.info(" - step " + cnt + ": " + listener.changedValues);
+    } while (cnt < 10);
+    if (cnt == 10) {
+      fail("failed to reach the balance: " + listener.changedValues);
+    }
+    assertTrue("maxSize adjusted more than allowed: " + cache.getMaxSize(), cache.getMaxSize()
>= initialSize / CacheManagerPool.DEFAULT_MAX_ADJUST_RATIO);
+
+    // ========= test expansion ===========
     listener.clear();
+    zipf = new ZipfDistribution(100000, 0.5);
+    for (int i = 0; i < NUM_LOOKUPS * 2; i++) {
+      cache.computeIfAbsent(zipf.sample(), k -> SMALL_ITEM);
+    }
     pool.manage();
-    totalValues = pool.aggregateTotalValues(pool.getCurrentValues());
-    assertEquals("should not adjust (within deadband): " + listener.changedValues.toString(),
0, listener.changedValues.size());
+    assertEquals(listener.changedValues.toString(), 1, listener.changedValues.size());
+    listener.changedValues.values().forEach(map -> assertEquals(ChangeListener.Reason.OPTIMIZATION,
map.get("reason")));
+    assertTrue(cache.getMaxSize() > initialSize);
+
+    cnt = 0;
+    do {
+      listener.clear();
+      cnt++;
+      for (int i = 0; i < NUM_LOOKUPS * 2; i++) {
+        cache.computeIfAbsent(zipf.sample(), k -> SMALL_ITEM);
+      }
+      pool.manage();
+      if (listener.changedValues.isEmpty()) {
+        break;
+      }
+      log.info(" - step " + cnt + ": " + listener.changedValues);
+    } while (cnt < 10);
+    if (cnt == 10) {
+      fail("failed to reach the balance: " + listener.changedValues);
+    }
+    assertTrue("maxSize adjusted more than allowed: " + cache.getMaxSize(), cache.getMaxSize()
<= initialSize * CacheManagerPool.DEFAULT_MAX_ADJUST_RATIO);
   }
 
   @After


Mime
View raw message