Switch from using raw HistoryEntries to typed subclasses thereof (#1150)

HistoryEntry is used to record all histories (contact, domain, host) in
Datastore. In SQL it is now split into three subclasses (and thus
tables): ContactHistory, DomainHistory and HostHistory. Its builder is
genericized as a result which led to a lot of compiler warnings for the
use of a raw HistoryEntry in the existing code base.

This PR cleans things up by replacing all the explicit use of
raw HistoryEntry with the corresponding subclass and also adds some
guardrails to prevent the use of raw HistoryEntry accidentally.

Note that because DomainHistory includes nsHosts and gracePeriodHistory,
both of which are assigned a roid from ofy when built, the assigned roids for
resources after history entries are built are incremented compared to
when only HistoryEntrys are built (before this PR) in
RdapDomainSearchActionTest.

Also added a convenient tm().updateAll() varargs method.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1150)
<!-- Reviewable:end -->
This commit is contained in:
Lai Jiang
2021-05-20 11:58:41 -04:00
committed by GitHub
parent f7dca7fa96
commit 02eb7cfcc3
42 changed files with 380 additions and 229 deletions
@@ -370,11 +370,10 @@ public class DeleteContactsAndHostsAction implements Runnable {
: "it was transferred prior to deletion");
HistoryEntry historyEntry =
new HistoryEntry.Builder()
HistoryEntry.createBuilderForResource(resource)
.setClientId(deletionRequest.requestingClientId())
.setModificationTime(now)
.setType(getHistoryEntryType(resource, deleteAllowed))
.setParent(deletionRequest.key())
.build();
PollMessage.OneTime pollMessage =
@@ -409,7 +408,9 @@ public class DeleteContactsAndHostsAction implements Runnable {
} else {
resourceToSave = resource.asBuilder().removeStatusValue(PENDING_DELETE).build();
}
auditedOfy().save().<ImmutableObject>entities(resourceToSave, historyEntry, pollMessage);
auditedOfy()
.save()
.<ImmutableObject>entities(resourceToSave, historyEntry.asHistoryEntry(), pollMessage);
return DeletionResult.create(
deleteAllowed ? Type.DELETED : Type.NOT_DELETED, pollMessageText);
}
@@ -44,11 +44,11 @@ import google.registry.mapreduce.MapreduceRunner;
import google.registry.mapreduce.inputs.EppResourceInputs;
import google.registry.model.EppResourceUtils;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldType;
import google.registry.model.reporting.HistoryEntry;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
@@ -253,9 +253,9 @@ public class DeleteProberDataAction implements Runnable {
.setDeletionTime(tm().getTransactionTime())
.setStatusValues(null)
.build();
HistoryEntry historyEntry =
new HistoryEntry.Builder()
.setParent(domain)
DomainHistory historyEntry =
new DomainHistory.Builder()
.setDomain(domain)
.setType(DOMAIN_DELETE)
.setModificationTime(tm().getTransactionTime())
.setBySuperuser(true)
@@ -263,11 +263,9 @@ public class DeleteProberDataAction implements Runnable {
.setClientId(registryAdminClientId)
.build();
// Note that we don't bother handling grace periods, billing events, pending
// transfers,
// poll messages, or auto-renews because these will all be hard-deleted the next
// time the
// mapreduce runs anyway.
auditedOfy().save().entities(deletedDomain, historyEntry);
// transfers, poll messages, or auto-renews because these will all be hard-deleted
// the next time the mapreduce runs anyway.
tm().putAll(deletedDomain, historyEntry);
updateForeignKeyIndexDeletionTime(deletedDomain);
dnsQueue.addDomainRefreshTask(deletedDomain.getDomainName());
});
@@ -49,6 +49,7 @@ import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.common.Cursor;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.DomainTransactionRecord;
@@ -188,12 +189,14 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
// an event persisted.
for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) {
// Construct a new HistoryEntry that parents over the OneTime
HistoryEntry historyEntry =
new HistoryEntry.Builder()
DomainHistory historyEntry =
new DomainHistory.Builder()
.setBySuperuser(false)
.setClientId(recurring.getClientId())
.setModificationTime(tm().getTransactionTime())
.setParent(domainKey)
// TODO (jianglai): modify this to use setDomain instead when
// converting this action to be SQL-aware.
.setDomainRepoId(domainKey.getName())
.setPeriod(Period.create(1, YEARS))
.setReason(
"Domain autorenewal by ExpandRecurringBillingEventsAction")
@@ -213,50 +213,78 @@ public class FlowModule {
return Strings.nullToEmpty(((Poll) eppInput.getCommandWrapper().getCommand()).getMessageId());
}
private static <B extends HistoryEntry.Builder<? extends HistoryEntry, ?>>
B makeHistoryEntryBuilder(
B builder,
Trid trid,
byte[] inputXmlBytes,
boolean isSuperuser,
String clientId,
EppInput eppInput) {
builder
.setTrid(trid)
.setXmlBytes(inputXmlBytes)
.setBySuperuser(isSuperuser)
.setClientId(clientId);
Optional<MetadataExtension> metadataExtension =
eppInput.getSingleExtension(MetadataExtension.class);
metadataExtension.ifPresent(
extension ->
builder
.setReason(extension.getReason())
.setRequestedByRegistrar(extension.getRequestedByRegistrar()));
return builder;
}
/**
* Provides a partially filled in {@link HistoryEntry} builder.
* Provides a partially filled in {@link ContactHistory.Builder}
*
* <p>This is not marked with {@link FlowScope} so that each retry gets a fresh one. Otherwise,
* the fact that the builder is one-use would cause NPEs.
*/
@Provides
static HistoryEntry.Builder provideHistoryEntryBuilder(
static ContactHistory.Builder provideContactHistoryBuilder(
Trid trid,
@InputXml byte[] inputXmlBytes,
@Superuser boolean isSuperuser,
@ClientId String clientId,
EppInput eppInput) {
HistoryEntry.Builder historyBuilder =
new HistoryEntry.Builder()
.setTrid(trid)
.setXmlBytes(inputXmlBytes)
.setBySuperuser(isSuperuser)
.setClientId(clientId);
Optional<MetadataExtension> metadataExtension =
eppInput.getSingleExtension(MetadataExtension.class);
metadataExtension.ifPresent(
extension ->
historyBuilder
.setReason(extension.getReason())
.setRequestedByRegistrar(extension.getRequestedByRegistrar()));
return historyBuilder;
return makeHistoryEntryBuilder(
new ContactHistory.Builder(), trid, inputXmlBytes, isSuperuser, clientId, eppInput);
}
/**
* Provides a partially filled in {@link HostHistory.Builder}
*
* <p>This is not marked with {@link FlowScope} so that each retry gets a fresh one. Otherwise,
* the fact that the builder is one-use would cause NPEs.
*/
@Provides
static ContactHistory.Builder provideContactHistoryBuilder(
HistoryEntry.Builder historyEntryBuilder) {
return new ContactHistory.Builder().copyFrom(historyEntryBuilder);
static HostHistory.Builder provideHostHistoryBuilder(
Trid trid,
@InputXml byte[] inputXmlBytes,
@Superuser boolean isSuperuser,
@ClientId String clientId,
EppInput eppInput) {
return makeHistoryEntryBuilder(
new HostHistory.Builder(), trid, inputXmlBytes, isSuperuser, clientId, eppInput);
}
/**
* Provides a partially filled in {@link DomainHistory.Builder}
*
* <p>This is not marked with {@link FlowScope} so that each retry gets a fresh one. Otherwise,
* the fact that the builder is one-use would cause NPEs.
*/
@Provides
static DomainHistory.Builder provideDomainHistoryBuilder(
HistoryEntry.Builder historyEntryBuilder) {
return new DomainHistory.Builder().copyFrom(historyEntryBuilder);
}
@Provides
static HostHistory.Builder provideHostHistoryBuilder(HistoryEntry.Builder historyEntryBuilder) {
return new HostHistory.Builder().copyFrom(historyEntryBuilder);
Trid trid,
@InputXml byte[] inputXmlBytes,
@Superuser boolean isSuperuser,
@ClientId String clientId,
EppInput eppInput) {
return makeHistoryEntryBuilder(
new DomainHistory.Builder(), trid, inputXmlBytes, isSuperuser, clientId, eppInput);
}
/**
@@ -41,6 +41,11 @@ import javax.persistence.PostLoad;
* <p>In addition to the general history fields (e.g. action time, registrar ID) we also persist a
* copy of the contact entity at this point in time. We persist a raw {@link ContactBase} so that
* the foreign-keyed fields in that class can refer to this object.
*
* <p>This class is only marked as a Datastore entity subclass and registered with Objectify so that
* when building it its ID can be auto-populated by Objectify. It is converted to its superclass
* {@link HistoryEntry} when persisted to Datastore using {@link
* google.registry.persistence.transaction.TransactionManager}.
*/
@Entity
@javax.persistence.Table(
@@ -21,7 +21,6 @@ import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
@@ -62,6 +61,11 @@ import javax.persistence.Table;
* <p>In addition to the general history fields (e.g. action time, registrar ID) we also persist a
* copy of the domain entity at this point in time. We persist a raw {@link DomainContent} so that
* the foreign-keyed fields in that class can refer to this object.
*
* <p>This class is only marked as a Datastore entity subclass and registered with Objectify so that
* when building it its ID can be auto-populated by Objectify. It is converted to its superclass
* {@link HistoryEntry} when persisted to Datastore using {@link
* google.registry.persistence.transaction.TransactionManager}.
*/
@Entity
@Table(
@@ -97,7 +101,6 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// We could have reused domainContent.nsHosts here, but Hibernate throws a weird exception after
// we change to use a composite primary key.
// TODO(b/166776754): Investigate if we can reuse domainContent.nsHosts for storing host keys.
@Ignore
@ElementCollection
@JoinTable(
name = "DomainHistoryHost",
@@ -111,7 +114,6 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
@Column(name = "host_repo_id")
Set<VKey<HostResource>> nsHosts;
@Ignore
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,
@@ -131,7 +133,6 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// HashSet rather than ImmutableSet so that Hibernate can fill them out lazily on request
Set<DomainDsDataHistory> dsDataHistories = new HashSet<>();
@Ignore
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,
@@ -41,6 +41,11 @@ import javax.persistence.PostLoad;
* <p>In addition to the general history fields (e.g. action time, registrar ID) we also persist a
* copy of the host entity at this point in time. We persist a raw {@link HostBase} so that the
* foreign-keyed fields in that class can refer to this object.
*
* <p>This class is only marked as a Datastore entity subclass and registered with Objectify so that
* when building it its ID can be auto-populated by Objectify. It is converted to its superclass
* {@link HistoryEntry} when persisted to Datastore using {@link
* google.registry.persistence.transaction.TransactionManager}.
*/
@Entity
@javax.persistence.Table(
@@ -173,6 +173,11 @@ public class DatastoreTransactionManager implements TransactionManager {
putAll(entities);
}
@Override
public void updateAll(Object... entities) {
updateAll(ImmutableList.of(entities));
}
@Override
public void updateWithoutBackup(Object entity) {
putWithoutBackup(entity);
@@ -32,14 +32,17 @@ import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostBase;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostHistory.HostHistoryId;
import google.registry.model.host.HostResource;
@@ -60,7 +63,21 @@ import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.joda.time.DateTime;
/** A record of an EPP command that mutated a resource. */
/**
* A record of an EPP command that mutated a resource.
*
* <p>Due to historical reasons this class is persisted only to Datastore. It has three subclasses
* that include the parent resource itself which are persisted to Cloud SQL. During migration this
* class cannot be made abstract in order for the class to be persisted and loaded to and from
* Datastore. However it should never be used directly in the Java code itself. When it is loaded
* from Datastore it should be converted to a subclass for handling and when a new history entry is
* built it should always be a subclass, which is automatically converted to HistoryEntry when
* persisting to Datastore.
*
* <p>Some care has been taken to make it close to impossible to use this class directly, but the
* user should still exercise caution. After the migration is complete this class will be made
* abstract.
*/
@ReportedOn
@Entity
@MappedSuperclass
@@ -203,6 +220,12 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
@ImmutableObject.EmptySetToNull
protected Set<DomainTransactionRecord> domainTransactionRecords;
// Make it impossible to instantiate a HistoryEntry explicitly. One should only instantiate a
// subtype of HistoryEntry.
protected HistoryEntry() {
super();
}
public long getId() {
// For some reason, Hibernate throws NPE during some initialization phase if we don't deal with
// the null case. Setting the id to 0L when it is null should be fine because 0L for primitive
@@ -285,13 +308,50 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
domainTransactionRecords == null ? null : ImmutableSet.copyOf(domainTransactionRecords);
}
/**
* Throws an error when trying to get a builder from a bare {@link HistoryEntry}.
*
* <p>This method only exists to satisfy the requirement that the {@link HistoryEntry} is NOT
* abstract, it should never be called directly and all three of the subclass of {@link
* HistoryEntry} implements it.
*/
@Override
public Builder asBuilder() {
return new Builder(clone(this));
public Builder<? extends HistoryEntry, ?> asBuilder() {
throw new UnsupportedOperationException(
"You should never attempt to build a HistoryEntry from a raw HistoryEntry. A raw "
+ "HistoryEntry should only exist internally when persisting to datastore. If you need "
+ "to build from a raw HistoryEntry, use "
+ "{Contact,Host,Domain}History.Builder.copyFrom(HistoryEntry) instead.");
}
/**
* Clones and returns a {@code HistoryEntry} objec
*
* <p>This is useful when converting a subclass to the base class to persist to Datastore.
*/
public HistoryEntry asHistoryEntry() {
return new Builder().copyFrom(this).build();
HistoryEntry historyEntry = new HistoryEntry();
copy(this, historyEntry);
return historyEntry;
}
protected static void copy(HistoryEntry src, HistoryEntry dst) {
dst.id = src.id;
dst.parent = src.parent;
dst.type = src.type;
dst.period = src.period;
dst.xmlBytes = src.xmlBytes;
dst.modificationTime = src.modificationTime;
dst.clientId = src.clientId;
dst.otherClientId = src.otherClientId;
dst.trid = src.trid;
dst.bySuperuser = src.bySuperuser;
dst.reason = src.reason;
dst.requestedByRegistrar = src.requestedByRegistrar;
dst.domainTransactionRecords =
src.domainTransactionRecords == null
? null
: ImmutableSet.copyOf(src.domainTransactionRecords);
}
@SuppressWarnings("unchecked")
@@ -349,33 +409,18 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
}
/** A builder for {@link HistoryEntry} since it is immutable */
public static class Builder<T extends HistoryEntry, B extends Builder<?, ?>>
public abstract static class Builder<T extends HistoryEntry, B extends Builder<?, ?>>
extends GenericBuilder<T, B> {
public Builder() {}
protected Builder() {}
public Builder(T instance) {
protected Builder(T instance) {
super(instance);
}
// Used to fill out the fields in this object from an object which may not be exactly the same
// as the class T, where both classes still subclass HistoryEntry
public B copyFrom(HistoryEntry historyEntry) {
setId(historyEntry.id);
setParent(historyEntry.parent);
setType(historyEntry.type);
setPeriod(historyEntry.period);
setXmlBytes(historyEntry.xmlBytes);
setModificationTime(historyEntry.modificationTime);
setClientId(historyEntry.clientId);
setOtherClientId(historyEntry.otherClientId);
setTrid(historyEntry.trid);
setBySuperuser(historyEntry.bySuperuser);
setReason(historyEntry.reason);
setRequestedByRegistrar(historyEntry.requestedByRegistrar);
setDomainTransactionRecords(
historyEntry.domainTransactionRecords == null
? null
: ImmutableSet.copyOf(historyEntry.domainTransactionRecords));
copy(historyEntry, getInstance());
return thisCastToDerived();
}
@@ -400,13 +445,13 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
return thisCastToDerived();
}
public B setParent(EppResource parent) {
protected B setParent(EppResource parent) {
getInstance().parent = Key.create(parent);
return thisCastToDerived();
}
// Until we move completely to SQL, override this in subclasses (e.g. HostHistory) to set VKeys
public B setParent(Key<? extends EppResource> parent) {
protected B setParent(Key<? extends EppResource> parent) {
getInstance().parent = parent;
return thisCastToDerived();
}
@@ -467,4 +512,19 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
return thisCastToDerived();
}
}
public static <E extends EppResource>
HistoryEntry.Builder<? extends HistoryEntry, ?> createBuilderForResource(E parent) {
if (parent instanceof DomainContent) {
return new DomainHistory.Builder().setDomain((DomainContent) parent);
} else if (parent instanceof ContactBase) {
return new ContactHistory.Builder().setContact((ContactBase) parent);
} else if (parent instanceof HostBase) {
return new HostHistory.Builder().setHost((HostBase) parent);
} else {
throw new IllegalStateException(
String.format(
"Class %s does not have an associated HistoryEntry", parent.getClass().getName()));
}
}
}
@@ -350,6 +350,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
entities.forEach(this::update);
}
@Override
public void updateAll(Object... entities) {
updateAll(ImmutableList.of(entities));
}
@Override
public void updateWithoutBackup(Object entity) {
update(entity);
@@ -159,6 +159,9 @@ public interface TransactionManager {
/** Updates all entities in the database, throws exception if any entity does not exist. */
void updateAll(ImmutableCollection<?> entities);
/** Updates all entities in the database, throws exception if any entity does not exist. */
void updateAll(Object... entities);
/**
* Updates an entity in the database without writing commit logs if the underlying database is
* Datastore.
@@ -22,7 +22,6 @@ import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STAT
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.Key;
import google.registry.batch.AsyncTaskEnqueuer;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.billing.BillingEvent;
@@ -370,7 +369,7 @@ public final class DomainLockUtils {
.setRequestedByRegistrar(!lock.isSuperuser())
.setType(HistoryEntry.Type.DOMAIN_UPDATE)
.setModificationTime(now)
.setParent(Key.create(domain))
.setDomain(domain)
.setReason(reason)
.build();
tm().update(domain);
@@ -185,7 +185,7 @@ class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemot
leapSafeSubtractYears(domain.getRegistrationExpirationTime(), period);
DomainHistory domainHistory =
new DomainHistory.Builder()
.setParent(domain)
.setDomain(domain)
.setModificationTime(now)
.setBySuperuser(true)
.setType(Type.SYNTHETIC)
@@ -113,12 +113,11 @@ public class CreateSyntheticHistoryEntriesAction implements Runnable {
() -> {
EppResource eppResource = ofy().load().key(resourceKey).now();
tm().put(
new HistoryEntry.Builder<>()
HistoryEntry.createBuilderForResource(eppResource)
.setClientId(registryAdminRegistrarId)
.setBySuperuser(true)
.setRequestedByRegistrar(false)
.setModificationTime(tm().getTransactionTime())
.setParent(eppResource)
.setReason(
"Backfill EppResource history objects during Cloud SQL migration")
.setType(HistoryEntry.Type.SYNTHETIC)