From 6569c1e0cd8de3d71d7cc1cceeeab27329f73f69 Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Wed, 11 Dec 2019 16:20:19 -0500 Subject: [PATCH] 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 --- .../registry/model/registry/Registry.java | 6 + .../registry/label/PremiumListUtils.java | 19 ++- .../registry/schema/tld/PremiumEntry.java | 43 ++++++ .../registry/schema/tld/PremiumList.java | 22 +++- .../registry/schema/tld/PremiumListCache.java | 107 +++++++++++++++ .../registry/schema/tld/PremiumListDao.java | 113 +++++++++++++++- .../main/resources/META-INF/persistence.xml | 1 + .../schema/tld/PremiumListDaoTest.java | 122 ++++++++++++++++-- .../V11__premium_entry_reorder_column.sql | 16 +++ .../flyway/V9__premium_list_currency_type.sql | 2 +- .../sql/schema/db-schema.sql.generated | 2 +- .../resources/sql/schema/nomulus.golden.sql | 2 +- 12 files changed, 438 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/google/registry/schema/tld/PremiumEntry.java create mode 100644 core/src/main/java/google/registry/schema/tld/PremiumListCache.java create mode 100644 db/src/main/resources/sql/flyway/V11__premium_entry_reorder_column.sql diff --git a/core/src/main/java/google/registry/model/registry/Registry.java b/core/src/main/java/google/registry/model/registry/Registry.java index 1fab709c1..bef302f52 100644 --- a/core/src/main/java/google/registry/model/registry/Registry.java +++ b/core/src/main/java/google/registry/model/registry/Registry.java @@ -791,6 +791,12 @@ public class Registry extends ImmutableObject implements Buildable { return this; } + @VisibleForTesting + public Builder setPremiumListKey(@Nullable Key premiumList) { + getInstance().premiumList = premiumList; + return this; + } + public Builder setRestoreBillingCost(Money amount) { checkArgument(amount.isPositiveOrZero(), "restoreBillingCost cannot be negative"); getInstance().restoreBillingCost = amount; diff --git a/core/src/main/java/google/registry/model/registry/label/PremiumListUtils.java b/core/src/main/java/google/registry/model/registry/label/PremiumListUtils.java index 3faeda30e..8f4a517b4 100644 --- a/core/src/main/java/google/registry/model/registry/label/PremiumListUtils.java +++ b/core/src/main/java/google/registry/model/registry/label/PremiumListUtils.java @@ -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 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(); } diff --git a/core/src/main/java/google/registry/schema/tld/PremiumEntry.java b/core/src/main/java/google/registry/schema/tld/PremiumEntry.java new file mode 100644 index 000000000..2e50a7a27 --- /dev/null +++ b/core/src/main/java/google/registry/schema/tld/PremiumEntry.java @@ -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. + * + *

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() {} +} diff --git a/core/src/main/java/google/registry/schema/tld/PremiumList.java b/core/src/main/java/google/registry/schema/tld/PremiumList.java index ad31ee75c..595da5e87 100644 --- a/core/src/main/java/google/registry/schema/tld/PremiumList.java +++ b/core/src/main/java/google/registry/schema/tld/PremiumList.java @@ -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. + * + *

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 getLabelsToPrices() { - return ImmutableMap.copyOf(labelsToPrices); + return labelsToPrices == null ? null : ImmutableMap.copyOf(labelsToPrices); } /** diff --git a/core/src/main/java/google/registry/schema/tld/PremiumListCache.java b/core/src/main/java/google/registry/schema/tld/PremiumListCache.java new file mode 100644 index 000000000..847083761 --- /dev/null +++ b/core/src/main/java/google/registry/schema/tld/PremiumListCache.java @@ -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. + * + *

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> cachePremiumLists = + createCachePremiumLists(getDomainLabelListCacheDuration()); + + @VisibleForTesting + static LoadingCache> createCachePremiumLists( + Duration cachePersistDuration) { + return CacheBuilder.newBuilder() + .expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS) + .build( + new CacheLoader>() { + @Override + public Optional load(@NotNull String premiumListName) { + return PremiumListDao.getLatestRevision(premiumListName); + } + }); + } + + /** + * In-memory price cache for for a given premium list revision and domain label. + * + *

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. + * + *

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. + * + *

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> cachePremiumEntries = + createCachePremiumEntries(getSingletonCachePersistDuration()); + + @VisibleForTesting + static LoadingCache> createCachePremiumEntries( + Duration cachePersistDuration) { + return CacheBuilder.newBuilder() + .expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS) + .maximumSize(getStaticPremiumListMaxCachedEntries()) + .build( + new CacheLoader>() { + @Override + public Optional 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() {} +} diff --git a/core/src/main/java/google/registry/schema/tld/PremiumListDao.java b/core/src/main/java/google/registry/schema/tld/PremiumListDao.java index 8392ea76a..f296f2cbc 100644 --- a/core/src/main/java/google/registry/schema/tld/PremiumListDao.java +++ b/core/src/main/java/google/registry/schema/tld/PremiumListDao.java @@ -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 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. + * + *

Note that this does not load PremiumList.labelsToPrices! If you need to check + * prices, use {@link #getPremiumPrice}. + */ + static Optional 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 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 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. * *

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 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 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() {} } diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index c6a1721e4..279f4c776 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -25,6 +25,7 @@ google.registry.schema.cursor.Cursor google.registry.model.transfer.BaseTransferObject google.registry.schema.tld.PremiumList + google.registry.schema.tld.PremiumEntry google.registry.schema.tld.ReservedList google.registry.model.domain.secdns.DelegationSignerData google.registry.model.domain.DesignatedContact diff --git a/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java b/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java index 1b37ad68e..9b7f8e6df 100644 --- a/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java +++ b/core/src/test/java/google/registry/schema/tld/PremiumListDaoTest.java @@ -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 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 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 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'"); + } } diff --git a/db/src/main/resources/sql/flyway/V11__premium_entry_reorder_column.sql b/db/src/main/resources/sql/flyway/V11__premium_entry_reorder_column.sql new file mode 100644 index 000000000..85011636d --- /dev/null +++ b/db/src/main/resources/sql/flyway/V11__premium_entry_reorder_column.sql @@ -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; diff --git a/db/src/main/resources/sql/flyway/V9__premium_list_currency_type.sql b/db/src/main/resources/sql/flyway/V9__premium_list_currency_type.sql index 9530ae0e6..73bf97b38 100644 --- a/db/src/main/resources/sql/flyway/V9__premium_list_currency_type.sql +++ b/db/src/main/resources/sql/flyway/V9__premium_list_currency_type.sql @@ -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'; diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index 819ef6778..7f990757d 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -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) ); diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index e0341b6f7..36fb4bf8c 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -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 );