Expose hot keys metrics

Expose metrics related to the hot keys in memory cache:
- Capacity: number of hot keys that can be kept in memory.
- Size: number of hot keys that are currently in memory.

Feature: Issue 13940
Change-Id: I8813eb5f404289aa57a2ad008d5cfa5a5bf62e13
diff --git a/metrics.md b/metrics.md
index 85e2745..8e176ef 100644
--- a/metrics.md
+++ b/metrics.md
@@ -15,4 +15,10 @@
   : the number of times the cache can automatically expand its capacity.
 
   See the [official documentation](https://javadoc.io/static/net.openhft/chronicle-map/3.20.83/net/openhft/chronicle/map/ChronicleMap.html#remainingAutoResizes--)
-  for more information.
\ No newline at end of file
+  for more information.
+
+* cache/chroniclemap/hot_keys_capacity_<cache-name>
+  : Constant number of hot keys for the cache that can be kept in memory.
+
+* cache/chroniclemap/hot_keys_size_<cache-name>
+  : The number of hot keys for the cache that are currently in memory.
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
index 6b95702..f3f6483 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
@@ -109,7 +109,7 @@
         store.remainingAutoResizes(),
         store.percentageFreeSpace());
 
-    metrics.registerCallBackMetrics(def.name(), store);
+    metrics.registerCallBackMetrics(def.name(), store, hotEntries);
   }
 
   private static class ChronicleMapStorageMetrics {
@@ -120,9 +120,12 @@
       this.metricMaker = metricMaker;
     }
 
