Restore the original expiration time on domain restore (#601)

* Restore the original expiration time on domain restore

Except if that time is now in the past, then add a year to it.

* Apply auto-formatter changes to fix my local build

* Merge branch 'master' into restore-expiry-date

* Fix reversed comments
This commit is contained in:
Ben McIlwain
2020-06-12 14:33:49 -04:00
committed by GitHub
parent f62473542f
commit cf1448bca8
14 changed files with 134 additions and 79 deletions
@@ -143,24 +143,30 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
verifyRestoreAllowed(command, existingDomain, feeUpdate, feesAndCredits, now);
HistoryEntry historyEntry = buildHistoryEntry(existingDomain, now);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.addAll(
createRestoreAndRenewBillingEvents(
historyEntry, feesAndCredits.getRestoreCost(), feesAndCredits.getRenewCost(), now));
// We don't preserve the original expiration time of the domain when we restore, since doing so
// would require us to know if they received a grace period refund when they deleted the domain,
// and to charge them for that again. Instead, we just say that all restores get a fresh year of
// registration and bill them for that accordingly.
DateTime newExpirationTime = now.plusYears(1);
BillingEvent.Recurring autorenewEvent = newAutorenewBillingEvent(existingDomain)
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
PollMessage.Autorenew autorenewPollMessage = newAutorenewPollMessage(existingDomain)
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
// Restore the expiration time on the deleted domain, except if that's already passed, then add
// a year and bill for it immediately, with no grace period.
DateTime newExpirationTime = existingDomain.getRegistrationExpirationTime();
if (newExpirationTime.isBefore(now)) {
entitiesToSave.add(createRenewBillingEvent(historyEntry, feesAndCredits.getRenewCost(), now));
newExpirationTime = newExpirationTime.plusYears(1);
}
// Always bill for the restore itself.
entitiesToSave.add(
createRestoreBillingEvent(historyEntry, feesAndCredits.getRestoreCost(), now));
BillingEvent.Recurring autorenewEvent =
newAutorenewBillingEvent(existingDomain)
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
PollMessage.Autorenew autorenewPollMessage =
newAutorenewPollMessage(existingDomain)
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
DomainBase newDomain =
performRestore(
existingDomain, newExpirationTime, autorenewEvent, autorenewPollMessage, now, clientId);
@@ -212,18 +218,6 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
validateFeeChallenge(targetId, now, feeUpdate, feesAndCredits);
}
private ImmutableSet<BillingEvent.OneTime> createRestoreAndRenewBillingEvents(
HistoryEntry historyEntry, Money restoreCost, Money renewCost, DateTime now) {
// Bill for the restore.
BillingEvent.OneTime restoreEvent = createRestoreBillingEvent(historyEntry, restoreCost, now);
// Create a new autorenew billing event and poll message starting at the new expiration time.
// Also bill for the 1 year cost of a domain renew. This is to avoid registrants being able to
// game the system for premium names by renewing, deleting, and then restoring to get a free
// year. Note that this billing event has no grace period; it is effective immediately.
BillingEvent.OneTime renewEvent = createRenewBillingEvent(historyEntry, renewCost, now);
return ImmutableSet.of(restoreEvent, renewEvent);
}
private static DomainBase performRestore(
DomainBase existingDomain,
DateTime newExpirationTime,
@@ -35,9 +35,9 @@ import java.util.List;
import org.joda.time.DateTime;
/**
* Shared entity for date cursors. This type supports both "scoped" cursors (i.e. per resource
* of a given type, such as a TLD) and global (i.e. one per environment) cursors, defined internally
* as scoped on {@link EntityGroupRoot}.
* Shared entity for date cursors. This type supports both "scoped" cursors (i.e. per resource of a
* given type, such as a TLD) and global (i.e. one per environment) cursors, defined internally as
* scoped on {@link EntityGroupRoot}.
*/
@Entity
public class Cursor extends ImmutableObject implements DatastoreEntity {
@@ -27,14 +27,14 @@ import google.registry.schema.replay.SqlEntity;
* reasons.
*
* <p>This exists as a storage place for common configuration options and global settings that
* aren't updated too frequently. Entities in this entity group are usually cached upon load. The
* aren't updated too frequently. Entities in this entity group are usually cached upon load. The
* reason this common entity group exists is because it enables strongly consistent queries and
* updates across this seldomly updated data. This shared entity group also helps cut down on
* a potential ballooning in the number of entity groups enlisted in transactions.
* updates across this seldomly updated data. This shared entity group also helps cut down on a
* potential ballooning in the number of entity groups enlisted in transactions.
*
* <p>Historically, each TLD used to have a separate namespace, and all entities for a TLD were in
* a single EntityGroupRoot for that TLD. Hence why there was a "cross-tld" entity group -- it was
* the entity group for the single namespace where global data applicable for all TLDs lived.
* <p>Historically, each TLD used to have a separate namespace, and all entities for a TLD were in a
* single EntityGroupRoot for that TLD. Hence why there was a "cross-tld" entity group -- it was the
* entity group for the single namespace where global data applicable for all TLDs lived.
*/
@Entity
public class EntityGroupRoot extends BackupGroupRoot implements DatastoreEntity {
@@ -44,10 +44,9 @@ import org.joda.time.DateTime;
/**
* Root for a random commit log bucket.
*
* <p>This is used to shard {@link CommitLogManifest} objects into
* {@link RegistryConfig#getCommitLogBucketCount() N} entity groups. This increases
* transaction throughput, while maintaining the ability to perform strongly-consistent ancestor
* queries.
* <p>This is used to shard {@link CommitLogManifest} objects into {@link
* RegistryConfig#getCommitLogBucketCount() N} entity groups. This increases transaction throughput,
* while maintaining the ability to perform strongly-consistent ancestor queries.
*
* @see <a href="https://cloud.google.com/appengine/articles/scaling/contention">Avoiding Datastore
* contention</a>
@@ -38,11 +38,11 @@ import org.joda.time.DateTime;
* Entity representing a point-in-time consistent view of Datastore, based on commit logs.
*
* <p>Conceptually, this entity consists of two pieces of information: the checkpoint "wall" time
* and a set of bucket checkpoint times. The former is the ID for this checkpoint (constrained
* to be unique upon checkpoint creation) and also represents the approximate wall time of the
* consistent Datastore view this checkpoint represents. The latter is really a mapping from
* bucket ID to timestamp, where the timestamp dictates the upper bound (inclusive) on commit logs
* from that bucket to include when restoring Datastore to this checkpoint.
* and a set of bucket checkpoint times. The former is the ID for this checkpoint (constrained to be
* unique upon checkpoint creation) and also represents the approximate wall time of the consistent
* Datastore view this checkpoint represents. The latter is really a mapping from bucket ID to
* timestamp, where the timestamp dictates the upper bound (inclusive) on commit logs from that
* bucket to include when restoring Datastore to this checkpoint.
*/
@Entity
@NotBackedUp(reason = Reason.COMMIT_LOGS)
@@ -28,9 +28,7 @@ import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import org.joda.time.DateTime;
/**
* Singleton parent entity for all commit log checkpoints.
*/
/** Singleton parent entity for all commit log checkpoints. */
@Entity
@NotBackedUp(reason = Reason.COMMIT_LOGS)
public class CommitLogCheckpointRoot extends ImmutableObject implements DatastoreEntity {
@@ -248,7 +248,7 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
}
/**
* A premium list entry entity, persisted to Datastore. Each instance represents the price of a
* A premium list entry entity, persisted to Datastore. Each instance represents the price of a
* single label on a given TLD.
*/
@ReportedOn
@@ -62,8 +62,8 @@ import org.joda.time.DateTime;
*/
@Entity
public final class ReservedList
extends BaseDomainLabelList<ReservationType, ReservedList.ReservedListEntry> implements
DatastoreEntity {
extends BaseDomainLabelList<ReservationType, ReservedList.ReservedListEntry>
implements DatastoreEntity {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -327,8 +327,8 @@ public class ClaimsListShard extends ImmutableObject implements DatastoreEntity
}
/**
* Serves as the coordinating claims list singleton linking to the {@link ClaimsListRevision}
* that is live.
* Serves as the coordinating claims list singleton linking to the {@link ClaimsListRevision} that
* is live.
*/
@Entity
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
@@ -88,8 +88,8 @@ public final class LevelDbLogReader implements Iterator<byte[]> {
}
/**
* Returns the next {@link #BLOCK_SIZE} bytes from the input channel, or {@link
* Optional#empty()} if there is no more data.
* Returns the next {@link #BLOCK_SIZE} bytes from the input channel, or {@link Optional#empty()}
* if there is no more data.
*/
// TODO(weiminyu): use ByteBuffer directly.
private Optional<byte[]> readFromChannel() throws IOException {