Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions core/src/main/java/google/registry/cache/CacheMetrics.java
Original file line number Diff line number Diff line change
@@ -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, if the object was found."));

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());
}
}
13 changes: 7 additions & 6 deletions core/src/main/java/google/registry/cache/CacheModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,23 @@ public static Optional<SimplifiedJedisClient> provideJedisClient(Optional<Unifie
@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -51,26 +54,30 @@ protected Optional<V> loadFromCaches(Class<V> clazz, String key) {
// 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;
}

// if not, try the remote cache
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
*/
public class MultilayerHostCache extends MultilayerEppResourceCache<Host> implements HostCache {

public MultilayerHostCache(SimplifiedJedisClient jedisClient) {
super(jedisClient);
public MultilayerHostCache(SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
super(jedisClient, cacheMetrics);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand All @@ -59,10 +60,13 @@ void testLoad_fromDatabase_populatesCaches() {
// 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
Expand All @@ -72,6 +76,8 @@ void testLoad_fromValkey() {
// 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
Expand All @@ -83,11 +89,15 @@ void testSkipsTestTld() {

// 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,10 +54,13 @@ void testLoad_fromDatabase_populatesCaches() {
// 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
Expand All @@ -66,10 +70,14 @@ void testLoad_fromValkey() {
// 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);
}
}
Loading