From 2a1748ba9cc45375236cd556e22d2e3be134b67d Mon Sep 17 00:00:00 2001 From: gbrodman Date: Wed, 25 Jun 2025 15:33:36 -0400 Subject: [PATCH] Cache history values for RDAP domain requests (#2777) In RDAP, domain queries are the most common by a factor of like 40,000 so we should optimize these as much as possible. We already have an EPP resource / foreign key cache which does improve performance somewhat but looking at some sample logs, it only cuts the RDAP request times by like 40% (looking at requests for the same domain a few seconds apart). History entries don't change often, so we should cache them to make subsequent queries faster as well. In addition, we're only caching two fields per repo ID (modification time, registrar ID) so we can cache more entries than we can for the EPP resource cache (which stores large objects). --- .../registry/rdap/RdapJsonFormatter.java | 109 ++++++++++-------- .../registry/rdap/RdapJsonFormatterTest.java | 6 +- 2 files changed, 64 insertions(+), 51 deletions(-) diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index f26ee127f..04a7c4b78 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -23,20 +23,21 @@ import static google.registry.model.EppResourceUtils.isLinked; import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm; import static google.registry.util.CollectionUtils.union; +import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; 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.config.RegistryConfig; import google.registry.config.RegistryConfig.Config; +import google.registry.model.CacheUtils; import google.registry.model.EppResource; import google.registry.model.adapters.EnumToAttributeAdapter.EppEnum; import google.registry.model.contact.Contact; @@ -73,13 +74,11 @@ import google.registry.rdap.RdapObjectClasses.VcardArray; import google.registry.request.RequestServerName; import google.registry.util.Clock; import jakarta.inject.Inject; -import jakarta.persistence.Entity; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.URI; import java.nio.file.Paths; -import java.util.HashMap; import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -103,6 +102,16 @@ public class RdapJsonFormatter { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + @VisibleForTesting + record HistoryTimeAndRegistrar(DateTime modificationTime, String registrarId) {} + + private static final LoadingCache> + DOMAIN_HISTORIES_BY_REPO_ID = + CacheUtils.newCacheBuilder(RegistryConfig.getEppResourceCachingDuration()) + // Cache more than the EPP resource cache because we're only caching small objects + .maximumSize(RegistryConfig.getEppResourceMaxCachedEntries() * 4L) + .build(repoId -> getLastHistoryByType(repoId, Domain.class)); + private DateTime requestTime = null; @Inject @@ -860,8 +869,18 @@ public class RdapJsonFormatter { } @VisibleForTesting - ImmutableMap getLastHistoryEntryByType(EppResource resource) { - HashMap lastEntryOfType = Maps.newHashMap(); + static ImmutableMap getLastHistoryByType( + EppResource eppResource) { + if (eppResource instanceof Domain) { + return DOMAIN_HISTORIES_BY_REPO_ID.get(eppResource.getRepoId()); + } + return getLastHistoryByType(eppResource.getRepoId(), eppResource.getClass()); + } + + private static ImmutableMap getLastHistoryByType( + String repoId, Class resourceType) { + ImmutableMap.Builder lastEntryOfType = + new ImmutableMap.Builder<>(); // Events (such as transfer, but also create) can appear multiple times. We only want the last // time they appeared. // @@ -873,35 +892,33 @@ public class RdapJsonFormatter { // 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the // domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the // domain name has not been transferred since it was created. - VKey resourceVkey = resource.createVKey(); - Class historyClass = - HistoryEntryDao.getHistoryClassFromParent(resourceVkey.getKind()); - String entityName = historyClass.getAnnotation(Entity.class).name(); - if (Strings.isNullOrEmpty(entityName)) { - entityName = historyClass.getSimpleName(); - } + String entityName = HistoryEntryDao.getHistoryClassFromParent(resourceType).getSimpleName(); String jpql = GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE .replace("%entityName%", entityName) - .replace("%repoIdValue%", resourceVkey.getKey().toString()); - Iterable historyEntries = - replicaTm() - .transact( - () -> - replicaTm() - .getEntityManager() - .createQuery(jpql, HistoryEntry.class) - .getResultList()); - for (HistoryEntry historyEntry : historyEntries) { - EventAction rdapEventAction = - HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType()); - // Only save the historyEntries if this is a type we care about. - if (rdapEventAction == null) { - continue; - } - lastEntryOfType.put(rdapEventAction, historyEntry); - } - return ImmutableMap.copyOf(lastEntryOfType); + .replace("%repoIdValue%", repoId); + replicaTm() + .transact( + () -> + replicaTm() + .getEntityManager() + .createQuery(jpql, HistoryEntry.class) + .getResultStream() + .forEach( + historyEntry -> { + EventAction rdapEventAction = + HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get( + historyEntry.getType()); + // Only save the entries if this is a type we care about. + if (rdapEventAction != null) { + lastEntryOfType.put( + rdapEventAction, + new HistoryTimeAndRegistrar( + historyEntry.getModificationTime(), + historyEntry.getRegistrarId())); + } + })); + return lastEntryOfType.buildKeepingLast(); } /** @@ -915,7 +932,8 @@ public class RdapJsonFormatter { * that we don't need to load HistoryEntries for "summary" responses). */ private ImmutableList makeOptionalEvents(EppResource resource) { - ImmutableMap lastEntryOfType = getLastHistoryEntryByType(resource); + ImmutableMap lastHistoryOfType = + getLastHistoryByType(resource); ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); DateTime creationTime = resource.getCreationTime(); DateTime lastChangeTime = @@ -923,12 +941,12 @@ public class RdapJsonFormatter { // The order of the elements is stable - it's the order in which the enum elements are defined // in EventAction for (EventAction rdapEventAction : EventAction.values()) { - HistoryEntry historyEntry = lastEntryOfType.get(rdapEventAction); + HistoryTimeAndRegistrar historyTimeAndRegistrar = lastHistoryOfType.get(rdapEventAction); // Check if there was any entry of this type - if (historyEntry == null) { + if (historyTimeAndRegistrar == null) { continue; } - DateTime modificationTime = historyEntry.getModificationTime(); + DateTime modificationTime = historyTimeAndRegistrar.modificationTime(); // We will ignore all events that happened before the "creation time", since these events are // from a "previous incarnation of the domain" (for a domain that was owned by someone, // deleted, and then bought by someone else) @@ -938,7 +956,7 @@ public class RdapJsonFormatter { eventsBuilder.add( Event.builder() .setEventAction(rdapEventAction) - .setEventActor(historyEntry.getRegistrarId()) + .setEventActor(historyTimeAndRegistrar.registrarId()) .setEventDate(modificationTime) .build()); // The last change time might not be the lastEppUpdateTime, since some changes happen without @@ -951,21 +969,16 @@ public class RdapJsonFormatter { // The event of eventAction type last changed MUST be omitted if the domain name has not been // updated since it was created if (lastChangeTime.isAfter(creationTime)) { - eventsBuilder.add(makeEvent(EventAction.LAST_CHANGED, null, lastChangeTime)); + // Creates an RDAP event object as defined by RFC 9083 + eventsBuilder.add( + Event.builder() + .setEventAction(EventAction.LAST_CHANGED) + .setEventDate(lastChangeTime) + .build()); } return eventsBuilder.build(); } - /** Creates an RDAP event object as defined by RFC 9083. */ - private static Event makeEvent( - EventAction eventAction, @Nullable String eventActor, DateTime eventDate) { - Event.Builder builder = Event.builder().setEventAction(eventAction).setEventDate(eventDate); - if (eventActor != null) { - builder.setEventActor(eventActor); - } - return builder.build(); - } - /** * Creates a vCard address entry: array of strings specifying the components of the address. * diff --git a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java index b1fd2fc8e..fa1312ab9 100644 --- a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java +++ b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java @@ -462,12 +462,12 @@ class RdapJsonFormatterTest { } @Test - void testGetLastHistoryEntryByType() { + void testGetLastHistoryByType() { // Expected data are from "rdapjson_domain_summary.json" assertThat( Maps.transformValues( - rdapJsonFormatter.getLastHistoryEntryByType(domainFull), - HistoryEntry::getModificationTime)) + RdapJsonFormatter.getLastHistoryByType(domainFull), + RdapJsonFormatter.HistoryTimeAndRegistrar::modificationTime)) .containsExactlyEntriesIn( ImmutableMap.of(TRANSFER, DateTime.parse("1999-12-01T00:00:00.000Z"))); }