Compare commits

..
Author SHA1 Message Date
Ben McIlwainandGitHub 4be70c8509 Refactor Fee handling so that each fee knows if it's premium (#626)
* Refactor Fee handling so that each fee knows if it's premium

This is a noop for now, as the new isPremium boolean isn't yet used by anything,
but it will be used in follow-up PRs to add additional fee information using the
fee extension (see: b/157621273).  Specifically what we're trying to do here is
return <fee:command name="create" standard="1"> (using the finalized version of
the fee extension) when an entire command has no premium fee associated with
it. And in the current earlier versions of the fee extension that we support,
we'll want to display the correct fee amount and class for creates/checks on
reserved domains when a valid allocation token is passed. This also needs the
isPremium information.

There are no testing implications yet because isPremium isn't exposed anywhere,
but there will definitely be lots of test changes once it's feeding into EPP-
visible changes.

* Rename things, add method Javadoc

* Apply formatting
2020-06-12 16:43:02 -04:00
Ben McIlwainandGitHub cf1448bca8 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
2020-06-12 14:33:49 -04:00
gbrodmanandGitHub f62473542f Use the requested server host when creating the registry lock verification URL (#624)
* Use the server host when creating the registry lock verification URL

The app doesn't know about any external configuration that may point to
this app, so there's no way of finding out that, for instance,
registry.google points to the app. Thus, we have to use what the user
gives us so that, in our case, the registry-lock verification
emails can point to https://registry.google/registry-lock-verify instead
of https://domain-registry.appspot.com/registry-lock-verify. The former
is used by clients / users to authenticate, and unfortunately
authenticating on registry.google does not give authentication to
domain-registry.apspot.com.

Tested using the RDAP code that uses getServerName() -- in that case, if
you access registry.google/rdap/<>, it uses registry.google in the URLs
but if you use domain-registry.appspot.com/rdap/<>, it uses
domain-registry.appspot.com in the URLs.

Relatedly, frontend_config_prod-appengine.asciiproto in Piper
is what configures registry.google to point to
domain-registry.appspot.com
2020-06-12 10:11:53 -04:00
26 changed files with 279 additions and 165 deletions
@@ -690,7 +690,7 @@ public class DomainFlowUtils {
List<Fee> fees = feeCommand.get().getFees();
// The schema guarantees that at least one fee will be present.
checkState(!fees.isEmpty());
BigDecimal total = BigDecimal.ZERO;
BigDecimal total = zeroInCurrency(feeCommand.get().getCurrency());
for (Fee fee : fees) {
if (!fee.hasDefaultAttributes()) {
throw new UnsupportedFeeAttributeException();
@@ -938,6 +938,16 @@ public class DomainFlowUtils {
}
}
/**
* Returns zero for a specific currency.
*
* <p>{@link BigDecimal} has a concept of significant figures, so zero is not always zero. E.g.
* zero in USD is 0.00, whereas zero in Yen is 0, and zero in Dinars is 0.000 (!).
*/
static BigDecimal zeroInCurrency(CurrencyUnit currencyUnit) {
return Money.of(currencyUnit, BigDecimal.ZERO).getAmount();
}
/**
* Check that if there's a claims notice it's on the claims list, and that if there's not one it's
* not on the claims list.
@@ -14,8 +14,9 @@
package google.registry.flows.domain;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.pricing.PricingEngineProxy.getDomainFeeClass;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
@@ -33,7 +34,6 @@ import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.model.registry.Registry;
import google.registry.pricing.PricingEngineProxy;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Optional;
@@ -64,21 +64,27 @@ public final class DomainPricingLogic {
public FeesAndCredits getCreatePrice(
Registry registry,
String domainName,
DateTime date,
DateTime dateTime,
int years,
boolean isAnchorTenant,
Optional<AllocationToken> allocationToken)
throws EppException {
CurrencyUnit currency = registry.getCurrency();
BaseFee createFeeOrCredit;
// Domain create cost is always zero for anchor tenants
Money domainCreateCost =
isAnchorTenant
? Money.of(currency, BigDecimal.ZERO)
: getDomainCreateCostWithDiscount(domainName, date, years, allocationToken);
BaseFee createFeeOrCredit = Fee.create(domainCreateCost.getAmount(), FeeType.CREATE);
if (isAnchorTenant) {
createFeeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
} else {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
Money domainCreateCost =
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken);
createFeeOrCredit =
Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium());
}
// Create fees for the cost and the EAP fee, if any.
Fee eapFee = registry.getEapFeeFor(date);
Fee eapFee = registry.getEapFeeFor(dateTime);
FeesAndCredits.Builder feesBuilder =
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFeeOrCredit);
// Don't charge anchor tenants EAP fees.
@@ -92,7 +98,7 @@ public final class DomainPricingLogic {
.setFeesAndCredits(feesBuilder.build())
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
.setAsOfDate(date)
.setAsOfDate(dateTime)
.setYears(years)
.build());
}
@@ -100,69 +106,73 @@ public final class DomainPricingLogic {
/** Returns a new renew price for the pricer. */
@SuppressWarnings("unused")
public FeesAndCredits getRenewPrice(
Registry registry,
String domainName,
DateTime date,
int years)
throws EppException {
Money renewCost = getDomainRenewCost(domainName, date, years);
Registry registry, String domainName, DateTime dateTime, int years) throws EppException {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
BigDecimal renewCost = domainPrices.getRenewCost().multipliedBy(years).getAmount();
return customLogic.customizeRenewPrice(
RenewPriceParameters.newBuilder()
.setFeesAndCredits(
new FeesAndCredits.Builder()
.setCurrency(registry.getCurrency())
.addFeeOrCredit(Fee.create(renewCost.getAmount(), FeeType.RENEW))
.addFeeOrCredit(Fee.create(renewCost, FeeType.RENEW, domainPrices.isPremium()))
.build())
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
.setAsOfDate(date)
.setAsOfDate(dateTime)
.setYears(years)
.build());
}
/** Returns a new restore price for the pricer. */
public FeesAndCredits getRestorePrice(Registry registry, String domainName, DateTime date)
public FeesAndCredits getRestorePrice(Registry registry, String domainName, DateTime dateTime)
throws EppException {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
FeesAndCredits feesAndCredits =
new FeesAndCredits.Builder()
.setCurrency(registry.getCurrency())
.addFeeOrCredit(
Fee.create(getDomainRenewCost(domainName, date, 1).getAmount(), FeeType.RENEW))
Fee.create(
domainPrices.getRenewCost().getAmount(),
FeeType.RENEW,
domainPrices.isPremium()))
.addFeeOrCredit(
Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE))
Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE, false))
.build();
return customLogic.customizeRestorePrice(
RestorePriceParameters.newBuilder()
.setFeesAndCredits(feesAndCredits)
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
.setAsOfDate(date)
.setAsOfDate(dateTime)
.build());
}
/** Returns a new transfer price for the pricer. */
public FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime date)
public FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime dateTime)
throws EppException {
Money renewCost = getDomainRenewCost(domainName, date, 1);
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
return customLogic.customizeTransferPrice(
TransferPriceParameters.newBuilder()
.setFeesAndCredits(
new FeesAndCredits.Builder()
.setCurrency(registry.getCurrency())
.addFeeOrCredit(Fee.create(renewCost.getAmount(), FeeType.RENEW))
.addFeeOrCredit(
Fee.create(
domainPrices.getRenewCost().getAmount(),
FeeType.RENEW,
domainPrices.isPremium()))
.build())
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
.setAsOfDate(date)
.setAsOfDate(dateTime)
.build());
}
/** Returns a new update price for the pricer. */
public FeesAndCredits getUpdatePrice(Registry registry, String domainName, DateTime date)
public FeesAndCredits getUpdatePrice(Registry registry, String domainName, DateTime dateTime)
throws EppException {
CurrencyUnit currency = registry.getCurrency();
BaseFee feeOrCredit =
Fee.create(Money.zero(registry.getCurrency()).getAmount(), FeeType.UPDATE);
BaseFee feeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.UPDATE, false);
return customLogic.customizeUpdatePrice(
UpdatePriceParameters.newBuilder()
.setFeesAndCredits(
@@ -172,19 +182,19 @@ public final class DomainPricingLogic {
.build())
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
.setAsOfDate(date)
.setAsOfDate(dateTime)
.build());
}
/** Returns the fee class for a given domain and date. */
public Optional<String> getFeeClass(String domainName, DateTime date) {
return getDomainFeeClass(domainName, date);
public Optional<String> getFeeClass(String domainName, DateTime dateTime) {
return getDomainFeeClass(domainName, dateTime);
}
/** Returns the domain create cost with allocation-token-related discounts applied. */
private Money getDomainCreateCostWithDiscount(
String domainName, DateTime date, int years, Optional<AllocationToken> allocationToken)
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken)
throws EppException {
DomainPrices domainPrices = PricingEngineProxy.getPricesForDomainName(domainName, date);
if (allocationToken.isPresent()
&& allocationToken.get().getDiscountFraction() != 0.0
&& domainPrices.isPremium()) {
@@ -211,8 +211,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
BeforeResponseParameters.newBuilder()
.setDomain(newDomain)
.setResData(DomainRenewData.create(targetId, newExpirationTime))
.setResponseExtensions(
createResponseExtensions(feesAndCredits.getTotalCost(), feeRenew))
.setResponseExtensions(createResponseExtensions(feesAndCredits, feeRenew))
.build());
return responseBuilder
.setResData(responseData.resData())
@@ -270,14 +269,19 @@ public final class DomainRenewFlow implements TransactionalFlow {
}
private ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
Money renewCost, Optional<FeeRenewCommandExtension> feeRenew) {
FeesAndCredits feesAndCredits, Optional<FeeRenewCommandExtension> feeRenew) {
return feeRenew.isPresent()
? ImmutableList.of(
feeRenew
.get()
.createResponseBuilder()
.setCurrency(renewCost.getCurrencyUnit())
.setFees(ImmutableList.of(Fee.create(renewCost.getAmount(), FeeType.RENEW)))
.setCurrency(feesAndCredits.getCurrency())
.setFees(
ImmutableList.of(
Fee.create(
feesAndCredits.getRenewCost().getAmount(),
FeeType.RENEW,
feesAndCredits.hasPremiumFeesOfType(FeeType.RENEW))))
.build())
: ImmutableList.of();
}
@@ -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);
@@ -170,9 +176,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
ofy().delete().key(existingDomain.getDeletePollMessage());
dnsQueue.addDomainRefreshTask(existingDomain.getFullyQualifiedDomainName());
return responseBuilder
.setExtensions(
createResponseExtensions(
feesAndCredits.getRestoreCost(), feesAndCredits.getRenewCost(), feeUpdate))
.setExtensions(createResponseExtensions(feesAndCredits, feeUpdate))
.build();
}
@@ -212,18 +216,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,
@@ -271,17 +263,23 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
}
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
Money restoreCost, Money renewCost, Optional<FeeUpdateCommandExtension> feeUpdate) {
FeesAndCredits feesAndCredits, Optional<FeeUpdateCommandExtension> feeUpdate) {
return feeUpdate.isPresent()
? ImmutableList.of(
feeUpdate
.get()
.createResponseBuilder()
.setCurrency(restoreCost.getCurrencyUnit())
.setCurrency(feesAndCredits.getCurrency())
.setFees(
ImmutableList.of(
Fee.create(restoreCost.getAmount(), FeeType.RESTORE),
Fee.create(renewCost.getAmount(), FeeType.RENEW)))
Fee.create(
feesAndCredits.getRestoreCost().getAmount(),
FeeType.RESTORE,
feesAndCredits.hasPremiumFeesOfType(FeeType.RESTORE)),
Fee.create(
feesAndCredits.getRenewCost().getAmount(),
FeeType.RENEW,
feesAndCredits.hasPremiumFeesOfType(FeeType.RENEW))))
.build())
: ImmutableList.of();
}
@@ -15,6 +15,7 @@
package google.registry.flows.domain;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -27,6 +28,7 @@ import google.registry.model.domain.fee.BaseFee;
import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.Fee;
import java.math.BigDecimal;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
@@ -39,26 +41,30 @@ public class FeesAndCredits extends ImmutableObject implements Buildable {
private ImmutableList<Credit> credits;
private Money getTotalCostForType(FeeType type) {
Money result = Money.zero(currency);
checkArgumentNotNull(type);
for (Fee fee : fees) {
if (fee.getType() == type) {
result = result.plus(fee.getCost());
}
}
return result;
return Money.of(
currency,
fees.stream()
.filter(f -> f.getType() == type)
.map(BaseFee::getCost)
.reduce(zeroInCurrency(currency), BigDecimal::add));
}
public boolean hasPremiumFeesOfType(FeeType type) {
return fees.stream().filter(f -> f.getType() == type).anyMatch(BaseFee::isPremium);
}
/** Returns the total cost of all fees and credits for the event. */
public Money getTotalCost() {
Money result = Money.zero(currency);
for (Fee fee : fees) {
result = result.plus(fee.getCost());
}
for (Credit credit : credits) {
result = result.plus(credit.getCost());
}
return result;
return Money.of(
currency,
Streams.concat(fees.stream(), credits.stream())
.map(BaseFee::getCost)
.reduce(zeroInCurrency(currency), BigDecimal::add));
}
public boolean hasAnyPremiumFees() {
return fees.stream().anyMatch(BaseFee::isPremium);
}
/** Returns the create cost for the event. */
@@ -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 {
@@ -104,6 +104,8 @@ public abstract class BaseFee extends ImmutableObject {
@XmlTransient Range<DateTime> validDateRange;
@XmlTransient boolean isPremium;
public String getDescription() {
return description;
}
@@ -120,6 +122,11 @@ public abstract class BaseFee extends ImmutableObject {
return firstNonNull(refundable, true);
}
/** Returns whether the fee in question is a premium price. */
public boolean isPremium() {
return isPremium;
}
/**
* According to the fee extension specification, a fee must always be non-negative, while a credit
* must always be negative. Essentially, they are the same thing, just with different sign.
@@ -31,25 +31,33 @@ import org.joda.time.DateTime;
public class Fee extends BaseFee {
/** Creates a Fee for the given cost and type with the default description. */
public static Fee create(BigDecimal cost, FeeType type, Object... descriptionArgs) {
public static Fee create(
BigDecimal cost, FeeType type, boolean isPremium, Object... descriptionArgs) {
checkArgumentNotNull(type, "Must specify the type of the fee");
return createWithCustomDescription(cost, type, type.renderDescription(descriptionArgs));
return createWithCustomDescription(
cost, type, isPremium, type.renderDescription(descriptionArgs));
}
/** Creates a Fee for the given cost, type, and valid date range with the default description. */
public static Fee create(
BigDecimal cost, FeeType type, Range<DateTime> validDateRange, Object... descriptionArgs) {
Fee instance = create(cost, type, descriptionArgs);
BigDecimal cost,
FeeType type,
boolean isPremium,
Range<DateTime> validDateRange,
Object... descriptionArgs) {
Fee instance = create(cost, type, isPremium, descriptionArgs);
instance.validDateRange = validDateRange;
return instance;
}
/** Creates a Fee for the given cost and type with a custom description. */
public static Fee createWithCustomDescription(BigDecimal cost, FeeType type, String description) {
private static Fee createWithCustomDescription(
BigDecimal cost, FeeType type, boolean isPremium, String description) {
Fee instance = new Fee();
instance.cost = checkNotNull(cost);
checkArgument(instance.cost.signum() >= 0);
checkArgument(instance.cost.signum() >= 0, "Cost must be a positive number");
instance.type = checkNotNull(type);
instance.isPremium = isPremium;
instance.description = description;
return instance;
}
@@ -56,7 +56,7 @@ public class FeeCheckCommandExtensionV11 extends ImmutableObject
/** The period to check. */
Period period;
/** The class to check. */
/** The fee class to check. */
@XmlElement(name = "class")
String feeClass;
@@ -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 {
@@ -579,6 +579,8 @@ public class Registry extends ImmutableObject implements Buildable {
return Fee.create(
eapFeeSchedule.getValueAtTime(now).getAmount(),
FeeType.EAP,
// An EAP fee counts as premium so the domain's overall Fee doesn't show as standard-priced.
true,
validPeriod,
validPeriod.upperEndpoint());
}
@@ -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 {
@@ -30,7 +30,6 @@ import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarContact;
@@ -49,12 +48,12 @@ import google.registry.tools.DomainLockUtils;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import org.apache.http.client.utils.URIBuilder;
import org.joda.time.Duration;
@@ -76,11 +75,11 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = new Gson();
private static final URL URL_BASE = RegistryConfig.getDefaultServer();
private static final String VERIFICATION_EMAIL_TEMPLATE =
"Please click the link below to perform the lock / unlock action on domain %s. Note: "
+ "this code will expire in one hour.\n\n%s";
private final HttpServletRequest req;
private final JsonActionRunner jsonActionRunner;
private final AuthResult authResult;
private final AuthenticatedRegistrarAccessor registrarAccessor;
@@ -90,12 +89,14 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
@Inject
RegistryLockPostAction(
HttpServletRequest req,
JsonActionRunner jsonActionRunner,
AuthResult authResult,
AuthenticatedRegistrarAccessor registrarAccessor,
SendEmailService sendEmailService,
DomainLockUtils domainLockUtils,
@Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress) {
this.req = req;
this.jsonActionRunner = jsonActionRunner;
this.authResult = authResult;
this.registrarAccessor = registrarAccessor;
@@ -161,7 +162,7 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
String url =
new URIBuilder()
.setScheme("https")
.setHost(URL_BASE.getHost())
.setHost(req.getServerName())
.setPath("registry-lock-verify")
.setParameter("lockVerificationCode", lock.getVerificationCode())
.setParameter("isLock", String.valueOf(isLock))
@@ -127,9 +127,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// TODO(mcilwain): The exp. date should be restored back to 2002-06-01T00:02:00Z,
// but this is old behavior of being 1 year after the moment of the restore.
"EXDATE", "2001-07-01T00:03:00Z",
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-07-01T00:03:00Z"));
assertThatLogoutSucceeds();
@@ -203,11 +201,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// TODO(mcilwain): The exp. date should be 2003-06-01T00:02:00Z, the same as its
// value prior to the deletion, because the year that was taken off when the
// autorenew was canceled will be re-added in renewal during the restore.
// For now though, the current behavior is 1 year after restore.
"EXDATE", "2003-07-05T00:03:00Z",
"EXDATE", "2003-06-01T00:02:00Z",
"UPDATE", "2002-07-05T00:03:00Z"));
assertThatLogoutSucceeds();
@@ -289,10 +283,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// TODO(mcilwain): The exp. date should be 2002-06-01T00:02:00Z, which is the
// current registration expiration time on the (deleted) domain, but for now is
// 1 year after restore.
"EXDATE", "2001-06-20T00:00:00Z",
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-06-20T00:00:00Z"));
assertThatLogoutSucceeds();
@@ -40,7 +40,7 @@ public class TestDomainPricingCustomLogic extends DomainPricingCustomLogic {
public FeesAndCredits customizeRenewPrice(RenewPriceParameters priceParameters) {
return priceParameters.domainName().toString().startsWith("costly-renew")
? addCustomFee(
priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.RENEW))
priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.RENEW, true))
: priceParameters.feesAndCredits();
}
@@ -48,7 +48,7 @@ public class TestDomainPricingCustomLogic extends DomainPricingCustomLogic {
public FeesAndCredits customizeTransferPrice(TransferPriceParameters priceParameters) {
return priceParameters.domainName().toString().startsWith("expensive")
? addCustomFee(
priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.TRANSFER))
priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.TRANSFER, true))
: priceParameters.feesAndCredits();
}
@@ -56,7 +56,7 @@ public class TestDomainPricingCustomLogic extends DomainPricingCustomLogic {
public FeesAndCredits customizeUpdatePrice(UpdatePriceParameters priceParameters) {
return priceParameters.domainName().toString().startsWith("non-free-update")
? addCustomFee(
priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.UPDATE))
priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.UPDATE, true))
: priceParameters.feesAndCredits();
}
@@ -72,6 +72,7 @@ import google.registry.model.reporting.DomainTransactionRecord.TransactionReport
import google.registry.model.reporting.HistoryEntry;
import java.util.Map;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
@@ -93,6 +94,10 @@ public class DomainRestoreRequestFlowTest
}
void persistPendingDeleteDomain() throws Exception {
persistPendingDeleteDomain(clock.nowUtc().plusYears(5).plusDays(45));
}
void persistPendingDeleteDomain(DateTime expirationTime) throws Exception {
DomainBase domain = newDomainBase(getUniqueIdFromCommand());
HistoryEntry historyEntry =
persistResource(
@@ -103,7 +108,7 @@ public class DomainRestoreRequestFlowTest
persistResource(
domain
.asBuilder()
.setRegistrationExpirationTime(clock.nowUtc().plusYears(5).plusDays(45))
.setRegistrationExpirationTime(expirationTime)
.setDeletionTime(clock.nowUtc().plusDays(35))
.addGracePeriod(
GracePeriod.create(
@@ -129,9 +134,10 @@ public class DomainRestoreRequestFlowTest
}
@Test
public void testSuccess() throws Exception {
public void testSuccess_expiryStillInFuture_notExtended() throws Exception {
setEppInput("domain_update_restore_request.xml", ImmutableMap.of("DOMAIN", "example.tld"));
persistPendingDeleteDomain();
DateTime expirationTime = clock.nowUtc().plusYears(5).plusDays(45);
persistPendingDeleteDomain(expirationTime);
assertTransactionalFlow(true);
// Double check that we see a poll message in the future for when the delete happens.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).hasSize(1);
@@ -140,11 +146,79 @@ public class DomainRestoreRequestFlowTest
HistoryEntry historyEntryDomainRestore =
getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_RESTORE);
assertThat(ofy().load().key(domain.getAutorenewBillingEvent()).now().getEventTime())
.isEqualTo(clock.nowUtc().plusYears(1));
.isEqualTo(expirationTime);
assertAboutDomains()
.that(domain)
// New expiration time should be the same as from before the deletion.
.hasRegistrationExpirationTime(expirationTime)
.and()
.doesNotHaveStatusValue(StatusValue.PENDING_DELETE)
.and()
.hasDeletionTime(END_OF_TIME)
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_DELETE, HistoryEntry.Type.DOMAIN_RESTORE)
.and()
.hasLastEppUpdateTime(clock.nowUtc())
.and()
.hasLastEppUpdateClientId("TheRegistrar");
assertThat(domain.getGracePeriods()).isEmpty();
assertDnsTasksEnqueued("example.tld");
// The poll message for the delete should now be gone. The only poll message should be the new
// autorenew poll message.
assertPollMessages(
"TheRegistrar",
new PollMessage.Autorenew.Builder()
.setTargetId("example.tld")
.setClientId("TheRegistrar")
.setEventTime(domain.getRegistrationExpirationTime())
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntryDomainRestore)
.build());
// There should be a onetime for the restore and a new recurring billing event, but no renew
// onetime.
assertBillingEvents(
new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId("example.tld")
.setClientId("TheRegistrar")
.setEventTime(expirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntryDomainRestore)
.build(),
new BillingEvent.OneTime.Builder()
.setReason(Reason.RESTORE)
.setTargetId("example.tld")
.setClientId("TheRegistrar")
.setCost(Money.of(USD, 17))
.setPeriodYears(1)
.setEventTime(clock.nowUtc())
.setBillingTime(clock.nowUtc())
.setParent(historyEntryDomainRestore)
.build());
}
@Test
public void testSuccess_expiryInPast_extendedByOneYear() throws Exception {
setEppInput("domain_update_restore_request.xml", ImmutableMap.of("DOMAIN", "example.tld"));
DateTime expirationTime = clock.nowUtc().minusDays(20);
DateTime newExpirationTime = expirationTime.plusYears(1);
persistPendingDeleteDomain(expirationTime);
assertTransactionalFlow(true);
// Double check that we see a poll message in the future for when the delete happens.
assertThat(getPollMessages("TheRegistrar", clock.nowUtc().plusMonths(1))).hasSize(1);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
DomainBase domain = reloadResourceByForeignKey();
HistoryEntry historyEntryDomainRestore =
getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_RESTORE);
assertThat(ofy().load().key(domain.getAutorenewBillingEvent()).now().getEventTime())
.isEqualTo(newExpirationTime);
assertAboutDomains()
.that(domain)
// New expiration time should be exactly a year from now.
.hasRegistrationExpirationTime(clock.nowUtc().plusYears(1))
.hasRegistrationExpirationTime(newExpirationTime)
.and()
.doesNotHaveStatusValue(StatusValue.PENDING_DELETE)
.and()
@@ -178,7 +252,7 @@ public class DomainRestoreRequestFlowTest
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId("example.tld")
.setClientId("TheRegistrar")
.setEventTime(domain.getRegistrationExpirationTime())
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntryDomainRestore)
.build(),
@@ -108,15 +108,16 @@ public class DomainTransferFlowTestCase<F extends Flow, R extends EppResource>
clock.nowUtc(),
DateTime.parse("1999-04-03T22:00:00.0Z"),
REGISTRATION_EXPIRATION_TIME);
subordinateHost = persistResource(
new HostResource.Builder()
.setRepoId("2-".concat(Ascii.toUpperCase(tld)))
.setFullyQualifiedHostName("ns1." + label + "." + tld)
.setPersistedCurrentSponsorClientId("TheRegistrar")
.setCreationClientId("TheRegistrar")
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
.setSuperordinateDomain(domain.createVKey())
.build());
subordinateHost =
persistResource(
new HostResource.Builder()
.setRepoId("2-".concat(Ascii.toUpperCase(tld)))
.setFullyQualifiedHostName("ns1." + label + "." + tld)
.setPersistedCurrentSponsorClientId("TheRegistrar")
.setCreationClientId("TheRegistrar")
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
.setSuperordinateDomain(domain.createVKey())
.build());
domain =
persistResource(
domain
@@ -101,8 +101,7 @@ public class TransactionManagerTest {
assertThrows(
RuntimeException.class,
() ->
tm()
.transact(
tm().transact(
() -> {
tm().saveNew(theEntity);
throw new RuntimeException();
@@ -28,6 +28,7 @@ import static google.registry.ui.server.registrar.RegistryLockGetActionTest.user
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableList;
@@ -56,6 +57,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.Duration;
import org.junit.Before;
@@ -74,7 +76,7 @@ public final class RegistryLockPostActionTest {
private static final String EMAIL_MESSAGE_TEMPLATE =
"Please click the link below to perform the lock \\/ unlock action on domain example.tld. "
+ "Note: this code will expire in one hour.\n\n"
+ "https:\\/\\/localhost\\/registry-lock-verify\\?lockVerificationCode="
+ "https:\\/\\/registrarconsole.tld\\/registry-lock-verify\\?lockVerificationCode="
+ "[0-9a-zA-Z_\\-]+&isLock=(true|false)";
private final FakeClock clock = new FakeClock();
@@ -93,6 +95,7 @@ public final class RegistryLockPostActionTest {
private RegistryLockPostAction action;
@Mock SendEmailService emailService;
@Mock HttpServletRequest mockRequest;
@Mock HttpServletResponse mockResponse;
@Before
@@ -103,6 +106,8 @@ public final class RegistryLockPostActionTest {
domain = persistResource(newDomainBase("example.tld"));
outgoingAddress = new InternetAddress("domain-registry@example.com");
when(mockRequest.getServerName()).thenReturn("registrarconsole.tld");
action =
createAction(
AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userWithLockPermission, false)));
@@ -432,6 +437,7 @@ public final class RegistryLockPostActionTest {
AsyncTaskEnqueuerTest.createForTesting(
mock(AppEngineServiceUtils.class), clock, Duration.ZERO));
return new RegistryLockPostAction(
mockRequest,
jsonActionRunner,
authResult,
registrarAccessor,
@@ -21,7 +21,7 @@
<domain:crDate>2000-06-01T00:04:00Z</domain:crDate>
<domain:upID>NewRegistrar</domain:upID>
<domain:upDate>2002-05-30T01:03:00Z</domain:upDate>
<domain:exDate>2003-05-30T01:03:00Z</domain:exDate>
<domain:exDate>2002-06-01T00:04:00Z</domain:exDate>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>