diff --git a/core/src/main/java/google/registry/cache/CacheModule.java b/core/src/main/java/google/registry/cache/CacheModule.java index 0b618867604..ec8e46e1806 100644 --- a/core/src/main/java/google/registry/cache/CacheModule.java +++ b/core/src/main/java/google/registry/cache/CacheModule.java @@ -38,6 +38,7 @@ 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 static DomainCache provideDomainCache( @Provides @Singleton public static HostCache provideHostCache( - Optional jedisClient, CacheMetrics cacheMetrics) { + Optional 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) { diff --git a/core/src/main/java/google/registry/cache/MultilayerDomainCache.java b/core/src/main/java/google/registry/cache/MultilayerDomainCache.java index 0f0bce054db..fdef3fb9c2f 100644 --- a/core/src/main/java/google/registry/cache/MultilayerDomainCache.java +++ b/core/src/main/java/google/registry/cache/MultilayerDomainCache.java @@ -19,7 +19,6 @@ 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 @@ public class MultilayerDomainCache extends MultilayerEppResourceCache 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 Optional loadByDomainName(String domainName) { @Override protected Optional loadFromDatabase(String domainName) { // Don't use the cache (avoid caching the same domain twice). Do use the replica SQL instance. - Optional 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 diff --git a/core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java b/core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java index 36cff38ae83..54f5b895a79 100644 --- a/core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java +++ b/core/src/main/java/google/registry/cache/MultilayerEppResourceCache.java @@ -18,7 +18,9 @@ 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 { .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 @@ protected boolean shouldPersistToRemoteCache(V value) { return true; } + @SuppressWarnings("unchecked") protected Optional loadFromCaches(Class clazz, String key) { + Instant now = clock.now(); + return (Optional) + loadFromCachesInternal(clazz, key) + .filter(v -> now.isBefore(v.getDeletionTime())) + .map(v -> v.cloneProjectedAtTime(now)); + } + + private Optional loadFromCachesInternal(Class clazz, String key) { // hopefully the resource is in the local cache Optional possibleValue = Optional.ofNullable(localCache.getIfPresent(key)); if (possibleValue.isPresent()) { diff --git a/core/src/main/java/google/registry/cache/MultilayerHostCache.java b/core/src/main/java/google/registry/cache/MultilayerHostCache.java index a0b4f587fd1..f596459c7f7 100644 --- a/core/src/main/java/google/registry/cache/MultilayerHostCache.java +++ b/core/src/main/java/google/registry/cache/MultilayerHostCache.java @@ -18,6 +18,7 @@ import google.registry.model.host.Host; import google.registry.persistence.VKey; +import google.registry.util.Clock; import java.util.Optional; /** @@ -27,8 +28,9 @@ */ public class MultilayerHostCache extends MultilayerEppResourceCache implements HostCache { - public MultilayerHostCache(SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) { - super(jedisClient, cacheMetrics); + public MultilayerHostCache( + SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) { + super(jedisClient, clock, cacheMetrics); } @Override diff --git a/core/src/test/java/google/registry/cache/MultilayerDomainCacheTest.java b/core/src/test/java/google/registry/cache/MultilayerDomainCacheTest.java index b09d9c399ea..0955a713fab 100644 --- a/core/src/test/java/google/registry/cache/MultilayerDomainCacheTest.java +++ b/core/src/test/java/google/registry/cache/MultilayerDomainCacheTest.java @@ -24,11 +24,14 @@ 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 @@ void testLoad_missing() { 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(); + } } diff --git a/core/src/test/java/google/registry/cache/MultilayerHostCacheTest.java b/core/src/test/java/google/registry/cache/MultilayerHostCacheTest.java index 9bf5107ec7b..3f089166504 100644 --- a/core/src/test/java/google/registry/cache/MultilayerHostCacheTest.java +++ b/core/src/test/java/google/registry/cache/MultilayerHostCacheTest.java @@ -25,6 +25,8 @@ 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 @@ void testLoad_missing() { 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(); + } } diff --git a/core/src/test/java/google/registry/rdap/RdapTestHelper.java b/core/src/test/java/google/registry/rdap/RdapTestHelper.java index ec691b12b34..845785c51b2 100644 --- a/core/src/test/java/google/registry/rdap/RdapTestHelper.java +++ b/core/src/test/java/google/registry/rdap/RdapTestHelper.java @@ -73,7 +73,10 @@ static RdapJsonFormatter getTestRdapJsonFormatter(Clock clock) { "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; }