-    <K, V> void registerCallBackMetrics(String name, ChronicleMap<K, TimedValue<V>> store) {
+    <K, V> void registerCallBackMetrics(
+        String name, ChronicleMap<K, TimedValue<V>> store, InMemoryLRU<K> hotEntries) {
       String PERCENTAGE_FREE_SPACE_METRIC = "cache/chroniclemap/percentage_free_space_" + name;
       String REMAINING_AUTORESIZES_METRIC = "cache/chroniclemap/remaining_autoresizes_" + name;
+      String HOT_KEYS_CAPACITY_METRIC = "cache/chroniclemap/hot_keys_capacity_" + name;
+      String HOT_KEYS_SIZE_METRIC = "cache/chroniclemap/hot_keys_size_" + name;
 
       metricMaker.newCallbackMetric(
           PERCENTAGE_FREE_SPACE_METRIC,
@@ -138,6 +141,21 @@
               String.format(
                   "The number of times the %s cache can automatically expand its capacity", name)),
           store::remainingAutoResizes);
+
+      metricMaker.newConstantMetric(
+          HOT_KEYS_CAPACITY_METRIC,
+          hotEntries.getCapacity(),
+          new Description(
+              String.format(
+                  "The number of hot cache keys for %s cache that can be kept in memory", name)));
+
+      metricMaker.newCallbackMetric(
+          HOT_KEYS_SIZE_METRIC,
+          Integer.class,
+          new Description(
+              String.format(
+                  "The number of hot cache keys for %s cache that are currently in memory", name)),
+          hotEntries::size);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/InMemoryLRU.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/InMemoryLRU.java
index 150ab18..ac5183e 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/InMemoryLRU.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/InMemoryLRU.java
@@ -23,8 +23,10 @@
   private final Map<K, Boolean> LRUMap;
 
   private static final Boolean dummyValue = Boolean.TRUE;
+  private final int capacity;
 
   public InMemoryLRU(int capacity) {
+    this.capacity = capacity;
 
     LRUMap =
         Collections.synchronizedMap(
@@ -52,8 +54,16 @@
     LRUMap.clear();
   }
 
+  public int size() {
+    return LRUMap.size();
+  }
+
   @VisibleForTesting
   protected Object[] toArray() {
     return LRUMap.keySet().toArray();
   }
+
+  public int getCapacity() {
+    return capacity;
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java
index 459e2f2..e8e6236 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java
@@ -391,6 +391,84 @@
         () -> (int) getMetric(autoResizeMetricName).getValue() == 0, Duration.ofSeconds(2));
   }
 
+  @Test
+  public void shouldTriggerHotKeysCapacityCacheMetric() throws Exception {
+    String cachedValue = UUID.randomUUID().toString();
+    int percentageHotKeys = 60;
+    int maxEntries = 10;
+    int expectedCapacity = 6;
+    String hotKeysCapacityMetricName = "cache/chroniclemap/hot_keys_capacity_" + cachedValue;
+    gerritConfig.setInt("cache", cachedValue, "maxEntries", maxEntries);
+    gerritConfig.setInt("cache", cachedValue, "percentageHotKeys", percentageHotKeys);
+    gerritConfig.save();
+
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+
+    assertThat(getMetric(hotKeysCapacityMetricName).getValue()).isEqualTo(expectedCapacity);
+  }
+
+  @Test
+  public void shouldTriggerHotKeysSizeCacheMetric() throws Exception {
+    String cachedValue = UUID.randomUUID().toString();
+    int percentageHotKeys = 30;
+    int maxEntries = 10;
+    int maxHotKeyCapacity = 3;
+    final Duration METRIC_TRIGGER_TIMEOUT = Duration.ofSeconds(2);
+    String hotKeysSizeMetricName = "cache/chroniclemap/hot_keys_size_" + cachedValue;
+    gerritConfig.setInt("cache", cachedValue, "maxEntries", maxEntries);
+    gerritConfig.setInt("cache", cachedValue, "percentageHotKeys", percentageHotKeys);
+    gerritConfig.save();
+
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+
+    assertThat(getMetric(hotKeysSizeMetricName).getValue()).isEqualTo(0);
+
+    for (int i = 0; i < maxHotKeyCapacity; i++) {
+      cache.put(cachedValue + i, cachedValue);
+    }
+
+    WaitUtil.waitUntil(
+        () -> (int) getMetric(hotKeysSizeMetricName).getValue() == maxHotKeyCapacity,
+        METRIC_TRIGGER_TIMEOUT);
+
+    cache.put(cachedValue + maxHotKeyCapacity + 1, cachedValue);
+
+    assertThrows(
+        InterruptedException.class,
+        () ->
+            WaitUtil.waitUntil(
+                () -> (int) getMetric(hotKeysSizeMetricName).getValue() > maxHotKeyCapacity,
+                METRIC_TRIGGER_TIMEOUT));
+  }
+
+  @Test
+  public void shouldResetHotKeysWhenInvalidateAll() throws Exception {
+    String cachedValue = UUID.randomUUID().toString();
+    int percentageHotKeys = 30;
+    int maxEntries = 10;
+    int maxHotKeyCapacity = 3;
+    final Duration METRIC_TRIGGER_TIMEOUT = Duration.ofSeconds(2);
+    String hotKeysSizeMetricName = "cache/chroniclemap/hot_keys_size_" + cachedValue;
+    gerritConfig.setInt("cache", cachedValue, "maxEntries", maxEntries);
+    gerritConfig.setInt("cache", cachedValue, "percentageHotKeys", percentageHotKeys);
+    gerritConfig.save();
+
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+
+    for (int i = 0; i < maxHotKeyCapacity; i++) {
+      cache.put(cachedValue + i, cachedValue);
+    }
+
+    WaitUtil.waitUntil(
+        () -> (int) getMetric(hotKeysSizeMetricName).getValue() == maxHotKeyCapacity,
+        METRIC_TRIGGER_TIMEOUT);
+
+    cache.invalidateAll();
+
+    WaitUtil.waitUntil(
+        () -> (int) getMetric(hotKeysSizeMetricName).getValue() == 0, METRIC_TRIGGER_TIMEOUT);
+  }
+
   private int valueSize(String value) {
     final TimedValueMarshaller<String> marshaller =
         new TimedValueMarshaller<>(StringCacheSerializer.INSTANCE);