mirror of
https://github.com/google/nomulus
synced 2026-05-13 11:21:46 +00:00
Add (remote) cache metrics (#3033)
This only applies to the CacheModule-provided caches because we don't want to have to deal with all the various other caches. We'll want to know the various ratios between types of cache hits/misses when evaluating the usefulness of the remote caching.
This commit is contained in:
51
core/src/main/java/google/registry/cache/CacheMetrics.java
vendored
Normal file
51
core/src/main/java/google/registry/cache/CacheMetrics.java
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.cache;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.monitoring.metrics.IncrementableMetric;
|
||||
import com.google.monitoring.metrics.LabelDescriptor;
|
||||
import com.google.monitoring.metrics.MetricRegistryImpl;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
/** Metrics tracking effectiveness of local and remote EPP resource caching. */
|
||||
@Singleton
|
||||
public class CacheMetrics {
|
||||
|
||||
public enum CacheHitType {
|
||||
LOCAL,
|
||||
REMOTE,
|
||||
MISS,
|
||||
MISS_NONEXISTENT
|
||||
}
|
||||
|
||||
private static final ImmutableSet<LabelDescriptor> LABEL_DESCRIPTORS =
|
||||
ImmutableSet.of(
|
||||
LabelDescriptor.create("cache_name", "The type of the cache (domain/host)."),
|
||||
LabelDescriptor.create("hit_type", "The type of cache hit or miss."));
|
||||
|
||||
private static final IncrementableMetric cacheLookups =
|
||||
MetricRegistryImpl.getDefault()
|
||||
.newIncrementableMetric(
|
||||
"/cache/lookups", "Count of cache lookups", "count", LABEL_DESCRIPTORS);
|
||||
|
||||
@Inject
|
||||
public CacheMetrics() {}
|
||||
|
||||
public void recordLookup(String cacheName, CacheHitType hitType) {
|
||||
cacheLookups.increment(cacheName, hitType.toString());
|
||||
}
|
||||
}
|
||||
@@ -92,22 +92,23 @@ public final class CacheModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
public static DomainCache provideDomainCache(
|
||||
Optional<SimplifiedJedisClient> domainJedisClient, Clock clock) {
|
||||
if (domainJedisClient.isEmpty()) {
|
||||
Optional<SimplifiedJedisClient> jedisClient, Clock clock, CacheMetrics cacheMetrics) {
|
||||
if (jedisClient.isEmpty()) {
|
||||
return domainName ->
|
||||
ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now());
|
||||
}
|
||||
return new MultilayerDomainCache(domainJedisClient.get(), clock);
|
||||
return new MultilayerDomainCache(jedisClient.get(), clock, cacheMetrics);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static HostCache provideHostCache(Optional<SimplifiedJedisClient> hostJedisClient) {
|
||||
if (hostJedisClient.isEmpty()) {
|
||||
public static HostCache provideHostCache(
|
||||
Optional<SimplifiedJedisClient> jedisClient, CacheMetrics cacheMetrics) {
|
||||
if (jedisClient.isEmpty()) {
|
||||
return repoId ->
|
||||
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));
|
||||
}
|
||||
return new MultilayerHostCache(hostJedisClient.get());
|
||||
return new MultilayerHostCache(jedisClient.get(), cacheMetrics);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -32,8 +32,9 @@ public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
public MultilayerDomainCache(SimplifiedJedisClient jedisClient, Clock clock) {
|
||||
super(jedisClient);
|
||||
public MultilayerDomainCache(
|
||||
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
|
||||
super(jedisClient, cacheMetrics);
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,12 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
|
||||
.build();
|
||||
|
||||
private final SimplifiedJedisClient jedisClient;
|
||||
private final CacheMetrics cacheMetrics;
|
||||
|
||||
protected MultilayerEppResourceCache(SimplifiedJedisClient jedisClient) {
|
||||
protected MultilayerEppResourceCache(
|
||||
SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
|
||||
this.jedisClient = jedisClient;
|
||||
this.cacheMetrics = cacheMetrics;
|
||||
}
|
||||
|
||||
protected abstract Optional<V> loadFromDatabase(String key);
|
||||
@@ -51,6 +54,7 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
|
||||
// hopefully the resource is in the local cache
|
||||
Optional<V> possibleValue = Optional.ofNullable(localCache.getIfPresent(key));
|
||||
if (possibleValue.isPresent()) {
|
||||
cacheMetrics.recordLookup(clazz.getSimpleName(), CacheMetrics.CacheHitType.LOCAL);
|
||||
return possibleValue;
|
||||
}
|
||||
|
||||
@@ -58,19 +62,22 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
|
||||
possibleValue = jedisClient.get(clazz, key);
|
||||
if (possibleValue.isPresent()) {
|
||||
localCache.put(key, possibleValue.get());
|
||||
cacheMetrics.recordLookup(clazz.getSimpleName(), CacheMetrics.CacheHitType.REMOTE);
|
||||
return possibleValue;
|
||||
}
|
||||
|
||||
// lastly, try the DB
|
||||
return loadFromDatabase(key)
|
||||
.map(
|
||||
v -> {
|
||||
// Optional has no direct "peek" functionality to fill the caches
|
||||
if (shouldPersistToRemoteCache(v)) {
|
||||
jedisClient.set(new SimplifiedJedisClient.JedisResource<>(key, v));
|
||||
}
|
||||
localCache.put(key, v);
|
||||
return v;
|
||||
});
|
||||
possibleValue = loadFromDatabase(key);
|
||||
if (possibleValue.isEmpty()) {
|
||||
cacheMetrics.recordLookup(clazz.getSimpleName(), CacheMetrics.CacheHitType.MISS_NONEXISTENT);
|
||||
return possibleValue;
|
||||
}
|
||||
V value = possibleValue.get();
|
||||
if (shouldPersistToRemoteCache(value)) {
|
||||
jedisClient.set(new SimplifiedJedisClient.JedisResource<>(key, value));
|
||||
}
|
||||
localCache.put(key, value);
|
||||
cacheMetrics.recordLookup(clazz.getSimpleName(), CacheMetrics.CacheHitType.MISS);
|
||||
return possibleValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import java.util.Optional;
|
||||
*/
|
||||
public class MultilayerHostCache extends MultilayerEppResourceCache<Host> implements HostCache {
|
||||
|
||||
public MultilayerHostCache(SimplifiedJedisClient jedisClient) {
|
||||
super(jedisClient);
|
||||
public MultilayerHostCache(SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
|
||||
super(jedisClient, cacheMetrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -43,11 +43,12 @@ public class MultilayerDomainCacheTest {
|
||||
|
||||
private final SimplifiedJedisClient jedisClient = mock(SimplifiedJedisClient.class);
|
||||
private final FakeClock clock = new FakeClock();
|
||||
private final CacheMetrics cacheMetrics = mock(CacheMetrics.class);
|
||||
private MultilayerDomainCache cache;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
cache = new MultilayerDomainCache(jedisClient, clock);
|
||||
cache = new MultilayerDomainCache(jedisClient, clock, cacheMetrics);
|
||||
createTld("tld");
|
||||
}
|
||||
|
||||
@@ -59,10 +60,13 @@ public class MultilayerDomainCacheTest {
|
||||
// We should have filled the caches after one attempt to load from Valkey
|
||||
verify(jedisClient).get(Domain.class, "example.tld");
|
||||
verify(jedisClient).set(new SimplifiedJedisClient.JedisResource<>("example.tld", domain));
|
||||
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.MISS);
|
||||
|
||||
// Further loads hit the local cache
|
||||
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
|
||||
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.LOCAL);
|
||||
verifyNoMoreInteractions(jedisClient);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -72,6 +76,8 @@ public class MultilayerDomainCacheTest {
|
||||
// We hit the Valkey cache first
|
||||
when(jedisClient.get(Domain.class, "example.tld")).thenReturn(Optional.of(domain));
|
||||
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
|
||||
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.REMOTE);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,11 +89,15 @@ public class MultilayerDomainCacheTest {
|
||||
|
||||
// This time, we don't populate the remote cache because it's prober data
|
||||
verify(jedisClient).get(Domain.class, "example.tld");
|
||||
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.MISS);
|
||||
verifyNoMoreInteractions(jedisClient);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_missing() {
|
||||
assertThat(cache.loadByDomainName("nonexistent.tld")).isEmpty();
|
||||
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.MISS_NONEXISTENT);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,12 @@ public class MultilayerHostCacheTest {
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
private final SimplifiedJedisClient jedisClient = mock(SimplifiedJedisClient.class);
|
||||
private final CacheMetrics cacheMetrics = mock(CacheMetrics.class);
|
||||
private MultilayerHostCache cache;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
cache = new MultilayerHostCache(jedisClient);
|
||||
cache = new MultilayerHostCache(jedisClient, cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,10 +54,13 @@ public class MultilayerHostCacheTest {
|
||||
// We should have filled the caches after one attempt to load from Valkey
|
||||
verify(jedisClient).get(Host.class, host.getRepoId());
|
||||
verify(jedisClient).set(new SimplifiedJedisClient.JedisResource<>(host.getRepoId(), host));
|
||||
verify(cacheMetrics).recordLookup("Host", CacheMetrics.CacheHitType.MISS);
|
||||
|
||||
// Further loads hit the local cache
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
|
||||
verify(cacheMetrics).recordLookup("Host", CacheMetrics.CacheHitType.LOCAL);
|
||||
verifyNoMoreInteractions(jedisClient);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,10 +70,14 @@ public class MultilayerHostCacheTest {
|
||||
// We hit the Valkey cache first
|
||||
when(jedisClient.get(Host.class, host.getRepoId())).thenReturn(Optional.of(host));
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
|
||||
verify(cacheMetrics).recordLookup("Host", CacheMetrics.CacheHitType.REMOTE);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_missing() {
|
||||
assertThat(cache.loadByRepoId("nonexistent")).isEmpty();
|
||||
verify(cacheMetrics).recordLookup("Host", CacheMetrics.CacheHitType.MISS_NONEXISTENT);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user