From c4c1c723067f40677522f8b7faee4a75795ad59c Mon Sep 17 00:00:00 2001 From: Lai Jiang Date: Mon, 19 Sep 2022 14:41:19 -0400 Subject: [PATCH] Refactor ForeignKeyIndex into ForeignKeyUtils (#1783) The old class is modeled after datastore with some logic jammed in for it to work with SQL as well. As of #1777, the ofy related logic is deleted, however the general structure of the class remained datastore oriented. This PR refactors the existing class into a ForeignKeyUtils helper class that does away wit the index subclasses and provides static helper methods to do the same, in a SQL-idiomatic fashion. Some minor changes are made to the EPP resource classes to make it possible to create them in a SQL only environment in tests. --- .../google/registry/flows/CheckApiAction.java | 4 +- .../registry/flows/ResourceFlowUtils.java | 16 +- .../flows/domain/DomainCheckFlow.java | 16 +- .../registry/flows/host/HostUpdateFlow.java | 4 +- .../registry/model/EppResourceUtils.java | 33 +- .../registry/model/ForeignKeyUtils.java | 236 +++++++++++++ .../registry/model/contact/Contact.java | 5 +- .../google/registry/model/domain/Domain.java | 4 +- .../registry/model/domain/DomainCommand.java | 12 +- .../java/google/registry/model/host/Host.java | 5 +- .../registry/model/index/ForeignKeyIndex.java | 311 ------------------ .../registry/rdap/RdapDomainSearchAction.java | 12 +- .../registry/tools/CommandUtilities.java | 4 +- .../registry/tools/UnrenewDomainCommand.java | 4 +- .../batch/ResaveEntityActionTest.java | 4 +- .../dns/PublishDnsUpdatesActionTest.java | 2 +- .../flows/contact/ContactUpdateFlowTest.java | 3 +- .../domain/DomainTransferApproveFlowTest.java | 2 - .../flows/domain/DomainUpdateFlowTest.java | 6 - .../flows/host/HostUpdateFlowTest.java | 12 +- .../registry/model/ForeignKeyUtilsTest.java | 153 +++++++++ .../registry/model/domain/DomainTest.java | 5 +- .../model/index/ForeignKeyIndexTest.java | 83 ----- .../registry/testing/TestCacheExtension.java | 27 +- .../registry/tools/GetContactCommandTest.java | 24 +- .../registry/tools/GetDomainCommandTest.java | 24 +- .../registry/tools/GetHostCommandTest.java | 24 +- .../tools/UpdateDomainCommandTest.java | 19 +- .../RegistrarConsoleScreenshotTest.java | 4 +- .../registry/whois/WhoisActionTest.java | 2 +- ...otTest_registryLockVerify_success_page.png | Bin 30226 -> 31114 bytes 31 files changed, 487 insertions(+), 573 deletions(-) create mode 100644 core/src/main/java/google/registry/model/ForeignKeyUtils.java delete mode 100644 core/src/main/java/google/registry/model/index/ForeignKeyIndex.java create mode 100644 core/src/test/java/google/registry/model/ForeignKeyUtilsTest.java delete mode 100644 core/src/test/java/google/registry/model/index/ForeignKeyIndexTest.java diff --git a/core/src/main/java/google/registry/flows/CheckApiAction.java b/core/src/main/java/google/registry/flows/CheckApiAction.java index c5e69f930..e4d339a08 100644 --- a/core/src/main/java/google/registry/flows/CheckApiAction.java +++ b/core/src/main/java/google/registry/flows/CheckApiAction.java @@ -44,8 +44,8 @@ import dagger.Module; import dagger.Provides; import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException; import google.registry.flows.domain.DomainFlowUtils.InvalidIdnDomainLabelException; +import google.registry.model.ForeignKeyUtils; import google.registry.model.domain.Domain; -import google.registry.model.index.ForeignKeyIndex; import google.registry.model.tld.Registry; import google.registry.model.tld.label.ReservationType; import google.registry.monitoring.whitebox.CheckApiMetric; @@ -156,7 +156,7 @@ public class CheckApiAction implements Runnable { } private boolean checkExists(String domainString, DateTime now) { - return !ForeignKeyIndex.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty(); + return !ForeignKeyUtils.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty(); } private Optional checkReserved(InternetDomainName domainName) { diff --git a/core/src/main/java/google/registry/flows/ResourceFlowUtils.java b/core/src/main/java/google/registry/flows/ResourceFlowUtils.java index 2248af63e..ff8e20959 100644 --- a/core/src/main/java/google/registry/flows/ResourceFlowUtils.java +++ b/core/src/main/java/google/registry/flows/ResourceFlowUtils.java @@ -17,7 +17,6 @@ package google.registry.flows; import static com.google.common.collect.Sets.intersection; import static google.registry.model.EppResourceUtils.isLinked; import static google.registry.model.EppResourceUtils.loadByForeignKey; -import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import com.google.common.collect.ImmutableSet; @@ -38,6 +37,7 @@ import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.EppResource; import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.EppResource.ResourceWithTransferData; +import google.registry.model.ForeignKeyUtils; import google.registry.model.contact.Contact; import google.registry.model.domain.Domain; import google.registry.model.domain.DomainBase; @@ -45,7 +45,6 @@ import google.registry.model.domain.Period; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; -import google.registry.model.index.ForeignKeyIndex; import google.registry.model.transfer.TransferStatus; import google.registry.persistence.VKey; import java.util.List; @@ -70,22 +69,17 @@ public final class ResourceFlowUtils { /** * Check whether if there are domains linked to the resource to be deleted. Throws an exception if * so. - * - *

Note that in datastore this is a smoke test as the query for linked domains is eventually - * consistent, so we only check a few domains to fail fast. */ public static void checkLinkedDomains( final String targetId, final DateTime now, final Class resourceClass) throws EppException { EppException failfastException = tm().transact( () -> { - final ForeignKeyIndex fki = ForeignKeyIndex.load(resourceClass, targetId, now); - if (fki == null) { + VKey key = ForeignKeyUtils.load(resourceClass, targetId, now); + if (key == null) { return new ResourceDoesNotExistException(resourceClass, targetId); } - return isLinked(fki.getResourceKey(), now) - ? new ResourceToDeleteIsReferencedException() - : null; + return isLinked(key, now) ? new ResourceToDeleteIsReferencedException() : null; }); if (failfastException != null) { throw failfastException; @@ -118,7 +112,7 @@ public final class ResourceFlowUtils { public static void verifyResourceDoesNotExist( Class clazz, String targetId, DateTime now, String registrarId) throws EppException { - VKey key = loadAndGetKey(clazz, targetId, now); + VKey key = ForeignKeyUtils.load(clazz, targetId, now); if (key != null) { R resource = tm().loadByKey(key); // These are similar exceptions, but we can track them internally as log-based metrics. diff --git a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java index c83b66671..f5c52884b 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java @@ -53,6 +53,7 @@ import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseRet import google.registry.flows.domain.token.AllocationTokenDomainCheckResults; import google.registry.flows.domain.token.AllocationTokenFlowUtils; import google.registry.model.EppResource; +import google.registry.model.ForeignKeyUtils; import google.registry.model.billing.BillingEvent; import google.registry.model.domain.Domain; import google.registry.model.domain.DomainCommand.Check; @@ -70,7 +71,6 @@ import google.registry.model.eppoutput.CheckData.DomainCheck; import google.registry.model.eppoutput.CheckData.DomainCheckData; import google.registry.model.eppoutput.EppResponse; import google.registry.model.eppoutput.EppResponse.ResponseExtension; -import google.registry.model.index.ForeignKeyIndex; import google.registry.model.reporting.IcannReportingTypes.ActivityReportField; import google.registry.model.tld.Registry; import google.registry.model.tld.Registry.TldState; @@ -169,8 +169,8 @@ public final class DomainCheckFlow implements Flow { // TODO: Use as of date from fee extension v0.12 instead of now, if specified. .setAsOfDate(now) .build()); - ImmutableMap> existingDomains = - ForeignKeyIndex.load(Domain.class, domainNames, now); + ImmutableMap> existingDomains = + ForeignKeyUtils.load(Domain.class, domainNames, now); Optional allocationTokenExtension = eppInput.getSingleExtension(AllocationTokenExtension.class); Optional tokenDomainCheckResults = @@ -227,7 +227,7 @@ public final class DomainCheckFlow implements Flow { private Optional getMessageForCheck( InternetDomainName domainName, - ImmutableMap> existingDomains, + ImmutableMap> existingDomains, ImmutableMap tokenCheckResults, ImmutableMap tldStates, Optional allocationToken) { @@ -251,7 +251,7 @@ public final class DomainCheckFlow implements Flow { /** Handle the fee check extension. */ private ImmutableList getResponseExtensions( ImmutableMap domainNames, - ImmutableMap> existingDomains, + ImmutableMap> existingDomains, ImmutableSet availableDomains, DateTime now, Optional allocationToken) @@ -297,14 +297,14 @@ public final class DomainCheckFlow implements Flow { * renewal is part of the cost of a restore. * *

This may be resource-intensive for large checks of many restore fees, but those are - * comparatively rare, and we are at least using an in-memory cache. Also this will get a lot + * comparatively rare, and we are at least using an in-memory cache. Also, this will get a lot * nicer in Cloud SQL when we can SELECT just the fields we want rather than having to load the * entire entity. */ private ImmutableMap loadDomainsForRestoreChecks( FeeCheckCommandExtension feeCheck, ImmutableMap domainNames, - ImmutableMap> existingDomains) { + ImmutableMap> existingDomains) { ImmutableList restoreCheckDomains; if (feeCheck instanceof FeeCheckCommandExtensionV06) { // The V06 fee extension supports specifying the command fees to check on a per-domain basis. @@ -329,7 +329,7 @@ public final class DomainCheckFlow implements Flow { ImmutableMap> existingDomainsToLoad = restoreCheckDomains.stream() .filter(existingDomains::containsKey) - .collect(toImmutableMap(d -> d, d -> existingDomains.get(d).getResourceKey())); + .collect(toImmutableMap(d -> d, existingDomains::get)); ImmutableMap, EppResource> loadedDomains = EppResource.loadCached(ImmutableList.copyOf(existingDomainsToLoad.values())); return ImmutableMap.copyOf( diff --git a/core/src/main/java/google/registry/flows/host/HostUpdateFlow.java b/core/src/main/java/google/registry/flows/host/HostUpdateFlow.java index 12a5cda99..db28d7548 100644 --- a/core/src/main/java/google/registry/flows/host/HostUpdateFlow.java +++ b/core/src/main/java/google/registry/flows/host/HostUpdateFlow.java @@ -26,7 +26,6 @@ import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain import static google.registry.flows.host.HostFlowUtils.validateHostName; import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete; import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership; -import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey; import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.CollectionUtils.isNullOrEmpty; @@ -46,6 +45,7 @@ import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; import google.registry.model.EppResource; +import google.registry.model.ForeignKeyUtils; import google.registry.model.ImmutableObject; import google.registry.model.domain.Domain; import google.registry.model.domain.metadata.MetadataExtension; @@ -147,7 +147,7 @@ public final class HostUpdateFlow implements TransactionalFlow { EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost); verifyUpdateAllowed( command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename); - if (isHostRename && loadAndGetKey(Host.class, newHostName, now) != null) { + if (isHostRename && ForeignKeyUtils.load(Host.class, newHostName, now) != null) { throw new HostAlreadyExistsException(newHostName); } AddRemove add = command.getInnerAdd(); diff --git a/core/src/main/java/google/registry/model/EppResourceUtils.java b/core/src/main/java/google/registry/model/EppResourceUtils.java index 04b25cf58..5c6d7318a 100644 --- a/core/src/main/java/google/registry/model/EppResourceUtils.java +++ b/core/src/main/java/google/registry/model/EppResourceUtils.java @@ -36,7 +36,6 @@ import google.registry.model.contact.Contact; import google.registry.model.domain.Domain; import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.Host; -import google.registry.model.index.ForeignKeyIndex; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntryDao; import google.registry.model.tld.Registry; @@ -117,20 +116,19 @@ public final class EppResourceUtils { } /** - * Loads the last created version of an {@link EppResource} from Datastore by foreign key, using a - * cache. + * Loads the last created version of an {@link EppResource} from the database by foreign key, + * using a cache. * *

Returns null if no resource with this foreign key was ever created, or if the most recently * created resource was deleted before time "now". * *

Loading an {@link EppResource} by itself is not sufficient to know its current state since * it may have various expirable conditions and status values that might implicitly change its - * state as time progresses even if it has not been updated in Datastore. Rather, the resource + * state as time progresses even if it has not been updated in the database. Rather, the resource * must be combined with a timestamp to view its current state. We use a global last updated - * timestamp on the resource's entity group (which is essentially free since all writes to the - * entity group must be serialized anyways) to guarantee monotonically increasing write times, and - * forward our projected time to the greater of this timestamp or "now". This guarantees that - * we're not projecting into the past. + * timestamp to guarantee monotonically increasing write times, and forward our projected time to + * the greater of this timestamp or "now". This guarantees that we're not projecting into the + * past. * *

Do not call this cached version for anything that needs transactional consistency. It should * only be used when it's OK if the data is potentially being out of date, e.g. WHOIS. @@ -150,19 +148,18 @@ public final class EppResourceUtils { checkArgument( ForeignKeyedEppResource.class.isAssignableFrom(clazz), "loadByForeignKey may only be called for foreign keyed EPP resources"); - ForeignKeyIndex fki = + VKey key = useCache - ? ForeignKeyIndex.loadCached(clazz, ImmutableList.of(foreignKey), now) - .getOrDefault(foreignKey, null) - : ForeignKeyIndex.load(clazz, foreignKey, now); - // The value of fki.getResourceKey() might be null for hard-deleted prober data. - if (fki == null || isAtOrAfter(now, fki.getDeletionTime()) || fki.getResourceKey() == null) { + ? ForeignKeyUtils.loadCached(clazz, ImmutableList.of(foreignKey), now).get(foreignKey) + : ForeignKeyUtils.load(clazz, foreignKey, now); + // The returned key is null if the resource is hard deleted or soft deleted by the given time. + if (key == null) { return Optional.empty(); } T resource = useCache - ? EppResource.loadCached(fki.getResourceKey()) - : tm().transact(() -> tm().loadByKeyIfPresent(fki.getResourceKey()).orElse(null)); + ? EppResource.loadCached(key) + : tm().transact(() -> tm().loadByKeyIfPresent(key).orElse(null)); if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) { return Optional.empty(); } @@ -178,7 +175,7 @@ public final class EppResourceUtils { } /** - * Checks multiple {@link EppResource} objects from Datastore by unique ids. + * Checks multiple {@link EppResource} objects from the database by unique ids. * *

There are currently no resources that support checks and do not use foreign keys. If we need * to support that case in the future, we can loosen the type to allow any {@link EppResource} and @@ -190,7 +187,7 @@ public final class EppResourceUtils { */ public static ImmutableSet checkResourcesExist( Class clazz, List uniqueIds, final DateTime now) { - return ForeignKeyIndex.load(clazz, uniqueIds, now).keySet(); + return ForeignKeyUtils.load(clazz, uniqueIds, now).keySet(); } /** diff --git a/core/src/main/java/google/registry/model/ForeignKeyUtils.java b/core/src/main/java/google/registry/model/ForeignKeyUtils.java new file mode 100644 index 000000000..0dbf8e7a1 --- /dev/null +++ b/core/src/main/java/google/registry/model/ForeignKeyUtils.java @@ -0,0 +1,236 @@ +// Copyright 2022 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.model; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static google.registry.config.RegistryConfig.getEppResourceCachingDuration; +import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +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.common.collect.Streams; +import google.registry.config.RegistryConfig; +import google.registry.model.contact.Contact; +import google.registry.model.domain.Domain; +import google.registry.model.host.Host; +import google.registry.persistence.VKey; +import google.registry.persistence.transaction.JpaTransactionManager; +import google.registry.util.NonFinalForTesting; +import java.time.Duration; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import javax.annotation.Nullable; +import org.joda.time.DateTime; + +/** + * Util class to map a foreign key to the {@link VKey} to the active instance of {@link EppResource} + * whose unique repoId matches the foreign key string at a given time. The instance is never + * deleted, but it is updated if a newer entity becomes the active entity. + */ +public final class ForeignKeyUtils { + + private ForeignKeyUtils() {} + + private static final ImmutableMap, String> + RESOURCE_TYPE_TO_FK_PROPERTY = + ImmutableMap.of( + Contact.class, "contactId", + Domain.class, "fullyQualifiedDomainName", + Host.class, "fullyQualifiedHostName"); + + /** + * Loads a {@link VKey} to an {@link EppResource} from the database by foreign key. + * + *

Returns null if no resource with this foreign key was ever created, or if the most recently + * created resource was deleted before time "now". + * + * @param clazz the resource type to load + * @param foreignKey foreign key to match + * @param now the current logical time to use when checking for soft deletion of the foreign key + * index + */ + @Nullable + public static VKey load( + Class clazz, String foreignKey, DateTime now) { + return load(clazz, ImmutableList.of(foreignKey), now).get(foreignKey); + } + + /** + * Load a map of {@link String} foreign keys to {@link VKey}s to {@link EppResource} that are + * active at or after the specified moment in time. + * + *

The returned map will omit any foreign keys for which the {@link EppResource} doesn't exist + * or has been soft deleted. + */ + public static ImmutableMap> load( + Class clazz, Collection foreignKeys, final DateTime now) { + return load(clazz, foreignKeys, false).entrySet().stream() + .filter(e -> now.isBefore(e.getValue().deletionTime())) + .collect(toImmutableMap(Entry::getKey, e -> VKey.createSql(clazz, e.getValue().repoId()))); + } + + /** + * Helper method to load {@link VKey}s to all the most recent {@link EppResource}s for the given + * foreign keys, regardless of whether or not they have been soft-deleted. + * + *

Used by both the cached (w/o deletion check) and the non-cached (with deletion check) calls. + * + *

Note that in production, the {@code deletionTime} for entities with the same foreign key + * should monotonically increase as one cannot create a domain/host/contact with the same foreign + * key without soft deleting the existing resource first. However, in test, there's no such + * guarantee and one must make sure that no two resources with the same foreign key exist with the + * same max {@code deleteTime}, usually {@code END_OF_TIME}, lest this method throws an error due + * to duplicate keys. + */ + private static ImmutableMap load( + Class clazz, Collection foreignKeys, boolean useReplicaJpaTm) { + String fkProperty = RESOURCE_TYPE_TO_FK_PROPERTY.get(clazz); + JpaTransactionManager jpaTmToUse = useReplicaJpaTm ? replicaJpaTm() : jpaTm(); + return jpaTmToUse.transact( + () -> + jpaTmToUse + .query( + ("SELECT %fkProperty%, repoId, deletionTime FROM %entity% WHERE (%fkProperty%," + + " deletionTime) IN (SELECT %fkProperty%, MAX(deletionTime) FROM" + + " %entity% WHERE %fkProperty% IN (:fks) GROUP BY %fkProperty%)") + .replace("%fkProperty%", fkProperty) + .replace("%entity%", clazz.getSimpleName()), + Object[].class) + .setParameter("fks", foreignKeys) + .getResultStream() + .collect( + toImmutableMap( + row -> (String) row[0], + row -> MostRecentResource.create((String) row[1], (DateTime) row[2])))); + } + + private static final CacheLoader, Optional> + CACHE_LOADER = + new CacheLoader, Optional>() { + + @Override + public Optional load(VKey key) { + return loadAll(ImmutableList.of(key)).get(key); + } + + @Override + public Map, Optional> loadAll( + Iterable> keys) { + if (!keys.iterator().hasNext()) { + return ImmutableMap.of(); + } + // It is safe to use the resource type of first element because when this function is + // called, it is always passed with a list of VKeys with the same type. + Class clazz = keys.iterator().next().getKind(); + ImmutableList foreignKeys = + Streams.stream(keys) + .map(key -> (String) key.getSqlKey()) + .collect(toImmutableList()); + ImmutableMap existingKeys = + ForeignKeyUtils.load(clazz, foreignKeys, true); + // The above map only contains keys that exist in the database, so we re-add the + // missing ones with Optional.empty() values for caching. + return Maps.asMap( + ImmutableSet.copyOf(keys), + key -> Optional.ofNullable(existingKeys.get((String) key.getSqlKey()))); + } + }; + + /** + * A limited size, limited time cache for foreign-keyed entities. + * + *

This is only used to cache foreign-keyed entities for the purposes of checking whether they + * exist (and if so, what entity they point to) during a few domain flows. Any other operations on + * foreign keys should not use this cache. + * + *

Note that here the key of the {@link LoadingCache} is of type {@code VKey}, but they are not legal {VKey}s to {@link EppResource}s, whose keys are the SQL + * primary keys, i.e. the {@code repoId}s. Instead, their keys are the foreign keys used to query + * the database. We use {@link VKey} here because it is a convenient composite class that contains + * both the resource type and the foreign key, which are needed to for the query and caching. + * + *

Also note that the value type of this cache is {@link Optional} because the foreign keys in + * question are coming from external commands, and thus don't necessarily represent entities in + * our system that actually exist. So we cache the fact that they *don't* exist by using + * Optional.empty(), then several layers up the EPP command will fail with an error message like + * "The contact with given IDs (blah) don't exist." + */ + @NonFinalForTesting + private static LoadingCache, Optional> + foreignKeyCache = createForeignKeyMapCache(getEppResourceCachingDuration()); + + private static LoadingCache, Optional> + createForeignKeyMapCache(Duration expiry) { + return CacheUtils.newCacheBuilder(expiry) + .maximumSize(getEppResourceMaxCachedEntries()) + .build(CACHE_LOADER); + } + + @VisibleForTesting + public static void setCacheForTest(Optional expiry) { + Duration effectiveExpiry = expiry.orElse(getEppResourceCachingDuration()); + foreignKeyCache = createForeignKeyMapCache(effectiveExpiry); + } + + /** + * Load a list of {@link VKey} to {@link EppResource} instances by class and foreign key strings + * that are active at or after the specified moment in time, using the cache if enabled. + * + *

The returned map will omit any keys for which the {@link EppResource} doesn't exist or has + * been soft deleted. + * + *

Don't use the cached version of this method unless you really need it for performance + * reasons, and are OK with the trade-offs in loss of transactional consistency. + */ + public static ImmutableMap> loadCached( + Class clazz, Collection foreignKeys, final DateTime now) { + if (!RegistryConfig.isEppResourceCachingEnabled()) { + return load(clazz, foreignKeys, now); + } + return foreignKeyCache + .getAll( + foreignKeys.stream().map(fk -> VKey.createSql(clazz, fk)).collect(toImmutableList())) + .entrySet() + .stream() + .filter(e -> e.getValue().isPresent() && now.isBefore(e.getValue().get().deletionTime())) + .collect( + toImmutableMap( + e -> (String) e.getKey().getSqlKey(), + e -> VKey.createSql(clazz, e.getValue().get().repoId()))); + } + + @AutoValue + abstract static class MostRecentResource { + + abstract String repoId(); + + abstract DateTime deletionTime(); + + static MostRecentResource create(String repoId, DateTime deletionTime) { + return new AutoValue_ForeignKeyUtils_MostRecentResource(repoId, deletionTime); + } + } +} diff --git a/core/src/main/java/google/registry/model/contact/Contact.java b/core/src/main/java/google/registry/model/contact/Contact.java index 68eac17b3..1b8547756 100644 --- a/core/src/main/java/google/registry/model/contact/Contact.java +++ b/core/src/main/java/google/registry/model/contact/Contact.java @@ -14,7 +14,6 @@ package google.registry.model.contact; -import com.googlecode.objectify.Key; import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.annotations.ExternalMessagingName; import google.registry.model.annotations.ReportedOn; @@ -46,13 +45,13 @@ import org.joda.time.DateTime; @Index(columnList = "searchName") }) @ExternalMessagingName("contact") -@WithStringVKey +@WithStringVKey(compositeKey = true) @Access(AccessType.FIELD) public class Contact extends ContactBase implements ForeignKeyedEppResource { @Override public VKey createVKey() { - return VKey.create(Contact.class, getRepoId(), Key.create(this)); + return VKey.createSql(Contact.class, getRepoId()); } @Override diff --git a/core/src/main/java/google/registry/model/domain/Domain.java b/core/src/main/java/google/registry/model/domain/Domain.java index 96d1dfaa0..bd9add44b 100644 --- a/core/src/main/java/google/registry/model/domain/Domain.java +++ b/core/src/main/java/google/registry/model/domain/Domain.java @@ -66,7 +66,7 @@ import org.joda.time.DateTime; @Index(columnList = "transfer_billing_event_id"), @Index(columnList = "transfer_billing_recurrence_id") }) -@WithStringVKey +@WithStringVKey(compositeKey = true) @ExternalMessagingName("domain") @Access(AccessType.FIELD) public class Domain extends DomainBase implements ForeignKeyedEppResource { @@ -148,7 +148,7 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource { @Override public VKey createVKey() { - return VKey.create(Domain.class, getRepoId(), Key.create(this)); + return VKey.createSql(Domain.class, getRepoId()); } @Override diff --git a/core/src/main/java/google/registry/model/domain/DomainCommand.java b/core/src/main/java/google/registry/model/domain/DomainCommand.java index 183b90212..b4dcd4615 100644 --- a/core/src/main/java/google/registry/model/domain/DomainCommand.java +++ b/core/src/main/java/google/registry/model/domain/DomainCommand.java @@ -17,7 +17,6 @@ package google.registry.model.domain; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Iterables.getOnlyElement; -import static com.google.common.collect.Maps.transformValues; import static com.google.common.collect.Sets.difference; import static google.registry.util.CollectionUtils.difference; import static google.registry.util.CollectionUtils.forceEmptyToNull; @@ -31,6 +30,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import google.registry.model.EppResource; +import google.registry.model.ForeignKeyUtils; import google.registry.model.ImmutableObject; import google.registry.model.contact.Contact; import google.registry.model.eppinput.ResourceCommand.AbstractSingleResourceCommand; @@ -39,7 +39,6 @@ import google.registry.model.eppinput.ResourceCommand.ResourceCreateOrChange; import google.registry.model.eppinput.ResourceCommand.ResourceUpdate; import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand; import google.registry.model.host.Host; -import google.registry.model.index.ForeignKeyIndex; import google.registry.persistence.VKey; import java.util.Set; import javax.annotation.Nullable; @@ -445,13 +444,12 @@ public class DomainCommand { private static ImmutableMap> loadByForeignKeysCached( final Set foreignKeys, final Class clazz, final DateTime now) throws InvalidReferencesException { - ImmutableMap> fkis = - ForeignKeyIndex.loadCached(clazz, foreignKeys, now); - if (!fkis.keySet().equals(foreignKeys)) { + ImmutableMap> fks = ForeignKeyUtils.loadCached(clazz, foreignKeys, now); + if (!fks.keySet().equals(foreignKeys)) { throw new InvalidReferencesException( - clazz, ImmutableSet.copyOf(difference(foreignKeys, fkis.keySet()))); + clazz, ImmutableSet.copyOf(difference(foreignKeys, fks.keySet()))); } - return ImmutableMap.copyOf(transformValues(fkis, ForeignKeyIndex::getResourceKey)); + return fks; } /** Exception to throw when referenced objects don't exist. */ diff --git a/core/src/main/java/google/registry/model/host/Host.java b/core/src/main/java/google/registry/model/host/Host.java index 414571212..6d76972d4 100644 --- a/core/src/main/java/google/registry/model/host/Host.java +++ b/core/src/main/java/google/registry/model/host/Host.java @@ -14,7 +14,6 @@ package google.registry.model.host; -import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.annotations.ExternalMessagingName; @@ -52,7 +51,7 @@ import javax.persistence.AccessType; @javax.persistence.Index(columnList = "currentSponsorRegistrarId") }) @ExternalMessagingName("host") -@WithStringVKey +@WithStringVKey(compositeKey = true) @Access(AccessType.FIELD) // otherwise it'll use the default if the repoId (property) public class Host extends HostBase implements ForeignKeyedEppResource { @@ -65,7 +64,7 @@ public class Host extends HostBase implements ForeignKeyedEppResource { @Override public VKey createVKey() { - return VKey.create(Host.class, getRepoId(), Key.create(this)); + return VKey.createSql(Host.class, getRepoId()); } @Override diff --git a/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java b/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java deleted file mode 100644 index 9547467ae..000000000 --- a/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.model.index; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static google.registry.config.RegistryConfig.getEppResourceCachingDuration; -import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries; -import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; -import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm; -import static google.registry.util.CollectionUtils.entriesToImmutableMap; -import static google.registry.util.TypeUtils.instantiate; - -import com.github.benmanes.caffeine.cache.CacheLoader; -import com.github.benmanes.caffeine.cache.LoadingCache; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableBiMap; -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.common.collect.Multimaps; -import com.google.common.collect.Streams; -import google.registry.config.RegistryConfig; -import google.registry.model.BackupGroupRoot; -import google.registry.model.CacheUtils; -import google.registry.model.EppResource; -import google.registry.model.contact.Contact; -import google.registry.model.domain.Domain; -import google.registry.model.host.Host; -import google.registry.persistence.VKey; -import google.registry.persistence.transaction.CriteriaQueryBuilder; -import google.registry.persistence.transaction.JpaTransactionManager; -import google.registry.util.NonFinalForTesting; -import java.time.Duration; -import java.util.Collection; -import java.util.Comparator; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nullable; -import org.joda.time.DateTime; - -/** - * Class to map a foreign key to the active instance of {@link EppResource} whose unique id matches - * the foreign key string. The instance is never deleted, but it is updated if a newer entity - * becomes the active entity. - */ -public abstract class ForeignKeyIndex extends BackupGroupRoot { - - /** The {@link ForeignKeyIndex} type for {@link Contact} entities. */ - public static class ForeignKeyContactIndex extends ForeignKeyIndex {} - - /** The {@link ForeignKeyIndex} type for {@link Domain} entities. */ - public static class ForeignKeyDomainIndex extends ForeignKeyIndex {} - - /** The {@link ForeignKeyIndex} type for {@link Host} entities. */ - public static class ForeignKeyHostIndex extends ForeignKeyIndex {} - - private static final ImmutableBiMap< - Class, Class>> - RESOURCE_CLASS_TO_FKI_CLASS = - ImmutableBiMap.of( - Contact.class, ForeignKeyContactIndex.class, - Domain.class, ForeignKeyDomainIndex.class, - Host.class, ForeignKeyHostIndex.class); - - private static final ImmutableMap, String> - RESOURCE_CLASS_TO_FKI_PROPERTY = - ImmutableMap.of( - Contact.class, "contactId", - Domain.class, "fullyQualifiedDomainName", - Host.class, "fullyQualifiedHostName"); - - String foreignKey; - - /** - * The deletion time of this {@link ForeignKeyIndex}. - * - *

This will generally be equal to the deletion time of {@link #reference}. However, in the - * case of a {@link Host} that was renamed, this field will hold the time of the rename. - */ - DateTime deletionTime; - - /** The referenced resource. */ - VKey reference; - - public String getForeignKey() { - return foreignKey; - } - - public DateTime getDeletionTime() { - return deletionTime; - } - - public VKey getResourceKey() { - return reference; - } - - @SuppressWarnings("unchecked") - public static Class> mapToFkiClass( - Class resourceClass) { - return (Class>) RESOURCE_CLASS_TO_FKI_CLASS.get(resourceClass); - } - - /** Create a {@link ForeignKeyIndex} instance for a resource, expiring at a specified time. */ - @SuppressWarnings("unchecked") - private static ForeignKeyIndex create( - E resource, DateTime deletionTime) { - Class resourceClass = (Class) resource.getClass(); - ForeignKeyIndex instance = instantiate(mapToFkiClass(resourceClass)); - instance.reference = (VKey) resource.createVKey(); - instance.foreignKey = resource.getForeignKey(); - instance.deletionTime = deletionTime; - return instance; - } - - /** - * Loads a {@link VKey} to an {@link EppResource} from the database by foreign key. - * - *

Returns null if no foreign key index with this foreign key was ever created, or if the most - * recently created foreign key index was deleted before time "now". This method does not actually - * check that the referenced resource actually exists. However, for normal epp resources, it is - * safe to assume that the referenced resource exists if the foreign key index does. - * - * @param clazz the resource type to load - * @param foreignKey id to match - * @param now the current logical time to use when checking for soft deletion of the foreign key - * index - */ - @Nullable - public static VKey loadAndGetKey( - Class clazz, String foreignKey, DateTime now) { - ForeignKeyIndex index = load(clazz, foreignKey, now); - return index == null ? null : index.getResourceKey(); - } - - /** - * Load a {@link ForeignKeyIndex} by class and id string that is active at or after the specified - * moment in time. - * - *

This will return null if the {@link ForeignKeyIndex} doesn't exist or has been soft deleted. - */ - @Nullable - public static ForeignKeyIndex load( - Class clazz, String foreignKey, DateTime now) { - return load(clazz, ImmutableList.of(foreignKey), now).get(foreignKey); - } - - /** - * Load a map of {@link ForeignKeyIndex} instances by class and id strings that are active at or - * after the specified moment in time. - * - *

The returned map will omit any keys for which the {@link ForeignKeyIndex} doesn't exist or - * has been soft deleted. - */ - public static ImmutableMap> load( - Class clazz, Collection foreignKeys, final DateTime now) { - return loadIndexesFromStore(clazz, foreignKeys, false).entrySet().stream() - .filter(e -> now.isBefore(e.getValue().getDeletionTime())) - .collect(entriesToImmutableMap()); - } - - /** - * Helper method to load all of the most recent {@link ForeignKeyIndex}es for the given foreign - * keys, regardless of whether or not they have been soft-deleted. - * - *

Used by both the cached (w/o deletion check) and the non-cached (with deletion check) calls. - */ - private static - ImmutableMap> loadIndexesFromStore( - Class clazz, Collection foreignKeys, boolean useReplicaJpaTm) { - String property = RESOURCE_CLASS_TO_FKI_PROPERTY.get(clazz); - JpaTransactionManager jpaTmToUse = useReplicaJpaTm ? replicaJpaTm() : jpaTm(); - ImmutableList> indexes = - jpaTmToUse.transact( - () -> - jpaTmToUse - .criteriaQuery( - CriteriaQueryBuilder.create(clazz) - .whereFieldIsIn(property, foreignKeys) - .build()) - .getResultStream() - .map(e -> create(e, e.getDeletionTime())) - .collect(toImmutableList())); - // We need to find and return the entities with the maximum deletionTime for each foreign key. - return Multimaps.index(indexes, ForeignKeyIndex::getForeignKey).asMap().entrySet().stream() - .map( - entry -> - Maps.immutableEntry( - entry.getKey(), - entry.getValue().stream() - .max(Comparator.comparing(ForeignKeyIndex::getDeletionTime)) - .get())) - .collect(entriesToImmutableMap()); - } - - static final CacheLoader>, Optional>> CACHE_LOADER = - new CacheLoader>, Optional>>() { - - @Override - public Optional> load(VKey> key) { - String foreignKey = key.getSqlKey().toString(); - return Optional.ofNullable( - loadIndexesFromStore( - RESOURCE_CLASS_TO_FKI_CLASS.inverse().get(key.getKind()), - ImmutableSet.of(foreignKey), - true) - .get(foreignKey)); - } - - @Override - public Map>, Optional>> loadAll( - Iterable>> keys) { - if (!keys.iterator().hasNext()) { - return ImmutableMap.of(); - } - Class resourceClass = - RESOURCE_CLASS_TO_FKI_CLASS.inverse().get(keys.iterator().next().getKind()); - ImmutableSet foreignKeys = - Streams.stream(keys).map(v -> v.getSqlKey().toString()).collect(toImmutableSet()); - ImmutableSet>> typedKeys = ImmutableSet.copyOf(keys); - ImmutableMap> existingFkis = - loadIndexesFromStore(resourceClass, foreignKeys, true); - // ofy omits keys that don't have values in Datastore, so re-add them in - // here with Optional.empty() values. - return Maps.asMap( - typedKeys, - (VKey> key) -> - Optional.ofNullable(existingFkis.getOrDefault(key.getSqlKey().toString(), null))); - } - }; - - /** - * A limited size, limited time cache for foreign key entities. - * - *

This is only used to cache foreign key entities for the purposes of checking whether they - * exist (and if so, what entity they point to) during a few domain flows. Any other operations on - * foreign keys should not use this cache. - * - *

Note that the value type of this cache is Optional because the foreign keys in question are - * coming from external commands, and thus don't necessarily represent entities in our system that - * actually exist. So we cache the fact that they *don't* exist by using Optional.empty(), and - * then several layers up the EPP command will fail with an error message like "The contact with - * given IDs (blah) don't exist." - */ - @NonFinalForTesting - private static LoadingCache>, Optional>> - cacheForeignKeyIndexes = createForeignKeyIndexesCache(getEppResourceCachingDuration()); - - private static LoadingCache>, Optional>> - createForeignKeyIndexesCache(Duration expiry) { - return CacheUtils.newCacheBuilder(expiry) - .maximumSize(getEppResourceMaxCachedEntries()) - .build(CACHE_LOADER); - } - - @VisibleForTesting - public static void setCacheForTest(Optional expiry) { - Duration effectiveExpiry = expiry.orElse(getEppResourceCachingDuration()); - cacheForeignKeyIndexes = createForeignKeyIndexesCache(effectiveExpiry); - } - - /** - * Load a list of {@link ForeignKeyIndex} instances by class and id strings that are active at or - * after the specified moment in time, using the cache if enabled. - * - *

The returned map will omit any keys for which the {@link ForeignKeyIndex} doesn't exist or - * has been soft deleted. - * - *

Don't use the cached version of this method unless you really need it for performance - * reasons, and are OK with the trade-offs in loss of transactional consistency. - */ - public static ImmutableMap> loadCached( - Class clazz, Collection foreignKeys, final DateTime now) { - if (!RegistryConfig.isEppResourceCachingEnabled()) { - return load(clazz, foreignKeys, now); - } - Class> fkiClass = mapToFkiClass(clazz); - // Safe to cast VKey> to VKey> - @SuppressWarnings("unchecked") - ImmutableList>> fkiVKeys = - foreignKeys.stream() - .map(fk -> (VKey>) VKey.createSql(fkiClass, fk)) - .collect(toImmutableList()); - // This cast is safe because when we loaded ForeignKeyIndexes above we used type clazz, which - // is scoped to E. - @SuppressWarnings("unchecked") - ImmutableMap> fkisFromCache = - cacheForeignKeyIndexes.getAll(fkiVKeys).entrySet().stream() - .filter(entry -> entry.getValue().isPresent()) - .filter(entry -> now.isBefore(entry.getValue().get().getDeletionTime())) - .collect( - toImmutableMap( - entry -> entry.getKey().getSqlKey().toString(), - entry -> (ForeignKeyIndex) entry.getValue().get())); - return fkisFromCache; - } -} diff --git a/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java b/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java index 86b0df057..8bafb6f7e 100644 --- a/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java +++ b/core/src/main/java/google/registry/rdap/RdapDomainSearchAction.java @@ -16,7 +16,6 @@ package google.registry.rdap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static google.registry.model.EppResourceUtils.loadByForeignKeyCached; -import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey; import static google.registry.model.ofy.ObjectifyService.auditedOfy; import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -36,6 +35,7 @@ import com.google.common.flogger.FluentLogger; import com.google.common.net.InetAddresses; import com.google.common.primitives.Booleans; import com.googlecode.objectify.cmd.Query; +import google.registry.model.ForeignKeyUtils; import google.registry.model.domain.Domain; import google.registry.model.host.Host; import google.registry.persistence.VKey; @@ -331,9 +331,9 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { return getNameserverRefsByLdhNameWithSuffix(partialStringQuery); } // If there's no suffix, query the host resources. Query the resources themselves, rather than - // the foreign key indexes, because then we have an index on fully qualified host name and - // deletion time, so we can check the deletion status in the query itself. The initial string - // must be present, to avoid querying every host in the system. This restriction is enforced by + // the foreign keys, because then we have an index on fully qualified host name and deletion + // time, so we can check the deletion status in the query itself. The initial string must be + // present, to avoid querying every host in the system. This restriction is enforced by // {@link queryItems}. // // Only return the first maxNameserversInFirstStage nameservers. This could result in an @@ -400,7 +400,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { : ImmutableList.of(host.get().createVKey()); } else { VKey hostKey = - loadAndGetKey( + ForeignKeyUtils.load( Host.class, partialStringQuery.getInitialString(), shouldIncludeDeleted() ? START_OF_TIME : getRequestTime()); @@ -442,7 +442,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } } else { VKey hostKey = - loadAndGetKey( + ForeignKeyUtils.load( Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime()); if (hostKey != null) { builder.add(hostKey); diff --git a/core/src/main/java/google/registry/tools/CommandUtilities.java b/core/src/main/java/google/registry/tools/CommandUtilities.java index 3d76ea3cc..7a30a8572 100644 --- a/core/src/main/java/google/registry/tools/CommandUtilities.java +++ b/core/src/main/java/google/registry/tools/CommandUtilities.java @@ -19,10 +19,10 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Ascii; import com.google.common.base.Strings; import google.registry.model.EppResource; +import google.registry.model.ForeignKeyUtils; import google.registry.model.contact.Contact; import google.registry.model.domain.Domain; import google.registry.model.host.Host; -import google.registry.model.index.ForeignKeyIndex; import google.registry.persistence.VKey; import org.joda.time.DateTime; @@ -42,7 +42,7 @@ class CommandUtilities { } public VKey getKey(String uniqueId, DateTime now) { - return ForeignKeyIndex.loadAndGetKey(clazz, uniqueId, now); + return ForeignKeyUtils.load(clazz, uniqueId, now); } } diff --git a/core/src/main/java/google/registry/tools/UnrenewDomainCommand.java b/core/src/main/java/google/registry/tools/UnrenewDomainCommand.java index 90e813c59..d514b1a0c 100644 --- a/core/src/main/java/google/registry/tools/UnrenewDomainCommand.java +++ b/core/src/main/java/google/registry/tools/UnrenewDomainCommand.java @@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import google.registry.model.ForeignKeyUtils; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Recurring; import google.registry.model.domain.Domain; @@ -38,7 +39,6 @@ import google.registry.model.domain.DomainHistory; import google.registry.model.domain.Period; import google.registry.model.domain.Period.Unit; import google.registry.model.eppcommon.StatusValue; -import google.registry.model.index.ForeignKeyIndex; import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry.Type; import google.registry.util.Clock; @@ -88,7 +88,7 @@ class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemot new ImmutableMap.Builder<>(); for (String domainName : mainParameters) { - if (ForeignKeyIndex.load(Domain.class, domainName, START_OF_TIME) == null) { + if (ForeignKeyUtils.load(Domain.class, domainName, START_OF_TIME) == null) { domainsNonexistentBuilder.add(domainName); continue; } diff --git a/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java b/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java index ef75dda1b..b129669f8 100644 --- a/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java +++ b/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java @@ -98,7 +98,7 @@ public class ResaveEntityActionTest { clock.advanceOneMilli(); assertThat(domain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar"); runAction( - domain.createVKey().getOfyKey().getString(), + domain.createVKey().stringify(), DateTime.parse("2016-02-06T10:00:01Z"), ImmutableSortedSet.of()); Domain resavedDomain = loadByEntity(domain); @@ -128,7 +128,7 @@ public class ResaveEntityActionTest { assertThat(domain.getGracePeriods()).isNotEmpty(); runAction( - domain.createVKey().getOfyKey().getString(), + domain.createVKey().stringify(), requestedTime, ImmutableSortedSet.of(requestedTime.plusDays(5))); Domain resavedDomain = loadByEntity(domain); diff --git a/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java b/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java index 65398d8b1..7d06c900f 100644 --- a/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java +++ b/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java @@ -104,7 +104,7 @@ public class PublishDnsUpdatesActionTest { persistActiveSubordinateHost("ns1.example.xn--q9jyb4c", domain1); persistActiveSubordinateHost("ns2.example.xn--q9jyb4c", domain1); Domain domain2 = persistActiveDomain("example2.xn--q9jyb4c"); - persistActiveSubordinateHost("ns1.example.xn--q9jyb4c", domain2); + persistActiveSubordinateHost("ns1.example2.xn--q9jyb4c", domain2); clock.advanceOneMilli(); } diff --git a/core/src/test/java/google/registry/flows/contact/ContactUpdateFlowTest.java b/core/src/test/java/google/registry/flows/contact/ContactUpdateFlowTest.java index f77f49458..c61d00d1a 100644 --- a/core/src/test/java/google/registry/flows/contact/ContactUpdateFlowTest.java +++ b/core/src/test/java/google/registry/flows/contact/ContactUpdateFlowTest.java @@ -52,7 +52,6 @@ class ContactUpdateFlowTest extends ResourceFlowTestCase { Host renamedHost = doSuccessfulTest(); assertThat(renamedHost.isSubordinate()).isTrue(); assertDnsTasksEnqueued("ns1.example.tld", "ns2.example.tld"); - ForeignKeyIndex oldFkiAfterRename = - ForeignKeyIndex.load(Host.class, oldHostName(), clock.nowUtc()); - assertThat(oldFkiAfterRename).isNull(); + VKey oldVKeyAfterRename = ForeignKeyUtils.load(Host.class, oldHostName(), clock.nowUtc()); + assertThat(oldVKeyAfterRename).isNull(); } @Test @@ -982,13 +982,13 @@ class HostUpdateFlowTest extends ResourceFlowTestCase { @Test void testFailure_addRemoveSameStatusValues() throws Exception { createTld("tld"); - persistActiveDomain("example.tld"); + Domain domain = persistActiveDomain("example.tld"); setEppHostUpdateInput( "ns1.example.tld", "ns2.example.tld", "", ""); - persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld")); + persistActiveSubordinateHost(oldHostName(), domain); EppException thrown = assertThrows(AddRemoveSameValueException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); } diff --git a/core/src/test/java/google/registry/model/ForeignKeyUtilsTest.java b/core/src/test/java/google/registry/model/ForeignKeyUtilsTest.java new file mode 100644 index 000000000..36ee561f3 --- /dev/null +++ b/core/src/test/java/google/registry/model/ForeignKeyUtilsTest.java @@ -0,0 +1,153 @@ +// Copyright 2022 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.model; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.persistActiveContact; +import static google.registry.testing.DatabaseHelper.persistActiveDomain; +import static google.registry.testing.DatabaseHelper.persistActiveHost; +import static google.registry.testing.DatabaseHelper.persistResource; +import static org.joda.time.DateTimeZone.UTC; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import google.registry.model.contact.Contact; +import google.registry.model.domain.Domain; +import google.registry.model.host.Host; +import google.registry.persistence.transaction.JpaTestExtensions; +import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; +import google.registry.testing.FakeClock; +import google.registry.testing.TestCacheExtension; +import java.time.Duration; +import org.joda.time.DateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit tests for {@link ForeignKeyUtils}. */ +class ForeignKeyUtilsTest { + + private final FakeClock fakeClock = new FakeClock(DateTime.now(UTC)); + + @RegisterExtension + public final JpaIntegrationTestExtension jpaIntegrationTestExtension = + new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension(); + + @RegisterExtension + public final TestCacheExtension testCacheExtension = + new TestCacheExtension.Builder().withForeignKeyCache(Duration.ofDays(1)).build(); + + @BeforeEach + void setUp() { + createTld("com"); + } + + @Test + void testSuccess_loadHost() { + Host host = persistActiveHost("ns1.example.com"); + assertThat(ForeignKeyUtils.load(Host.class, "ns1.example.com", fakeClock.nowUtc())) + .isEqualTo(host.createVKey()); + } + + @Test + void testSuccess_loadDomain() { + Domain domain = persistActiveDomain("example.com"); + assertThat(ForeignKeyUtils.load(Domain.class, "example.com", fakeClock.nowUtc())) + .isEqualTo(domain.createVKey()); + } + + @Test + void testSuccess_loadContact() { + Contact contact = persistActiveContact("john-doe"); + assertThat(ForeignKeyUtils.load(Contact.class, "john-doe", fakeClock.nowUtc())) + .isEqualTo(contact.createVKey()); + } + + @Test + void testSuccess_loadMostRecentResource() { + Host host = persistActiveHost("ns1.example.com"); + persistResource(host.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build()); + fakeClock.advanceOneMilli(); + Host newHost = persistActiveHost("ns1.example.com"); + assertThat(ForeignKeyUtils.load(Host.class, "ns1.example.com", fakeClock.nowUtc())) + .isEqualTo(newHost.createVKey()); + } + + @Test + void testSuccess_loadNonexistentForeignKey_returnsNull() { + assertThat(ForeignKeyUtils.load(Host.class, "ns1.example.com", fakeClock.nowUtc())).isNull(); + } + + @Test + void testSuccess_loadDeletedForeignKey_returnsNull() { + Host host = persistActiveHost("ns1.example.com"); + persistResource(host.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build()); + assertThat(ForeignKeyUtils.load(Host.class, "ns1.example.com", fakeClock.nowUtc())).isNull(); + } + + @Test + void testSuccess_mostRecentKeySoftDeleted_returnsNull() { + Host host1 = persistActiveHost("ns1.example.com"); + fakeClock.advanceOneMilli(); + persistResource(host1.asBuilder().setDeletionTime(fakeClock.nowUtc()).build()); + assertThat(ForeignKeyUtils.load(Host.class, "ns1.example.com", fakeClock.nowUtc())).isNull(); + } + + @Test + void testSuccess_batchLoad_skipsDeletedAndNonexistent() { + Host host1 = persistActiveHost("ns1.example.com"); + Host host2 = persistActiveHost("ns2.example.com"); + persistResource(host2.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build()); + assertThat( + ForeignKeyUtils.load( + Host.class, + ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"), + fakeClock.nowUtc())) + .containsExactlyEntriesIn(ImmutableMap.of("ns1.example.com", host1.createVKey())); + persistResource(host1.asBuilder().setDeletionTime(fakeClock.nowUtc()).build()); + fakeClock.advanceOneMilli(); + Host newHost1 = persistActiveHost("ns1.example.com"); + assertThat( + ForeignKeyUtils.loadCached( + Host.class, + ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"), + fakeClock.nowUtc())) + .containsExactlyEntriesIn(ImmutableMap.of("ns1.example.com", newHost1.createVKey())); + } + + @Test + void testSuccess_loadHostsCached_cacheIsStale() { + Host host1 = persistActiveHost("ns1.example.com"); + Host host2 = persistActiveHost("ns2.example.com"); + persistResource(host2.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build()); + assertThat( + ForeignKeyUtils.loadCached( + Host.class, + ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"), + fakeClock.nowUtc())) + .containsExactlyEntriesIn(ImmutableMap.of("ns1.example.com", host1.createVKey())); + persistResource(host1.asBuilder().setDeletionTime(fakeClock.nowUtc()).build()); + fakeClock.advanceOneMilli(); + persistActiveHost("ns1.example.com"); + // Even though a new host1 is now live, the cache still returns the VKey to the old one. + assertThat( + ForeignKeyUtils.loadCached( + Host.class, + ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"), + fakeClock.nowUtc())) + .containsExactlyEntriesIn(ImmutableMap.of("ns1.example.com", host1.createVKey())); + } +} diff --git a/core/src/test/java/google/registry/model/domain/DomainTest.java b/core/src/test/java/google/registry/model/domain/DomainTest.java index e1a9fb4d2..bf702c041 100644 --- a/core/src/test/java/google/registry/model/domain/DomainTest.java +++ b/core/src/test/java/google/registry/model/domain/DomainTest.java @@ -168,9 +168,8 @@ public class DomainTest { domain = persistResource( cloneAndSetAutoTimestamps( - new Domain.Builder() - .setDomainName("example.com") - .setRepoId("4-COM") + domain + .asBuilder() .setCreationRegistrarId("TheRegistrar") .setLastEppUpdateTime(fakeClock.nowUtc()) .setLastEppUpdateRegistrarId("NewRegistrar") diff --git a/core/src/test/java/google/registry/model/index/ForeignKeyIndexTest.java b/core/src/test/java/google/registry/model/index/ForeignKeyIndexTest.java deleted file mode 100644 index 6ce393696..000000000 --- a/core/src/test/java/google/registry/model/index/ForeignKeyIndexTest.java +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.model.index; - -import static com.google.common.truth.Truth.assertThat; -import static google.registry.testing.DatabaseHelper.createTld; -import static google.registry.testing.DatabaseHelper.persistActiveHost; -import static google.registry.testing.DatabaseHelper.persistResource; - -import com.google.common.collect.ImmutableList; -import google.registry.model.EntityTestCase; -import google.registry.model.host.Host; -import google.registry.testing.TestCacheExtension; -import java.time.Duration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -/** Unit tests for {@link ForeignKeyIndex}. */ -class ForeignKeyIndexTest extends EntityTestCase { - - @RegisterExtension - public final TestCacheExtension testCacheExtension = - new TestCacheExtension.Builder().withForeignIndexKeyCache(Duration.ofDays(1)).build(); - - @BeforeEach - void setUp() { - createTld("com"); - } - - @Test - void testLoadForNonexistentForeignKey_returnsNull() { - assertThat(ForeignKeyIndex.load(Host.class, "ns1.example.com", fakeClock.nowUtc())).isNull(); - } - - @Test - void testLoadForDeletedForeignKey_returnsNull() { - Host host = persistActiveHost("ns1.example.com"); - persistResource(host.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build()); - assertThat(ForeignKeyIndex.load(Host.class, "ns1.example.com", fakeClock.nowUtc())).isNull(); - } - - @Test - void testLoad_newerKeyHasBeenSoftDeleted() { - Host host1 = persistActiveHost("ns1.example.com"); - fakeClock.advanceOneMilli(); - persistResource(host1.asBuilder().setDeletionTime(fakeClock.nowUtc()).build()); - assertThat(ForeignKeyIndex.load(Host.class, "ns1.example.com", fakeClock.nowUtc())).isNull(); - } - - @Test - void testBatchLoad_skipsDeletedAndNonexistent() { - persistActiveHost("ns1.example.com"); - Host host = persistActiveHost("ns2.example.com"); - persistResource(host.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build()); - assertThat( - ForeignKeyIndex.load( - Host.class, - ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"), - fakeClock.nowUtc()) - .keySet()) - .containsExactly("ns1.example.com"); - } - - @Test - void testDeadCodeThatDeletedScrapCommandsReference() { - persistActiveHost("omg"); - assertThat(ForeignKeyIndex.load(Host.class, "omg", fakeClock.nowUtc()).getForeignKey()) - .isEqualTo("omg"); - } -} diff --git a/core/src/test/java/google/registry/testing/TestCacheExtension.java b/core/src/test/java/google/registry/testing/TestCacheExtension.java index 2c5c06673..cdfd3cb21 100644 --- a/core/src/test/java/google/registry/testing/TestCacheExtension.java +++ b/core/src/test/java/google/registry/testing/TestCacheExtension.java @@ -15,13 +15,13 @@ package google.registry.testing; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; import google.registry.model.EppResource; -import google.registry.model.index.ForeignKeyIndex; +import google.registry.model.ForeignKeyUtils; import google.registry.model.tld.label.PremiumListDao; import google.registry.model.tmch.ClaimsListDao; import java.time.Duration; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; @@ -53,37 +53,30 @@ public class TestCacheExtension implements BeforeEachCallback, AfterEachCallback /** Builder for {@link TestCacheExtension}. */ public static class Builder { - private final Map cacheHandlerMap = Maps.newHashMap(); + private final List cacheHandlers = new ArrayList<>(); public Builder withEppResourceCache(Duration expiry) { - cacheHandlerMap.put( - "EppResource.cacheEppResources", - new TestCacheHandler(EppResource::setCacheForTest, expiry)); + cacheHandlers.add(new TestCacheHandler(EppResource::setCacheForTest, expiry)); return this; } - public Builder withForeignIndexKeyCache(Duration expiry) { - cacheHandlerMap.put( - "ForeignKeyIndex.cacheForeignKeyIndexes", - new TestCacheHandler(ForeignKeyIndex::setCacheForTest, expiry)); + public Builder withForeignKeyCache(Duration expiry) { + cacheHandlers.add(new TestCacheHandler(ForeignKeyUtils::setCacheForTest, expiry)); return this; } public Builder withPremiumListsCache(Duration expiry) { - cacheHandlerMap.put( - "PremiumListSqlDao.premiumListCache", - new TestCacheHandler(PremiumListDao::setPremiumListCacheForTest, expiry)); + cacheHandlers.add(new TestCacheHandler(PremiumListDao::setPremiumListCacheForTest, expiry)); return this; } public Builder withClaimsListCache(Duration expiry) { - cacheHandlerMap.put( - "ClaimsListDao.CACHE", new TestCacheHandler(ClaimsListDao::setCacheForTest, expiry)); + cacheHandlers.add(new TestCacheHandler(ClaimsListDao::setCacheForTest, expiry)); return this; } public TestCacheExtension build() { - return new TestCacheExtension(ImmutableList.copyOf(cacheHandlerMap.values())); + return new TestCacheExtension(ImmutableList.copyOf(cacheHandlers)); } } diff --git a/core/src/test/java/google/registry/tools/GetContactCommandTest.java b/core/src/test/java/google/registry/tools/GetContactCommandTest.java index 59dbf3158..d4493f7cb 100644 --- a/core/src/test/java/google/registry/tools/GetContactCommandTest.java +++ b/core/src/test/java/google/registry/tools/GetContactCommandTest.java @@ -42,11 +42,7 @@ class GetContactCommandTest extends CommandTestCase { persistActiveContact("sh8013"); runCommand("sh8013"); assertInStdout("contactId=sh8013"); - assertInStdout( - "Websafe key: " - + "kind:Contact" - + "@sql:rO0ABXQABjItUk9JRA" - + "@ofy:agR0ZXN0chMLEgdDb250YWN0IgYyLVJPSUQM"); + assertInStdout("Websafe key: " + "kind:Contact" + "@sql:rO0ABXQABjItUk9JRA"); } @Test @@ -54,11 +50,7 @@ class GetContactCommandTest extends CommandTestCase { persistActiveContact("sh8013"); runCommand("sh8013", "--expand"); assertInStdout("contactId=sh8013"); - assertInStdout( - "Websafe key: " - + "kind:Contact" - + "@sql:rO0ABXQABjItUk9JRA" - + "@ofy:agR0ZXN0chMLEgdDb250YWN0IgYyLVJPSUQM"); + assertInStdout("Websafe key: " + "kind:Contact" + "@sql:rO0ABXQABjItUk9JRA"); assertNotInStdout("LiveRef"); } @@ -69,16 +61,8 @@ class GetContactCommandTest extends CommandTestCase { runCommand("sh8013", "jd1234"); assertInStdout("contactId=sh8013"); assertInStdout("contactId=jd1234"); - assertInStdout( - "Websafe key: " - + "kind:Contact" - + "@sql:rO0ABXQABjItUk9JRA" - + "@ofy:agR0ZXN0chMLEgdDb250YWN0IgYyLVJPSUQM"); - assertInStdout( - "Websafe key: " - + "kind:Contact" - + "@sql:rO0ABXQABjMtUk9JRA" - + "@ofy:agR0ZXN0chMLEgdDb250YWN0IgYzLVJPSUQM"); + assertInStdout("Websafe key: " + "kind:Contact" + "@sql:rO0ABXQABjItUk9JRA"); + assertInStdout("Websafe key: " + "kind:Contact" + "@sql:rO0ABXQABjMtUk9JRA"); } @Test diff --git a/core/src/test/java/google/registry/tools/GetDomainCommandTest.java b/core/src/test/java/google/registry/tools/GetDomainCommandTest.java index b74cddbef..e66a01d52 100644 --- a/core/src/test/java/google/registry/tools/GetDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/GetDomainCommandTest.java @@ -39,11 +39,7 @@ class GetDomainCommandTest extends CommandTestCase { runCommand("example.tld"); assertInStdout("fullyQualifiedDomainName=example.tld"); assertInStdout("Contact=VKey(sql:3-ROID"); - assertInStdout( - "Websafe key: " - + "kind:Domain" - + "@sql:rO0ABXQABTItVExE" - + "@ofy:agR0ZXN0chELEgZEb21haW4iBTItVExEDA"); + assertInStdout("Websafe key: " + "kind:Domain" + "@sql:rO0ABXQABTItVExE"); } @Test @@ -52,11 +48,7 @@ class GetDomainCommandTest extends CommandTestCase { runCommand("example.tld", "--expand"); assertInStdout("fullyQualifiedDomainName=example.tld"); assertInStdout("sqlKey=3-ROID"); - assertInStdout( - "Websafe key: " - + "kind:Domain" - + "@sql:rO0ABXQABTItVExE" - + "@ofy:agR0ZXN0chELEgZEb21haW4iBTItVExEDA"); + assertInStdout("Websafe key: " + "kind:Domain" + "@sql:rO0ABXQABTItVExE"); assertNotInStdout("LiveRef"); } @@ -76,16 +68,8 @@ class GetDomainCommandTest extends CommandTestCase { runCommand("example.tld", "example2.tld"); assertInStdout("fullyQualifiedDomainName=example.tld"); assertInStdout("fullyQualifiedDomainName=example2.tld"); - assertInStdout( - "Websafe key: " - + "kind:Domain" - + "@sql:rO0ABXQABTItVExE" - + "@ofy:agR0ZXN0chELEgZEb21haW4iBTItVExEDA"); - assertInStdout( - "Websafe key: " - + "kind:Domain" - + "@sql:rO0ABXQABTQtVExE" - + "@ofy:agR0ZXN0chELEgZEb21haW4iBTQtVExEDA"); + assertInStdout("Websafe key: " + "kind:Domain" + "@sql:rO0ABXQABTItVExE"); + assertInStdout("Websafe key: " + "kind:Domain" + "@sql:rO0ABXQABTQtVExE"); } @Test diff --git a/core/src/test/java/google/registry/tools/GetHostCommandTest.java b/core/src/test/java/google/registry/tools/GetHostCommandTest.java index cbff4a284..55366907b 100644 --- a/core/src/test/java/google/registry/tools/GetHostCommandTest.java +++ b/core/src/test/java/google/registry/tools/GetHostCommandTest.java @@ -42,11 +42,7 @@ class GetHostCommandTest extends CommandTestCase { persistActiveHost("ns1.example.tld"); runCommand("ns1.example.tld"); assertInStdout("fullyQualifiedHostName=ns1.example.tld"); - assertInStdout( - "Websafe key: " - + "kind:Host" - + "@sql:rO0ABXQABjItUk9JRA" - + "@ofy:agR0ZXN0chALEgRIb3N0IgYyLVJPSUQM"); + assertInStdout("Websafe key: " + "kind:Host" + "@sql:rO0ABXQABjItUk9JRA"); } @Test @@ -54,11 +50,7 @@ class GetHostCommandTest extends CommandTestCase { persistActiveHost("ns1.example.tld"); runCommand("ns1.example.tld", "--expand"); assertInStdout("fullyQualifiedHostName=ns1.example.tld"); - assertInStdout( - "Websafe key: " - + "kind:Host" - + "@sql:rO0ABXQABjItUk9JRA" - + "@ofy:agR0ZXN0chALEgRIb3N0IgYyLVJPSUQM"); + assertInStdout("Websafe key: " + "kind:Host" + "@sql:rO0ABXQABjItUk9JRA"); assertNotInStdout("LiveRef"); } @@ -69,16 +61,8 @@ class GetHostCommandTest extends CommandTestCase { runCommand("ns1.example.tld", "ns2.example.tld"); assertInStdout("fullyQualifiedHostName=ns1.example.tld"); assertInStdout("fullyQualifiedHostName=ns2.example.tld"); - assertInStdout( - "Websafe key: " - + "kind:Host" - + "@sql:rO0ABXQABjItUk9JRA" - + "@ofy:agR0ZXN0chALEgRIb3N0IgYyLVJPSUQM"); - assertInStdout( - "Websafe key: " - + "kind:Host" - + "@sql:rO0ABXQABjMtUk9JRA" - + "@ofy:agR0ZXN0chALEgRIb3N0IgYzLVJPSUQM"); + assertInStdout("Websafe key: " + "kind:Host" + "@sql:rO0ABXQABjItUk9JRA"); + assertInStdout("Websafe key: " + "kind:Host" + "@sql:rO0ABXQABjMtUk9JRA"); } @Test diff --git a/core/src/test/java/google/registry/tools/UpdateDomainCommandTest.java b/core/src/test/java/google/registry/tools/UpdateDomainCommandTest.java index ec7fcab06..72612da25 100644 --- a/core/src/test/java/google/registry/tools/UpdateDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/UpdateDomainCommandTest.java @@ -160,11 +160,7 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase> nameservers = ImmutableSet.of(host1.createVKey(), host2.createVKey()); - persistResource( - DatabaseHelper.newDomain("example.tld").asBuilder().setNameservers(nameservers).build()); + persistResource(domain.asBuilder().setNameservers(nameservers).build()); runCommandForced( "--client=NewRegistrar", "--nameservers=ns2.zdns.google,ns3.zdns.google", "example.tld"); eppVerifier.verifySent("domain_update_set_nameservers.xml"); @@ -243,7 +238,7 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase techContactKey = techContact.createVKey(); persistResource( - DatabaseHelper.newDomain("example.tld") + domain .asBuilder() .setContacts( ImmutableSet.of( @@ -261,7 +256,7 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase> nameservers = ImmutableSet.of(host.createVKey()); persistResource( - DatabaseHelper.newDomain("example.tld") + domain .asBuilder() .setStatusValues( ImmutableSet.of( @@ -370,7 +365,7 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase techContactKey = techContact.createVKey(); persistResource( - DatabaseHelper.newDomain("example.tld") + domain .asBuilder() .setContacts( ImmutableSet.of( @@ -394,7 +389,7 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase> nameservers = ImmutableSet.of(host.createVKey()); persistResource( - DatabaseHelper.newDomain("example.tld") + domain .asBuilder() .setStatusValues(ImmutableSet.of(SERVER_UPDATE_PROHIBITED)) .setNameservers(nameservers) @@ -419,7 +414,7 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase> nameservers = ImmutableSet.of(host.createVKey()); persistResource( - DatabaseHelper.newDomain("example.tld") + domain .asBuilder() .setStatusValues(ImmutableSet.of(PENDING_DELETE)) .setNameservers(nameservers) diff --git a/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java b/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java index 1c690cd42..ea6e9de97 100644 --- a/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java +++ b/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java @@ -398,7 +398,7 @@ class RegistrarConsoleScreenshotTest extends WebDriverTestCase { server.runInAppEngineEnvironment( () -> { createTld("tld"); - persistResource(DatabaseHelper.newDomain("example.tld")); + persistResource(DatabaseHelper.newDomain("example-lock.tld")); saveRegistryLock( new RegistryLock.Builder() .setRegistrarPocId("johndoe@theregistrar.com") @@ -406,7 +406,7 @@ class RegistrarConsoleScreenshotTest extends WebDriverTestCase { .setRegistrarId("TheRegistrar") .setVerificationCode("f1be78a2-2d61-458c-80f0-9dd8f2f8625f") .isSuperuser(false) - .setDomainName("example.tld") + .setDomainName("example-lock.tld") .build()); return null; }); diff --git a/core/src/test/java/google/registry/whois/WhoisActionTest.java b/core/src/test/java/google/registry/whois/WhoisActionTest.java index c70c62988..b27bf41fb 100644 --- a/core/src/test/java/google/registry/whois/WhoisActionTest.java +++ b/core/src/test/java/google/registry/whois/WhoisActionTest.java @@ -81,7 +81,7 @@ public class WhoisActionTest { public final TestCacheExtension testCacheExtension = new TestCacheExtension.Builder() .withEppResourceCache(Duration.ofDays(1)) - .withForeignIndexKeyCache(Duration.ofDays(1)) + .withForeignKeyCache(Duration.ofDays(1)) .build(); private final FakeResponse response = new FakeResponse(); diff --git a/core/src/test/resources/google/registry/webdriver/goldens/chrome-linux/RegistrarConsoleScreenshotTest_registryLockVerify_success_page.png b/core/src/test/resources/google/registry/webdriver/goldens/chrome-linux/RegistrarConsoleScreenshotTest_registryLockVerify_success_page.png index 76acb814d0e43e1636f73937d0828dc394c44778..00fef9e0520021086d282853c42410080cd64910 100644 GIT binary patch literal 31114 zcmeIac~n#98!n2p)CwxKMG!$~L6pc;K}2L~wV=!r<{5-Q5gEf2Aw+E*svrVk9%5w@ z2!l)^OhFNm1c=HI86!f75E3K^A%rC7i|y~6d(Qpmo^$WI_nx)LvMZ$7N%nrfcX;0C zeZKwICA$mzBo9l9iHYsAy!abbOzi7=F|pmRzx@XMpSLFE--?ML94vqP)hWSedHPO{ zn)5Sl){1W{M-aXl-nCE&A2QUCym|M=kMduCz31j{2ftEzr1JgIyGMV$^vlg3E&XqU z11!zILvO9JqHv?5^cc*uyeI=>vd2mT&#A~M_L|pv1IJVtC**VDRsQp+S)krFxci?+ zlZ-_6fBrsox&1+o zkQ-IJjpz%} zKlV(94w-PKSVmN?9d2u>I{uDg^u>58k~8>@-k(7gY~aeKJjeQelo)%d5MeyG;UC_` zU0{X!@;o379!LJlI)LOVHg9eH_Rk}beag{qR55Y+DXeNjI{DKd@{`4TEkl}z&N9$* zv8%muk8R~v&_QNi{&S?gJdhZt@fLIeyy7o znUE;euii2pH(Ap@ds`vss%PQ$f&l?iB7O&V{~xD$)nJ_|;ga$FaWxlYZYX1AXC}h4?y>1q(+LwPt-du0$v^KDKM10?o_<>er6rAE6KiC?U zYfSmnf^TTLSFE;LoY@OLKn!^+uF>}ybHpKz$Hb9anHbBVS+6v1f+V(YZT!1`zkD^M zrQ6ERc=ZKK%6IcQR>HFUE`K@LjM24C)5$sD2~jvrOByXzdy z?asl|lW0nQWfUalHitV}m+ag^i=}z~P}ln5o;>gK8jmXMngqT=x=BY&*&0kYVHL9U zOK(NFl*dIR*%V9I`t|J#Z)+@eY5rhK0F&iTW zwvmYQ+^?+DPhY%Xw&ma+s#OP(dYk5*h}PBS`T7*7RD7Cfc>ufL|sxdE??cYkdR>yGq{UaG9fWeCuIo5DK^xxK`w!Y|~95(A3jXArQM38lnk z2dg5PNclZklos$t991ab1RKwkx)+KwCPM49`ULO)kS>0z)t~5z;;)lW9h26hGlwYbWlQ~WT|n`y}~HfChfM5k98%Fmkm5YUh#+1vRt!u zpUA4{n+aCatShtFALldewRt|^0vsnDygA{|rSFO7h>V(LNu(!#} zW=D=hkymXN(QkvMR2Y!`jxn@WX`I>xWn>KGX>=icKQwY3m!<&%NW1U%%~rz&?%usr zvX|qxj&EU3J{I;getAsIWNp{hQsvvI1VS?OI?eDl-SOY+l`Z3nYd9|6X83tuTP!R4 zvHw-^te_{b%GC-h2F%5mru(l*aWmSQ?X_0p=CtyNn z?F#gLpr>Tjc)8vJKltdxkkN{9fakrwlw#8FZL=-O$$W-yj*3>ADDkEH;gN1mwAi%6 zl?JXivfgT_wna}}s9DVlGuxh(4xXAJ$9Jt2MSJ}xQE8?IR%X+(J!?w|pY5_QSK@pV zW^d`S3|vh)cyd-~(=4-5jk26)iDuLubaDIJGj`P3S~`+m6rat^d-M4(`=rY+`Pnet zyMZI$pYPWpw>2mB<>;^k9HLaakXw0cTEU>J-e~Ew`B#RrUU9)3nr$!k;1g;@|n-VIKmn6ni4bhaNfk#0Kg|TYZ_8 zl^33S6FzdWeZIUP{6t^U85Nr^P7oMUChJDhA#69T|JiuTp7!ZZ+vvM(sim~T$lj#OAWY4*676BRkYy6-s zC&pkpE+YQ@Uj|t!p$Sx!vk&3Tt?9OQU&FZ#?!yHstc=WV#mMfF6?wHxDS@Tb-F&o< zJL3t;)ATyRyYb*TdG}1@OWPHzlj%%u$>HzrJ;+ zpWe z?=!SMd{3j+tqaYls;)hO3m&tr?bZG2qa$X^318#u2YcApG8yjKf)uP_FfCm%X1-ja zEAdseASHVsri${8f4~i4iwMI94TbiH5*be@vGPaL8P&$YKCa0_8G7S!lVP~5=&M=I zE{mpXd1M>UuMi;BXp=K3x*f9iFAusM*XlsN{I+^{h|p>D{?7tyyuRX%*yo10SRG%^ zO%@8|CfsNj1&z_WrZL$S`*9scMLy`POkDM##|!Dh^tgf*lWPm3Sq4+nT7)^+G|D&1 zU!L3iGQLg|=H3$4N!6!C4H2T>=m%LfZznIWgfo3w7q~R~{9u9Csktam`!!(wUuZp^ zDBpU&UlCYKi_OoO(!qj+E!?Ph=o#@Rn51DptCCr!KE$7W2#%cdx2oD!O<#YYJF)QR z4|eJ_b5`QUc(DCse2aMK)W!OI<2Z|ueG!OVqJapvqaPr1X;tKT7{So%M89xhq z{S(^1fR$Hot+eVJM(hLz6o33n7B#y;UGIS@NZ2_#SYk@Kj0N6eC}94qcCX_%KhJNS z#;r?XHRUpy1m5%tu>pb@z~&eyY03Yd5r$_CJ^E|blrOHK=i3lbyGt+kr$2sRCk7eN z&g$r4p_gdufrl>_o0+?^+Ig(@oWB0%%eBgILR<3ZU^Cu0^crn)NiQ{e^KjomH%KMa zf}fTp8|SkWHYx0fQl}=7PS@@hgE%b)@$9L~Bhz(_4Gw1laxV`QS zy>GsPleOjYUr6BMSEiRT*gSj=S#kN(n61tU35tC((hkZMh3shDIIk2 zD~g_Z{5-6z&Uc>S-ZSr1FN93D<`Km6`>BMW-M+|xp}?2!EWwYA;Nt>7X zF-BI|&-pP{-WFH1Sl?p(b*E1CSYJR|9Ea4irMEwA_(z-GxzDJ0G4L&Rw(B1>Xsy@W zZS^-gl;u?yc+`2!aVV_0o4D>0qzk*}ZJ|fo@c2BREMGLL{M}ofdUhC7{y-M5>|)ie z@lVa4GZ+8jerQPfT;-^6;h$9toB`0UI$^ew@H*C!gLp|N7diOB^8J&| zwnsdPEAp^WRPkIgYBHsdyNuJ;!Fi)dosw9!bO&1Z)&7fxGP`@CkDFgo(oi0nMFv@y zN9`jFMur@8bI&zWgIdTAZrCQRJ?*w)UxSB3Pw&Hp;ilsnmFn;H@6ZP!0_>G!gFK>m=%{KEdl`uPQOTy)w#L`E^Z_D;td<03qe2~1JuTS}z zFCMzdybetEtMdkYu5CmZd9pDAwYt{RroJ6vrVhx*!u zh{ZlDO!wEPc;lXYogcc&jRMOW_GyjU^|*aLAGNpp)m<$5w@Kv$W|KIh&{~e)@r)x^ z^V8|@ugAP3#w1+rl>4*hPc|sISokp`-^apui*lMJ$I77}2p(DR9o0Ai|vG4S|M;&zn zhLTc6ew4)6n_$fod2FpejrgD2SPEwCwTXNSIopE{d~rr`;>t>K^AciO=m`?%>%Tza zWL@i>J5isZxm~PZuRHyz{el;j-EC`t0ka3HjM*4!L z;HfD&$qEh%+|2w}h0<%S;_COiX{*pPdvU2 HHpPpjE3H?qu@ZORJ5K08YmZ|%DS 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 Va%w6d-cMvgOLM#5YR~!I{4dSs5_$jt literal 30226 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