From 6ffbee9db47cd922a398cc38a4a6a65464615bd2 Mon Sep 17 00:00:00 2001 From: Gus Brodman Date: Wed, 6 May 2026 14:51:59 -0400 Subject: [PATCH] Use remote caches in RDAP queries 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 f3cee274a34..7caa079e9ca 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.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 @@ protected enum DeletedItemHandling { @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 @@ final String getActionPath() { /** * 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 c5b6d7cb0af..e47bf7c2c49 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 RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHe } // 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 f79878f29a2..b56df0faaed 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 @@ private DomainSearchResponse searchByDomainName(final RdapSearchPattern partialS /** 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 @@ private ImmutableList> getNameserverRefsByLdhNameWithSuffix( // 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 2014924d0bc..830b221e92c 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.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 @@ record HistoryTimeAndRegistrar(Instant modificationTime, String registrarId) {} @Inject @RequestServerName String serverName; @Inject RdapAuthorization rdapAuthorization; @Inject Clock clock; + @Inject HostCache hostCache; @Inject RdapJsonFormatter() {} @@ -393,20 +395,11 @@ RdapDomain createRdapDomain(Domain domain, OutputDataType outputDataType) { 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 6aaed93465c..5a15107767a 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 @@ private NameserverSearchResponse searchByNameUsingForeignKey( /** 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 062badbd39f..9bf04675fe2 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.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 @@ public void beforeEachRdapActionBaseTestCase() { 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 1448d2e23ea..ec691b12b34 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.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 @@ static RdapJsonFormatter getTestRdapJsonFormatter(Clock clock) { + " 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; }