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:
Ben McIlwain 2019-12-11 16:20:19 -05:00 committed by GitHub
parent 3aad8b6aa7
commit db7fcf6c38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 438 additions and 17 deletions

View file

@ -791,6 +791,12 @@ public class Registry extends ImmutableObject implements Buildable {
return this; return this;
} }
@VisibleForTesting
public Builder setPremiumListKey(@Nullable Key<PremiumList> premiumList) {
getInstance().premiumList = premiumList;
return this;
}
public Builder setRestoreBillingCost(Money amount) { public Builder setRestoreBillingCost(Money amount) {
checkArgument(amount.isPositiveOrZero(), "restoreBillingCost cannot be negative"); checkArgument(amount.isPositiveOrZero(), "restoreBillingCost cannot be negative");
getInstance().restoreBillingCost = amount; getInstance().restoreBillingCost = amount;

View file

@ -36,11 +36,13 @@ import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome; import google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome;
import google.registry.model.registry.label.PremiumList.PremiumListEntry; import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.model.registry.label.PremiumList.PremiumListRevision; import google.registry.model.registry.label.PremiumList.PremiumListRevision;
import google.registry.schema.tld.PremiumListDao;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -52,7 +54,9 @@ import org.joda.time.DateTime;
public final class PremiumListUtils { public final class PremiumListUtils {
/** The number of premium list entry entities that are created and deleted per batch. */ /** 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. */ /** Value type class used by {@link #checkStatus} to return the results of a premiumness check. */
@AutoValue @AutoValue
@ -97,6 +101,19 @@ public final class PremiumListUtils {
listName, listName,
checkResults.checkOutcome(), checkResults.checkOutcome(),
DateTime.now(UTC).getMillis() - startTime.getMillis()); 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(); return checkResults.premiumPrice();
} }

View file

@ -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() {}
}

View file

@ -23,6 +23,7 @@ import com.google.common.hash.BloomFilter;
import google.registry.model.CreateAutoTimestamp; import google.registry.model.CreateAutoTimestamp;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable;
import javax.persistence.CollectionTable; import javax.persistence.CollectionTable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.ElementCollection; import javax.persistence.ElementCollection;
@ -34,6 +35,7 @@ import javax.persistence.Index;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn; import javax.persistence.MapKeyColumn;
import javax.persistence.Table; import javax.persistence.Table;
import org.hibernate.LazyInitializationException;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -97,10 +99,16 @@ public class PremiumList {
return name; 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. */ /** Returns the ID of this revision, or throws if null. */
public Long getRevisionId() { public Long getRevisionId() {
checkState( 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; return revisionId;
} }
@ -109,9 +117,17 @@ public class PremiumList {
return creationTimestamp.getTimestamp(); 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() { public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
return ImmutableMap.copyOf(labelsToPrices); return labelsToPrices == null ? null : ImmutableMap.copyOf(labelsToPrices);
} }
/** /**

View file

@ -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() {}
}

View file

@ -17,9 +17,37 @@ package google.registry.schema.tld;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; 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}. */ /** Data access object class for {@link PremiumList}. */
public class PremiumListDao { 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. */ /** Persist a new premium list to Cloud SQL. */
public static void saveNew(PremiumList premiumList) { public static void saveNew(PremiumList premiumList) {
jpaTm() jpaTm()
@ -27,18 +55,80 @@ public class PremiumListDao {
() -> { () -> {
checkArgument( checkArgument(
!checkExists(premiumList.getName()), !checkExists(premiumList.getName()),
"A premium list of this name already exists: %s.", "Premium list '%s' already exists",
premiumList.getName()); premiumList.getName());
jpaTm().getEntityManager().persist(premiumList); 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. * 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. * <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() return jpaTm()
.transact( .transact(
() -> () ->
@ -52,5 +142,24 @@ public class PremiumListDao {
> 0); > 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() {} private PremiumListDao() {}
} }

View file

@ -25,6 +25,7 @@
<class>google.registry.schema.cursor.Cursor</class> <class>google.registry.schema.cursor.Cursor</class>
<class>google.registry.model.transfer.BaseTransferObject</class> <class>google.registry.model.transfer.BaseTransferObject</class>
<class>google.registry.schema.tld.PremiumList</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.schema.tld.ReservedList</class>
<class>google.registry.model.domain.secdns.DelegationSignerData</class> <class>google.registry.model.domain.secdns.DelegationSignerData</class>
<class>google.registry.model.domain.DesignatedContact</class> <class>google.registry.model.domain.DesignatedContact</class>

View file

@ -15,13 +15,25 @@
package google.registry.schema.tld; package google.registry.schema.tld;
import static com.google.common.truth.Truth.assertThat; 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.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 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.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.model.registry.Registry;
import google.registry.model.transaction.JpaTransactionManagerRule; import google.registry.model.transaction.JpaTransactionManagerRule;
import google.registry.testing.AppEngineRule;
import java.math.BigDecimal; 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.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -35,6 +47,8 @@ public class PremiumListDaoTest {
public final JpaTransactionManagerRule jpaTmRule = public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build(); new JpaTransactionManagerRule.Builder().build();
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
private static final ImmutableMap<String, BigDecimal> TEST_PRICES = private static final ImmutableMap<String, BigDecimal> TEST_PRICES =
ImmutableMap.of( ImmutableMap.of(
"silver", "silver",
@ -46,7 +60,7 @@ public class PremiumListDaoTest {
@Test @Test
public void saveNew_worksSuccessfully() { public void saveNew_worksSuccessfully() {
PremiumList premiumList = PremiumList.create("testname", CurrencyUnit.USD, TEST_PRICES); PremiumList premiumList = PremiumList.create("testname", USD, TEST_PRICES);
PremiumListDao.saveNew(premiumList); PremiumListDao.saveNew(premiumList);
jpaTm() jpaTm()
.transact( .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 @Test
public void saveNew_throwsWhenPremiumListAlreadyExists() { public void saveNew_throwsWhenPremiumListAlreadyExists() {
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES)); PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES));
IllegalArgumentException thrown = IllegalArgumentException thrown =
assertThrows( assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> () -> PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES)));
PremiumListDao.saveNew( assertThat(thrown).hasMessageThat().isEqualTo("Premium list 'testlist' already exists");
PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES))); }
assertThat(thrown).hasMessageThat().contains("A premium list of this name 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 @Test
public void checkExists_worksSuccessfully() { public void checkExists_worksSuccessfully() {
assertThat(PremiumListDao.checkExists("testlist")).isFalse(); 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(); 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'");
}
} }

View file

@ -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;

View file

@ -15,5 +15,5 @@
-- Note: We're OK with dropping this data since it's not live in production yet. -- Note: We're OK with dropping this data since it's not live in production yet.
alter table "PremiumList" drop column if exists currency; 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'; alter table "PremiumList" add column currency text not null default 'USD';

View file

@ -132,8 +132,8 @@
create table "PremiumEntry" ( create table "PremiumEntry" (
revision_id int8 not null, revision_id int8 not null,
price numeric(19, 2) not null,
domain_label text not null, domain_label text not null,
price numeric(19, 2) not null,
primary key (revision_id, domain_label) primary key (revision_id, domain_label)
); );

View file

@ -93,7 +93,7 @@ CREATE TABLE public."PremiumList" (
creation_timestamp timestamp with time zone NOT NULL, creation_timestamp timestamp with time zone NOT NULL,
name text NOT NULL, name text NOT NULL,
bloom_filter bytea NOT NULL, bloom_filter bytea NOT NULL,
currency text DEFAULT 'USD'::text NOT NULL currency text NOT NULL
); );