mirror of
https://github.com/google/nomulus.git
synced 2025-07-09 12:43:24 +02:00
Convert EppResourceUtils::loadAtPointInTime to SQL+DS (#1194)
* Convert EppResourceUtils::loadAtPointInTime to SQL+DS This required the following changes: - The branching / conversion logic itself, where we load the most recent history object for the resource in question (or just return the resource itself) - For simplicity's sake, adding a method in the *History objects that returns the generic resource -- this means that it can be called when we don't know or care which subclass it is. - Populating the domain's dsData and gracePeriods fields from the DomainHistory fields, and adding factories in the relevant classes to allow us to do the conversions nicely (the history classes are almost the same as the regular ones, but not quite). - Change the tests to use the clocks properly and to allow comparison of e.g. DomainContent to DomainBase. The objects aren't the same (one is a superclass of the other) but the fields are. Note as well a slight behavioral change: commit logs only allow us 24-hour granularity, so two updates in the same day mean that the earlier update is ignored and inaccessible. This is not the case for *History objects in SQL; all versions are accessible.
This commit is contained in:
parent
d113b718d7
commit
b59a30ffed
9 changed files with 202 additions and 55 deletions
|
@ -17,10 +17,10 @@ package google.registry.model;
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
||||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||||
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
import static google.registry.util.DateTimeUtils.isAtOrAfter;
|
import static google.registry.util.DateTimeUtils.isAtOrAfter;
|
||||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||||
import static google.registry.util.DateTimeUtils.latestOf;
|
import static google.registry.util.DateTimeUtils.latestOf;
|
||||||
|
@ -43,10 +43,13 @@ import google.registry.model.index.ForeignKeyIndex;
|
||||||
import google.registry.model.ofy.CommitLogManifest;
|
import google.registry.model.ofy.CommitLogManifest;
|
||||||
import google.registry.model.ofy.CommitLogMutation;
|
import google.registry.model.ofy.CommitLogMutation;
|
||||||
import google.registry.model.registry.Registry;
|
import google.registry.model.registry.Registry;
|
||||||
|
import google.registry.model.reporting.HistoryEntry;
|
||||||
|
import google.registry.model.reporting.HistoryEntryDao;
|
||||||
import google.registry.model.transfer.DomainTransferData;
|
import google.registry.model.transfer.DomainTransferData;
|
||||||
import google.registry.model.transfer.TransferData;
|
import google.registry.model.transfer.TransferData;
|
||||||
import google.registry.model.transfer.TransferStatus;
|
import google.registry.model.transfer.TransferStatus;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -266,26 +269,43 @@ public final class EppResourceUtils {
|
||||||
* Rewinds an {@link EppResource} object to a given point in time.
|
* Rewinds an {@link EppResource} object to a given point in time.
|
||||||
*
|
*
|
||||||
* <p>This method costs nothing if {@code resource} is already current. Otherwise it needs to
|
* <p>This method costs nothing if {@code resource} is already current. Otherwise it needs to
|
||||||
* perform a single asynchronous key fetch operation.
|
* perform a single fetch operation.
|
||||||
*
|
*
|
||||||
* <p><b>Warning:</b> A resource can only be rolled backwards in time, not forwards; therefore
|
* <p><b>Warning:</b> A resource can only be rolled backwards in time, not forwards; therefore
|
||||||
* {@code resource} should be whatever's currently in Datastore.
|
* {@code resource} should be whatever's currently in Datastore.
|
||||||
*
|
*
|
||||||
* <p><b>Warning:</b> Revisions are granular to 24-hour periods. It's recommended that
|
* <p><b>Warning:</b> In Datastore, revisions are granular to 24-hour periods. It's recommended
|
||||||
* {@code timestamp} be set to midnight. Otherwise you must take into consideration that under
|
* that {@code timestamp} be set to midnight. If you don't use midnight, you must take into
|
||||||
* certain circumstances, a resource might be restored to a revision on the previous day, even if
|
* consideration that under certain circumstances, a resource might be restored to a revision on
|
||||||
* there were revisions made earlier on the same date as {@code timestamp}; however, a resource
|
* the previous day, even if there were revisions made earlier on the same date as {@code
|
||||||
* will never be restored to a revision occurring after {@code timestamp}. This behavior is due to
|
* timestamp}; however, a resource will never be restored to a revision occurring after {@code
|
||||||
* the way {@link google.registry.model.translators.CommitLogRevisionsTranslatorFactory
|
* timestamp}. This behavior is due to the way {@link
|
||||||
|
* google.registry.model.translators.CommitLogRevisionsTranslatorFactory
|
||||||
* CommitLogRevisionsTranslatorFactory} manages the {@link EppResource#revisions} field. Please
|
* CommitLogRevisionsTranslatorFactory} manages the {@link EppResource#revisions} field. Please
|
||||||
* note however that the creation and deletion times of a resource are granular to the
|
* note however that the creation and deletion times of a resource are granular to the
|
||||||
* millisecond.
|
* millisecond.
|
||||||
*
|
*
|
||||||
|
* <p>Example: a resource in Datastore has three revisions A, B, and C
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>A: Day 0, 1pm
|
||||||
|
* <li>B: Day 1, 1pm
|
||||||
|
* <li>C: Day 1, 3pm
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>If one requests the resource as of day 1 at 2pm, we will return revision A because as far as
|
||||||
|
* the commit logs are concerned, revision C completely overwrites the existence of revision B.
|
||||||
|
*
|
||||||
|
* <p>When using the SQL backend (post-Registry-3.0-migration) this restriction goes away and
|
||||||
|
* objects can be restored to any revision.
|
||||||
|
*
|
||||||
|
* <p>TODO(b/177567432): Once Datastore is completely removed, remove the Result wrapping.
|
||||||
|
*
|
||||||
* @return an asynchronous operation returning resource at {@code timestamp} or {@code null} if
|
* @return an asynchronous operation returning resource at {@code timestamp} or {@code null} if
|
||||||
* resource is deleted or not yet created
|
* resource is deleted or not yet created
|
||||||
*/
|
*/
|
||||||
public static <T extends EppResource>
|
public static <T extends EppResource> Result<T> loadAtPointInTime(
|
||||||
Result<T> loadAtPointInTime(final T resource, final DateTime timestamp) {
|
final T resource, final DateTime timestamp) {
|
||||||
// If we're before the resource creation time, don't try to find a "most recent revision".
|
// If we're before the resource creation time, don't try to find a "most recent revision".
|
||||||
if (timestamp.isBefore(resource.getCreationTime())) {
|
if (timestamp.isBefore(resource.getCreationTime())) {
|
||||||
return new ResultNow<>(null);
|
return new ResultNow<>(null);
|
||||||
|
@ -300,7 +320,8 @@ public final class EppResourceUtils {
|
||||||
: loadMostRecentRevisionAtTime(resource, timestamp);
|
: loadMostRecentRevisionAtTime(resource, timestamp);
|
||||||
return () -> {
|
return () -> {
|
||||||
T loadedResource = loadResult.now();
|
T loadedResource = loadResult.now();
|
||||||
return (loadedResource == null) ? null
|
return (loadedResource == null)
|
||||||
|
? null
|
||||||
: (isActive(loadedResource, timestamp)
|
: (isActive(loadedResource, timestamp)
|
||||||
? cloneProjectedAtTime(loadedResource, timestamp)
|
? cloneProjectedAtTime(loadedResource, timestamp)
|
||||||
: null);
|
: null);
|
||||||
|
@ -308,26 +329,43 @@ public final class EppResourceUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an asynchronous result holding the most recent Datastore revision of a given
|
* Returns an asynchronous result holding the most recent revision of a given EppResource before
|
||||||
* EppResource before or at the provided timestamp using the EppResource revisions map, falling
|
* or at the provided timestamp, falling back to using the resource as-is if there are no
|
||||||
* back to using the earliest revision or the resource as-is if there are no revisions.
|
* revisions.
|
||||||
*
|
*
|
||||||
* @see #loadAtPointInTime(EppResource, DateTime)
|
* @see #loadAtPointInTime(EppResource, DateTime)
|
||||||
*/
|
*/
|
||||||
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTime(
|
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTime(
|
||||||
final T resource, final DateTime timestamp) {
|
final T resource, final DateTime timestamp) {
|
||||||
|
if (tm().isOfy()) {
|
||||||
|
return loadMostRecentRevisionAtTimeDatastore(resource, timestamp);
|
||||||
|
} else {
|
||||||
|
return loadMostRecentRevisionAtTimeSql(resource, timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an asynchronous result holding the most recent Datastore revision of a given
|
||||||
|
* EppResource before or at the provided timestamp using the EppResource revisions map, falling
|
||||||
|
* back to using the resource as-is if there are no revisions.
|
||||||
|
*
|
||||||
|
* @see #loadAtPointInTime(EppResource, DateTime)
|
||||||
|
*/
|
||||||
|
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTimeDatastore(
|
||||||
|
final T resource, final DateTime timestamp) {
|
||||||
final Key<T> resourceKey = Key.create(resource);
|
final Key<T> resourceKey = Key.create(resource);
|
||||||
final Key<CommitLogManifest> revision = findMostRecentRevisionAtTime(resource, timestamp);
|
final Key<CommitLogManifest> revision =
|
||||||
|
findMostRecentDatastoreRevisionAtTime(resource, timestamp);
|
||||||
if (revision == null) {
|
if (revision == null) {
|
||||||
logger.atSevere().log("No revision found for %s, falling back to resource.", resourceKey);
|
logger.atSevere().log("No revision found for %s, falling back to resource.", resourceKey);
|
||||||
return new ResultNow<>(resource);
|
return new ResultNow<>(resource);
|
||||||
}
|
}
|
||||||
final Result<CommitLogMutation> mutationResult =
|
final Result<CommitLogMutation> mutationResult =
|
||||||
ofy().load().key(CommitLogMutation.createKey(revision, resourceKey));
|
auditedOfy().load().key(CommitLogMutation.createKey(revision, resourceKey));
|
||||||
return () -> {
|
return () -> {
|
||||||
CommitLogMutation mutation = mutationResult.now();
|
CommitLogMutation mutation = mutationResult.now();
|
||||||
if (mutation != null) {
|
if (mutation != null) {
|
||||||
return ofy().load().fromEntity(mutation.getEntity());
|
return auditedOfy().load().fromEntity(mutation.getEntity());
|
||||||
}
|
}
|
||||||
logger.atSevere().log(
|
logger.atSevere().log(
|
||||||
"Couldn't load mutation for revision at %s for %s, falling back to resource."
|
"Couldn't load mutation for revision at %s for %s, falling back to resource."
|
||||||
|
@ -337,9 +375,37 @@ public final class EppResourceUtils {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an asynchronous result holding the most recent SQL revision of a given EppResource
|
||||||
|
* before or at the provided timestamp using *History objects, falling back to using the resource
|
||||||
|
* as-is if there are no revisions.
|
||||||
|
*
|
||||||
|
* @see #loadAtPointInTime(EppResource, DateTime)
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTimeSql(
|
||||||
|
T resource, DateTime timestamp) {
|
||||||
|
T resourceAtPointInTime =
|
||||||
|
(T)
|
||||||
|
HistoryEntryDao.loadHistoryObjectsForResource(
|
||||||
|
resource.createVKey(), START_OF_TIME, timestamp)
|
||||||
|
.stream()
|
||||||
|
.max(Comparator.comparing(HistoryEntry::getModificationTime))
|
||||||
|
.flatMap(HistoryEntry::getResourceAtPointInTime)
|
||||||
|
.orElse(null);
|
||||||
|
if (resourceAtPointInTime == null) {
|
||||||
|
logger.atSevere().log(
|
||||||
|
"Couldn't load resource at % for key %s, falling back to resource %s.",
|
||||||
|
timestamp, resource.createVKey(), resource);
|
||||||
|
return new ResultNow<>(resource);
|
||||||
|
}
|
||||||
|
return new ResultNow<>(resourceAtPointInTime);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static <T extends EppResource> Key<CommitLogManifest>
|
private static <T extends EppResource>
|
||||||
findMostRecentRevisionAtTime(final T resource, final DateTime timestamp) {
|
Key<CommitLogManifest> findMostRecentDatastoreRevisionAtTime(
|
||||||
|
final T resource, final DateTime timestamp) {
|
||||||
final Key<T> resourceKey = Key.create(resource);
|
final Key<T> resourceKey = Key.create(resource);
|
||||||
Entry<?, Key<CommitLogManifest>> revision = resource.getRevisions().floorEntry(timestamp);
|
Entry<?, Key<CommitLogManifest>> revision = resource.getRevisions().floorEntry(timestamp);
|
||||||
if (revision != null) {
|
if (revision != null) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||||
|
|
||||||
import com.googlecode.objectify.Key;
|
import com.googlecode.objectify.Key;
|
||||||
import com.googlecode.objectify.annotation.EntitySubclass;
|
import com.googlecode.objectify.annotation.EntitySubclass;
|
||||||
|
import google.registry.model.EppResource;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.model.contact.ContactHistory.ContactHistoryId;
|
import google.registry.model.contact.ContactHistory.ContactHistoryId;
|
||||||
import google.registry.model.reporting.HistoryEntry;
|
import google.registry.model.reporting.HistoryEntry;
|
||||||
|
@ -107,6 +108,11 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
|
||||||
return (VKey<ContactHistory>) createVKey(Key.create(this));
|
return (VKey<ContactHistory>) createVKey(Key.create(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<? extends EppResource> getResourceAtPointInTime() {
|
||||||
|
return getContactBase();
|
||||||
|
}
|
||||||
|
|
||||||
@PostLoad
|
@PostLoad
|
||||||
void postLoad() {
|
void postLoad() {
|
||||||
// Normally Hibernate would see that the contact fields are all null and would fill contactBase
|
// Normally Hibernate would see that the contact fields are all null and would fill contactBase
|
||||||
|
|
|
@ -21,9 +21,11 @@ import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.googlecode.objectify.Key;
|
import com.googlecode.objectify.Key;
|
||||||
import com.googlecode.objectify.annotation.EntitySubclass;
|
import com.googlecode.objectify.annotation.EntitySubclass;
|
||||||
|
import google.registry.model.EppResource;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.model.domain.DomainHistory.DomainHistoryId;
|
import google.registry.model.domain.DomainHistory.DomainHistoryId;
|
||||||
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
|
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
|
||||||
|
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||||
import google.registry.model.domain.secdns.DomainDsDataHistory;
|
import google.registry.model.domain.secdns.DomainDsDataHistory;
|
||||||
import google.registry.model.host.HostResource;
|
import google.registry.model.host.HostResource;
|
||||||
import google.registry.model.reporting.DomainTransactionRecord;
|
import google.registry.model.reporting.DomainTransactionRecord;
|
||||||
|
@ -248,10 +250,21 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||||
return (VKey<DomainHistory>) createVKey(Key.create(this));
|
return (VKey<DomainHistory>) createVKey(Key.create(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<? extends EppResource> getResourceAtPointInTime() {
|
||||||
|
return getDomainContent();
|
||||||
|
}
|
||||||
|
|
||||||
@PostLoad
|
@PostLoad
|
||||||
void postLoad() {
|
void postLoad() {
|
||||||
if (domainContent != null) {
|
if (domainContent != null) {
|
||||||
domainContent.nsHosts = nullToEmptyImmutableCopy(nsHosts);
|
domainContent.nsHosts = nullToEmptyImmutableCopy(nsHosts);
|
||||||
|
domainContent.gracePeriods =
|
||||||
|
gracePeriodHistories.stream()
|
||||||
|
.map(GracePeriod::createFromHistory)
|
||||||
|
.collect(toImmutableSet());
|
||||||
|
domainContent.dsData =
|
||||||
|
dsDataHistories.stream().map(DelegationSignerData::create).collect(toImmutableSet());
|
||||||
// Normally Hibernate would see that the domain fields are all null and would fill
|
// Normally Hibernate would see that the domain fields are all null and would fill
|
||||||
// domainContent with a null object. Unfortunately, the updateTimestamp is never null in SQL.
|
// domainContent with a null object. Unfortunately, the updateTimestamp is never null in SQL.
|
||||||
if (domainContent.getDomainName() == null) {
|
if (domainContent.getDomainName() == null) {
|
||||||
|
|
|
@ -115,6 +115,17 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
|
||||||
type, domainRepoId, expirationTime, clientId, billingEventOneTime, null, gracePeriodId);
|
type, domainRepoId, expirationTime, clientId, billingEventOneTime, null, gracePeriodId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GracePeriod createFromHistory(GracePeriodHistory history) {
|
||||||
|
return createInternal(
|
||||||
|
history.type,
|
||||||
|
history.domainRepoId,
|
||||||
|
history.expirationTime,
|
||||||
|
history.clientId,
|
||||||
|
history.billingEventOneTime == null ? null : history.billingEventOneTime.createVKey(),
|
||||||
|
history.billingEventRecurring == null ? null : history.billingEventRecurring.createVKey(),
|
||||||
|
history.gracePeriodId);
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates a GracePeriod for a Recurring billing event. */
|
/** Creates a GracePeriod for a Recurring billing event. */
|
||||||
public static GracePeriod createForRecurring(
|
public static GracePeriod createForRecurring(
|
||||||
GracePeriodStatus type,
|
GracePeriodStatus type,
|
||||||
|
|
|
@ -114,6 +114,15 @@ public class DelegationSignerData extends DomainDsDataBase {
|
||||||
return create(keyTag, algorithm, digestType, DatatypeConverter.parseHexBinary(digestAsHex));
|
return create(keyTag, algorithm, digestType, DatatypeConverter.parseHexBinary(digestAsHex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DelegationSignerData create(DomainDsDataHistory history) {
|
||||||
|
return create(
|
||||||
|
history.keyTag,
|
||||||
|
history.algorithm,
|
||||||
|
history.digestType,
|
||||||
|
history.digest,
|
||||||
|
history.domainRepoId);
|
||||||
|
}
|
||||||
|
|
||||||
/** Class to represent the composite primary key of {@link DelegationSignerData} entity. */
|
/** Class to represent the composite primary key of {@link DelegationSignerData} entity. */
|
||||||
static class DomainDsDataId extends ImmutableObject implements Serializable {
|
static class DomainDsDataId extends ImmutableObject implements Serializable {
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||||
|
|
||||||
import com.googlecode.objectify.Key;
|
import com.googlecode.objectify.Key;
|
||||||
import com.googlecode.objectify.annotation.EntitySubclass;
|
import com.googlecode.objectify.annotation.EntitySubclass;
|
||||||
|
import google.registry.model.EppResource;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.model.host.HostHistory.HostHistoryId;
|
import google.registry.model.host.HostHistory.HostHistoryId;
|
||||||
import google.registry.model.reporting.HistoryEntry;
|
import google.registry.model.reporting.HistoryEntry;
|
||||||
|
@ -108,6 +109,11 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
|
||||||
return (VKey<HostHistory>) createVKey(Key.create(this));
|
return (VKey<HostHistory>) createVKey(Key.create(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<? extends EppResource> getResourceAtPointInTime() {
|
||||||
|
return getHostBase();
|
||||||
|
}
|
||||||
|
|
||||||
@PostLoad
|
@PostLoad
|
||||||
void postLoad() {
|
void postLoad() {
|
||||||
// Normally Hibernate would see that the host fields are all null and would fill hostBase
|
// Normally Hibernate would see that the host fields are all null and would fill hostBase
|
||||||
|
|
|
@ -290,6 +290,17 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
|
||||||
return nullToEmptyImmutableCopy(domainTransactionRecords);
|
return nullToEmptyImmutableCopy(domainTransactionRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an error when attempting to retrieve the EppResource at this point in time.
|
||||||
|
*
|
||||||
|
* <p>Subclasses must override this to return the resource; it is non-abstract for legacy reasons
|
||||||
|
* and objects created prior to the Registry 3.0 migration.
|
||||||
|
*/
|
||||||
|
public Optional<? extends EppResource> getResourceAtPointInTime() {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Raw HistoryEntry objects do not store the resource at that point in time.");
|
||||||
|
}
|
||||||
|
|
||||||
/** This method exists solely to satisfy Hibernate. Use the {@link Builder} instead. */
|
/** This method exists solely to satisfy Hibernate. Use the {@link Builder} instead. */
|
||||||
@SuppressWarnings("UnusedMethod")
|
@SuppressWarnings("UnusedMethod")
|
||||||
private void setPeriod(Period period) {
|
private void setPeriod(Period period) {
|
||||||
|
|
|
@ -16,8 +16,11 @@ package google.registry.flows;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
|
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
|
||||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||||
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.testing.DatabaseHelper.createTld;
|
import static google.registry.testing.DatabaseHelper.createTld;
|
||||||
|
import static google.registry.testing.DatabaseHelper.loadAllOf;
|
||||||
|
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||||
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
||||||
import static google.registry.testing.DatabaseHelper.persistActiveHost;
|
import static google.registry.testing.DatabaseHelper.persistActiveHost;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
@ -25,31 +28,38 @@ import static org.joda.time.DateTimeZone.UTC;
|
||||||
import static org.joda.time.Duration.standardDays;
|
import static org.joda.time.Duration.standardDays;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.googlecode.objectify.Key;
|
import com.google.common.collect.Iterables;
|
||||||
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
|
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
|
||||||
import google.registry.model.domain.DomainBase;
|
import google.registry.model.domain.DomainBase;
|
||||||
import google.registry.model.ofy.Ofy;
|
import google.registry.model.ofy.Ofy;
|
||||||
import google.registry.monitoring.whitebox.EppMetric;
|
import google.registry.monitoring.whitebox.EppMetric;
|
||||||
import google.registry.testing.AppEngineExtension;
|
import google.registry.testing.AppEngineExtension;
|
||||||
|
import google.registry.testing.DualDatabaseTest;
|
||||||
import google.registry.testing.EppLoader;
|
import google.registry.testing.EppLoader;
|
||||||
import google.registry.testing.FakeClock;
|
import google.registry.testing.FakeClock;
|
||||||
import google.registry.testing.FakeHttpSession;
|
import google.registry.testing.FakeHttpSession;
|
||||||
import google.registry.testing.InjectExtension;
|
import google.registry.testing.InjectExtension;
|
||||||
|
import google.registry.testing.TestOfyAndSql;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
/** Test that domain flows create the commit logs needed to reload at points in the past. */
|
/** Test that we can reload EPP resources as they were in the past. */
|
||||||
class EppCommitLogsTest {
|
@DualDatabaseTest
|
||||||
|
class EppPointInTimeTest {
|
||||||
|
|
||||||
|
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
final AppEngineExtension appEngine =
|
final AppEngineExtension appEngine =
|
||||||
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
|
AppEngineExtension.builder()
|
||||||
|
.withDatastoreAndCloudSql()
|
||||||
|
.withClock(clock)
|
||||||
|
.withTaskQueue()
|
||||||
|
.build();
|
||||||
|
|
||||||
@RegisterExtension final InjectExtension inject = new InjectExtension();
|
@RegisterExtension final InjectExtension inject = new InjectExtension();
|
||||||
|
|
||||||
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
|
|
||||||
private EppLoader eppLoader;
|
private EppLoader eppLoader;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -81,7 +91,7 @@ class EppCommitLogsTest {
|
||||||
.run(EppMetric.builder());
|
.run(EppMetric.builder());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@TestOfyAndSql
|
||||||
void testLoadAtPointInTime() throws Exception {
|
void testLoadAtPointInTime() throws Exception {
|
||||||
clock.setTo(DateTime.parse("1984-12-18T12:30Z")); // not midnight
|
clock.setTo(DateTime.parse("1984-12-18T12:30Z")); // not midnight
|
||||||
|
|
||||||
|
@ -95,64 +105,75 @@ class EppCommitLogsTest {
|
||||||
clock.setTo(timeAtCreate);
|
clock.setTo(timeAtCreate);
|
||||||
eppLoader = new EppLoader(this, "domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
eppLoader = new EppLoader(this, "domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
||||||
runFlow();
|
runFlow();
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
Key<DomainBase> key = Key.create(auditedOfy().load().type(DomainBase.class).first().now());
|
DomainBase domainAfterCreate = Iterables.getOnlyElement(loadAllOf(DomainBase.class));
|
||||||
DomainBase domainAfterCreate = auditedOfy().load().key(key).now();
|
|
||||||
assertThat(domainAfterCreate.getDomainName()).isEqualTo("example.tld");
|
assertThat(domainAfterCreate.getDomainName()).isEqualTo("example.tld");
|
||||||
|
|
||||||
clock.advanceBy(standardDays(2));
|
clock.advanceBy(standardDays(2));
|
||||||
DateTime timeAtFirstUpdate = clock.nowUtc();
|
DateTime timeAtFirstUpdate = clock.nowUtc();
|
||||||
eppLoader = new EppLoader(this, "domain_update_dsdata_add.xml");
|
eppLoader = new EppLoader(this, "domain_update_dsdata_add.xml");
|
||||||
runFlow();
|
runFlow();
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
|
|
||||||
DomainBase domainAfterFirstUpdate = auditedOfy().load().key(key).now();
|
DomainBase domainAfterFirstUpdate = loadByEntity(domainAfterCreate);
|
||||||
assertThat(domainAfterCreate).isNotEqualTo(domainAfterFirstUpdate);
|
assertThat(domainAfterCreate).isNotEqualTo(domainAfterFirstUpdate);
|
||||||
|
|
||||||
clock.advanceOneMilli(); // same day as first update
|
clock.advanceOneMilli(); // same day as first update
|
||||||
DateTime timeAtSecondUpdate = clock.nowUtc();
|
DateTime timeAtSecondUpdate = clock.nowUtc();
|
||||||
eppLoader = new EppLoader(this, "domain_update_dsdata_rem.xml");
|
eppLoader = new EppLoader(this, "domain_update_dsdata_rem.xml");
|
||||||
runFlow();
|
runFlow();
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
DomainBase domainAfterSecondUpdate = auditedOfy().load().key(key).now();
|
DomainBase domainAfterSecondUpdate = loadByEntity(domainAfterCreate);
|
||||||
|
|
||||||
clock.advanceBy(standardDays(2));
|
clock.advanceBy(standardDays(2));
|
||||||
DateTime timeAtDelete = clock.nowUtc(); // before 'add' grace period ends
|
DateTime timeAtDelete = clock.nowUtc(); // before 'add' grace period ends
|
||||||
eppLoader = new EppLoader(this, "domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
eppLoader = new EppLoader(this, "domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"));
|
||||||
runFlow();
|
runFlow();
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
|
|
||||||
assertThat(domainAfterFirstUpdate).isNotEqualTo(domainAfterSecondUpdate);
|
assertThat(domainAfterFirstUpdate).isNotEqualTo(domainAfterSecondUpdate);
|
||||||
|
|
||||||
// Point-in-time can only rewind an object from the current version, not roll forward.
|
// Point-in-time can only rewind an object from the current version, not roll forward.
|
||||||
DomainBase latest = auditedOfy().load().key(key).now();
|
DomainBase latest = loadByEntity(domainAfterCreate);
|
||||||
|
|
||||||
// Creation time has millisecond granularity due to isActive() check.
|
// Creation time has millisecond granularity due to isActive() check.
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtCreate.minusMillis(1)).now()).isNull();
|
assertThat(loadAtPointInTime(latest, timeAtCreate.minusMillis(1)).now()).isNull();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtCreate).now()).isNotNull();
|
assertThat(loadAtPointInTime(latest, timeAtCreate).now()).isNotNull();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtCreate.plusMillis(1)).now()).isNotNull();
|
assertThat(loadAtPointInTime(latest, timeAtCreate.plusMillis(1)).now()).isNotNull();
|
||||||
|
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtCreate.plusDays(1)).now())
|
assertAboutImmutableObjects()
|
||||||
.isEqualTo(domainAfterCreate);
|
.that(loadAtPointInTime(latest, timeAtCreate.plusDays(1)).now())
|
||||||
|
.hasFieldsEqualTo(domainAfterCreate);
|
||||||
|
|
||||||
// Both updates happened on the same day. Since the revisions field has day granularity, the
|
tm().clearSessionCache();
|
||||||
// key to the first update should have been overwritten by the second, and its timestamp rolled
|
if (tm().isOfy()) {
|
||||||
// forward. So we have to fall back to the last revision before midnight.
|
// Both updates happened on the same day. Since the revisions field has day granularity in
|
||||||
auditedOfy().clearSessionCache();
|
// Datastore, the key to the first update should have been overwritten by the second, and its
|
||||||
assertThat(loadAtPointInTime(latest, timeAtFirstUpdate).now()).isEqualTo(domainAfterCreate);
|
// timestamp rolled forward. So we have to fall back to the last revision before midnight.
|
||||||
|
assertThat(loadAtPointInTime(latest, timeAtFirstUpdate).now()).isEqualTo(domainAfterCreate);
|
||||||
|
} else {
|
||||||
|
// In SQL, however, we are not limited by the day granularity, so when we request the object
|
||||||
|
// at timeAtFirstUpdate we should receive the object at that first update, even though the
|
||||||
|
// second update occurred one millisecond later.
|
||||||
|
assertAboutImmutableObjects()
|
||||||
|
.that(loadAtPointInTime(latest, timeAtFirstUpdate).now())
|
||||||
|
.hasFieldsEqualTo(domainAfterFirstUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtSecondUpdate).now())
|
assertAboutImmutableObjects()
|
||||||
.isEqualTo(domainAfterSecondUpdate);
|
.that(loadAtPointInTime(latest, timeAtSecondUpdate).now())
|
||||||
|
.hasFieldsEqualTo(domainAfterSecondUpdate);
|
||||||
|
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtSecondUpdate.plusDays(1)).now())
|
assertAboutImmutableObjects()
|
||||||
.isEqualTo(domainAfterSecondUpdate);
|
.that(loadAtPointInTime(latest, timeAtSecondUpdate.plusDays(1)).now())
|
||||||
|
.hasFieldsEqualTo(domainAfterSecondUpdate);
|
||||||
|
|
||||||
// Deletion time has millisecond granularity due to isActive() check.
|
// Deletion time has millisecond granularity due to isActive() check.
|
||||||
auditedOfy().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtDelete.minusMillis(1)).now()).isNotNull();
|
assertThat(loadAtPointInTime(latest, timeAtDelete.minusMillis(1)).now()).isNotNull();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtDelete).now()).isNull();
|
assertThat(loadAtPointInTime(latest, timeAtDelete).now()).isNull();
|
||||||
assertThat(loadAtPointInTime(latest, timeAtDelete.plusMillis(1)).now()).isNull();
|
assertThat(loadAtPointInTime(latest, timeAtDelete.plusMillis(1)).now()).isNull();
|
|
@ -41,14 +41,18 @@ import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
@DualDatabaseTest
|
@DualDatabaseTest
|
||||||
class EppResourceUtilsTest {
|
class EppResourceUtilsTest {
|
||||||
|
|
||||||
|
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
public final AppEngineExtension appEngine =
|
public final AppEngineExtension appEngine =
|
||||||
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
|
AppEngineExtension.builder()
|
||||||
|
.withDatastoreAndCloudSql()
|
||||||
|
.withClock(clock)
|
||||||
|
.withTaskQueue()
|
||||||
|
.build();
|
||||||
|
|
||||||
@RegisterExtension public final InjectExtension inject = new InjectExtension();
|
@RegisterExtension public final InjectExtension inject = new InjectExtension();
|
||||||
|
|
||||||
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeEach() {
|
void beforeEach() {
|
||||||
createTld("tld");
|
createTld("tld");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue