diff --git a/core/src/main/java/google/registry/flows/FlowUtils.java b/core/src/main/java/google/registry/flows/FlowUtils.java index 0c18d0465..7a1cd8daf 100644 --- a/core/src/main/java/google/registry/flows/FlowUtils.java +++ b/core/src/main/java/google/registry/flows/FlowUtils.java @@ -15,7 +15,7 @@ package google.registry.flows; import static com.google.common.base.Preconditions.checkState; -import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.xml.ValidationMode.LENIENT; import static google.registry.xml.ValidationMode.STRICT; import static java.nio.charset.StandardCharsets.UTF_8; @@ -51,8 +51,8 @@ public final class FlowUtils { /** Persists the saves and deletes in an {@link EntityChanges} to Datastore. */ public static void persistEntityChanges(EntityChanges entityChanges) { - ofy().save().entities(entityChanges.getSaves()); - ofy().delete().keys(entityChanges.getDeletes()); + tm().putAll(entityChanges.getSaves()); + tm().delete(entityChanges.getDeletes()); } /** diff --git a/core/src/main/java/google/registry/flows/custom/EntityChanges.java b/core/src/main/java/google/registry/flows/custom/EntityChanges.java index 7a64f7422..263937e50 100644 --- a/core/src/main/java/google/registry/flows/custom/EntityChanges.java +++ b/core/src/main/java/google/registry/flows/custom/EntityChanges.java @@ -16,8 +16,8 @@ package google.registry.flows.custom; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; -import com.googlecode.objectify.Key; import google.registry.model.ImmutableObject; +import google.registry.persistence.VKey; /** A wrapper class that encapsulates Datastore entities to both save and delete. */ @AutoValue @@ -25,7 +25,7 @@ public abstract class EntityChanges { public abstract ImmutableSet getSaves(); - public abstract ImmutableSet> getDeletes(); + public abstract ImmutableSet> getDeletes(); public static Builder newBuilder() { // Default both entities to save and entities to delete to empty sets, so that the build() @@ -48,11 +48,11 @@ public abstract class EntityChanges { return this; } - public abstract Builder setDeletes(ImmutableSet> entitiesToDelete); + public abstract Builder setDeletes(ImmutableSet> entitiesToDelete); - public abstract ImmutableSet.Builder> deletesBuilder(); + public abstract ImmutableSet.Builder> deletesBuilder(); - public Builder addDelete(Key entityToDelete) { + public Builder addDelete(VKey entityToDelete) { deletesBuilder().add(entityToDelete); return this; } diff --git a/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java b/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java index 4e2d7a517..0629381e9 100644 --- a/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java +++ b/core/src/main/java/google/registry/model/index/ForeignKeyIndex.java @@ -16,17 +16,20 @@ 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.model.ofy.ObjectifyService.ofy; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.util.CollectionUtils.entriesToImmutableMap; import static google.registry.util.TypeUtils.instantiate; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -48,7 +51,6 @@ import google.registry.persistence.VKey; import google.registry.schema.replay.DatastoreOnlyEntity; import google.registry.util.NonFinalForTesting; import java.util.Comparator; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -81,10 +83,10 @@ public abstract class ForeignKeyIndex extends BackupGroup public static class ForeignKeyHostIndex extends ForeignKeyIndex implements DatastoreOnlyEntity {} - private static final ImmutableMap< + private static final ImmutableBiMap< Class, Class>> RESOURCE_CLASS_TO_FKI_CLASS = - ImmutableMap.of( + ImmutableBiMap.of( ContactResource.class, ForeignKeyContactIndex.class, DomainBase.class, ForeignKeyDomainIndex.class, HostResource.class, ForeignKeyHostIndex.class); @@ -184,7 +186,7 @@ public abstract class ForeignKeyIndex extends BackupGroup } /** - * Load a list of {@link ForeignKeyIndex} instances by class and id strings that are active at or + * 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 @@ -192,13 +194,26 @@ public abstract class ForeignKeyIndex extends BackupGroup */ public static ImmutableMap> load( Class clazz, Iterable foreignKeys, final DateTime now) { + return loadIndexesFromStore(clazz, foreignKeys).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, Iterable foreignKeys) { if (tm().isOfy()) { - return ofy().load().type(mapToFkiClass(clazz)).ids(foreignKeys).entrySet().stream() - .filter(e -> now.isBefore(e.getValue().deletionTime)) - .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + return ImmutableMap.copyOf( + tm().doTransactionless(() -> ofy().load().type(mapToFkiClass(clazz)).ids(foreignKeys))); } else { String property = RESOURCE_CLASS_TO_FKI_PROPERTY.get(clazz); - List entities = + ImmutableList> indexes = tm().transact( () -> { String entityName = @@ -206,49 +221,58 @@ public abstract class ForeignKeyIndex extends BackupGroup return jpaTm() .query( String.format( - "FROM %s WHERE %s IN :propertyValue and deletionTime > :now ", - entityName, property), + "FROM %s WHERE %s IN :propertyValue", entityName, property), clazz) .setParameter("propertyValue", foreignKeys) - .setParameter("now", now) - .getResultList(); + .getResultStream() + .map(e -> ForeignKeyIndex.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(entities, EppResource::getForeignKey).asMap().entrySet().stream() + return Multimaps.index(indexes, ForeignKeyIndex::getForeignKey).asMap().entrySet().stream() .map( entry -> Maps.immutableEntry( entry.getKey(), entry.getValue().stream() - .max(Comparator.comparing(EppResource::getDeletionTime)) + .max(Comparator.comparing(ForeignKeyIndex::getDeletionTime)) .get())) - .collect( - toImmutableMap( - Map.Entry::getKey, - entry -> create(entry.getValue(), entry.getValue().getDeletionTime()))); + .collect(entriesToImmutableMap()); } } - static final CacheLoader>, Optional>> CACHE_LOADER = - new CacheLoader>, Optional>>() { + static final CacheLoader>, Optional>> CACHE_LOADER = + new CacheLoader>, Optional>>() { @Override - public Optional> load(Key> key) { - return Optional.ofNullable(tm().doTransactionless(() -> ofy().load().key(key).now())); + 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)) + .get(foreignKey)); } @Override - public Map>, Optional>> loadAll( - Iterable>> keys) { - ImmutableSet>> typedKeys = ImmutableSet.copyOf(keys); - Map>, ForeignKeyIndex> existingFkis = - tm().doTransactionless(() -> ofy().load().keys(typedKeys)); + 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); // ofy() omits keys that don't have values in Datastore, so re-add them in // here with Optional.empty() values. return Maps.asMap( typedKeys, - (Key> key) -> - Optional.ofNullable(existingFkis.getOrDefault(key, null))); + (VKey> key) -> + Optional.ofNullable(existingFkis.getOrDefault(key.getSqlKey().toString(), null))); } }; @@ -266,10 +290,10 @@ public abstract class ForeignKeyIndex extends BackupGroup * given IDs (blah) don't exist." */ @NonFinalForTesting - private static LoadingCache>, Optional>> + private static LoadingCache>, Optional>> cacheForeignKeyIndexes = createForeignKeyIndexesCache(getEppResourceCachingDuration()); - private static LoadingCache>, Optional>> + private static LoadingCache>, Optional>> createForeignKeyIndexesCache(Duration expiry) { return CacheBuilder.newBuilder() .expireAfterWrite(java.time.Duration.ofMillis(expiry.getMillis())) @@ -298,21 +322,24 @@ public abstract class ForeignKeyIndex extends BackupGroup if (!RegistryConfig.isEppResourceCachingEnabled()) { return tm().doTransactionless(() -> load(clazz, foreignKeys, now)); } - ImmutableList>> fkiKeys = + Class> fkiClass = mapToFkiClass(clazz); + // Safe to cast VKey> to VKey> + @SuppressWarnings("unchecked") + ImmutableList>> fkiVKeys = Streams.stream(foreignKeys) - .map(fk -> Key.>create(mapToFkiClass(clazz), fk)) + .map(fk -> (VKey>) VKey.create(fkiClass, fk)) .collect(toImmutableList()); try { // 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(fkiKeys).entrySet().stream() + cacheForeignKeyIndexes.getAll(fkiVKeys).entrySet().stream() .filter(entry -> entry.getValue().isPresent()) .filter(entry -> now.isBefore(entry.getValue().get().getDeletionTime())) .collect( toImmutableMap( - entry -> entry.getKey().getName(), + entry -> entry.getKey().getSqlKey().toString(), entry -> (ForeignKeyIndex) entry.getValue().get())); return fkisFromCache; } catch (ExecutionException e) { diff --git a/core/src/main/java/google/registry/model/poll/PollMessage.java b/core/src/main/java/google/registry/model/poll/PollMessage.java index 7dfd62899..50211cac1 100644 --- a/core/src/main/java/google/registry/model/poll/PollMessage.java +++ b/core/src/main/java/google/registry/model/poll/PollMessage.java @@ -398,6 +398,7 @@ public abstract class PollMessage extends ImmutableObject } if (!isNullOrEmpty(domainPendingActionNotificationResponses)) { pendingActionNotificationResponse = domainPendingActionNotificationResponses.get(0); + fullyQualifiedDomainName = pendingActionNotificationResponse.nameOrId.value; } if (!isNullOrEmpty(domainTransferResponses)) { fullyQualifiedDomainName = domainTransferResponses.get(0).getFullyQualifiedDomainName(); @@ -414,21 +415,23 @@ public abstract class PollMessage extends ImmutableObject // Take the SQL-specific fields and map them to the Objectify-specific fields, if applicable if (pendingActionNotificationResponse != null) { if (contactId != null) { - contactPendingActionNotificationResponses = - ImmutableList.of( - ContactPendingActionNotificationResponse.create( - pendingActionNotificationResponse.nameOrId.value, - pendingActionNotificationResponse.getActionResult(), - pendingActionNotificationResponse.getTrid(), - pendingActionNotificationResponse.processedDate)); + ContactPendingActionNotificationResponse contactPendingResponse = + ContactPendingActionNotificationResponse.create( + pendingActionNotificationResponse.nameOrId.value, + pendingActionNotificationResponse.getActionResult(), + pendingActionNotificationResponse.getTrid(), + pendingActionNotificationResponse.processedDate); + pendingActionNotificationResponse = contactPendingResponse; + contactPendingActionNotificationResponses = ImmutableList.of(contactPendingResponse); } else if (fullyQualifiedDomainName != null) { - domainPendingActionNotificationResponses = - ImmutableList.of( - DomainPendingActionNotificationResponse.create( - pendingActionNotificationResponse.nameOrId.value, - pendingActionNotificationResponse.getActionResult(), - pendingActionNotificationResponse.getTrid(), - pendingActionNotificationResponse.processedDate)); + DomainPendingActionNotificationResponse domainPendingResponse = + DomainPendingActionNotificationResponse.create( + pendingActionNotificationResponse.nameOrId.value, + pendingActionNotificationResponse.getActionResult(), + pendingActionNotificationResponse.getTrid(), + pendingActionNotificationResponse.processedDate); + pendingActionNotificationResponse = domainPendingResponse; + domainPendingActionNotificationResponses = ImmutableList.of(domainPendingResponse); } } if (transferResponse != null) { @@ -474,38 +477,35 @@ public abstract class PollMessage extends ImmutableObject } public Builder setResponseData(ImmutableList responseData) { - getInstance().contactPendingActionNotificationResponses = + OneTime instance = getInstance(); + instance.contactPendingActionNotificationResponses = forceEmptyToNull( - responseData - .stream() + responseData.stream() .filter(ContactPendingActionNotificationResponse.class::isInstance) .map(ContactPendingActionNotificationResponse.class::cast) .collect(toImmutableList())); - getInstance().contactTransferResponses = + instance.contactTransferResponses = forceEmptyToNull( - responseData - .stream() + responseData.stream() .filter(ContactTransferResponse.class::isInstance) .map(ContactTransferResponse.class::cast) .collect(toImmutableList())); - getInstance().domainPendingActionNotificationResponses = + instance.domainPendingActionNotificationResponses = forceEmptyToNull( - responseData - .stream() + responseData.stream() .filter(DomainPendingActionNotificationResponse.class::isInstance) .map(DomainPendingActionNotificationResponse.class::cast) .collect(toImmutableList())); - getInstance().domainTransferResponses = + instance.domainTransferResponses = forceEmptyToNull( - responseData - .stream() + responseData.stream() .filter(DomainTransferResponse.class::isInstance) .map(DomainTransferResponse.class::cast) .collect(toImmutableList())); - getInstance().hostPendingActionNotificationResponses = + instance.hostPendingActionNotificationResponses = forceEmptyToNull( responseData.stream() .filter(HostPendingActionNotificationResponse.class::isInstance) @@ -513,26 +513,30 @@ public abstract class PollMessage extends ImmutableObject .collect(toImmutableList())); // Set the generic pending-action field as appropriate - if (getInstance().contactPendingActionNotificationResponses != null) { - getInstance().pendingActionNotificationResponse = - getInstance().contactPendingActionNotificationResponses.get(0); - } else if (getInstance().domainPendingActionNotificationResponses != null) { - getInstance().pendingActionNotificationResponse = - getInstance().domainPendingActionNotificationResponses.get(0); - } else if (getInstance().hostPendingActionNotificationResponses != null) { - getInstance().pendingActionNotificationResponse = - getInstance().hostPendingActionNotificationResponses.get(0); + if (instance.contactPendingActionNotificationResponses != null) { + instance.pendingActionNotificationResponse = + instance.contactPendingActionNotificationResponses.get(0); + instance.contactId = + instance.contactPendingActionNotificationResponses.get(0).nameOrId.value; + } else if (instance.domainPendingActionNotificationResponses != null) { + instance.pendingActionNotificationResponse = + instance.domainPendingActionNotificationResponses.get(0); + instance.fullyQualifiedDomainName = + instance.domainPendingActionNotificationResponses.get(0).nameOrId.value; + } else if (instance.hostPendingActionNotificationResponses != null) { + instance.pendingActionNotificationResponse = + instance.hostPendingActionNotificationResponses.get(0); } // Set the generic transfer response field as appropriate - if (getInstance().contactTransferResponses != null) { - getInstance().contactId = getInstance().contactTransferResponses.get(0).getContactId(); - getInstance().transferResponse = getInstance().contactTransferResponses.get(0); - } else if (getInstance().domainTransferResponses != null) { - getInstance().fullyQualifiedDomainName = - getInstance().domainTransferResponses.get(0).getFullyQualifiedDomainName(); - getInstance().transferResponse = getInstance().domainTransferResponses.get(0); - getInstance().extendedRegistrationExpirationTime = - getInstance().domainTransferResponses.get(0).getExtendedRegistrationExpirationTime(); + if (instance.contactTransferResponses != null) { + instance.contactId = getInstance().contactTransferResponses.get(0).getContactId(); + instance.transferResponse = getInstance().contactTransferResponses.get(0); + } else if (instance.domainTransferResponses != null) { + instance.fullyQualifiedDomainName = + instance.domainTransferResponses.get(0).getFullyQualifiedDomainName(); + instance.transferResponse = getInstance().domainTransferResponses.get(0); + instance.extendedRegistrationExpirationTime = + instance.domainTransferResponses.get(0).getExtendedRegistrationExpirationTime(); } return this; } diff --git a/core/src/main/java/google/registry/model/smd/SignedMarkRevocationListDao.java b/core/src/main/java/google/registry/model/smd/SignedMarkRevocationListDao.java index db308100d..e0989246b 100644 --- a/core/src/main/java/google/registry/model/smd/SignedMarkRevocationListDao.java +++ b/core/src/main/java/google/registry/model/smd/SignedMarkRevocationListDao.java @@ -25,6 +25,7 @@ import static google.registry.model.ofy.ObjectifyService.allocateId; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.smd.SignedMarkRevocationList.SHARD_SIZE; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.CollectionUtils.isNullOrEmpty; import static google.registry.util.DateTimeUtils.START_OF_TIME; @@ -127,7 +128,8 @@ public class SignedMarkRevocationListDao { /** Loads the shards from Datastore and combines them into one list. */ private static Optional loadFromDatastore() { - return tm().transactNewReadOnly( + return ofyTm() + .transactNewReadOnly( () -> { Iterable shards = ofy().load().type(SignedMarkRevocationList.class).ancestor(getCrossTldKey()); diff --git a/core/src/main/java/google/registry/model/tmch/ClaimsListShard.java b/core/src/main/java/google/registry/model/tmch/ClaimsListShard.java index e26b8970e..cb0e9e3d3 100644 --- a/core/src/main/java/google/registry/model/tmch/ClaimsListShard.java +++ b/core/src/main/java/google/registry/model/tmch/ClaimsListShard.java @@ -20,7 +20,7 @@ import static com.google.common.base.Throwables.throwIfUnchecked; import static com.google.common.base.Verify.verify; import static google.registry.model.ofy.ObjectifyService.allocateId; import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm; import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.annotations.VisibleForTesting; @@ -157,7 +157,8 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt Concurrent.transform( shardKeys, key -> - tm().transactNewReadOnly( + ofyTm() + .transactNewReadOnly( () -> { ClaimsListShard claimsListShard = ofy().load().key(key).now(); checkState( @@ -244,7 +245,8 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt Concurrent.transform( CollectionUtils.partitionMap(labelsToKeys, shardSize), (final ImmutableMap labelsToKeysShard) -> - tm().transact( + ofyTm() + .transact( () -> { ClaimsListShard shard = create(creationTime, labelsToKeysShard); shard.isShard = true; @@ -254,7 +256,8 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt })); // Persist the new revision, thus causing the newly created shards to go live. - tm().transact( + ofyTm() + .transact( () -> { verify( (getCurrentRevision() == null && oldRevision == null) diff --git a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java index 83f49ba4e..ff2798900 100644 --- a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java +++ b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java @@ -219,16 +219,15 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { transact(work); } + // For now, read-only transactions and "transactNew" methods only create (or use existing) + // standard transactions. Attempting to use a read-only transaction can break larger transactions + // (if we were already in one) so we don't set read-only mode. + // + // TODO(gbrodman): If necessary, implement transactNew and readOnly transactions using Postgres + // savepoints, see https://www.postgresql.org/docs/8.1/sql-savepoint.html @Override public T transactNewReadOnly(Supplier work) { - return retrier.callWithRetry( - () -> - transact( - () -> { - getEntityManager().createNativeQuery("SET TRANSACTION READ ONLY").executeUpdate(); - return work.get(); - }), - JpaRetries::isFailedQueryRetriable); + return retrier.callWithRetry(() -> transact(work), JpaRetries::isFailedQueryRetriable); } @Override diff --git a/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java b/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java index eac1946cc..cea34264f 100644 --- a/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java +++ b/core/src/test/java/google/registry/flows/ResourceFlowTestCase.java @@ -121,6 +121,10 @@ public abstract class ResourceFlowTestCase void assertEppResourceIndexEntityFor(final T resource) { + if (!tm().isOfy()) { + // Indices aren't explicitly stored as objects in SQL + return; + } ImmutableList indices = Streams.stream( ofy() diff --git a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java index fc4c911b1..bf01a5030 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java @@ -26,12 +26,12 @@ import static google.registry.model.domain.token.AllocationToken.TokenType.SINGL import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE; import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD; -import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY; import static google.registry.model.registry.Registry.TldState.PREDELEGATION; import static google.registry.model.registry.Registry.TldState.QUIET_PERIOD; import static google.registry.model.registry.Registry.TldState.START_DATE_SUNRISE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm; import static google.registry.pricing.PricingEngineProxy.isDomainPremium; import static google.registry.testing.DatabaseHelper.assertBillingEvents; import static google.registry.testing.DatabaseHelper.assertPollMessagesForResource; @@ -162,8 +162,11 @@ import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; import google.registry.monitoring.whitebox.EppMetric; +import google.registry.persistence.VKey; +import google.registry.testing.DualDatabaseTest; import google.registry.testing.ReplayExtension; import google.registry.testing.TaskQueueHelper.TaskMatcher; +import google.registry.testing.TestOfyAndSql; import java.math.BigDecimal; import java.util.Map; import javax.annotation.Nullable; @@ -172,10 +175,10 @@ import org.joda.time.DateTime; import org.joda.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; /** Unit tests for {@link DomainCreateFlow}. */ +@DualDatabaseTest class DomainCreateFlowTest extends ResourceFlowTestCase { private static final String CLAIMS_KEY = "2013041500/2/6/9/rJ1NrDO92vDsAzf7EQzgjX4R0000000001"; @@ -266,7 +269,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadByKey(domain.getAutorenewBillingEvent()).getEventTime())) .and() .hasOnlyOneHistoryEntryWhich() .hasType(HistoryEntry.Type.DOMAIN_CREATE) @@ -409,20 +412,20 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadByEntity(token)).getRedemptionHistoryEntry()) .hasValue(HistoryEntry.createVKey(Key.create(historyEntry))); } - @Test + @TestOfyAndSql void testSuccess_validAllocationToken_multiUse() throws Exception { setEppInput( "domain_create_allocationtoken.xml", @@ -559,7 +562,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadByKey(VKey.create(AllocationToken.class, token))); assertThat(reloadedToken.isRedeemed()).isTrue(); assertThat(reloadedToken.getRedemptionHistoryEntry()) .hasValue( @@ -1280,11 +1283,11 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadByKey(VKey.create(AllocationToken.class, token))); assertThat(reloadedToken.isRedeemed()).isFalse(); } - @Test + @TestOfyAndSql void testSuccess_allocationTokenPromotion() throws Exception { // A discount of 0.5 means that the first-year cost (13) is cut in half, so a discount of 6.5 // Note: we're asking to register it for two years so the total cost should be 13 + (13/2) @@ -1308,18 +1311,18 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, BigDecimal.valueOf(19.5))); } - @Test + @TestOfyAndSql void testSuccess_allocationToken_multiYearDiscount_maxesAtTokenDiscountYears() throws Exception { // 2yrs @ $13 + 3yrs @ $13 * (1 - 0.73) = $36.53 runTest_allocationToken_multiYearDiscount(false, 0.73, 3, Money.of(USD, 36.53)); } - @Test + @TestOfyAndSql void testSuccess_allocationToken_multiYearDiscount_maxesAtNumRegistrationYears() throws Exception { // 5yrs @ $13 * (1 - 0.276) = $47.06 @@ -1358,12 +1361,12 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); assertThat(billingEvent.getCost()).isEqualTo(expectedPrice); } - @Test + @TestOfyAndSql void testSuccess_allocationToken_multiYearDiscount_worksForPremiums() throws Exception { createTld("example"); persistContactsAndHosts(); @@ -1391,13 +1394,13 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); assertThat(billingEvent.getTargetId()).isEqualTo("rich.example"); // 1yr @ $100 + 2yrs @ $100 * (1 - 0.98) = $104 assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 104.00)); } - @Test + @TestOfyAndSql void testSuccess_allocationToken_singleYearDiscount_worksForPremiums() throws Exception { createTld("example"); persistContactsAndHosts(); @@ -1424,13 +1427,13 @@ class DomainCreateFlowTest extends ResourceFlowTestCase tm().loadAllOf(BillingEvent.OneTime.class))); assertThat(billingEvent.getTargetId()).isEqualTo("rich.example"); // 2yrs @ $100 + 1yr @ $100 * (1 - 0.95555) = $204.44 assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 204.44)); } - @Test + @TestOfyAndSql void testSuccess_promotionDoesNotApplyToPremiumPrice() { // Discounts only apply to premium domains if the token is explicitly configured to allow it. createTld("example"); @@ -1456,7 +1459,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase substitutions = ImmutableMap.of("DOMAIN", "custom-logic-test.tld"); @@ -1755,7 +1758,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase