1
0
mirror of https://github.com/google/nomulus synced 2026-07-11 18:42:25 +00:00

Project domains and hosts on cache retrieval (#3143)

It's not perfect because we're running outside of a transaction, but we
should make a best-effort attempt to project domains/hosts to the
current time.
This commit is contained in:
gbrodman
2026-07-10 13:15:59 -04:00
committed by GitHub
parent b8a51cacc4
commit 4df734da2a
7 changed files with 92 additions and 23 deletions
+9 -4
View File
@@ -38,6 +38,7 @@ import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Optional;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
@@ -106,12 +107,16 @@ public final class CacheModule {
@Provides
@Singleton
public static HostCache provideHostCache(
Optional<SimplifiedJedisClient> jedisClient, CacheMetrics cacheMetrics) {
Optional<SimplifiedJedisClient> jedisClient, Clock clock, CacheMetrics cacheMetrics) {
if (jedisClient.isEmpty()) {
return repoId ->
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));
return repoId -> {
Instant now = clock.now();
return Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)))
.filter(host -> now.isBefore(host.getDeletionTime()))
.map(host -> (Host) host.cloneProjectedAtTime(now));
};
}
return new MultilayerHostCache(jedisClient.get(), cacheMetrics);
return new MultilayerHostCache(jedisClient.get(), clock, cacheMetrics);
}
private static SSLSocketFactory createValkeySslSocketFactory(String valkeyCertificateAuthority) {
@@ -19,7 +19,6 @@ import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.util.Clock;
import java.time.Instant;
import java.util.Optional;
/**
@@ -30,12 +29,9 @@ import java.util.Optional;
public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
implements DomainCache {
private final Clock clock;
public MultilayerDomainCache(
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
super(jedisClient, cacheMetrics);
this.clock = clock;
super(jedisClient, clock, cacheMetrics);
}
@Override
@@ -46,15 +42,10 @@ public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
@Override
protected Optional<Domain> loadFromDatabase(String domainName) {
// Don't use the cache (avoid caching the same domain twice). Do use the replica SQL instance.
Optional<Domain> possibleDomain =
Optional.ofNullable(
ForeignKeyUtils.loadMostRecentResourceObjects(
Domain.class, ImmutableList.of(domainName), true)
.get(domainName));
Instant now = clock.now();
return possibleDomain
.filter(domain -> now.isBefore(domain.getDeletionTime()))
.map(domain -> domain.cloneProjectedAtTime(now));
return Optional.ofNullable(
ForeignKeyUtils.loadMostRecentResourceObjects(
Domain.class, ImmutableList.of(domainName), true)
.get(domainName));
}
@Override
@@ -18,7 +18,9 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import google.registry.config.RegistryConfig;
import google.registry.model.EppResource;
import google.registry.util.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
/**
@@ -36,11 +38,13 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
.build();
private final SimplifiedJedisClient jedisClient;
private final Clock clock;
private final CacheMetrics cacheMetrics;
protected MultilayerEppResourceCache(
SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
this.jedisClient = jedisClient;
this.clock = clock;
this.cacheMetrics = cacheMetrics;
}
@@ -50,7 +54,16 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
return true;
}
@SuppressWarnings("unchecked")
protected Optional<V> loadFromCaches(Class<V> clazz, String key) {
Instant now = clock.now();
return (Optional<V>)
loadFromCachesInternal(clazz, key)
.filter(v -> now.isBefore(v.getDeletionTime()))
.map(v -> v.cloneProjectedAtTime(now));
}
private Optional<V> loadFromCachesInternal(Class<V> clazz, String key) {
// hopefully the resource is in the local cache
Optional<V> possibleValue = Optional.ofNullable(localCache.getIfPresent(key));
if (possibleValue.isPresent()) {
@@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import google.registry.util.Clock;
import java.util.Optional;
/**
@@ -27,8 +28,9 @@ import java.util.Optional;
*/
public class MultilayerHostCache extends MultilayerEppResourceCache<Host> implements HostCache {
public MultilayerHostCache(SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
super(jedisClient, cacheMetrics);
public MultilayerHostCache(
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
super(jedisClient, clock, cacheMetrics);
}
@Override
@@ -24,11 +24,14 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import java.time.Duration;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -100,4 +103,39 @@ public class MultilayerDomainCacheTest {
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.MISS_NONEXISTENT);
verifyNoMoreInteractions(cacheMetrics);
}
@Test
void testLoad_filtersOutDeletedDomain() {
Domain domain =
persistActiveDomain("example.tld")
.asBuilder()
.setDeletionTime(clock.now().plus(Duration.ofDays(1)))
.build();
when(jedisClient.get(Domain.class, "example.tld")).thenReturn(Optional.of(domain));
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
clock.advanceBy(Duration.ofDays(2));
assertThat(cache.loadByDomainName("example.tld")).isEmpty();
}
@Test
void testLoad_projectsToCurrentTime() {
Domain domain =
persistActiveDomain("example.tld")
.asBuilder()
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.ADD,
"example.tld",
clock.now().plus(Duration.ofDays(5)),
"TheRegistrar",
null))
.build();
when(jedisClient.get(Domain.class, "example.tld")).thenReturn(Optional.of(domain));
assertThat(cache.loadByDomainName("example.tld").get().getGracePeriods())
.containsExactlyElementsIn(domain.getGracePeriods());
clock.advanceBy(Duration.ofDays(10));
assertThat(cache.loadByDomainName("example.tld").get().getGracePeriods()).isEmpty();
}
}
@@ -25,6 +25,8 @@ import google.registry.model.host.Host;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import java.time.Duration;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -38,12 +40,13 @@ public class MultilayerHostCacheTest {
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final SimplifiedJedisClient jedisClient = mock(SimplifiedJedisClient.class);
private final FakeClock clock = new FakeClock();
private final CacheMetrics cacheMetrics = mock(CacheMetrics.class);
private MultilayerHostCache cache;
@BeforeEach
void beforeEach() {
cache = new MultilayerHostCache(jedisClient, cacheMetrics);
cache = new MultilayerHostCache(jedisClient, clock, cacheMetrics);
}
@Test
@@ -80,4 +83,18 @@ public class MultilayerHostCacheTest {
verify(cacheMetrics).recordLookup("Host", CacheMetrics.CacheHitType.MISS_NONEXISTENT);
verifyNoMoreInteractions(cacheMetrics);
}
@Test
void testLoad_filtersOutDeletedHost() {
Host host =
persistActiveHost("ns1.example.tld")
.asBuilder()
.setDeletionTime(clock.now().plus(Duration.ofDays(1)))
.build();
when(jedisClient.get(Host.class, host.getRepoId())).thenReturn(Optional.of(host));
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
clock.advanceBy(Duration.ofDays(2));
assertThat(cache.loadByRepoId(host.getRepoId())).isEmpty();
}
}
@@ -73,7 +73,10 @@ class RdapTestHelper {
"We reserve the right to modify this agreement at any time.");
rdapJsonFormatter.rdapTosStaticUrl = "https://www.example.tld/about/rdap/tos.html";
rdapJsonFormatter.hostCache =
(repoId) -> Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));
(repoId) ->
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)))
.filter(host -> clock.now().isBefore(host.getDeletionTime()))
.map(host -> (Host) host.cloneProjectedAtTime(clock.now()));
return rdapJsonFormatter;
}