From 8047d1e3e3efbd665e7550b8ed3660b976db740b Mon Sep 17 00:00:00 2001 From: gbrodman Date: Tue, 12 May 2026 12:28:36 -0400 Subject: [PATCH] Use remote caches in RDAP queries (#3034) Note that this primarily affects domain lookups. We choose to use the remote cache for hosts based on repo ID (not host name), so the remote caches are not particularly useful for host lookups. We chose this because the number of domain queries is orders of magnitude higher than the number of host queries. --- .../google/registry/rdap/RdapActionBase.java | 12 +++++++----- .../registry/rdap/RdapDomainAction.java | 7 +++---- .../registry/rdap/RdapDomainSearchAction.java | 11 +++++------ .../registry/rdap/RdapJsonFormatter.java | 19 ++++++------------- .../rdap/RdapNameserverSearchAction.java | 4 +--- .../registry/rdap/RdapActionBaseTestCase.java | 4 ++++ .../google/registry/rdap/RdapTestHelper.java | 6 ++++++ 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/google/registry/rdap/RdapActionBase.java b/core/src/main/java/google/registry/rdap/RdapActionBase.java index f3cee274a..7caa079e9 100644 --- a/core/src/main/java/google/registry/rdap/RdapActionBase.java +++ b/core/src/main/java/google/registry/rdap/RdapActionBase.java @@ -32,6 +32,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import google.registry.cache.DomainCache; import google.registry.config.RegistryConfig.Config; import google.registry.model.EppResource; import google.registry.model.registrar.Registrar; @@ -88,6 +89,7 @@ public abstract class RdapActionBase implements Runnable { @Inject @Parameter("formatOutput") Optional formatOutputParam; @Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize; @Inject RdapMetrics rdapMetrics; + @Inject DomainCache domainCache; @Inject Clock clock; /** Builder for metric recording. */ @@ -117,14 +119,14 @@ public abstract class RdapActionBase implements Runnable { /** * Does the actual search and returns an RDAP JSON object. * - * RFC7480 4.1 - we have to support GET and HEAD. + *

RFC7480 4.1 - we have to support GET and HEAD. * * @param pathSearchString the search string in the URL path * @param isHeadRequest whether the returned map will actually be used. HTTP HEAD requests don't - * actually return anything. However, we usually still want to go through the process of - * building a map, to make sure that the request would return a 500 status if it were - * invoked using GET. So this field should usually be ignored, unless there's some - * expensive task required to create the map which will never result in a request failure. + * actually return anything. However, we usually still want to go through the process of + * building a map, to make sure that the request would return a 500 status if it were invoked + * using GET. So this field should usually be ignored, unless there's some expensive task + * required to create the map which will never result in a request failure. * @return A map (probably containing nested maps and lists) with the final JSON response data. */ abstract ReplyPayloadBase getJsonObjectForResource( diff --git a/core/src/main/java/google/registry/rdap/RdapDomainAction.java b/core/src/main/java/google/registry/rdap/RdapDomainAction.java index c5b6d7cb0..e47bf7c2c 100644 --- a/core/src/main/java/google/registry/rdap/RdapDomainAction.java +++ b/core/src/main/java/google/registry/rdap/RdapDomainAction.java @@ -67,10 +67,9 @@ public class RdapDomainAction extends RdapActionBase { } // The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com. Optional domain = - ForeignKeyUtils.loadResourceByCache( - Domain.class, - pathSearchString, - shouldIncludeDeleted() ? START_INSTANT : getRequestTime()); + shouldIncludeDeleted() // the remote domain cache cannot handle times in the past + ? ForeignKeyUtils.loadResourceByCache(Domain.class, pathSearchString, START_INSTANT) + : domainCache.loadByDomainName(pathSearchString); if (domain.isEmpty() || !isAuthorized(domain.get())) { handlePossibleBsaBlock(domainName); // RFC7480 5.3 - if the server wishes to respond that it doesn't have data satisfying the diff --git a/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java b/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java index f79878f29..b56df0faa 100644 --- a/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java +++ b/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java @@ -181,11 +181,10 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { /** Searches for domains by domain name without a wildcard or interest in deleted entries. */ private DomainSearchResponse searchByDomainNameWithoutWildcard( final RdapSearchPattern partialStringQuery) { - Optional domain = - ForeignKeyUtils.loadResourceByCache( - Domain.class, partialStringQuery.getInitialString(), getRequestTime()); return makeSearchResults( - shouldBeVisible(domain) ? ImmutableList.of(domain.get()) : ImmutableList.of()); + domainCache.loadByDomainName(partialStringQuery.getInitialString()).stream() + .filter(this::shouldBeVisible) + .toList()); } /** Searches for domains by domain name with an initial string, wildcard and possible suffix. */ @@ -359,8 +358,8 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { // through the subordinate hosts. This is more efficient, and lets us permit wildcard searches // with no initial string. Domain domain = - ForeignKeyUtils.loadResourceByCache( - Domain.class, partialStringQuery.getSuffix(), timeToQuery) + domainCache + .loadByDomainName(partialStringQuery.getSuffix()) .orElseThrow( () -> new UnprocessableEntityException( diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index 2014924d0..830b221e9 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -32,6 +32,7 @@ import com.google.common.collect.Streams; import com.google.common.flogger.FluentLogger; import com.google.common.net.InetAddresses; import com.google.gson.JsonArray; +import google.registry.cache.HostCache; import google.registry.config.RegistryConfig; import google.registry.config.RegistryConfig.Config; import google.registry.model.CacheUtils; @@ -122,6 +123,7 @@ public class RdapJsonFormatter { @Inject @RequestServerName String serverName; @Inject RdapAuthorization rdapAuthorization; @Inject Clock clock; + @Inject HostCache hostCache; @Inject RdapJsonFormatter() {} @@ -393,20 +395,11 @@ public class RdapJsonFormatter { domain.getDomainName(), domain.getRepoId()); } - // We're just trying to load the hosts by cache here, but the generics and casting require - // a lot of boilerplate to make the compiler happy - Iterable> nameservers = - ImmutableSet.copyOf(domain.getNameservers()); ImmutableSet loadedHosts = - replicaTm() - .transact( - () -> { - ImmutableSet.Builder hostBuilder = new ImmutableSet.Builder<>(); - for (EppResource host : EppResource.loadByCacheIfEnabled(nameservers).values()) { - hostBuilder.add((Host) host); - } - return hostBuilder.build(); - }); + domain.getNameservers().stream() + .map(key -> hostCache.loadByRepoId((String) key.getKey())) + .flatMap(Optional::stream) + .collect(toImmutableSet()); // Add the nameservers to the data; the load was kicked off above for efficiency. // RDAP Response Profile 2.8: we MUST have the nameservers diff --git a/core/src/main/java/google/registry/rdap/RdapNameserverSearchAction.java b/core/src/main/java/google/registry/rdap/RdapNameserverSearchAction.java index 6aaed9346..5a1510776 100644 --- a/core/src/main/java/google/registry/rdap/RdapNameserverSearchAction.java +++ b/core/src/main/java/google/registry/rdap/RdapNameserverSearchAction.java @@ -175,9 +175,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { /** Searches for nameservers by name using the superordinate domain as a suffix. */ private NameserverSearchResponse searchByNameUsingSuperordinateDomain( RdapSearchPattern partialStringQuery) { - Optional domain = - ForeignKeyUtils.loadResourceByCache( - Domain.class, partialStringQuery.getSuffix(), getRequestTime()); + Optional domain = domainCache.loadByDomainName(partialStringQuery.getSuffix()); if (domain.isEmpty()) { // Don't allow wildcards with suffixes which are not domains we manage. That would risk a // table scan in many easily foreseeable cases. The user might ask for ns*.zombo.com, diff --git a/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java b/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java index 062badbd3..9bf04675f 100644 --- a/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java +++ b/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java @@ -28,8 +28,10 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import google.registry.model.ForeignKeyUtils; import google.registry.model.console.User; import google.registry.model.console.UserRoles; +import google.registry.model.domain.Domain; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.request.Actions; @@ -92,6 +94,8 @@ abstract class RdapActionBaseTestCase { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(clock); action.rdapMetrics = rdapMetrics; action.requestMethod = GET; + action.domainCache = + (domainName) -> ForeignKeyUtils.loadResourceByCache(Domain.class, domainName, clock.now()); action.clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z")); logout(); } diff --git a/core/src/test/java/google/registry/rdap/RdapTestHelper.java b/core/src/test/java/google/registry/rdap/RdapTestHelper.java index 1448d2e23..ec691b12b 100644 --- a/core/src/test/java/google/registry/rdap/RdapTestHelper.java +++ b/core/src/test/java/google/registry/rdap/RdapTestHelper.java @@ -27,8 +27,12 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import google.registry.model.EppResource; +import google.registry.model.host.Host; +import google.registry.persistence.VKey; import google.registry.util.Clock; import java.util.Map; +import java.util.Optional; /** Test helper methods for RDAP tests. */ class RdapTestHelper { @@ -68,6 +72,8 @@ class RdapTestHelper { + " suspect that you have failed to comply with these terms.", "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))); return rdapJsonFormatter; }