mirror of
https://github.com/google/nomulus.git
synced 2025-05-19 10:49:35 +02:00
Add Cloud SQL premium list caches and compare prices with Datastore (#376)
* Add Cloud SQL premium list caches and compare prices with Datastore Nothing will fail if the prices can't be loaded from Cloud SQL, or if the prices are different. All that happens is that the error is logged. Then, once this is running in production for awhile, we'll look at the logs and see if there will be any pricing implications from switching over to the Cloud SQL version of the premium lists. * Add setMaxResults(1) per code review * Add tests and reorder public functions * Don't statically import caches * Improve test pass rate * Merge branch 'master' into dual-read-premium * Add PremiumEntry mapping * Allow update * Revert column order * Alphabetize PremiumEntry columns * Don't bother trying to enforce order * Private constructor
This commit is contained in:
parent
3aad8b6aa7
commit
db7fcf6c38
12 changed files with 438 additions and 17 deletions
|
@ -791,6 +791,12 @@ public class Registry extends ImmutableObject implements Buildable {
|
|||
return this;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Builder setPremiumListKey(@Nullable Key<PremiumList> premiumList) {
|
||||
getInstance().premiumList = premiumList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRestoreBillingCost(Money amount) {
|
||||
checkArgument(amount.isPositiveOrZero(), "restoreBillingCost cannot be negative");
|
||||
getInstance().restoreBillingCost = amount;
|
||||
|
|
|
@ -36,11 +36,13 @@ import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
|
|||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome;
|
||||
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
|
||||
import google.registry.model.registry.label.PremiumList.PremiumListRevision;
|
||||
import google.registry.schema.tld.PremiumListDao;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
@ -52,7 +54,9 @@ import org.joda.time.DateTime;
|
|||
public final class PremiumListUtils {
|
||||
|
||||
/** The number of premium list entry entities that are created and deleted per batch. */
|
||||
static final int TRANSACTION_BATCH_SIZE = 200;
|
||||
private static final int TRANSACTION_BATCH_SIZE = 200;
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** Value type class used by {@link #checkStatus} to return the results of a premiumness check. */
|
||||
@AutoValue
|
||||
|
@ -97,6 +101,19 @@ public final class PremiumListUtils {
|
|||
listName,
|
||||
checkResults.checkOutcome(),
|
||||
DateTime.now(UTC).getMillis() - startTime.getMillis());
|
||||
|
||||
// Also load the value from Cloud SQL, compare the two results, and log if different.
|
||||
try {
|
||||
Optional<Money> priceFromSql = PremiumListDao.getPremiumPrice(label, registry);
|
||||
if (!priceFromSql.equals(checkResults.premiumPrice())) {
|
||||
logger.atWarning().log(
|
||||
"Unequal prices for domain %s.%s from Datastore (%s) and Cloud SQL (%s).",
|
||||
label, registry.getTldStr(), checkResults.premiumPrice(), priceFromSql);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log(
|
||||
"Error loading price of domain %s.%s from Cloud SQL.", label, registry.getTldStr());
|
||||
}
|
||||
return checkResults.premiumPrice();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.schema.tld;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
||||
/**
|
||||
* Entity class for the premium price of an individual domain label.
|
||||
*
|
||||
* <p>These are not persisted directly, but rather, using {@link PremiumList#getLabelsToPrices()}.
|
||||
*/
|
||||
@Entity
|
||||
public class PremiumEntry implements Serializable {
|
||||
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
Long revisionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
BigDecimal price;
|
||||
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
String domainLabel;
|
||||
|
||||
private PremiumEntry() {}
|
||||
}
|
|
@ -23,6 +23,7 @@ import com.google.common.hash.BloomFilter;
|
|||
import google.registry.model.CreateAutoTimestamp;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.CollectionTable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.ElementCollection;
|
||||
|
@ -34,6 +35,7 @@ import javax.persistence.Index;
|
|||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.Table;
|
||||
import org.hibernate.LazyInitializationException;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
|
@ -97,10 +99,16 @@ public class PremiumList {
|
|||
return name;
|
||||
}
|
||||
|
||||
/** Returns the {@link CurrencyUnit} used for this list. */
|
||||
public CurrencyUnit getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
/** Returns the ID of this revision, or throws if null. */
|
||||
public Long getRevisionId() {
|
||||
checkState(
|
||||
revisionId != null, "revisionId is null because it is not persisted in the database");
|
||||
revisionId != null,
|
||||
"revisionId is null because this object has not yet been persisted to the DB");
|
||||
return revisionId;
|
||||
}
|
||||
|
||||
|
@ -109,9 +117,17 @@ public class PremiumList {
|
|||
return creationTimestamp.getTimestamp();
|
||||
}
|
||||
|
||||
/** Returns a {@link Map} of domain labels to prices. */
|
||||
/**
|
||||
* Returns a {@link Map} of domain labels to prices.
|
||||
*
|
||||
* <p>Note that this is lazily loaded and thus will throw a {@link LazyInitializationException} if
|
||||
* used outside the transaction in which the given entity was loaded. You generally should not be
|
||||
* using this anyway as it's inefficient to load all of the PremiumEntry rows if you don't need
|
||||
* them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
|
||||
*/
|
||||
@Nullable
|
||||
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
return ImmutableMap.copyOf(labelsToPrices);
|
||||
return labelsToPrices == null ? null : ImmutableMap.copyOf(labelsToPrices);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.schema.tld;
|
||||
|
||||
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
|
||||
import static google.registry.config.RegistryConfig.getSingletonCachePersistDuration;
|
||||
import static google.registry.config.RegistryConfig.getStaticPremiumListMaxCachedEntries;
|
||||
import static google.registry.schema.tld.PremiumListDao.getPriceForLabel;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/** Caching utils for {@link PremiumList}s. */
|
||||
class PremiumListCache {
|
||||
|
||||
/**
|
||||
* In-memory cache for premium lists.
|
||||
*
|
||||
* <p>This is cached for a shorter duration because we need to periodically reload from the DB to
|
||||
* check if a new revision has been published, and if so, then use that.
|
||||
*/
|
||||
@NonFinalForTesting
|
||||
static LoadingCache<String, Optional<PremiumList>> cachePremiumLists =
|
||||
createCachePremiumLists(getDomainLabelListCacheDuration());
|
||||
|
||||
@VisibleForTesting
|
||||
static LoadingCache<String, Optional<PremiumList>> createCachePremiumLists(
|
||||
Duration cachePersistDuration) {
|
||||
return CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS)
|
||||
.build(
|
||||
new CacheLoader<String, Optional<PremiumList>>() {
|
||||
@Override
|
||||
public Optional<PremiumList> load(@NotNull String premiumListName) {
|
||||
return PremiumListDao.getLatestRevision(premiumListName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory price cache for for a given premium list revision and domain label.
|
||||
*
|
||||
* <p>Note that premium list revision ids are globally unique, so this cache is specific to a
|
||||
* given premium list. Premium list entries might not be present, as indicated by the Optional
|
||||
* wrapper, and we want to cache that as well.
|
||||
*
|
||||
* <p>This is cached for a long duration (essentially indefinitely) because premium list revisions
|
||||
* are immutable and cannot ever be changed once created, so the cache need not ever expire.
|
||||
*
|
||||
* <p>A maximum size is set here on the cache because it can potentially grow too big to fit in
|
||||
* memory if there are a large number of distinct premium list entries being queried (both those
|
||||
* that exist, as well as those that might exist according to the Bloom filter, must be cached).
|
||||
* The entries judged least likely to be accessed again will be evicted first.
|
||||
*/
|
||||
@NonFinalForTesting
|
||||
static LoadingCache<RevisionIdAndLabel, Optional<BigDecimal>> cachePremiumEntries =
|
||||
createCachePremiumEntries(getSingletonCachePersistDuration());
|
||||
|
||||
@VisibleForTesting
|
||||
static LoadingCache<RevisionIdAndLabel, Optional<BigDecimal>> createCachePremiumEntries(
|
||||
Duration cachePersistDuration) {
|
||||
return CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS)
|
||||
.maximumSize(getStaticPremiumListMaxCachedEntries())
|
||||
.build(
|
||||
new CacheLoader<RevisionIdAndLabel, Optional<BigDecimal>>() {
|
||||
@Override
|
||||
public Optional<BigDecimal> load(RevisionIdAndLabel revisionIdAndLabel) {
|
||||
return getPriceForLabel(revisionIdAndLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class RevisionIdAndLabel {
|
||||
abstract long revisionId();
|
||||
|
||||
abstract String label();
|
||||
|
||||
static RevisionIdAndLabel create(long revisionId, String label) {
|
||||
return new AutoValue_PremiumListCache_RevisionIdAndLabel(revisionId, label);
|
||||
}
|
||||
}
|
||||
|
||||
private PremiumListCache() {}
|
||||
}
|
|
@ -17,9 +17,37 @@ package google.registry.schema.tld;
|
|||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.schema.tld.PremiumListCache.RevisionIdAndLabel;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.joda.money.Money;
|
||||
|
||||
/** Data access object class for {@link PremiumList}. */
|
||||
public class PremiumListDao {
|
||||
|
||||
/**
|
||||
* Returns the premium price for the specified label and registry, or absent if the label is not
|
||||
* premium.
|
||||
*/
|
||||
public static Optional<Money> getPremiumPrice(String label, Registry registry) {
|
||||
// If the registry has no configured premium list, then no labels are premium.
|
||||
if (registry.getPremiumList() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String premiumListName = registry.getPremiumList().getName();
|
||||
PremiumList premiumList =
|
||||
getLatestRevisionCached(premiumListName)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalStateException(
|
||||
String.format("Could not load premium list '%s'", premiumListName)));
|
||||
return getPremiumPriceFromList(label, premiumList);
|
||||
}
|
||||
|
||||
/** Persist a new premium list to Cloud SQL. */
|
||||
public static void saveNew(PremiumList premiumList) {
|
||||
jpaTm()
|
||||
|
@ -27,18 +55,80 @@ public class PremiumListDao {
|
|||
() -> {
|
||||
checkArgument(
|
||||
!checkExists(premiumList.getName()),
|
||||
"A premium list of this name already exists: %s.",
|
||||
"Premium list '%s' already exists",
|
||||
premiumList.getName());
|
||||
jpaTm().getEntityManager().persist(premiumList);
|
||||
});
|
||||
}
|
||||
|
||||
/** Persist a new revision of an existing premium list to Cloud SQL. */
|
||||
public static void update(PremiumList premiumList) {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
checkArgument(
|
||||
checkExists(premiumList.getName()),
|
||||
"Can't update non-existent premium list '%s'",
|
||||
premiumList.getName());
|
||||
jpaTm().getEntityManager().persist(premiumList);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent revision of the PremiumList with the specified name, if it exists.
|
||||
*
|
||||
* <p>Note that this does not load <code>PremiumList.labelsToPrices</code>! If you need to check
|
||||
* prices, use {@link #getPremiumPrice}.
|
||||
*/
|
||||
static Optional<PremiumList> getLatestRevision(String premiumListName) {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT pl FROM PremiumList pl WHERE pl.name = :name ORDER BY"
|
||||
+ " pl.revisionId DESC",
|
||||
PremiumList.class)
|
||||
.setParameter("name", premiumListName)
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst());
|
||||
}
|
||||
|
||||
static Optional<BigDecimal> getPriceForLabel(RevisionIdAndLabel revisionIdAndLabel) {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT pe.price FROM PremiumEntry pe WHERE pe.revisionId = :revisionId"
|
||||
+ " AND pe.domainLabel = :label",
|
||||
BigDecimal.class)
|
||||
.setParameter("revisionId", revisionIdAndLabel.revisionId())
|
||||
.setParameter("label", revisionIdAndLabel.label())
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst());
|
||||
}
|
||||
|
||||
/** Returns the most recent revision of the PremiumList with the specified name, from cache. */
|
||||
static Optional<PremiumList> getLatestRevisionCached(String premiumListName) {
|
||||
try {
|
||||
return PremiumListCache.cachePremiumLists.get(premiumListName);
|
||||
} catch (ExecutionException e) {
|
||||
throw new UncheckedExecutionException(
|
||||
"Could not retrieve premium list named " + premiumListName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the premium list of the given name exists.
|
||||
*
|
||||
* <p>This means that at least one premium list revision must exist for the given name.
|
||||
*/
|
||||
public static boolean checkExists(String premiumListName) {
|
||||
static boolean checkExists(String premiumListName) {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
|
@ -52,5 +142,24 @@ public class PremiumListDao {
|
|||
> 0);
|
||||
}
|
||||
|
||||
private static Optional<Money> getPremiumPriceFromList(String label, PremiumList premiumList) {
|
||||
// Consult the bloom filter and immediately return if the label definitely isn't premium.
|
||||
if (!premiumList.getBloomFilter().mightContain(label)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
RevisionIdAndLabel revisionIdAndLabel =
|
||||
RevisionIdAndLabel.create(premiumList.getRevisionId(), label);
|
||||
try {
|
||||
Optional<BigDecimal> price = PremiumListCache.cachePremiumEntries.get(revisionIdAndLabel);
|
||||
return price.map(p -> Money.of(premiumList.getCurrency(), p));
|
||||
} catch (InvalidCacheLoadException | ExecutionException e) {
|
||||
throw new RuntimeException(
|
||||
String.format(
|
||||
"Could not load premium entry %s for list %s",
|
||||
revisionIdAndLabel, premiumList.getName()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private PremiumListDao() {}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
<class>google.registry.schema.cursor.Cursor</class>
|
||||
<class>google.registry.model.transfer.BaseTransferObject</class>
|
||||
<class>google.registry.schema.tld.PremiumList</class>
|
||||
<class>google.registry.schema.tld.PremiumEntry</class>
|
||||
<class>google.registry.schema.tld.ReservedList</class>
|
||||
<class>google.registry.model.domain.secdns.DelegationSignerData</class>
|
||||
<class>google.registry.model.domain.DesignatedContact</class>
|
||||
|
|
|
@ -15,13 +15,25 @@
|
|||
package google.registry.schema.tld;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.testing.DatastoreHelper.createTld;
|
||||
import static google.registry.testing.DatastoreHelper.newRegistry;
|
||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.joda.money.CurrencyUnit.JPY;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.transaction.JpaTransactionManagerRule;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.math.BigDecimal;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.joda.money.Money;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
@ -35,6 +47,8 @@ public class PremiumListDaoTest {
|
|||
public final JpaTransactionManagerRule jpaTmRule =
|
||||
new JpaTransactionManagerRule.Builder().build();
|
||||
|
||||
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
|
||||
|
||||
private static final ImmutableMap<String, BigDecimal> TEST_PRICES =
|
||||
ImmutableMap.of(
|
||||
"silver",
|
||||
|
@ -46,7 +60,7 @@ public class PremiumListDaoTest {
|
|||
|
||||
@Test
|
||||
public void saveNew_worksSuccessfully() {
|
||||
PremiumList premiumList = PremiumList.create("testname", CurrencyUnit.USD, TEST_PRICES);
|
||||
PremiumList premiumList = PremiumList.create("testname", USD, TEST_PRICES);
|
||||
PremiumListDao.saveNew(premiumList);
|
||||
jpaTm()
|
||||
.transact(
|
||||
|
@ -64,22 +78,114 @@ public class PremiumListDaoTest {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void update_worksSuccessfully() {
|
||||
PremiumListDao.saveNew(
|
||||
PremiumList.create(
|
||||
"testname", USD, ImmutableMap.of("firstversion", BigDecimal.valueOf(123.45))));
|
||||
PremiumListDao.update(PremiumList.create("testname", USD, TEST_PRICES));
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
List<PremiumList> persistedLists =
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT pl FROM PremiumList pl WHERE pl.name = :name ORDER BY"
|
||||
+ " pl.revisionId",
|
||||
PremiumList.class)
|
||||
.setParameter("name", "testname")
|
||||
.getResultList();
|
||||
assertThat(persistedLists).hasSize(2);
|
||||
assertThat(persistedLists.get(1).getLabelsToPrices())
|
||||
.containsExactlyEntriesIn(TEST_PRICES);
|
||||
assertThat(persistedLists.get(1).getCreationTimestamp())
|
||||
.isEqualTo(jpaTmRule.getTxnClock().nowUtc());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveNew_throwsWhenPremiumListAlreadyExists() {
|
||||
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES));
|
||||
PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES));
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
PremiumListDao.saveNew(
|
||||
PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES)));
|
||||
assertThat(thrown).hasMessageThat().contains("A premium list of this name already exists");
|
||||
() -> PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES)));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Premium list 'testlist' already exists");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void update_throwsWhenPremiumListDoesntExist() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PremiumListDao.update(PremiumList.create("testlist", USD, TEST_PRICES)));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Can't update non-existent premium list 'testlist'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkExists_worksSuccessfully() {
|
||||
assertThat(PremiumListDao.checkExists("testlist")).isFalse();
|
||||
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES));
|
||||
PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES));
|
||||
assertThat(PremiumListDao.checkExists("testlist")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLatestRevision_returnsEmptyForNonexistentList() {
|
||||
assertThat(PremiumListDao.getLatestRevision("nonexistentlist")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLatestRevision_worksSuccessfully() {
|
||||
PremiumListDao.saveNew(
|
||||
PremiumList.create("list1", JPY, ImmutableMap.of("wrong", BigDecimal.valueOf(1000.50))));
|
||||
PremiumListDao.update(PremiumList.create("list1", JPY, TEST_PRICES));
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
Optional<PremiumList> persistedList = PremiumListDao.getLatestRevision("list1");
|
||||
assertThat(persistedList).isPresent();
|
||||
assertThat(persistedList.get().getName()).isEqualTo("list1");
|
||||
assertThat(persistedList.get().getCurrency()).isEqualTo(JPY);
|
||||
assertThat(persistedList.get().getLabelsToPrices())
|
||||
.containsExactlyEntriesIn(TEST_PRICES);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPremiumPrice_returnsNoneWhenNoPremiumListConfigured() {
|
||||
persistResource(newRegistry("foobar", "FOOBAR").asBuilder().setPremiumList(null).build());
|
||||
assertThat(PremiumListDao.getPremiumPrice("rich", Registry.get("foobar"))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPremiumPrice_worksSuccessfully() {
|
||||
persistResource(
|
||||
newRegistry("foobar", "FOOBAR")
|
||||
.asBuilder()
|
||||
.setPremiumListKey(
|
||||
Key.create(
|
||||
getCrossTldKey(),
|
||||
google.registry.model.registry.label.PremiumList.class,
|
||||
"premlist"))
|
||||
.build());
|
||||
PremiumListDao.saveNew(PremiumList.create("premlist", USD, TEST_PRICES));
|
||||
assertThat(PremiumListDao.getPremiumPrice("silver", Registry.get("foobar")))
|
||||
.hasValue(Money.of(USD, 10.23));
|
||||
assertThat(PremiumListDao.getPremiumPrice("gold", Registry.get("foobar")))
|
||||
.hasValue(Money.of(USD, 1305.47));
|
||||
assertThat(PremiumListDao.getPremiumPrice("zirconium", Registry.get("foobar"))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPremiumPrice_throwsWhenPremiumListCantBeLoaded() {
|
||||
createTld("tld");
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> PremiumListDao.getPremiumPrice("foobar", Registry.get("tld")));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Could not load premium list 'tld'");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
-- Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
-- Remove default set in V9 that we no longer need.
|
||||
alter table "PremiumList" alter column currency drop default;
|
|
@ -15,5 +15,5 @@
|
|||
-- Note: We're OK with dropping this data since it's not live in production yet.
|
||||
alter table "PremiumList" drop column if exists currency;
|
||||
|
||||
-- TODO(mcilwain): Add a subsequent schema change to remove this default.
|
||||
-- Note: This default was removed in V11.
|
||||
alter table "PremiumList" add column currency text not null default 'USD';
|
||||
|
|
|
@ -132,8 +132,8 @@
|
|||
|
||||
create table "PremiumEntry" (
|
||||
revision_id int8 not null,
|
||||
price numeric(19, 2) not null,
|
||||
domain_label text not null,
|
||||
price numeric(19, 2) not null,
|
||||
primary key (revision_id, domain_label)
|
||||
);
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ CREATE TABLE public."PremiumList" (
|
|||
creation_timestamp timestamp with time zone NOT NULL,
|
||||
name text NOT NULL,
|
||||
bloom_filter bytea NOT NULL,
|
||||
currency text DEFAULT 'USD'::text NOT NULL
|
||||
currency text NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue