Optimize RDAP entity event query (#1635)

* Optimize RDAP entity event query

For each EPP entity, directly load the latest HistoryEntry per event type
instead of loading all events through the HistoryEntryDao.

Although most entities have a small number of history entries, there are
a few entities with many entries, enough to cause OutOfMemory error.
This commit is contained in:
Weimin Yu 2022-05-19 23:35:55 -04:00 committed by GitHub
parent dcaae78657
commit 50891e0e72
3 changed files with 71 additions and 15 deletions

View file

@ -211,7 +211,7 @@ public class HistoryEntryDao {
jpaTm().criteriaQuery(criteriaQuery).getResultList());
}
private static Class<? extends HistoryEntry> getHistoryClassFromParent(
public static Class<? extends HistoryEntry> getHistoryClassFromParent(
Class<? extends EppResource> parent) {
if (!RESOURCE_TYPES_TO_HISTORY_TYPES.containsKey(parent)) {
throw new IllegalArgumentException(
@ -220,7 +220,7 @@ public class HistoryEntryDao {
return RESOURCE_TYPES_TO_HISTORY_TYPES.get(parent);
}
private static String getRepoIdFieldNameFromHistoryClass(
public static String getRepoIdFieldNameFromHistoryClass(
Class<? extends HistoryEntry> historyClass) {
if (!REPO_ID_FIELD_NAMES.containsKey(historyClass)) {
throw new IllegalArgumentException(

View file

@ -20,11 +20,14 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.rdap.RdapIcannStandardInformation.CONTACT_REDACTED_VALUE;
import static google.registry.util.CollectionUtils.union;
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;
@ -81,6 +84,7 @@ import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.persistence.Entity;
import org.joda.time.DateTime;
/**
@ -150,6 +154,19 @@ public class RdapJsonFormatter {
INTERNAL
}
/**
* JPQL query template for finding the latest history entry per event type for an EPP entity.
*
* <p>User should replace '%entityName%', '%repoIdField%', and '%repoIdValue%' with valid values.
* A DomainHistory query may look like below: {@code select e from DomainHistory e where
* domainRepoId = '17-Q9JYB4C' and modificationTime in (select max(modificationTime) from
* DomainHistory where domainRepoId = '17-Q9JYB4C' and type is not null group by type)}
*/
private static final String GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE =
"select e from %entityName% e where %repoIdField% = '%repoIdValue%' and modificationTime in "
+ " (select max(modificationTime) from %entityName% where "
+ " %repoIdField% = '%repoIdValue%' and type is not null group by type)";
/** Map of EPP status values to the RDAP equivalents. */
private static final ImmutableMap<StatusValue, RdapStatus> STATUS_TO_RDAP_STATUS_MAP =
new ImmutableMap.Builder<StatusValue, RdapStatus>()
@ -855,17 +872,8 @@ public class RdapJsonFormatter {
return rolesBuilder.build();
}
/**
* Creates the list of optional events to list in domain, nameserver, or contact replies.
*
* <p>Only has entries for optional events that won't be shown in "SUMMARY" versions of these
* objects. These are either stated as optional in the RDAP Response Profile 15feb19, or not
* mentioned at all but thought to be useful anyway.
*
* <p>Any required event should be added elsewhere, preferably without using HistoryEntries (so
* that we don't need to load HistoryEntries for "summary" responses).
*/
private ImmutableList<Event> makeOptionalEvents(EppResource resource) {
@VisibleForTesting
ImmutableMap<EventAction, HistoryEntry> getLastHistoryEntryByType(EppResource resource) {
HashMap<EventAction, HistoryEntry> lastEntryOfType = Maps.newHashMap();
// Events (such as transfer, but also create) can appear multiple times. We only want the last
// time they appeared.
@ -878,8 +886,26 @@ 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.
Iterable<? extends HistoryEntry> historyEntries =
HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey());
Iterable<? extends HistoryEntry> historyEntries;
if (tm().isOfy()) {
historyEntries = HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey());
} else {
VKey<? extends EppResource> resourceVkey = resource.createVKey();
Class<? extends HistoryEntry> historyClass =
HistoryEntryDao.getHistoryClassFromParent(resourceVkey.getKind());
String entityName = historyClass.getAnnotation(Entity.class).name();
if (Strings.isNullOrEmpty(entityName)) {
entityName = historyClass.getSimpleName();
}
String repoIdFieldName = HistoryEntryDao.getRepoIdFieldNameFromHistoryClass(historyClass);
String jpql =
GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE
.replace("%entityName%", entityName)
.replace("%repoIdField%", repoIdFieldName)
.replace("%repoIdValue%", resourceVkey.getSqlKey().toString());
historyEntries =
jpaTm().transact(() -> jpaTm().getEntityManager().createQuery(jpql).getResultList());
}
for (HistoryEntry historyEntry : historyEntries) {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType());
@ -889,6 +915,21 @@ public class RdapJsonFormatter {
}
lastEntryOfType.put(rdapEventAction, historyEntry);
}
return ImmutableMap.copyOf(lastEntryOfType);
}
/**
* Creates the list of optional events to list in domain, nameserver, or contact replies.
*
* <p>Only has entries for optional events that won't be shown in "SUMMARY" versions of these
* objects. These are either stated as optional in the RDAP Response Profile 15feb19, or not
* mentioned at all but thought to be useful anyway.
*
* <p>Any required event should be added elsewhere, preferably without using HistoryEntries (so
* that we don't need to load HistoryEntries for "summary" responses).
*/
private ImmutableList<Event> makeOptionalEvents(EppResource resource) {
ImmutableMap<EventAction, HistoryEntry> lastEntryOfType = getLastHistoryEntryByType(resource);
ImmutableList.Builder<Event> eventsBuilder = new ImmutableList.Builder<>();
DateTime creationTime = resource.getCreationTime();
DateTime lastChangeTime =

View file

@ -15,6 +15,7 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.rdap.RdapDataStructures.EventAction.TRANSFER;
import static google.registry.rdap.RdapTestHelper.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
@ -28,7 +29,9 @@ import static google.registry.testing.TestDataHelper.loadFile;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import google.registry.model.contact.ContactResource;
@ -51,6 +54,7 @@ import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestSqlOnly;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -482,6 +486,17 @@ class RdapJsonFormatterTest {
.isEqualTo(loadJson("rdapjson_domain_summary.json"));
}
@TestSqlOnly
void testGetLastHistoryEntryByType() {
// Expected data are from "rdapjson_domain_summary.json"
assertThat(
Maps.transformValues(
rdapJsonFormatter.getLastHistoryEntryByType(domainBaseFull),
HistoryEntry::getModificationTime))
.containsExactlyEntriesIn(
ImmutableMap.of(TRANSFER, DateTime.parse("1999-12-01T00:00:00.000Z")));
}
@TestOfyAndSql
void testDomain_logged_out() {
rdapJsonFormatter.rdapAuthorization = RdapAuthorization.PUBLIC_AUTHORIZATION;