1
0
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:
gbrodman
2026-05-11 14:19:52 -04:00
committed by GitHub
parent b69d51add1
commit 5854ccf00d
7 changed files with 101 additions and 23 deletions

View 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());
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}