Use wrapper to store cache key

Wrap cache key to abstract the actual serializer.
The serializer will be accessed at runtime by looking up the cache name.
This allows to change the name of the serializer as well as the
name of the serialized object without breaking the recovery phase of
the persisted cache.

Bug: Issue 14511
Change-Id: Iedf138be055f3a329d1f8866e4b2158fe4660cde
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheSerializers.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheSerializers.java
new file mode 100644
index 0000000..8546981
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheSerializers.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.modules.cache.chroniclemap;
+
+import com.google.gerrit.server.cache.PersistentCacheDef;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.inject.Singleton;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Singleton
+public class CacheSerializers {
+  private static final Map<String, CacheSerializer<?>> keySerializers = new ConcurrentHashMap<>();
+  private static final Map<String, CacheSerializer<?>> valueSerializers = new ConcurrentHashMap<>();
+
+  static <K, V> void registerCacheDef(PersistentCacheDef<K, V> def) {
+    String cacheName = def.name();
+    registerCacheKeySerializer(cacheName, def.keySerializer());
+    registerCacheValueSerializer(cacheName, def.valueSerializer());
+  }
+
+  static <K, V> void registerCacheKeySerializer(
+      String cacheName, CacheSerializer<K> keySerializer) {
+    keySerializers.computeIfAbsent(cacheName, (name) -> keySerializer);
+  }
+
+  static <K, V> void registerCacheValueSerializer(
+      String cacheName, CacheSerializer<V> valueSerializer) {
+    valueSerializers.computeIfAbsent(cacheName, (name) -> valueSerializer);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <K> CacheSerializer<K> getKeySerializer(String name) {
+    if (keySerializers.containsKey(name)) {
+      return (CacheSerializer<K>) keySerializers.get(name);
+    }
+    throw new IllegalStateException("Could not find key serializer for " + name);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <V> CacheSerializer<V> getValueSerializer(String name) {
+    if (valueSerializers.containsKey(name)) {
+      return (CacheSerializer<V>) valueSerializers.get(name);
+    }
+    throw new IllegalStateException("Could not find value serializer for " + name);
+  }
+}
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 311445b..bde4fd1 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
@@ -39,7 +39,7 @@
 
   private final ChronicleMapCacheConfig config;
   private final CacheLoader<K, V> loader;
-  private final ChronicleMap<K, TimedValue<V>> store;
+  private final ChronicleMap<KeyWrapper<K>, TimedValue<V>> store;
   private final LongAdder hitCount = new LongAdder();
   private final LongAdder missCount = new LongAdder();
   private final LongAdder loadSuccessCount = new LongAdder();
@@ -55,6 +55,8 @@
       CacheLoader<K, V> loader,
       MetricMaker metricMaker)
       throws IOException {
+    CacheSerializers.registerCacheDef(def);
+
     this.config = config;
     this.loader = loader;
     this.hotEntries =
@@ -63,11 +65,11 @@
 
     ChronicleMapStorageMetrics metrics = new ChronicleMapStorageMetrics(metricMaker);
 
-    final Class<K> keyClass = (Class<K>) def.keyType().getRawType();
+    final Class<KeyWrapper<K>> keyWrapperClass = (Class<KeyWrapper<K>>) (Class) KeyWrapper.class;
     final Class<TimedValue<V>> valueWrapperClass = (Class<TimedValue<V>>) (Class) TimedValue.class;
 
-    final ChronicleMapBuilder<K, TimedValue<V>> mapBuilder =
-        ChronicleMap.of(keyClass, valueWrapperClass).name(def.name());
+    final ChronicleMapBuilder<KeyWrapper<K>, TimedValue<V>> mapBuilder =
+        ChronicleMap.of(keyWrapperClass, valueWrapperClass).name(def.name());
 
     // Chronicle-map does not allow to custom-serialize boxed primitives
     // such as Boolean, Integer, for which size is statically determined.
@@ -75,11 +77,11 @@
     // it cannot be used.
     if (!mapBuilder.constantlySizedKeys()) {
       mapBuilder.averageKeySize(config.getAverageKeySize());
-      mapBuilder.keyMarshaller(new ChronicleMapMarshallerAdapter<>(def.keySerializer()));
+      mapBuilder.keyMarshaller(new KeyWrapperMarshaller<>(def.name()));
     }
 
     mapBuilder.averageValueSize(config.getAverageValueSize());
-    mapBuilder.valueMarshaller(new TimedValueMarshaller<>(def.valueSerializer()));
+    mapBuilder.valueMarshaller(new TimedValueMarshaller<>(def.name()));
 
     mapBuilder.entries(config.getMaxEntries());
 
@@ -117,7 +119,7 @@
     }
 
     <K, V> void registerCallBackMetrics(
-        String name, ChronicleMap<K, TimedValue<V>> store, InMemoryLRU<K> hotEntries) {
+        String name, ChronicleMap<KeyWrapper<K>, TimedValue<V>> store, InMemoryLRU<K> hotEntries) {
       String sanitizedName = metricMaker.sanitizeMetricName(name);
       String PERCENTAGE_FREE_SPACE_METRIC =
           "cache/chroniclemap/percentage_free_space_" + sanitizedName;
@@ -162,10 +164,12 @@
     return config;
   }
 
+  @SuppressWarnings("unchecked")
   @Override
   public V getIfPresent(Object objKey) {
-    if (store.containsKey(objKey)) {
-      TimedValue<V> vTimedValue = store.get(objKey);
+    KeyWrapper<K> keyWrapper = (KeyWrapper<K>) new KeyWrapper<>(objKey);
+    if (store.containsKey(keyWrapper)) {
+      TimedValue<V> vTimedValue = store.get(keyWrapper);
       if (!expired(vTimedValue.getCreated())) {
         hitCount.increment();
         hotEntries.add((K) objKey);
@@ -180,8 +184,9 @@
 
   @Override
   public V get(K key) throws ExecutionException {
-    if (store.containsKey(key)) {
-      TimedValue<V> vTimedValue = store.get(key);
+    KeyWrapper<K> keyWrapper = new KeyWrapper<>(key);
+    if (store.containsKey(keyWrapper)) {
+      TimedValue<V> vTimedValue = store.get(keyWrapper);
       if (!needsRefresh(vTimedValue.getCreated())) {
         hitCount.increment();
         hotEntries.add(key);
@@ -211,8 +216,9 @@
 
   @Override
   public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException {
-    if (store.containsKey(key)) {
-      TimedValue<V> vTimedValue = store.get(key);
+    KeyWrapper<K> keyWrapper = new KeyWrapper<>(key);
+    if (store.containsKey(keyWrapper)) {
+      TimedValue<V> vTimedValue = store.get(keyWrapper);
       if (!needsRefresh(vTimedValue.getCreated())) {
         hitCount.increment();
         return vTimedValue.getValue();
@@ -235,14 +241,16 @@
 
   @SuppressWarnings("unchecked")
   public void putUnchecked(Object key, Object value, Timestamp created) {
-    TimedValue<?> wrapped = new TimedValue<>(value, created.toInstant().toEpochMilli());
-    store.put((K) key, (TimedValue<V>) wrapped);
+    TimedValue<?> wrappedValue = new TimedValue<>(value, created.toInstant().toEpochMilli());
+    KeyWrapper<?> wrappedKey = new KeyWrapper<>(key);
+    store.put((KeyWrapper<K>) wrappedKey, (TimedValue<V>) wrappedValue);
   }
 
   @Override
   public void put(K key, V val) {
-    TimedValue<V> wrapped = new TimedValue<>(val);
-    store.put(key, wrapped);
+    KeyWrapper<K> wrappedKey = new KeyWrapper<>(key);
+    TimedValue<V> wrappedValue = new TimedValue<>(val);
+    store.put(wrappedKey, wrappedValue);
     hotEntries.add(key);
   }
 
@@ -251,7 +259,7 @@
       store.forEachEntry(
           c -> {
             if (expired(c.value().get().getCreated())) {
-              hotEntries.remove(c.key().get());
+              hotEntries.remove(c.key().get().getValue());
               c.context().remove(c);
             }
           });
@@ -282,17 +290,19 @@
   private void evictColdEntries() {
     store.forEachEntryWhile(
         e -> {
-          if (!hotEntries.contains(e.key().get())) {
+          if (!hotEntries.contains(e.key().get().getValue())) {
             e.doRemove();
           }
           return runningOutOfFreeSpace();
         });
   }
 
+  @SuppressWarnings("unchecked")
   @Override
   public void invalidate(Object key) {
-    store.remove(key);
-    hotEntries.remove((K) key);
+    KeyWrapper<K> wrappedKey = (KeyWrapper<K>) new KeyWrapper<>(key);
+    store.remove(wrappedKey);
+    hotEntries.remove(wrappedKey.getValue());
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapMarshallerAdapter.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapMarshallerAdapter.java
deleted file mode 100644
index 0c60dca..0000000
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapMarshallerAdapter.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed 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 com.googlesource.gerrit.modules.cache.chroniclemap;
-
-import com.google.gerrit.server.cache.serialize.CacheSerializer;
-import net.openhft.chronicle.bytes.Bytes;
-import net.openhft.chronicle.core.util.ReadResolvable;
-import net.openhft.chronicle.hash.serialization.BytesReader;
-import net.openhft.chronicle.hash.serialization.BytesWriter;
-
-public class ChronicleMapMarshallerAdapter<T>
-    implements BytesWriter<T>, BytesReader<T>, ReadResolvable<ChronicleMapMarshallerAdapter<T>> {
-
-  private final CacheSerializer<T> cacheSerializer;
-
-  ChronicleMapMarshallerAdapter(CacheSerializer<T> cacheSerializer) {
-    this.cacheSerializer = cacheSerializer;
-  }
-
-  @Override
-  public ChronicleMapMarshallerAdapter<T> readResolve() {
-    return new ChronicleMapMarshallerAdapter<>(cacheSerializer);
-  }
-
-  @Override
-  public T read(Bytes in, T using) {
-    int serializedLength = (int) in.readUnsignedInt();
-    byte[] serialized = new byte[serializedLength];
-    in.read(serialized, 0, serializedLength);
-    using = cacheSerializer.deserialize(serialized);
-    return using;
-  }
-
-  @Override
-  public void write(Bytes out, T toWrite) {
-    final byte[] serialized = cacheSerializer.serialize(toWrite);
-    out.writeUnsignedInt(serialized.length);
-    out.write(serialized);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapper.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapper.java
new file mode 100644
index 0000000..cfab341
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapper.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.modules.cache.chroniclemap;
+
+import com.google.common.base.Objects;
+
+public class KeyWrapper<V> {
+
+  private final V value;
+
+  KeyWrapper(V value) {
+    this.value = value;
+  }
+
+  public V getValue() {
+    return value;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (!(o instanceof KeyWrapper)) return false;
+    KeyWrapper<?> that = (KeyWrapper<?>) o;
+    return Objects.equal(value, that.value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(value);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapperMarshaller.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapperMarshaller.java
new file mode 100644
index 0000000..7c7801c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapperMarshaller.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.modules.cache.chroniclemap;
+
+import net.openhft.chronicle.bytes.Bytes;
+import net.openhft.chronicle.core.util.ReadResolvable;
+import net.openhft.chronicle.hash.serialization.BytesReader;
+import net.openhft.chronicle.hash.serialization.BytesWriter;
+
+public class KeyWrapperMarshaller<V>
+    implements BytesWriter<KeyWrapper<V>>,
+        BytesReader<KeyWrapper<V>>,
+        ReadResolvable<KeyWrapperMarshaller<V>> {
+
+  private final String name;
+
+  KeyWrapperMarshaller(String name) {
+    this.name = name;
+  }
+
+  @Override
+  public KeyWrapperMarshaller<V> readResolve() {
+    return new KeyWrapperMarshaller<>(name);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public KeyWrapper<V> read(Bytes in, KeyWrapper<V> using) {
+    int serializedLength = (int) in.readUnsignedInt();
+    byte[] serialized = new byte[serializedLength];
+    in.read(serialized, 0, serializedLength);
+    V v = (V) CacheSerializers.getKeySerializer(name).deserialize(serialized);
+    using = new KeyWrapper<>(v);
+
+    return using;
+  }
+
+  @Override
+  public void write(Bytes out, KeyWrapper<V> toWrite) {
+    final byte[] serialized = CacheSerializers.getKeySerializer(name).serialize(toWrite.getValue());
+    out.writeUnsignedInt(serialized.length);
+    out.write(serialized);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshaller.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshaller.java
index 60897bf..ec30043 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshaller.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshaller.java
@@ -13,7 +13,6 @@
 // limitations under the License.
 package com.googlesource.gerrit.modules.cache.chroniclemap;
 
-import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import java.nio.ByteBuffer;
 import net.openhft.chronicle.bytes.Bytes;
 import net.openhft.chronicle.core.util.ReadResolvable;
@@ -25,17 +24,18 @@
         BytesReader<TimedValue<V>>,
         ReadResolvable<TimedValueMarshaller<V>> {
 
-  private final CacheSerializer<V> serializer;
+  private final String name;
 
-  TimedValueMarshaller(CacheSerializer<V> serializer) {
-    this.serializer = serializer;
+  TimedValueMarshaller(String name) {
+    this.name = name;
   }
 
   @Override
   public TimedValueMarshaller<V> readResolve() {
-    return new TimedValueMarshaller<>(serializer);
+    return new TimedValueMarshaller<>(name);
   }
 
+  @SuppressWarnings("unchecked")
   @Override
   public TimedValue<V> read(Bytes in, TimedValue<V> using) {
     long initialPosition = in.readPosition();
@@ -57,7 +57,7 @@
     // Deserialize object V (remaining bytes)
     byte[] serializedV = new byte[vLength];
     in.read(serializedV, 0, vLength);
-    V v = serializer.deserialize(serializedV);
+    V v = (V) CacheSerializers.getValueSerializer(name).deserialize(serializedV);
 
     using = new TimedValue<>(v, created);
 
@@ -66,7 +66,7 @@
 
   @Override
   public void write(Bytes out, TimedValue<V> toWrite) {
-    byte[] serialized = serializer.serialize(toWrite.getValue());
+    byte[] serialized = CacheSerializers.getValueSerializer(name).serialize(toWrite.getValue());
 
     // Serialize as follows:
     // created | length of serialized V | serialized value V
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheIT.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheIT.java
index df6fc38..36ce53c 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheIT.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheIT.java
@@ -48,7 +48,7 @@
     final int negativeDiskLimit = -1;
     final Cache<String, String> cache =
         persistentCacheFactory.build(
-            new TestPersistentCacheDef("foo", negativeDiskLimit), CacheBackend.CAFFEINE);
+            new TestPersistentCacheDef("foo", null, negativeDiskLimit), CacheBackend.CAFFEINE);
 
     assertThat(cache.getClass().getSimpleName()).isEqualTo("CaffeinatedGuavaCache");
   }
@@ -58,7 +58,7 @@
     final int positiveDiskLimit = 1024;
     assertThat(
             persistentCacheFactory.build(
-                new TestPersistentCacheDef("foo", positiveDiskLimit), CacheBackend.CAFFEINE))
+                new TestPersistentCacheDef("foo", null, positiveDiskLimit), CacheBackend.CAFFEINE))
         .isInstanceOf(ChronicleMapCacheImpl.class);
   }
 
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 e8c1f4a..5d380ff 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
@@ -25,6 +25,7 @@
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Guice;
@@ -47,6 +48,7 @@
 import org.junit.rules.TemporaryFolder;
 
 public class ChronicleMapCacheTest {
+  private static final String TEST_CACHE_NAME = "test-cache-name";
   @Inject MetricMaker metricMaker;
   @Inject MetricRegistry metricRegistry;
 
@@ -58,6 +60,8 @@
 
   @Before
   public void setUp() throws Exception {
+    CacheSerializers.registerCacheKeySerializer(TEST_CACHE_NAME, StringCacheSerializer.INSTANCE);
+    CacheSerializers.registerCacheValueSerializer(TEST_CACHE_NAME, StringCacheSerializer.INSTANCE);
     sitePaths = new SitePaths(temporaryFolder.newFolder().toPath());
     Files.createDirectories(sitePaths.etc_dir);
 
@@ -82,7 +86,7 @@
   }
 
   @Test
-  public void getIfPresentShouldReturnNullWhenThereisNoCachedValue() throws Exception {
+  public void getIfPresentShouldReturnNullWhenThereIsNoCachedValue() throws Exception {
     assertThat(newCacheWithLoader(null).getIfPresent("foo")).isNull();
   }
 
@@ -237,7 +241,7 @@
   @Test
   public void getIfPresentShouldReturnNullWhenValueIsExpired() throws Exception {
     ChronicleMapCacheImpl<String, String> cache =
-        newCache(true, null, Duration.ofSeconds(1), null, 1);
+        newCache(true, TEST_CACHE_NAME, null, Duration.ofSeconds(1), null, 1);
     cache.put("foo", "some-stale-value");
     Thread.sleep(1010); // Allow cache entry to expire
     assertThat(cache.getIfPresent("foo")).isNull();
@@ -247,7 +251,7 @@
   public void getShouldRefreshValueWhenExpired() throws Exception {
     String newCachedValue = UUID.randomUUID().toString();
     ChronicleMapCacheImpl<String, String> cache =
-        newCache(true, newCachedValue, null, Duration.ofSeconds(1), 1);
+        newCache(true, TEST_CACHE_NAME, newCachedValue, null, Duration.ofSeconds(1), 1);
     cache.put("foo", "some-stale-value");
     Thread.sleep(1010); // Allow cache to be flagged as needing refresh
     assertThat(cache.get("foo")).isEqualTo(newCachedValue);
@@ -256,7 +260,7 @@
   @Test
   public void shouldPruneExpiredValues() throws Exception {
     ChronicleMapCacheImpl<String, String> cache =
-        newCache(true, null, Duration.ofSeconds(1), null, 1);
+        newCache(true, TEST_CACHE_NAME, null, Duration.ofSeconds(1), null, 1);
     cache.put("foo1", "some-stale-value1");
     cache.put("foo2", "some-stale-value1");
     Thread.sleep(1010); // Allow cache entries to expire
@@ -293,10 +297,10 @@
   public void shouldEvictOldestElementInCacheWhenIsNeverAccessed() throws Exception {
     final String fooValue = "foo";
 
-    gerritConfig.setInt("cache", "foo", "maxEntries", 2);
-    gerritConfig.setInt("cache", "foo", "percentageHotKeys", 10);
-    gerritConfig.setInt("cache", "foo", "avgKeySize", "foo1".getBytes().length);
-    gerritConfig.setInt("cache", "foo", "avgValueSize", valueSize(fooValue));
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", 2);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "percentageHotKeys", 10);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgKeySize", "foo1".getBytes().length);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgValueSize", valueSize(fooValue));
     gerritConfig.save();
 
     ChronicleMapCacheImpl<String, String> cache = newCacheWithLoader(fooValue);
@@ -313,10 +317,10 @@
   public void shouldEvictRecentlyInsertedElementInCacheWhenOldestElementIsAccessed()
       throws Exception {
     final String fooValue = "foo";
-    gerritConfig.setInt("cache", "foo", "maxEntries", 2);
-    gerritConfig.setInt("cache", "foo", "percentageHotKeys", 10);
-    gerritConfig.setInt("cache", "foo", "avgKeySize", "foo1".getBytes().length);
-    gerritConfig.setInt("cache", "foo", "avgValueSize", valueSize(fooValue));
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", 2);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "percentageHotKeys", 10);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgKeySize", "foo1".getBytes().length);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgValueSize", valueSize(fooValue));
     gerritConfig.save();
 
     ChronicleMapCacheImpl<String, String> cache = newCacheWithLoader(fooValue);
@@ -354,13 +358,13 @@
   @Test
   public void shouldTriggerPercentageFreeMetric() throws Exception {
     String cachedValue = UUID.randomUUID().toString();
-    String freeSpaceMetricName = "cache/chroniclemap/percentage_free_space_" + cachedValue;
-    gerritConfig.setInt("cache", cachedValue, "maxEntries", 2);
-    gerritConfig.setInt("cache", cachedValue, "avgKeySize", cachedValue.getBytes().length);
-    gerritConfig.setInt("cache", cachedValue, "avgValueSize", valueSize(cachedValue));
+    String freeSpaceMetricName = "cache/chroniclemap/percentage_free_space_" + TEST_CACHE_NAME;
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", 2);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgKeySize", cachedValue.getBytes().length);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgValueSize", valueSize(cachedValue));
     gerritConfig.save();
 
-    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(TEST_CACHE_NAME, cachedValue);
 
     assertThat(getMetric(freeSpaceMetricName).getValue()).isEqualTo(100);
 
@@ -373,13 +377,13 @@
   @Test
   public void shouldTriggerRemainingAutoResizeMetric() throws Exception {
     String cachedValue = UUID.randomUUID().toString();
-    String autoResizeMetricName = "cache/chroniclemap/remaining_autoresizes_" + cachedValue;
-    gerritConfig.setInt("cache", cachedValue, "maxEntries", 2);
-    gerritConfig.setInt("cache", cachedValue, "avgKeySize", cachedValue.getBytes().length);
-    gerritConfig.setInt("cache", cachedValue, "avgValueSize", valueSize(cachedValue));
+    String autoResizeMetricName = "cache/chroniclemap/remaining_autoresizes_" + TEST_CACHE_NAME;
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", 2);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgKeySize", cachedValue.getBytes().length);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "avgValueSize", valueSize(cachedValue));
     gerritConfig.save();
 
-    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(TEST_CACHE_NAME, cachedValue);
 
     assertThat(getMetric(autoResizeMetricName).getValue()).isEqualTo(1);
 
@@ -397,12 +401,12 @@
     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);
+    String hotKeysCapacityMetricName = "cache/chroniclemap/hot_keys_capacity_" + TEST_CACHE_NAME;
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", maxEntries);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "percentageHotKeys", percentageHotKeys);
     gerritConfig.save();
 
-    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(TEST_CACHE_NAME, cachedValue);
 
     assertThat(getMetric(hotKeysCapacityMetricName).getValue()).isEqualTo(expectedCapacity);
   }
@@ -414,12 +418,12 @@
     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);
+    String hotKeysSizeMetricName = "cache/chroniclemap/hot_keys_size_" + TEST_CACHE_NAME;
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", maxEntries);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "percentageHotKeys", percentageHotKeys);
     gerritConfig.save();
 
-    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(TEST_CACHE_NAME, cachedValue);
 
     assertThat(getMetric(hotKeysSizeMetricName).getValue()).isEqualTo(0);
 
@@ -448,12 +452,12 @@
     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);
+    String hotKeysSizeMetricName = "cache/chroniclemap/hot_keys_size_" + TEST_CACHE_NAME;
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "maxEntries", maxEntries);
+    gerritConfig.setInt("cache", TEST_CACHE_NAME, "percentageHotKeys", percentageHotKeys);
     gerritConfig.save();
 
-    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(cachedValue);
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(TEST_CACHE_NAME, cachedValue);
 
     for (int i = 0; i < maxHotKeyCapacity; i++) {
       cache.put(cachedValue + i, cachedValue);
@@ -478,7 +482,7 @@
     String autoResizeMetricName = "cache/chroniclemap/remaining_autoresizes_" + sanitized;
     String hotKeyCapacityMetricName = "cache/chroniclemap/hot_keys_capacity_" + sanitized;
 
-    newCacheWithMetrics(cacheName);
+    newCacheWithMetrics(cacheName, null);
 
     getMetric(hotKeySizeMetricName);
     getMetric(percentageFreeMetricName);
@@ -487,44 +491,61 @@
   }
 
   private int valueSize(String value) {
-    final TimedValueMarshaller<String> marshaller =
-        new TimedValueMarshaller<>(StringCacheSerializer.INSTANCE);
+    final TimedValueMarshaller<String> marshaller = new TimedValueMarshaller<>(TEST_CACHE_NAME);
 
     Bytes<ByteBuffer> out = Bytes.elasticByteBuffer();
     marshaller.write(out, new TimedValue<>(value));
     return out.toByteArray().length;
   }
 
-  private ChronicleMapCacheImpl<String, String> newCacheWithMetrics(String cachedValue)
+  private ChronicleMapCacheImpl<String, String> newCacheWithMetrics(
+      String cacheName, @Nullable String cachedValue) throws IOException {
+    return newCache(true, cacheName, cachedValue, null, null, null, null, 1, metricMaker);
+  }
+
+  private ChronicleMapCacheImpl<String, String> newCacheWithMetrics(
+      String cacheName,
+      String cachedValue,
+      CacheSerializer<String> keySerializer,
+      CacheSerializer<String> valueSerializer)
       throws IOException {
-    return newCache(true, cachedValue, null, null, 1, metricMaker);
+    return newCache(
+        true, cacheName, cachedValue, null, null, null, null, 1, new DisabledMetricMaker());
   }
 
   private ChronicleMapCacheImpl<String, String> newCache(
       Boolean withLoader,
-      @Nullable String cachedValue,
+      String cacheName,
+      @Nullable String loadedValue,
       @Nullable Duration expireAfterWrite,
       @Nullable Duration refreshAfterWrite,
       Integer version)
       throws IOException {
     return newCache(
         withLoader,
-        cachedValue,
+        cacheName,
+        loadedValue,
         expireAfterWrite,
         refreshAfterWrite,
+        null,
+        null,
         version,
         new DisabledMetricMaker());
   }
 
   private ChronicleMapCacheImpl<String, String> newCache(
       Boolean withLoader,
+      String cacheName,
       @Nullable String cachedValue,
       @Nullable Duration expireAfterWrite,
       @Nullable Duration refreshAfterWrite,
+      @Nullable CacheSerializer<String> keySerializer,
+      @Nullable CacheSerializer<String> valueSerializer,
       Integer version,
       MetricMaker metricMaker)
       throws IOException {
-    TestPersistentCacheDef cacheDef = new TestPersistentCacheDef(cachedValue);
+    TestPersistentCacheDef cacheDef =
+        new TestPersistentCacheDef(cacheName, cachedValue, keySerializer, valueSerializer);
 
     File persistentFile =
         ChronicleMapCacheFactory.fileName(
@@ -542,21 +563,21 @@
         cacheDef, config, withLoader ? cacheDef.loader() : null, metricMaker);
   }
 
-  private ChronicleMapCacheImpl<String, String> newCacheWithLoader(@Nullable String cachedValue)
+  private ChronicleMapCacheImpl<String, String> newCacheWithLoader(@Nullable String loadedValue)
       throws IOException {
-    return newCache(true, cachedValue, null, null, 1);
+    return newCache(true, TEST_CACHE_NAME, loadedValue, null, null, 1);
   }
 
   private ChronicleMapCacheImpl<String, String> newCacheWithLoader() throws IOException {
-    return newCache(true, null, null, null, 1);
+    return newCache(true, TEST_CACHE_NAME, null, null, null, 1);
   }
 
   private ChronicleMapCacheImpl<String, String> newCacheVersion(int version) throws IOException {
-    return newCache(true, null, null, null, version);
+    return newCache(true, TEST_CACHE_NAME, null, null, null, version);
   }
 
   private ChronicleMapCacheImpl<String, String> newCacheWithoutLoader() throws IOException {
-    return newCache(false, null, null, null, 1);
+    return newCache(false, TEST_CACHE_NAME, null, null, null, 1);
   }
 
   private <V> Gauge<V> getMetric(String name) {
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapMarshallerAdapterTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapMarshallerAdapterTest.java
deleted file mode 100644
index 09f7f17..0000000
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapMarshallerAdapterTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed 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.
-// limitations under the License.
-// See the License for the specific language governing permissions and
-package com.googlesource.gerrit.modules.cache.chroniclemap;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import net.openhft.chronicle.bytes.Bytes;
-import org.eclipse.jgit.lib.ObjectId;
-import org.junit.Test;
-
-public class ChronicleMapMarshallerAdapterTest {
-
-  @Test
-  public void shouldDeserializeToTheSameObject() {
-    ObjectId id = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
-    byte[] serializedId = ObjectIdCacheSerializer.INSTANCE.serialize(id);
-    ByteBuffer serializedIdWithLen = ByteBuffer.allocate(serializedId.length + 4);
-    serializedIdWithLen.order(ByteOrder.LITTLE_ENDIAN);
-    serializedIdWithLen.putInt(serializedId.length);
-    serializedIdWithLen.put(serializedId);
-
-    ChronicleMapMarshallerAdapter<ObjectId> marshaller =
-        new ChronicleMapMarshallerAdapter<>(ObjectIdCacheSerializer.INSTANCE);
-    ObjectId out = marshaller.read(Bytes.allocateDirect(serializedIdWithLen.array()), null);
-
-    assertThat(id).isEqualTo(out);
-  }
-
-  @Test
-  public void shouldSerializeToTheSameObject() {
-    ObjectId id = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
-    ChronicleMapMarshallerAdapter<ObjectId> marshaller =
-        new ChronicleMapMarshallerAdapter<>(ObjectIdCacheSerializer.INSTANCE);
-
-    Bytes<ByteBuffer> out = Bytes.elasticByteBuffer();
-    marshaller.write(out, id);
-    assertThat(marshaller.read(out, null)).isEqualTo(id);
-  }
-}
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapperMarshallerTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapperMarshallerTest.java
new file mode 100644
index 0000000..44a5dd6
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/KeyWrapperMarshallerTest.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.modules.cache.chroniclemap;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
+import java.nio.ByteBuffer;
+import net.openhft.chronicle.bytes.Bytes;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
+import org.junit.Test;
+
+public class KeyWrapperMarshallerTest {
+  private static final String TEST_CACHE_NAME = "key-wrapper-test";
+
+  @Before
+  public void setup() {
+    CacheSerializers.registerCacheKeySerializer(TEST_CACHE_NAME, ObjectIdCacheSerializer.INSTANCE);
+  }
+
+  @Test
+  public void shouldSerializeAndDeserializeBack() {
+    ObjectId id = ObjectId.fromString("1234567890123456789012345678901234567890");
+    KeyWrapperMarshaller<ObjectId> marshaller = new KeyWrapperMarshaller<>(TEST_CACHE_NAME);
+
+    final KeyWrapper<ObjectId> wrapped = new KeyWrapper<>(id);
+
+    Bytes<ByteBuffer> out = Bytes.elasticByteBuffer();
+    marshaller.write(out, wrapped);
+    final KeyWrapper<ObjectId> actual = marshaller.read(out, null);
+    assertThat(actual).isEqualTo(wrapped);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TestPersistentCacheDef.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TestPersistentCacheDef.java
index b81baec..0e52f1d 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TestPersistentCacheDef.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TestPersistentCacheDef.java
@@ -21,42 +21,61 @@
 import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
 import com.google.inject.TypeLiteral;
 import java.time.Duration;
+import java.util.Optional;
 import java.util.UUID;
 
 public class TestPersistentCacheDef implements PersistentCacheDef<String, String> {
 
   private static final Integer DEFAULT_DISK_LIMIT = 1024;
 
+  private final String name;
   private final String loadedValue;
   private final Duration expireAfterWrite;
   private final Duration refreshAfterWrite;
   private final Integer diskLimit;
+  private final CacheSerializer<String> keySerializer;
+  private final CacheSerializer<String> valueSerializer;
 
   public TestPersistentCacheDef(
-      String loadedValue,
+      String name,
+      @Nullable String loadedValue,
       @Nullable Duration expireAfterWrite,
       @Nullable Duration refreshAfterWrite) {
 
+    this.name = name;
     this.loadedValue = loadedValue;
     this.expireAfterWrite = expireAfterWrite;
     this.refreshAfterWrite = refreshAfterWrite;
     this.diskLimit = DEFAULT_DISK_LIMIT;
+    this.keySerializer = StringCacheSerializer.INSTANCE;
+    this.valueSerializer = StringCacheSerializer.INSTANCE;
   }
 
-  public TestPersistentCacheDef(String loadedValue, Integer diskLimit) {
+  public TestPersistentCacheDef(String name, @Nullable String loadedValue, Integer diskLimit) {
 
+    this.name = name;
     this.loadedValue = loadedValue;
     this.expireAfterWrite = null;
     this.refreshAfterWrite = null;
     this.diskLimit = diskLimit;
+    this.keySerializer = StringCacheSerializer.INSTANCE;
+    this.valueSerializer = StringCacheSerializer.INSTANCE;
   }
 
-  public TestPersistentCacheDef(String loadedValue) {
+  public TestPersistentCacheDef(
+      String name,
+      @Nullable String loadedValue,
+      @Nullable CacheSerializer<String> keySerializer,
+      @Nullable CacheSerializer<String> valueSerializer) {
 
+    this.name = name;
     this.loadedValue = loadedValue;
     this.expireAfterWrite = Duration.ZERO;
     this.refreshAfterWrite = Duration.ZERO;
     this.diskLimit = DEFAULT_DISK_LIMIT;
+    this.keySerializer = Optional.ofNullable(keySerializer).orElse(StringCacheSerializer.INSTANCE);
+    this.valueSerializer =
+        Optional.ofNullable(valueSerializer).orElse(StringCacheSerializer.INSTANCE);
   }
 
   @Override
@@ -71,17 +90,17 @@
 
   @Override
   public CacheSerializer<String> keySerializer() {
-    return StringCacheSerializer.INSTANCE;
+    return keySerializer;
   }
 
   @Override
   public CacheSerializer<String> valueSerializer() {
-    return StringCacheSerializer.INSTANCE;
+    return valueSerializer;
   }
 
   @Override
   public String name() {
-    return loadedValue;
+    return name;
   }
 
   @Override
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshallerTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshallerTest.java
index 62c1d44..b630e36 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshallerTest.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/TimedValueMarshallerTest.java
@@ -19,16 +19,23 @@
 import java.nio.ByteBuffer;
 import net.openhft.chronicle.bytes.Bytes;
 import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
 import org.junit.Test;
 
 public class TimedValueMarshallerTest {
+  private static final String TEST_CACHE_NAME = "timed-value-cache";
+
+  @Before
+  public void setup() {
+    CacheSerializers.registerCacheValueSerializer(
+        TEST_CACHE_NAME, ObjectIdCacheSerializer.INSTANCE);
+  }
 
   @Test
   public void shouldSerializeAndDeserializeBack() {
     ObjectId id = ObjectId.fromString("1234567890123456789012345678901234567890");
     long timestamp = 1600329018L;
-    TimedValueMarshaller<ObjectId> marshaller =
-        new TimedValueMarshaller<>(ObjectIdCacheSerializer.INSTANCE);
+    TimedValueMarshaller<ObjectId> marshaller = new TimedValueMarshaller<>(TEST_CACHE_NAME);
 
     final TimedValue<ObjectId> wrapped = new TimedValue<>(id, timestamp);