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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package google.registry.batch;

import static google.registry.flows.domain.DomainFlowUtils.isDomainEligibleForXap;
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC;
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
Expand All @@ -32,6 +33,7 @@
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.cache.SimplifiedJedisClient;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.common.Cursor;
import google.registry.model.domain.Domain;
Expand Down Expand Up @@ -87,13 +89,23 @@ public enum SyncStatus {
private final LockHandler lockHandler;
private final Response response;
private final Optional<SimplifiedJedisClient> jedisClient;
private final Duration domainExpiryAccessPeriodTotalLength;

@Inject
public SyncRemoteCacheAction(
LockHandler lockHandler, Response response, Optional<SimplifiedJedisClient> jedisClient) {
LockHandler lockHandler,
Response response,
Optional<SimplifiedJedisClient> jedisClient,
@Config("domainExpiryAccessPeriodTotalLength") Duration domainExpiryAccessPeriodTotalLength) {
this.lockHandler = lockHandler;
this.response = response;
this.jedisClient = jedisClient;
this.domainExpiryAccessPeriodTotalLength = domainExpiryAccessPeriodTotalLength;
}

public SyncRemoteCacheAction(
LockHandler lockHandler, Response response, Optional<SimplifiedJedisClient> jedisClient) {
this(lockHandler, response, jedisClient, Duration.ofDays(10));
}

@Override
Expand Down Expand Up @@ -187,10 +199,13 @@ private <T extends EppResource> void processResources(
ImmutableList.Builder<SimplifiedJedisClient.JedisResource<T>> toSaveBuilder =
new ImmutableList.Builder<>();

Instant now = tm().getTxTime();
for (T resource : resources) {
String key = getKeyFunction.apply(resource);
if (resource.getDeletionTime().isAfter(tm().getTxTime())) {
toSaveBuilder.add(new SimplifiedJedisClient.JedisResource<>(key, resource));
if (shouldSaveResourceInRemoteCache(resource, now)) {
toSaveBuilder.add(
new SimplifiedJedisClient.JedisResource<>(
key, resource, getExpirationTime(resource, now)));
} else {
toDeleteBuilder.add(key);
}
Expand All @@ -204,6 +219,32 @@ private <T extends EppResource> void processResources(
logger.atInfo().log("Set %d in the remote cache", toSave.size());
}

private <T extends EppResource> Optional<Instant> getExpirationTime(T resource, Instant now) {
if (resource instanceof Domain domain) {
Tld tld = Tld.get(domain.getTld());
if (isDomainInXap(domain, tld, now)) {
return Optional.of(domain.getDeletionTime().plus(domainExpiryAccessPeriodTotalLength));
}
}
return Optional.empty();
}

private <T extends EppResource> boolean shouldSaveResourceInRemoteCache(T resource, Instant now) {
if (resource.getDeletionTime().isAfter(now)) {
return true;
}
if (resource instanceof Domain domain) {
return isDomainInXap(domain, Tld.get(domain.getTld()), now);
}
return false;
}

private boolean isDomainInXap(Domain domain, Tld tld, Instant now) {
return tld.getExpiryAccessPeriodModeAt(now) == Tld.ExpiryAccessPeriodMode.ENABLED
&& isDomainEligibleForXap(domain, tld, now)
&& domain.getDeletionTime().isAfter(now.minus(domainExpiryAccessPeriodTotalLength));
}

private Instant getPreviousCursorTime(Cursor.CursorType cursorType) {
return tm().loadByKeyIfPresent(Cursor.createGlobalVKey(cursorType))
.map(Cursor::getCursorTime)
Expand Down
9 changes: 7 additions & 2 deletions core/src/main/java/google/registry/cache/CacheModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -96,12 +97,16 @@ public static Optional<SimplifiedJedisClient> provideJedisClient(Optional<Unifie
@Provides
@Singleton
public static DomainCache provideDomainCache(
Optional<SimplifiedJedisClient> jedisClient, Clock clock, CacheMetrics cacheMetrics) {
Optional<SimplifiedJedisClient> jedisClient,
Clock clock,
CacheMetrics cacheMetrics,
@Config("domainExpiryAccessPeriodTotalLength") Duration domainExpiryAccessPeriodTotalLength) {
if (jedisClient.isEmpty()) {
return domainName ->
ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now());
}
return new MultilayerDomainCache(jedisClient.get(), clock, cacheMetrics);
return new MultilayerDomainCache(
jedisClient.get(), clock, cacheMetrics, domainExpiryAccessPeriodTotalLength);
}

@Provides
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/google/registry/cache/DomainCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,15 @@

/** Interface for some type of cache that loads {@link Domain}s by domain name. */
public interface DomainCache {

/** Loads the active domain by domain name, filtering out soft-deleted domains. */
Optional<Domain> loadByDomainName(String domainName);

/**
* Loads the most recent domain by domain name from cache or database, including soft-deleted
* domains.
*/
default Optional<Domain> loadMostRecentByDomainName(String domainName) {
return loadByDomainName(domainName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@

package google.registry.cache;

import static google.registry.flows.domain.DomainFlowUtils.isDomainEligibleForXap;

import com.google.common.collect.ImmutableList;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.ExpiryAccessPeriodMode;
import google.registry.model.tld.Tld.TldType;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

/**
Expand All @@ -29,16 +37,33 @@
public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
implements DomainCache {

private final Duration domainExpiryAccessPeriodTotalLength;

@Inject
public MultilayerDomainCache(
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
SimplifiedJedisClient jedisClient,
Clock clock,
CacheMetrics cacheMetrics,
@Config("domainExpiryAccessPeriodTotalLength") Duration domainExpiryAccessPeriodTotalLength) {
super(jedisClient, clock, cacheMetrics);
this.domainExpiryAccessPeriodTotalLength = domainExpiryAccessPeriodTotalLength;
}

public MultilayerDomainCache(
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
this(jedisClient, clock, cacheMetrics, Duration.ofDays(10));
}

@Override
public Optional<Domain> loadByDomainName(String domainName) {
return loadFromCaches(Domain.class, domainName);
}

@Override
public Optional<Domain> loadMostRecentByDomainName(String domainName) {
return loadMostRecentFromCaches(Domain.class, domainName);
}

@Override
protected Optional<Domain> loadFromDatabase(String domainName) {
// Don't use the cache (avoid caching the same domain twice). Do use the replica SQL instance.
Expand All @@ -50,6 +75,30 @@ protected Optional<Domain> loadFromDatabase(String domainName) {

@Override
protected boolean shouldPersistToRemoteCache(Domain domain) {
return Tld.get(domain.getTld()).getTldType().equals(Tld.TldType.REAL);
Tld tld = Tld.get(domain.getTld());
if (!tld.getTldType().equals(TldType.REAL)) {
return false;
}
Instant now = clock.now();
if (domain.getDeletionTime().isAfter(now)) {
return true;
}
return isDomainInXap(domain, tld, now);
}

@Override
protected Optional<Instant> getExpirationTime(Domain domain) {
Instant now = clock.now();
Tld tld = Tld.get(domain.getTld());
if (isDomainInXap(domain, tld, now)) {
return Optional.of(domain.getDeletionTime().plus(domainExpiryAccessPeriodTotalLength));
}
return Optional.empty();
}

private boolean isDomainInXap(Domain domain, Tld tld, Instant now) {
return tld.getExpiryAccessPeriodModeAt(now) == ExpiryAccessPeriodMode.ENABLED
&& isDomainEligibleForXap(domain, tld, now)
&& domain.getDeletionTime().isAfter(now.minus(domainExpiryAccessPeriodTotalLength));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
.build();

private final SimplifiedJedisClient jedisClient;
private final Clock clock;
protected final Clock clock;
private final CacheMetrics cacheMetrics;

protected MultilayerEppResourceCache(
Expand All @@ -54,6 +54,10 @@ protected boolean shouldPersistToRemoteCache(V value) {
return true;
}

protected Optional<Instant> getExpirationTime(V resource) {
return Optional.empty();
}

@SuppressWarnings("unchecked")
protected Optional<V> loadFromCaches(Class<V> clazz, String key) {
Instant now = clock.now();
Expand All @@ -63,6 +67,12 @@ protected Optional<V> loadFromCaches(Class<V> clazz, String key) {
.map(v -> v.cloneProjectedAtTime(now));
}

@SuppressWarnings("unchecked")
protected Optional<V> loadMostRecentFromCaches(Class<V> clazz, String key) {
Instant now = clock.now();
return (Optional<V>) loadFromCachesInternal(clazz, key).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));
Expand All @@ -87,7 +97,9 @@ private Optional<V> loadFromCachesInternal(Class<V> clazz, String key) {
}
V value = possibleValue.get();
if (shouldPersistToRemoteCache(value)) {
jedisClient.set(new SimplifiedJedisClient.JedisResource<>(key, value));
jedisClient.set(
new SimplifiedJedisClient.JedisResource<>(
key, value, getExpirationTime(value).orElse(null)));
}
localCache.put(key, value);
cacheMetrics.recordLookup(clazz.getSimpleName(), CacheMetrics.CacheHitType.MISS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Optional;
import redis.clients.jedis.AbstractPipeline;
import redis.clients.jedis.UnifiedJedis;
Expand All @@ -55,7 +56,27 @@
*/
public class SimplifiedJedisClient {

public record JedisResource<V extends EppResource>(String key, V value) {}
public record JedisResource<V extends EppResource>(
String key, V value, Optional<Instant> expirationTime) {

public JedisResource {
checkNotNull(key, "Key cannot be null");
checkNotNull(value, "Value cannot be null");
checkNotNull(expirationTime, "expirationTime cannot be null");
}

public JedisResource(String key, V value) {
this(key, value, Optional.empty());
}

public JedisResource(String key, V value, Instant expirationTime) {
this(key, value, Optional.ofNullable(expirationTime));
}

public Instant getExpirationTime() {
return expirationTime.orElseGet(() -> value.getDeletionTime());
}
}

private static final ImmutableMap<Class<? extends EppResource>, String> TYPE_PREFIXES =
ImmutableMap.of(
Expand Down Expand Up @@ -101,7 +122,7 @@ public <V extends EppResource> void set(JedisResource<V> resource) {
jedis.set(
convertKey(resource.value.getClass(), resource.key),
serialize(resource.value),
new SetParams().pxAt(resource.value.getDeletionTime().toEpochMilli()));
new SetParams().pxAt(resource.getExpirationTime().toEpochMilli()));
}

/** Sets multiple values in the remote cache using a Jedis {@link AbstractPipeline}. */
Expand All @@ -114,12 +135,33 @@ public <V extends EppResource> void setAll(ImmutableCollection<JedisResource<V>>
pipeline.set(
convertKey(resource.value.getClass(), resource.key),
serialize(resource.value),
new SetParams().pxAt(resource.value.getDeletionTime().toEpochMilli())));
new SetParams().pxAt(resource.getExpirationTime().toEpochMilli())));
pipeline.sync();
}
}
}

/**
* Deletes the value associated with the given key in Valkey.
*
* <p>If the given key does not exist, it does nothing.
*/
public void delete(Class<? extends EppResource> resourceClass, String key) {
checkNotNull(resourceClass, "resourceClass cannot be null");
checkNotNull(key, "Key cannot be null");
jedis.unlink(getRedisKey(resourceClass, key));
}

/** Deletes the given resource in Valkey. */
public void delete(JedisResource<?> resource) {
checkNotNull(resource, "resource cannot be null");
delete(resource.value.getClass().asSubclass(EppResource.class), resource.key);
}

byte[] getRedisKey(Class<?> clazz, String key) {
return convertKey(clazz, key);
}

/**
* Deletes all values associated with the given keys in Valkey.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
import static google.registry.flows.domain.DomainFlowUtils.getReservationTypes;
import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import static google.registry.flows.domain.DomainFlowUtils.isAnchorTenant;
import static google.registry.flows.domain.DomainFlowUtils.isDomainEligibleForXap;
import static google.registry.flows.domain.DomainFlowUtils.isRegisterBsaCreate;
import static google.registry.flows.domain.DomainFlowUtils.isReserved;
import static google.registry.flows.domain.DomainFlowUtils.isValidReservedCreate;
import static google.registry.flows.domain.DomainFlowUtils.loadDomainIfInXap;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPredelegation;
Expand Down Expand Up @@ -259,13 +259,6 @@ private Optional<String> getMessageForCheck(
idn, existingDomains, bsaBlockedDomainNames, tldStates, token, now);
}

private static Optional<Domain> loadDomainIfInXap(
String domainName, Instant now, Duration domainExpiryAccessPeriodTotalLength) {
return ForeignKeyUtils.loadResource(
Domain.class, domainName, now.minus(domainExpiryAccessPeriodTotalLength))
.filter(domain -> isDomainEligibleForXap(domain, Tld.get(domain.getTld()), now));
}

private Optional<String> getMessageForCheckWithToken(
InternetDomainName domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
Expand Down
Loading
Loading