Refactor PremiumList storage and retrieval for dual-database setup (#950)

* Refactor PremiumList storage and retrieval for dual-database setup

Previously, the storage and retrieval code was scattered across various
places haphazardly and there was no good way to set up dual database
access. This reorganizes the code so that retrieval is simpler and it
allows for dual-write and dual-read.

This includes the following changes:

- Move all static / object retrieval code out of PremiumList -- the
class should solely consist of its data and methods on its data and it
shouldn't have to worry about complicated caching or retrieval

- Split all PremiumList retrieval methods into PremiumListDatastoreDao
and PremiumListSqlDao that handle retrieval of the premium list entry
objects from the corresponding databases (since the way the actual data
itself is stored is not the same between the two

- Create a dual-DAO for PremiumList retrieval that branches between
SQL/Datastore depending on which is appropriate -- it will read from
and write to both but only log errors for the secondary DB

- Cache the mapping from name to premium list in the dual-DAO. This is a
common code path regardless of database so we can cache it at a high
level

- Cache the ways to go from premium list -> premium entries in the
Datastore and SQL DAOs. These caches are specific to the corresponding
DB and should thus be stored in the corresponding DAO.

- Moves the database-choosing code from the actions to the lower-level
dual-DAO. This is because we will often wish to access this premium list
data in flows and all accesses should use the proper DB-selecting code
This commit is contained in:
gbrodman 2021-02-22 21:19:48 -05:00 committed by GitHub
parent ffe3124ee1
commit a07fbb27c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1270 additions and 1046 deletions

View file

@ -17,7 +17,6 @@ package google.registry.export;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.model.registry.label.PremiumListUtils.loadPremiumListEntries;
import static google.registry.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
@ -32,7 +31,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType; import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.request.Action; import google.registry.request.Action;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import google.registry.request.RequestParameters; import google.registry.request.RequestParameters;
@ -137,10 +136,11 @@ public class ExportPremiumTermsAction implements Runnable {
} }
private String getFormattedPremiumTerms(Registry registry) { private String getFormattedPremiumTerms(Registry registry) {
Optional<PremiumList> premiumList = PremiumList.getCached(registry.getPremiumList().getName()); String premiumListName = registry.getPremiumList().getName();
checkState(premiumList.isPresent(), "Could not load premium list for " + tld); checkState(
PremiumListDualDao.exists(premiumListName), "Could not load premium list for " + tld);
SortedSet<String> premiumTerms = SortedSet<String> premiumTerms =
Streams.stream(loadPremiumListEntries(premiumList.get())) Streams.stream(PremiumListDualDao.loadAllPremiumListEntries(premiumListName))
.map(entry -> Joiner.on(",").join(entry.getLabel(), entry.getValue())) .map(entry -> Joiner.on(",").join(entry.getLabel(), entry.getValue()))
.collect(ImmutableSortedSet.toImmutableSortedSet(String::compareTo)); .collect(ImmutableSortedSet.toImmutableSortedSet(String::compareTo));

View file

@ -40,6 +40,7 @@ import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState; import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
@ -288,7 +289,7 @@ public final class OteAccountBuilder {
boolean isEarlyAccess, boolean isEarlyAccess,
int roidSuffix) { int roidSuffix) {
String tldNameAlphaNumerical = tldName.replaceAll("[^a-z0-9]", ""); String tldNameAlphaNumerical = tldName.replaceAll("[^a-z0-9]", "");
Optional<PremiumList> premiumList = PremiumList.getUncached(DEFAULT_PREMIUM_LIST); Optional<PremiumList> premiumList = PremiumListDualDao.getLatestRevision(DEFAULT_PREMIUM_LIST);
checkState(premiumList.isPresent(), "Couldn't find premium list %s.", DEFAULT_PREMIUM_LIST); checkState(premiumList.isPresent(), "Couldn't find premium list %s.", DEFAULT_PREMIUM_LIST);
Registry.Builder builder = Registry.Builder builder =
new Registry.Builder() new Registry.Builder()

View file

@ -15,11 +15,11 @@
package google.registry.model.pricing; package google.registry.model.pricing;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.registry.label.PremiumListUtils.getPremiumPrice;
import static google.registry.util.DomainNameUtils.getTldFromDomainName; import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import com.google.common.net.InternetDomainName; import com.google.common.net.InternetDomainName;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumListDualDao;
import java.util.Optional; import java.util.Optional;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.money.Money; import org.joda.money.Money;
@ -38,7 +38,7 @@ public final class StaticPremiumListPricingEngine implements PremiumPricingEngin
String tld = getTldFromDomainName(fullyQualifiedDomainName); String tld = getTldFromDomainName(fullyQualifiedDomainName);
String label = InternetDomainName.from(fullyQualifiedDomainName).parts().get(0); String label = InternetDomainName.from(fullyQualifiedDomainName).parts().get(0);
Registry registry = Registry.get(checkNotNull(tld, "tld")); Registry registry = Registry.get(checkNotNull(tld, "tld"));
Optional<Money> premiumPrice = getPremiumPrice(label, registry); Optional<Money> premiumPrice = PremiumListDualDao.getPremiumPrice(label, registry);
return DomainPrices.create( return DomainPrices.create(
premiumPrice.isPresent(), premiumPrice.isPresent(),
premiumPrice.orElse(registry.getStandardCreateCost()), premiumPrice.orElse(registry.getStandardCreateCost()),

View file

@ -18,23 +18,12 @@ import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.hash.Funnels.stringFunnel; import static com.google.common.hash.Funnels.stringFunnel;
import static com.google.common.hash.Funnels.unencodedCharsFunnel; import static com.google.common.hash.Funnels.unencodedCharsFunnel;
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.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.allocateId; import static google.registry.model.ofy.ObjectifyService.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.hash.BloomFilter; import com.google.common.hash.BloomFilter;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Id;
@ -46,18 +35,14 @@ import google.registry.model.annotations.ReportedOn;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.schema.replay.DatastoreOnlyEntity; import google.registry.schema.replay.DatastoreOnlyEntity;
import google.registry.schema.replay.NonReplicatedEntity; import google.registry.schema.replay.NonReplicatedEntity;
import google.registry.schema.tld.PremiumListDao; import google.registry.schema.tld.PremiumListSqlDao;
import google.registry.util.NonFinalForTesting;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.persistence.CollectionTable; import javax.persistence.CollectionTable;
import javax.persistence.Column; import javax.persistence.Column;
@ -72,7 +57,6 @@ import javax.persistence.Transient;
import org.hibernate.LazyInitializationException; import org.hibernate.LazyInitializationException;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.money.Money; import org.joda.money.Money;
import org.joda.time.Duration;
/** /**
* A premium list entity that is used to check domain label prices. * A premium list entity that is used to check domain label prices.
@ -171,124 +155,11 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
} }
} }
/**
* In-memory cache for premium lists.
*
* <p>This is cached for a shorter duration because we need to periodically reload this entity to
* check if a new revision has been published, and if so, then use that.
*/
@NonFinalForTesting
static LoadingCache<String, PremiumList> cachePremiumLists =
createCachePremiumLists(getDomainLabelListCacheDuration());
@VisibleForTesting
public static void setPremiumListCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getDomainLabelListCacheDuration());
cachePremiumLists = createCachePremiumLists(effectiveExpiry);
}
@VisibleForTesting
static LoadingCache<String, PremiumList> createCachePremiumLists(Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.build(
new CacheLoader<String, PremiumList>() {
@Override
public PremiumList load(final String name) {
return tm().doTransactionless(() -> loadPremiumList(name));
}
});
}
private static PremiumList loadPremiumList(String name) {
return tm().isOfy()
? ofy().load().type(PremiumList.class).parent(getCrossTldKey()).id(name).now()
: PremiumListDao.getLatestRevision(name).orElseThrow(NoSuchElementException::new);
}
/**
* In-memory cache for {@link PremiumListRevision}s, used for retrieving Bloom filters quickly.
*
* <p>This is cached for a long duration (essentially indefinitely) because a given {@link
* PremiumListRevision} is immutable and cannot ever be changed once created, so its cache need
* not ever expire.
*/
static final LoadingCache<Key<PremiumListRevision>, PremiumListRevision>
cachePremiumListRevisions =
CacheBuilder.newBuilder()
.expireAfterWrite(
java.time.Duration.ofMillis(getSingletonCachePersistDuration().getMillis()))
.build(
new CacheLoader<Key<PremiumListRevision>, PremiumListRevision>() {
@Override
public PremiumListRevision load(final Key<PremiumListRevision> revisionKey) {
return tm().doTransactionless(() -> ofy().load().key(revisionKey).now());
}
});
/**
* In-memory cache for {@link PremiumListEntry}s for a given label and {@link PremiumListRevision}
*
* <p>Because the PremiumList itself makes up part of the PremiumListRevision's key, this 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 a given {@link
* PremiumListRevision} and its child {@link PremiumListEntry}s 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<Key<PremiumListEntry>, Optional<PremiumListEntry>> cachePremiumListEntries =
createCachePremiumListEntries(getSingletonCachePersistDuration());
@VisibleForTesting
public static void setPremiumListEntriesCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getSingletonCachePersistDuration());
cachePremiumListEntries = createCachePremiumListEntries(effectiveExpiry);
}
@VisibleForTesting
static LoadingCache<Key<PremiumListEntry>, Optional<PremiumListEntry>>
createCachePremiumListEntries(Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.maximumSize(getStaticPremiumListMaxCachedEntries())
.build(
new CacheLoader<Key<PremiumListEntry>, Optional<PremiumListEntry>>() {
@Override
public Optional<PremiumListEntry> load(final Key<PremiumListEntry> entryKey) {
return tm().doTransactionless(
() -> Optional.ofNullable(ofy().load().key(entryKey).now()));
}
});
}
@VisibleForTesting @VisibleForTesting
public Key<PremiumListRevision> getRevisionKey() { public Key<PremiumListRevision> getRevisionKey() {
return revisionKey; return revisionKey;
} }
/** Returns the PremiumList with the specified name, from cache. */
public static Optional<PremiumList> getCached(String name) {
try {
return Optional.of(cachePremiumLists.get(name));
} catch (InvalidCacheLoadException e) {
return Optional.empty();
} catch (ExecutionException e) {
throw new UncheckedExecutionException("Could not retrieve premium list named " + name, e);
}
}
/** Returns the PremiumList with the specified name, uncached. */
public static Optional<PremiumList> getUncached(String name) {
return Optional.ofNullable(loadPremiumList(name));
}
/** Returns the {@link CurrencyUnit} used for this list. */ /** Returns the {@link CurrencyUnit} used for this list. */
public CurrencyUnit getCurrency() { public CurrencyUnit getCurrency() {
return currency; return currency;
@ -300,7 +171,7 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
* <p>Note that this is lazily loaded and thus will throw a {@link LazyInitializationException} if * <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 * 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 * 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. * them. To check prices, use {@link PremiumListSqlDao#getPremiumPrice} instead.
*/ */
@Nullable @Nullable
public ImmutableMap<String, BigDecimal> getLabelsToPrices() { public ImmutableMap<String, BigDecimal> getLabelsToPrices() {

View file

@ -0,0 +1,365 @@
// Copyright 2021 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.model.registry.label;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.partition;
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.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.BLOOM_FILTER_NEGATIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.CACHED_NEGATIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.CACHED_POSITIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.UNCACHED_NEGATIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.UNCACHED_POSITIVE;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
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.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
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.persistence.VKey;
import google.registry.util.NonFinalForTesting;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* DAO for {@link PremiumList} objects stored in Datastore.
*
* <p>This class handles both the mapping from string to Datastore-level PremiumList objects as well
* as the mapping from PremiumList objects to the contents of those premium lists in the Datastore
* world. Specifically, this deals with retrieving the most recent revision for a given list and
* retrieving (or writing/deleting) all entries associated with that particular revision. The {@link
* PremiumList} object itself, in the Datastore world, does not store the premium pricing data.
*/
public class PremiumListDatastoreDao {
/** The number of premium list entry entities that are created and deleted per batch. */
private static final int TRANSACTION_BATCH_SIZE = 200;
/**
* In-memory cache for premium lists.
*
* <p>This is cached for a shorter duration because we need to periodically reload this entity to
* check if a new revision has been published, and if so, then use that.
*
* <p>We also cache the absence of premium lists with a given name to avoid unnecessary pointless
* lookups. Note that this cache is only applicable to PremiumList objects stored in Datastore.
*/
@NonFinalForTesting
static LoadingCache<String, Optional<PremiumList>> premiumListCache =
createPremiumListCache(getDomainLabelListCacheDuration());
@VisibleForTesting
public static void setPremiumListCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getSingletonCachePersistDuration());
premiumListCache = createPremiumListCache(effectiveExpiry);
}
@VisibleForTesting
public static LoadingCache<String, Optional<PremiumList>> createPremiumListCache(
Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.build(
new CacheLoader<String, Optional<PremiumList>>() {
@Override
public Optional<PremiumList> load(final String name) {
return tm().doTransactionless(() -> getLatestRevisionUncached(name));
}
});
}
/**
* In-memory cache for {@link PremiumListRevision}s, used for retrieving Bloom filters quickly.
*
* <p>This is cached for a long duration (essentially indefinitely) because a given {@link
* PremiumListRevision} is immutable and cannot ever be changed once created, so its cache need
* not ever expire.
*/
static final LoadingCache<Key<PremiumListRevision>, PremiumListRevision>
premiumListRevisionsCache =
CacheBuilder.newBuilder()
.expireAfterWrite(
java.time.Duration.ofMillis(getSingletonCachePersistDuration().getMillis()))
.build(
new CacheLoader<Key<PremiumListRevision>, PremiumListRevision>() {
@Override
public PremiumListRevision load(final Key<PremiumListRevision> revisionKey) {
return ofyTm().doTransactionless(() -> ofy().load().key(revisionKey).now());
}
});
/**
* In-memory cache for {@link PremiumListEntry}s for a given label and {@link PremiumListRevision}
*
* <p>Because the PremiumList itself makes up part of the PremiumListRevision's key, this 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 a given {@link
* PremiumListRevision} and its child {@link PremiumListEntry}s 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<Key<PremiumListEntry>, Optional<PremiumListEntry>> premiumListEntriesCache =
createPremiumListEntriesCache(getSingletonCachePersistDuration());
@VisibleForTesting
public static void setPremiumListEntriesCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getSingletonCachePersistDuration());
premiumListEntriesCache = createPremiumListEntriesCache(effectiveExpiry);
}
@VisibleForTesting
static LoadingCache<Key<PremiumListEntry>, Optional<PremiumListEntry>>
createPremiumListEntriesCache(Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.maximumSize(getStaticPremiumListMaxCachedEntries())
.build(
new CacheLoader<Key<PremiumListEntry>, Optional<PremiumListEntry>>() {
@Override
public Optional<PremiumListEntry> load(final Key<PremiumListEntry> entryKey) {
return ofyTm()
.doTransactionless(() -> Optional.ofNullable(ofy().load().key(entryKey).now()));
}
});
}
public static Optional<PremiumList> getLatestRevision(String name) {
return premiumListCache.getUnchecked(name);
}
/**
* Returns the premium price for the specified list, label, and TLD, or absent if the label is not
* premium.
*/
public static Optional<Money> getPremiumPrice(String premiumListName, String label, String tld) {
DateTime startTime = DateTime.now(UTC);
Optional<PremiumList> maybePremumList = getLatestRevision(premiumListName);
if (!maybePremumList.isPresent()) {
return Optional.empty();
}
PremiumList premiumList = maybePremumList.get();
// If we're dealing with a list from SQL, reload from Datastore if necessary
if (premiumList.getRevisionKey() == null) {
Optional<PremiumList> fromDatastore = getLatestRevision(premiumList.getName());
if (fromDatastore.isPresent()) {
premiumList = fromDatastore.get();
} else {
return Optional.empty();
}
}
PremiumListRevision revision;
try {
revision = premiumListRevisionsCache.get(premiumList.getRevisionKey());
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException(
"Could not load premium list revision " + premiumList.getRevisionKey(), e);
}
checkState(
revision.getProbablePremiumLabels() != null,
"Probable premium labels Bloom filter is null on revision '%s'",
premiumList.getRevisionKey());
CheckResults checkResults = checkStatus(revision, label);
DomainLabelMetrics.recordPremiumListCheckOutcome(
tld,
premiumList.getName(),
checkResults.checkOutcome(),
DateTime.now(UTC).getMillis() - startTime.getMillis());
return checkResults.premiumPrice();
}
/**
* Persists a new or updated PremiumList object and its descendant entities to Datastore.
*
* <p>The flow here is: save the new premium list entries parented on that revision entity,
* save/update the PremiumList, and then delete the old premium list entries associated with the
* old revision.
*
* <p>This is the only valid way to save these kinds of entities!
*/
public static PremiumList save(String name, List<String> inputData) {
PremiumList premiumList = new PremiumList.Builder().setName(name).build();
ImmutableMap<String, PremiumListEntry> premiumListEntries = premiumList.parse(inputData);
final Optional<PremiumList> oldPremiumList = getLatestRevisionUncached(premiumList.getName());
// Create the new revision (with its Bloom filter) and parent the entries on it.
final PremiumListRevision newRevision =
PremiumListRevision.create(premiumList, premiumListEntries.keySet());
final Key<PremiumListRevision> newRevisionKey = Key.create(newRevision);
ImmutableSet<PremiumListEntry> parentedEntries =
parentPremiumListEntriesOnRevision(premiumListEntries.values(), newRevisionKey);
// Save the new child entities in a series of transactions.
for (final List<PremiumListEntry> batch : partition(parentedEntries, TRANSACTION_BATCH_SIZE)) {
ofyTm().transactNew(() -> ofy().save().entities(batch));
}
// Save the new PremiumList and revision itself.
return ofyTm()
.transactNew(
() -> {
DateTime now = ofyTm().getTransactionTime();
// Assert that the premium list hasn't been changed since we started this process.
Key<PremiumList> key =
Key.create(getCrossTldKey(), PremiumList.class, premiumList.getName());
Optional<PremiumList> existing =
ofyTm().loadByKeyIfPresent(VKey.createOfy(PremiumList.class, key));
checkOfyFieldsEqual(existing, oldPremiumList);
PremiumList newList =
premiumList
.asBuilder()
.setLastUpdateTime(now)
.setCreationTime(
oldPremiumList.isPresent() ? oldPremiumList.get().creationTime : now)
.setRevision(newRevisionKey)
.build();
ofy().save().entities(newList, newRevision);
premiumListCache.invalidate(premiumList.getName());
return newList;
});
}
public static void delete(PremiumList premiumList) {
ofyTm().transactNew(() -> ofy().delete().entity(premiumList));
if (premiumList.getRevisionKey() == null) {
return;
}
for (final List<Key<PremiumListEntry>> batch :
partition(
ofy().load().type(PremiumListEntry.class).ancestor(premiumList.revisionKey).keys(),
TRANSACTION_BATCH_SIZE)) {
ofyTm().transactNew(() -> ofy().delete().keys(batch));
batch.forEach(premiumListEntriesCache::invalidate);
}
ofyTm().transactNew(() -> ofy().delete().key(premiumList.getRevisionKey()));
premiumListCache.invalidate(premiumList.getName());
premiumListRevisionsCache.invalidate(premiumList.getRevisionKey());
}
/** Re-parents the given {@link PremiumListEntry}s on the given {@link PremiumListRevision}. */
@VisibleForTesting
public static ImmutableSet<PremiumListEntry> parentPremiumListEntriesOnRevision(
Iterable<PremiumListEntry> entries, final Key<PremiumListRevision> revisionKey) {
return Streams.stream(entries)
.map((PremiumListEntry entry) -> entry.asBuilder().setParent(revisionKey).build())
.collect(toImmutableSet());
}
/**
* Returns all {@link PremiumListEntry PremiumListEntries} in the given {@code premiumList}.
*
* <p>This is an expensive operation and should only be used when the entire list is required.
*/
public static Iterable<PremiumListEntry> loadPremiumListEntriesUncached(PremiumList premiumList) {
return ofy().load().type(PremiumListEntry.class).ancestor(premiumList.revisionKey).iterable();
}
private static Optional<PremiumList> getLatestRevisionUncached(String name) {
return Optional.ofNullable(
ofy().load().key(Key.create(getCrossTldKey(), PremiumList.class, name)).now());
}
private static void checkOfyFieldsEqual(
Optional<PremiumList> oneOptional, Optional<PremiumList> twoOptional) {
if (!oneOptional.isPresent()) {
checkState(!twoOptional.isPresent(), "Premium list concurrently deleted");
return;
} else {
checkState(twoOptional.isPresent(), "Premium list concurrently deleted");
}
PremiumList one = oneOptional.get();
PremiumList two = twoOptional.get();
checkState(
Objects.equals(one.revisionKey, two.revisionKey),
"Premium list revision key concurrently edited");
checkState(Objects.equals(one.name, two.name), "Premium list name concurrently edited");
checkState(Objects.equals(one.parent, two.parent), "Premium list parent concurrently edited");
checkState(
Objects.equals(one.creationTime, two.creationTime),
"Premium list creation time concurrently edited");
}
private static CheckResults checkStatus(PremiumListRevision premiumListRevision, String label) {
if (!premiumListRevision.getProbablePremiumLabels().mightContain(label)) {
return CheckResults.create(BLOOM_FILTER_NEGATIVE, Optional.empty());
}
Key<PremiumListEntry> entryKey =
Key.create(Key.create(premiumListRevision), PremiumListEntry.class, label);
try {
// getIfPresent() returns null if the key is not in the cache
Optional<PremiumListEntry> entry = premiumListEntriesCache.getIfPresent(entryKey);
if (entry != null) {
if (entry.isPresent()) {
return CheckResults.create(CACHED_POSITIVE, Optional.of(entry.get().getValue()));
} else {
return CheckResults.create(CACHED_NEGATIVE, Optional.empty());
}
}
entry = premiumListEntriesCache.get(entryKey);
if (entry.isPresent()) {
return CheckResults.create(UNCACHED_POSITIVE, Optional.of(entry.get().getValue()));
} else {
return CheckResults.create(UNCACHED_NEGATIVE, Optional.empty());
}
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException("Could not load premium list entry " + entryKey, e);
}
}
/** Value type class used by {@link #checkStatus} to return the results of a premiumness check. */
@AutoValue
abstract static class CheckResults {
static CheckResults create(PremiumListCheckOutcome checkOutcome, Optional<Money> premiumPrice) {
return new AutoValue_PremiumListDatastoreDao_CheckResults(checkOutcome, premiumPrice);
}
abstract PremiumListCheckOutcome checkOutcome();
abstract Optional<Money> premiumPrice();
}
private PremiumListDatastoreDao() {}
}

View file

@ -0,0 +1,199 @@
// Copyright 2021 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.model.registry.label;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.Streams;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.schema.tld.PremiumListSqlDao;
import java.util.List;
import java.util.Optional;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
/**
* DAO for {@link PremiumList} objects that handles the branching paths for SQL and Datastore.
*
* <p>For write actions, this class will perform the action against the primary database then, after
* that success or failure, against the secondary database. If the secondary database fails, an
* error is logged (but not thrown).
*
* <p>For read actions, when retrieving a price, we will log if the primary and secondary databases
* have different values (or if the retrieval from the second database fails).
*
* <p>TODO (gbrodman): Change the isOfy() calls to the runtime selection of DBs when available
*/
public class PremiumListDualDao {
/**
* Retrieves from the appropriate DB and returns the most recent premium list with the given name,
* or absent if no such list exists.
*/
public static Optional<PremiumList> getLatestRevision(String premiumListName) {
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
return PremiumListDatastoreDao.getLatestRevision(premiumListName);
} else {
return PremiumListSqlDao.getLatestRevision(premiumListName);
}
}
/**
* Returns the premium price for the specified label and registry.
*
* <p>Returns absent if the label is not premium or there is no premium list for this registry.
*
* <p>Retrieves the price from both primary and secondary databases, and logs in the event of a
* failure in the secondary (but does not throw an exception).
*/
public static Optional<Money> getPremiumPrice(String label, Registry registry) {
if (registry.getPremiumList() == null) {
return Optional.empty();
}
String premiumListName = registry.getPremiumList().getName();
Optional<Money> primaryResult;
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
primaryResult =
PremiumListDatastoreDao.getPremiumPrice(premiumListName, label, registry.getTldStr());
} else {
primaryResult = PremiumListSqlDao.getPremiumPrice(premiumListName, label);
}
// Also load the value from the secondary DB, compare the two results, and log if different.
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
suppressExceptionUnlessInTest(
() -> {
Optional<Money> secondaryResult =
PremiumListSqlDao.getPremiumPrice(premiumListName, label);
if (!primaryResult.equals(secondaryResult)) {
throw new IllegalStateException(
String.format(
"Unequal prices for domain %s.%s from primary Datastore DB (%s) and "
+ "secondary SQL db (%s).",
label, registry.getTldStr(), primaryResult, secondaryResult));
}
},
String.format(
"Error loading price of domain %s.%s from Cloud SQL.", label, registry.getTldStr()));
} else {
suppressExceptionUnlessInTest(
() -> {
Optional<Money> secondaryResult =
PremiumListDatastoreDao.getPremiumPrice(
premiumListName, label, registry.getTldStr());
if (!primaryResult.equals(secondaryResult)) {
throw new IllegalStateException(
String.format(
"Unequal prices for domain %s.%s from primary SQL DB (%s) and secondary "
+ "Datastore db (%s).",
label, registry.getTldStr(), primaryResult, secondaryResult));
}
},
String.format(
"Error loading price of domain %s.%s from Datastore.", label, registry.getTldStr()));
}
return primaryResult;
}
/**
* Saves the given list data to both primary and secondary databases.
*
* <p>Logs but doesn't throw an exception in the event of a failure when writing to the secondary
* database.
*/
public static PremiumList save(String name, List<String> inputData) {
PremiumList result;
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
result = PremiumListDatastoreDao.save(name, inputData);
suppressExceptionUnlessInTest(
() -> PremiumListSqlDao.save(name, inputData), "Error when saving premium list to SQL.");
} else {
result = PremiumListSqlDao.save(name, inputData);
suppressExceptionUnlessInTest(
() -> PremiumListDatastoreDao.save(name, inputData),
"Error when saving premium list to Datastore.");
}
return result;
}
/**
* Deletes the premium list.
*
* <p>Logs but doesn't throw an exception in the event of a failure when deleting from the
* secondary database.
*/
public static void delete(PremiumList premiumList) {
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
PremiumListDatastoreDao.delete(premiumList);
suppressExceptionUnlessInTest(
() -> PremiumListSqlDao.delete(premiumList),
"Error when deleting premium list from SQL.");
} else {
PremiumListSqlDao.delete(premiumList);
suppressExceptionUnlessInTest(
() -> PremiumListDatastoreDao.delete(premiumList),
"Error when deleting premium list from Datastore.");
}
}
/** Returns whether or not there exists a premium list with the given name. */
public static boolean exists(String premiumListName) {
// It may seem like overkill, but loading the list has ways been the way we check existence and
// given that we usually load the list around the time we check existence, we'll hit the cache
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
return PremiumListDatastoreDao.getLatestRevision(premiumListName).isPresent();
} else {
return PremiumListSqlDao.getLatestRevision(premiumListName).isPresent();
}
}
/**
* Returns all {@link PremiumListEntry PremiumListEntries} in the list with the given name.
*
* <p>This is an expensive operation and should only be used when the entire list is required.
*/
public static Iterable<PremiumListEntry> loadAllPremiumListEntries(String premiumListName) {
PremiumList premiumList =
getLatestRevision(premiumListName)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("No premium list with name %s.", premiumListName)));
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
if (tm().isOfy()) {
return PremiumListDatastoreDao.loadPremiumListEntriesUncached(premiumList);
} else {
CurrencyUnit currencyUnit = premiumList.getCurrency();
return Streams.stream(PremiumListSqlDao.loadPremiumListEntriesUncached(premiumList))
.map(
premiumEntry ->
new PremiumListEntry.Builder()
.setPrice(Money.of(currencyUnit, premiumEntry.getPrice()))
.setLabel(premiumEntry.getDomainLabel())
.build())
.collect(toImmutableList());
}
}
private PremiumListDualDao() {}
}

View file

@ -1,255 +0,0 @@
// Copyright 2017 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.model.registry.label;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.partition;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.BLOOM_FILTER_NEGATIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.CACHED_NEGATIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.CACHED_POSITIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.UNCACHED_NEGATIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.UNCACHED_POSITIVE;
import static google.registry.model.registry.label.PremiumList.cachePremiumListEntries;
import static google.registry.model.registry.label.PremiumList.cachePremiumListRevisions;
import static google.registry.model.registry.label.PremiumList.cachePremiumLists;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
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;
import java.util.concurrent.ExecutionException;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** Static helper methods for working with {@link PremiumList}s. */
public final class PremiumListUtils {
/** The number of premium list entry entities that are created and deleted per batch. */
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
abstract static class CheckResults {
static CheckResults create(PremiumListCheckOutcome checkOutcome, Optional<Money> premiumPrice) {
return new AutoValue_PremiumListUtils_CheckResults(checkOutcome, premiumPrice);
}
abstract PremiumListCheckOutcome checkOutcome();
abstract Optional<Money> premiumPrice();
}
/**
* 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();
}
DateTime startTime = DateTime.now(UTC);
String listName = registry.getPremiumList().getName();
Optional<PremiumList> optionalPremiumList = PremiumList.getCached(listName);
checkState(optionalPremiumList.isPresent(), "Could not load premium list '%s'", listName);
PremiumList premiumList = optionalPremiumList.get();
PremiumListRevision revision;
try {
revision = cachePremiumListRevisions.get(premiumList.getRevisionKey());
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException(
"Could not load premium list revision " + premiumList.getRevisionKey(), e);
}
checkState(
revision.getProbablePremiumLabels() != null,
"Probable premium labels Bloom filter is null on revision '%s'",
premiumList.getRevisionKey());
CheckResults checkResults = checkStatus(revision, label);
DomainLabelMetrics.recordPremiumListCheckOutcome(
registry.getTldStr(),
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();
}
private static CheckResults checkStatus(PremiumListRevision premiumListRevision, String label) {
if (!premiumListRevision.getProbablePremiumLabels().mightContain(label)) {
return CheckResults.create(BLOOM_FILTER_NEGATIVE, Optional.empty());
}
Key<PremiumListEntry> entryKey =
Key.create(Key.create(premiumListRevision), PremiumListEntry.class, label);
try {
// getIfPresent() returns null if the key is not in the cache
Optional<PremiumListEntry> entry = cachePremiumListEntries.getIfPresent(entryKey);
if (entry != null) {
if (entry.isPresent()) {
return CheckResults.create(CACHED_POSITIVE, Optional.of(entry.get().getValue()));
} else {
return CheckResults.create(CACHED_NEGATIVE, Optional.empty());
}
}
entry = cachePremiumListEntries.get(entryKey);
if (entry.isPresent()) {
return CheckResults.create(UNCACHED_POSITIVE, Optional.of(entry.get().getValue()));
} else {
return CheckResults.create(UNCACHED_NEGATIVE, Optional.empty());
}
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException("Could not load premium list entry " + entryKey, e);
}
}
/**
* Persists a new or updated PremiumList object and its descendant entities to Datastore.
*
* <p>The flow here is: save the new premium list entries parented on that revision entity,
* save/update the PremiumList, and then delete the old premium list entries associated with the
* old revision.
*
* <p>This is the only valid way to save these kinds of entities!
*/
public static PremiumList savePremiumListAndEntries(
final PremiumList premiumList,
ImmutableMap<String, PremiumListEntry> premiumListEntries) {
final Optional<PremiumList> oldPremiumList = PremiumList.getUncached(premiumList.getName());
// Create the new revision (with its Bloom filter) and parent the entries on it.
final PremiumListRevision newRevision =
PremiumListRevision.create(premiumList, premiumListEntries.keySet());
final Key<PremiumListRevision> newRevisionKey = Key.create(newRevision);
ImmutableSet<PremiumListEntry> parentedEntries =
parentPremiumListEntriesOnRevision(premiumListEntries.values(), newRevisionKey);
// Save the new child entities in a series of transactions.
for (final List<PremiumListEntry> batch : partition(parentedEntries, TRANSACTION_BATCH_SIZE)) {
tm().transactNew(() -> ofy().save().entities(batch));
}
// Save the new PremiumList and revision itself.
PremiumList updated = tm().transactNew(() -> {
DateTime now = tm().getTransactionTime();
// Assert that the premium list hasn't been changed since we started this process.
PremiumList existing = ofy().load()
.type(PremiumList.class)
.parent(getCrossTldKey())
.id(premiumList.getName())
.now();
checkState(
Objects.equals(existing, oldPremiumList.orElse(null)),
"PremiumList was concurrently edited");
PremiumList newList = premiumList.asBuilder()
.setLastUpdateTime(now)
.setCreationTime(oldPremiumList.isPresent() ? oldPremiumList.get().creationTime : now)
.setRevision(newRevisionKey)
.build();
ofy().save().entities(newList, newRevision);
return newList;
});
// Invalidate the cache on this premium list so the change will take effect instantly. This only
// clears the cache on the same instance that the update was run on, which will typically be the
// only tools instance.
PremiumList.cachePremiumLists.invalidate(premiumList.getName());
// TODO(b/79888775): Enqueue the oldPremiumList for deletion after at least
// RegistryConfig.getDomainLabelListCacheDuration() has elapsed.
return updated;
}
public static PremiumList savePremiumListAndEntries(
PremiumList premiumList, Iterable<String> premiumListLines) {
return savePremiumListAndEntries(premiumList, premiumList.parse(premiumListLines));
}
/** Re-parents the given {@link PremiumListEntry}s on the given {@link PremiumListRevision}. */
@VisibleForTesting
public static ImmutableSet<PremiumListEntry> parentPremiumListEntriesOnRevision(
Iterable<PremiumListEntry> entries, final Key<PremiumListRevision> revisionKey) {
return Streams.stream(entries)
.map((PremiumListEntry entry) -> entry.asBuilder().setParent(revisionKey).build())
.collect(toImmutableSet());
}
/** Deletes the PremiumList and all of its child entities. */
public static void deletePremiumList(final PremiumList premiumList) {
tm().transactNew(() -> ofy().delete().entity(premiumList));
deleteRevisionAndEntriesOfPremiumList(premiumList);
cachePremiumLists.invalidate(premiumList.getName());
}
static void deleteRevisionAndEntriesOfPremiumList(final PremiumList premiumList) {
if (premiumList.getRevisionKey() == null) {
return;
}
for (final List<Key<PremiumListEntry>> batch :
partition(
ofy().load().type(PremiumListEntry.class).ancestor(premiumList.revisionKey).keys(),
TRANSACTION_BATCH_SIZE)) {
tm().transactNew(() -> ofy().delete().keys(batch));
}
tm().transactNew(() -> ofy().delete().key(premiumList.getRevisionKey()));
}
/**
* Returns all {@link PremiumListEntry PremiumListEntries} in the given {@code premiumList}.
*
* <p>This is an expensive operation and should only be used when the entire list is required.
*/
public static Iterable<PremiumListEntry> loadPremiumListEntries(PremiumList premiumList) {
return ofy().load().type(PremiumListEntry.class).ancestor(premiumList.revisionKey).iterable();
}
/** Returns whether a PremiumList of the given name exists, bypassing the cache. */
public static boolean doesPremiumListExist(String name) {
return ofy().load().key(Key.create(getCrossTldKey(), PremiumList.class, name)).now() != null;
}
private PremiumListUtils() {}
}

View file

@ -50,4 +50,19 @@ public class PremiumEntry extends ImmutableObject implements Serializable, SqlEn
public Optional<DatastoreEntity> toDatastoreEntity() { public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // PremiumList is dually-written return Optional.empty(); // PremiumList is dually-written
} }
public BigDecimal getPrice() {
return price;
}
public String getDomainLabel() {
return domainLabel;
}
public static PremiumEntry create(BigDecimal price, String domainLabel) {
PremiumEntry result = new PremiumEntry();
result.price = price;
result.domainLabel = domainLabel;
return result;
}
} }

View file

@ -1,106 +0,0 @@
// 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 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.model.registry.label.PremiumList;
import google.registry.util.NonFinalForTesting;
import java.math.BigDecimal;
import java.util.Optional;
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(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.build(
new CacheLoader<String, Optional<PremiumList>>() {
@Override
public Optional<PremiumList> load(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(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.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

@ -1,175 +0,0 @@
// 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 com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.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.model.registry.label.PremiumList;
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()
.transact(
() -> {
checkArgument(
!checkExists(premiumList.getName()),
"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(
() -> {
// This check is currently disabled because, during the Cloud SQL migration, we need
// to be able to update premium lists in Datastore while simultaneously creating their
// first revision in Cloud SQL (i.e. if they haven't been migrated over yet).
// TODO(b/147246613): Reinstate this once all premium lists are migrated to Cloud SQL,
// and re-enable the test update_throwsWhenListDoesntExist().
// 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}.
*/
public 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.
*/
static boolean checkExists(String premiumListName) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery("SELECT 1 FROM PremiumList WHERE name = :name", Integer.class)
.setParameter("name", premiumListName)
.setMaxResults(1)
.getResultList()
.size()
> 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.setScale(premiumList.getCurrency().getDecimalPlaces())));
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException(
String.format(
"Could not load premium entry %s for list %s",
revisionIdAndLabel, premiumList.getName()),
e);
}
}
private PremiumListDao() {}
}

View file

@ -0,0 +1,238 @@
// 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.persistence.transaction.TransactionManagerFactory.jpaTm;
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.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.util.NonFinalForTesting;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.joda.money.Money;
import org.joda.time.Duration;
/**
* Data access object class for accessing {@link PremiumList} objects from Cloud SQL.
*
* <p>This class handles both the mapping from string to SQL-level PremiumList objects as well as
* the mapping and retrieval of {@link PremiumEntry} objects that correspond to the particular
* {@link PremiumList} object in SQL, and caching these entries so that future lookups can be
* quicker.
*/
public class PremiumListSqlDao {
/**
* In-memory cache for premium lists.
*
* <p>This is cached for a shorter duration because we need to periodically reload this entity to
* check if a new revision has been published, and if so, then use that.
*
* <p>We also cache the absence of premium lists with a given name to avoid unnecessary pointless
* lookups. Note that this cache is only applicable to PremiumList objects stored in SQL.
*/
@NonFinalForTesting
static LoadingCache<String, Optional<PremiumList>> premiumListCache =
createPremiumListCache(getDomainLabelListCacheDuration());
@VisibleForTesting
public static void setPremiumListCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getDomainLabelListCacheDuration());
premiumListCache = createPremiumListCache(effectiveExpiry);
}
@VisibleForTesting
public static LoadingCache<String, Optional<PremiumList>> createPremiumListCache(
Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.build(
new CacheLoader<String, Optional<PremiumList>>() {
@Override
public Optional<PremiumList> load(final String name) {
return jpaTm().doTransactionless(() -> getLatestRevisionUncached(name));
}
});
}
/**
* 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>> premiumEntryCache =
createPremiumEntryCache(getSingletonCachePersistDuration());
@VisibleForTesting
static LoadingCache<RevisionIdAndLabel, Optional<BigDecimal>> createPremiumEntryCache(
Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(java.time.Duration.ofMillis(cachePersistDuration.getMillis()))
.maximumSize(getStaticPremiumListMaxCachedEntries())
.build(
new CacheLoader<RevisionIdAndLabel, Optional<BigDecimal>>() {
@Override
public Optional<BigDecimal> load(RevisionIdAndLabel revisionIdAndLabel) {
return getPriceForLabelUncached(revisionIdAndLabel);
}
});
}
/**
* 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}.
*/
public static Optional<PremiumList> getLatestRevision(String premiumListName) {
return premiumListCache.getUnchecked(premiumListName);
}
/**
* Returns the premium price for the specified label and registry, or absent if the label is not
* premium.
*/
public static Optional<Money> getPremiumPrice(String premiumListName, String label) {
Optional<PremiumList> maybeLoadedList = getLatestRevision(premiumListName);
if (!maybeLoadedList.isPresent()) {
return Optional.empty();
}
PremiumList loadedList = maybeLoadedList.get();
// Consult the bloom filter and immediately return if the label definitely isn't premium.
if (!loadedList.getBloomFilter().mightContain(label)) {
return Optional.empty();
}
RevisionIdAndLabel revisionIdAndLabel =
RevisionIdAndLabel.create(loadedList.getRevisionId(), label);
try {
Optional<BigDecimal> price = premiumEntryCache.get(revisionIdAndLabel);
return price.map(
p ->
Money.of(
loadedList.getCurrency(),
p.setScale(loadedList.getCurrency().getDecimalPlaces())));
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException(
String.format(
"Could not load premium entry %s for list %s",
revisionIdAndLabel, loadedList.getName()),
e);
}
}
public static PremiumList save(String name, List<String> inputData) {
return save(PremiumListUtils.parseToPremiumList(name, inputData));
}
public static PremiumList save(PremiumList premiumList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(premiumList));
premiumListCache.invalidate(premiumList.getName());
return premiumList;
}
public static void delete(PremiumList premiumList) {
jpaTm().transact(() -> getLatestRevision(premiumList.getName()).ifPresent(jpaTm()::delete));
premiumListCache.invalidate(premiumList.getName());
}
private static Optional<PremiumList> getLatestRevisionUncached(String premiumListName) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"FROM PremiumList WHERE name = :name ORDER BY revisionId DESC",
PremiumList.class)
.setParameter("name", premiumListName)
.setMaxResults(1)
.getResultStream()
.findFirst());
}
/**
* Returns all {@link PremiumListEntry PremiumListEntries} in the given {@code premiumList}.
*
* <p>This is an expensive operation and should only be used when the entire list is required.
*/
public static Iterable<PremiumEntry> loadPremiumListEntriesUncached(PremiumList premiumList) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"FROM PremiumEntry pe WHERE pe.revisionId = :revisionId",
PremiumEntry.class)
.setParameter("revisionId", premiumList.getRevisionId())
.getResultList());
}
/**
* Loads the price for the given revisionId + label combination. Note that this does a database
* retrieval so it should only be done in a cached context.
*/
static Optional<BigDecimal> getPriceForLabelUncached(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());
}
@AutoValue
abstract static class RevisionIdAndLabel {
abstract long revisionId();
abstract String label();
static RevisionIdAndLabel create(long revisionId, String label) {
return new AutoValue_PremiumListSqlDao_RevisionIdAndLabel(revisionId, label);
}
}
private PremiumListSqlDao() {}
}

View file

@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import com.google.common.base.Splitter;
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.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
@ -35,12 +34,9 @@ import org.joda.time.DateTime;
/** Static utility methods for {@link PremiumList}. */ /** Static utility methods for {@link PremiumList}. */
public class PremiumListUtils { public class PremiumListUtils {
public static PremiumList parseToPremiumList(String name, String inputData) { public static PremiumList parseToPremiumList(String name, List<String> inputData) {
List<String> inputDataPreProcessed =
Splitter.on('\n').omitEmptyStrings().splitToList(inputData);
ImmutableMap<String, PremiumListEntry> prices = ImmutableMap<String, PremiumListEntry> prices =
new PremiumList.Builder().setName(name).build().parse(inputDataPreProcessed); new PremiumList.Builder().setName(name).build().parse(inputData);
ImmutableSet<CurrencyUnit> currencies = ImmutableSet<CurrencyUnit> currencies =
prices.values().stream() prices.values().stream()
.map(e -> e.getValue().getCurrencyUnit()) .map(e -> e.getValue().getCurrencyUnit())

View file

@ -31,6 +31,7 @@ import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState; import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.Registry.TldType; import google.registry.model.registry.Registry.TldType;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.tools.params.OptionalStringParameter; import google.registry.tools.params.OptionalStringParameter;
import google.registry.tools.params.TransitionListParameter.BillingCostTransitions; import google.registry.tools.params.TransitionListParameter.BillingCostTransitions;
import google.registry.tools.params.TransitionListParameter.TldStateTransitions; import google.registry.tools.params.TransitionListParameter.TldStateTransitions;
@ -342,7 +343,8 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
if (premiumListName != null) { if (premiumListName != null) {
if (premiumListName.isPresent()) { if (premiumListName.isPresent()) {
Optional<PremiumList> premiumList = PremiumList.getUncached(premiumListName.get()); Optional<PremiumList> premiumList =
PremiumListDualDao.getLatestRevision(premiumListName.get());
checkArgument( checkArgument(
premiumList.isPresent(), premiumList.isPresent(),
String.format("The premium list '%s' doesn't exist", premiumListName.get())); String.format("The premium list '%s' doesn't exist", premiumListName.get()));

View file

@ -15,25 +15,23 @@
package google.registry.tools; package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList;
import static google.registry.model.registry.label.PremiumListUtils.doesPremiumListExist;
import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters; import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
* Command to delete a {@link PremiumList} in Datastore. This command will fail if the premium * Command to delete a {@link PremiumList} in Datastore. This command will fail if the premium list
* list is currently in use on a tld. * is currently in use on a tld.
*/ */
@Parameters(separators = " =", commandDescription = "Delete a PremiumList from Datastore.") @Parameters(separators = " =", commandDescription = "Delete a PremiumList from Datastore.")
final class DeletePremiumListCommand extends ConfirmingCommand implements CommandWithRemoteApi { final class DeletePremiumListCommand extends ConfirmingCommand implements CommandWithRemoteApi {
@Nullable @Nullable PremiumList premiumList;
PremiumList premiumList;
@Parameter( @Parameter(
names = {"-n", "--name"}, names = {"-n", "--name"},
@ -44,10 +42,10 @@ final class DeletePremiumListCommand extends ConfirmingCommand implements Comman
@Override @Override
protected void init() { protected void init() {
checkArgument( checkArgument(
doesPremiumListExist(name), PremiumListDualDao.exists(name),
"Cannot delete the premium list %s because it doesn't exist.", "Cannot delete the premium list %s because it doesn't exist.",
name); name);
premiumList = PremiumList.getUncached(name).get(); premiumList = PremiumListDualDao.getLatestRevision(name).get();
ImmutableSet<String> tldsUsedOn = premiumList.getReferencingTlds(); ImmutableSet<String> tldsUsedOn = premiumList.getReferencingTlds();
checkArgument( checkArgument(
tldsUsedOn.isEmpty(), tldsUsedOn.isEmpty(),
@ -62,7 +60,7 @@ final class DeletePremiumListCommand extends ConfirmingCommand implements Comman
@Override @Override
protected String execute() { protected String execute() {
deletePremiumList(premiumList); PremiumListDualDao.delete(premiumList);
return String.format("Deleted premium list '%s'.\n", premiumList.getName()); return String.format("Deleted premium list '%s'.\n", premiumList.getName());
} }
} }

View file

@ -45,25 +45,15 @@ public abstract class CreateOrUpdatePremiumListAction implements Runnable {
@Override @Override
public void run() { public void run() {
try { try {
saveToDatastore(); save();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.atInfo().withCause(e).log( logger.atInfo().withCause(e).log(
"Usage error in attempting to save premium list from nomulus tool command"); "Usage error in attempting to save premium list from nomulus tool command");
response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error")); response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error"));
return;
} catch (Exception e) { } catch (Exception e) {
logger.atSevere().withCause(e).log( logger.atSevere().withCause(e).log(
"Unexpected error saving premium list to Datastore from nomulus tool command"); "Unexpected error saving premium list to Datastore from nomulus tool command");
response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error")); response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error"));
return;
}
try {
saveToCloudSql();
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Unexpected error saving premium list to Cloud SQL from nomulus tool command");
response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error"));
} }
} }
@ -78,9 +68,6 @@ public abstract class CreateOrUpdatePremiumListAction implements Runnable {
: (inputData.substring(0, MAX_LOGGING_PREMIUM_LIST_LENGTH) + "<truncated>"))); : (inputData.substring(0, MAX_LOGGING_PREMIUM_LIST_LENGTH) + "<truncated>")));
} }
/** Saves the premium list to Datastore. */ /** Saves the premium list to both Datastore and Cloud SQL. */
protected abstract void saveToDatastore(); protected abstract void save();
/** Saves the premium list to Cloud SQL. */
protected abstract void saveToCloudSql();
} }

View file

@ -16,19 +16,15 @@ package google.registry.tools.server;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.registry.Registries.assertTldExists; import static google.registry.model.registry.Registries.assertTldExists;
import static google.registry.model.registry.label.PremiumListUtils.doesPremiumListExist;
import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries;
import static google.registry.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static google.registry.schema.tld.PremiumListUtils.parseToPremiumList;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.request.Action; import google.registry.request.Action;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import google.registry.request.auth.Auth; import google.registry.request.auth.Auth;
import google.registry.schema.tld.PremiumListDao;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -49,46 +45,24 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction {
public static final String PATH = "/_dr/admin/createPremiumList"; public static final String PATH = "/_dr/admin/createPremiumList";
@Inject @Parameter(OVERRIDE_PARAM) boolean override; @Inject @Parameter(OVERRIDE_PARAM) boolean override;
@Inject CreatePremiumListAction() {} @Inject CreatePremiumListAction() {}
@Override @Override
protected void saveToDatastore() { protected void save() {
checkArgument( checkArgument(
!doesPremiumListExist(name), "A premium list of this name already exists: %s.", name); !PremiumListDualDao.exists(name), "A premium list of this name already exists: %s.", name);
if (!override) { if (!override) {
assertTldExists(name); assertTldExists(name);
} }
logger.atInfo().log("Saving premium list for TLD %s", name); logger.atInfo().log("Saving premium list for TLD %s", name);
logInputData(); logInputData();
List<String> inputDataPreProcessed = List<String> inputDataPreProcessed =
Splitter.on('\n').omitEmptyStrings().splitToList(inputData); Splitter.on('\n').omitEmptyStrings().splitToList(inputData);
PremiumList premiumList = new PremiumList.Builder().setName(name).build(); PremiumListDualDao.save(name, inputDataPreProcessed);
savePremiumListAndEntries(premiumList, inputDataPreProcessed);
String message = String message =
String.format( String.format("Saved premium list %s with %d entries", name, inputDataPreProcessed.size());
"Saved premium list %s with %d entries",
premiumList.getName(), inputDataPreProcessed.size());
logger.atInfo().log(message); logger.atInfo().log(message);
response.setPayload(ImmutableMap.of("status", "success", "message", message)); response.setPayload(ImmutableMap.of("status", "success", "message", message));
} }
@Override
protected void saveToCloudSql() {
if (!override) {
assertTldExists(name);
}
logger.atInfo().log("Saving premium list to Cloud SQL for TLD %s", name);
// TODO(mcilwain): Call logInputData() here once Datastore persistence is removed.
PremiumList premiumList = parseToPremiumList(name, inputData);
PremiumListDao.saveNew(premiumList);
String message =
String.format(
"Saved premium list %s with %d entries", name, premiumList.getLabelsToPrices().size());
logger.atInfo().log(message);
// TODO(mcilwain): Call response.setPayload(...) here once Datastore persistence is removed.
}
} }

View file

@ -15,19 +15,16 @@
package google.registry.tools.server; package google.registry.tools.server;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries;
import static google.registry.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static google.registry.schema.tld.PremiumListUtils.parseToPremiumList;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.request.Action; import google.registry.request.Action;
import google.registry.request.auth.Auth; import google.registry.request.auth.Auth;
import google.registry.schema.tld.PremiumListDao;
import java.util.List; import java.util.List;
import java.util.Optional;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
@ -48,10 +45,9 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction {
@Inject UpdatePremiumListAction() {} @Inject UpdatePremiumListAction() {}
@Override @Override
protected void saveToDatastore() { protected void save() {
Optional<PremiumList> existingPremiumList = PremiumList.getUncached(name);
checkArgument( checkArgument(
existingPremiumList.isPresent(), PremiumListDualDao.exists(name),
"Could not update premium list %s because it doesn't exist.", "Could not update premium list %s because it doesn't exist.",
name); name);
@ -59,8 +55,7 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction {
logInputData(); logInputData();
List<String> inputDataPreProcessed = List<String> inputDataPreProcessed =
Splitter.on('\n').omitEmptyStrings().splitToList(inputData); Splitter.on('\n').omitEmptyStrings().splitToList(inputData);
PremiumList newPremiumList = PremiumList newPremiumList = PremiumListDualDao.save(name, inputDataPreProcessed);
savePremiumListAndEntries(existingPremiumList.get(), inputDataPreProcessed);
String message = String message =
String.format( String.format(
@ -69,18 +64,4 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction {
logger.atInfo().log(message); logger.atInfo().log(message);
response.setPayload(ImmutableMap.of("status", "success", "message", message)); response.setPayload(ImmutableMap.of("status", "success", "message", message));
} }
@Override
protected void saveToCloudSql() {
logger.atInfo().log("Updating premium list '%s' in Cloud SQL.", name);
// TODO(mcilwain): Add logInputData() call here once DB migration is complete.
PremiumList premiumList = parseToPremiumList(name, inputData);
PremiumListDao.update(premiumList);
String message =
String.format(
"Updated premium list '%s' with %d entries.",
premiumList.getName(), premiumList.getLabelsToPrices().size());
logger.atInfo().log(message);
// TODO(mcilwain): Call response.setPayload() here once DB migration is complete.
}
} }

View file

@ -53,6 +53,7 @@ import google.registry.model.ofy.CommitLogBucket;
import google.registry.model.ofy.CommitLogManifest; import google.registry.model.ofy.CommitLogManifest;
import google.registry.model.ofy.CommitLogMutation; import google.registry.model.ofy.CommitLogMutation;
import google.registry.model.registrar.RegistrarContact; import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.ReservedList; import google.registry.model.registry.label.ReservedList;
import google.registry.model.server.Lock; import google.registry.model.server.Lock;
import google.registry.model.tmch.ClaimsListShard; import google.registry.model.tmch.ClaimsListShard;
@ -93,13 +94,14 @@ public class ReplayCommitLogsToSqlActionTest {
.withClock(fakeClock) .withClock(fakeClock)
.withOfyTestEntities(TestObject.class) .withOfyTestEntities(TestObject.class)
.withJpaUnitTestEntities( .withJpaUnitTestEntities(
RegistrarContact.class,
TestObject.class,
SqlReplayCheckpoint.class,
ContactResource.class, ContactResource.class,
DelegationSignerData.class,
DomainBase.class, DomainBase.class,
GracePeriod.class, GracePeriod.class,
DelegationSignerData.class) PremiumList.class,
RegistrarContact.class,
SqlReplayCheckpoint.class,
TestObject.class)
.build(); .build();
/** Local GCS service. */ /** Local GCS service. */

View file

@ -34,6 +34,9 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.Ofy; import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension; import google.registry.testing.InjectExtension;
import google.registry.tools.LevelDbLogReader; import google.registry.tools.LevelDbLogReader;
@ -48,6 +51,7 @@ import org.apache.beam.sdk.values.KV;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@ -58,6 +62,15 @@ public class BackupTestStoreTest {
@TempDir File tempDir; @TempDir File tempDir;
@RegisterExtension
final transient JpaIntegrationTestExtension jpaIntegrationTestExtension =
new JpaTestRules.Builder().buildIntegrationTestRule();
@RegisterExtension
@Order(value = 1)
final transient DatastoreEntityExtension datastoreEntityExtension =
new DatastoreEntityExtension();
@RegisterExtension InjectExtension injectRule = new InjectExtension(); @RegisterExtension InjectExtension injectRule = new InjectExtension();
private FakeClock fakeClock; private FakeClock fakeClock;

View file

@ -26,6 +26,9 @@ import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.Ofy; import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension; import google.registry.testing.InjectExtension;
import java.io.File; import java.io.File;
@ -44,6 +47,7 @@ import org.apache.beam.sdk.values.PCollection;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@ -52,6 +56,7 @@ import org.junit.jupiter.api.io.TempDir;
class CommitLogTransformsTest implements Serializable { class CommitLogTransformsTest implements Serializable {
private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z"); private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z");
private final FakeClock fakeClock = new FakeClock(START_TIME);
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@TempDir @TempDir
@ -59,11 +64,19 @@ class CommitLogTransformsTest implements Serializable {
@RegisterExtension final transient InjectExtension injectRule = new InjectExtension(); @RegisterExtension final transient InjectExtension injectRule = new InjectExtension();
@RegisterExtension
final transient JpaIntegrationTestExtension jpaIntegrationTestExtension =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationTestRule();
@RegisterExtension
@Order(value = 1)
final transient DatastoreEntityExtension datastoreEntityExtension =
new DatastoreEntityExtension();
@RegisterExtension @RegisterExtension
final transient TestPipelineExtension testPipeline = final transient TestPipelineExtension testPipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true); TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
private FakeClock fakeClock;
private transient BackupTestStore store; private transient BackupTestStore store;
private File commitLogsDir; private File commitLogsDir;
private File firstCommitLogFile; private File firstCommitLogFile;
@ -75,7 +88,6 @@ class CommitLogTransformsTest implements Serializable {
@BeforeEach @BeforeEach
void beforeEach() throws Exception { void beforeEach() throws Exception {
fakeClock = new FakeClock(START_TIME);
store = new BackupTestStore(fakeClock); store = new BackupTestStore(fakeClock);
injectRule.setStaticField(Ofy.class, "clock", fakeClock); injectRule.setStaticField(Ofy.class, "clock", fakeClock);

View file

@ -68,7 +68,7 @@ public class DomainBaseUtilTest {
@RegisterExtension @RegisterExtension
AppEngineExtension appEngineRule = AppEngineExtension appEngineRule =
AppEngineExtension.builder().withDatastore().withClock(fakeClock).build(); AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
@RegisterExtension InjectExtension injectRule = new InjectExtension(); @RegisterExtension InjectExtension injectRule = new InjectExtension();

View file

@ -27,6 +27,9 @@ import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.Ofy; import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension; import google.registry.testing.InjectExtension;
import java.io.File; import java.io.File;
@ -44,6 +47,7 @@ import org.apache.beam.sdk.values.PCollection;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@ -53,7 +57,7 @@ import org.junit.jupiter.api.io.TempDir;
* *
* <p>This class implements {@link Serializable} so that test {@link DoFn} classes may be inlined. * <p>This class implements {@link Serializable} so that test {@link DoFn} classes may be inlined.
*/ */
class ExportloadingTransformsTest implements Serializable { class ExportLoadingTransformsTest implements Serializable {
private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z"); private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z");
@ -68,6 +72,15 @@ class ExportloadingTransformsTest implements Serializable {
@RegisterExtension final transient InjectExtension injectRule = new InjectExtension(); @RegisterExtension final transient InjectExtension injectRule = new InjectExtension();
@RegisterExtension
final transient JpaIntegrationTestExtension jpaIntegrationTestExtension =
new JpaTestRules.Builder().buildIntegrationTestRule();
@RegisterExtension
@Order(value = 1)
final transient DatastoreEntityExtension datastoreEntityExtension =
new DatastoreEntityExtension();
@RegisterExtension @RegisterExtension
final transient TestPipelineExtension testPipeline = final transient TestPipelineExtension testPipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true); TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);

View file

@ -29,6 +29,9 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.ofy.Ofy; import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension; import google.registry.testing.InjectExtension;
import java.io.File; import java.io.File;
@ -38,6 +41,7 @@ import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollectionTuple; import org.apache.beam.sdk.values.PCollectionTuple;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@ -84,6 +88,15 @@ class LoadDatastoreSnapshotTest {
@RegisterExtension final transient InjectExtension injectRule = new InjectExtension(); @RegisterExtension final transient InjectExtension injectRule = new InjectExtension();
@RegisterExtension
final transient JpaIntegrationTestExtension jpaIntegrationTestExtension =
new JpaTestRules.Builder().buildIntegrationTestRule();
@RegisterExtension
@Order(value = 1)
final transient DatastoreEntityExtension datastoreEntityExtension =
new DatastoreEntityExtension();
@RegisterExtension @RegisterExtension
final transient TestPipelineExtension testPipeline = final transient TestPipelineExtension testPipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true); TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);

View file

@ -17,8 +17,6 @@ package google.registry.export;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.export.ExportPremiumTermsAction.EXPORT_MIME_TYPE; import static google.registry.export.ExportPremiumTermsAction.EXPORT_MIME_TYPE;
import static google.registry.export.ExportPremiumTermsAction.PREMIUM_TERMS_FILENAME; import static google.registry.export.ExportPremiumTermsAction.PREMIUM_TERMS_FILENAME;
import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList;
import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.deleteTld; import static google.registry.testing.DatabaseHelper.deleteTld;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
@ -39,6 +37,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.net.MediaType; import com.google.common.net.MediaType;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.request.Response; import google.registry.request.Response;
import google.registry.storage.drive.DriveConnection; import google.registry.storage.drive.DriveConnection;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
@ -76,8 +75,7 @@ public class ExportPremiumTermsActionTest {
@BeforeEach @BeforeEach
void beforeEach() throws Exception { void beforeEach() throws Exception {
createTld("tld"); createTld("tld");
PremiumList pl = new PremiumList.Builder().setName("pl-name").build(); PremiumList pl = PremiumListDualDao.save("pl-name", PREMIUM_NAMES);
savePremiumListAndEntries(pl, PREMIUM_NAMES);
persistResource( persistResource(
Registry.get("tld").asBuilder().setPremiumList(pl).setDriveFolderId("folder_id").build()); Registry.get("tld").asBuilder().setPremiumList(pl).setDriveFolderId("folder_id").build());
when(driveConnection.createOrUpdateFile( when(driveConnection.createOrUpdateFile(
@ -106,26 +104,6 @@ public class ExportPremiumTermsActionTest {
verifyNoMoreInteractions(response); verifyNoMoreInteractions(response);
} }
@Test
void test_exportPremiumTerms_success_emptyPremiumList() throws IOException {
PremiumList pl = new PremiumList.Builder().setName("pl-name").build();
savePremiumListAndEntries(pl, ImmutableList.of());
runAction("tld");
verify(driveConnection)
.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
EXPORT_MIME_TYPE,
"folder_id",
DISCLAIMER_WITH_NEWLINE.getBytes(UTF_8));
verifyNoMoreInteractions(driveConnection);
verify(response).setStatus(SC_OK);
verify(response).setPayload("file_id");
verify(response).setContentType(PLAIN_TEXT_UTF_8);
verifyNoMoreInteractions(response);
}
@Test @Test
void test_exportPremiumTerms_doNothing_listNotConfigured() { void test_exportPremiumTerms_doNothing_listNotConfigured() {
persistResource(Registry.get("tld").asBuilder().setPremiumList(null).build()); persistResource(Registry.get("tld").asBuilder().setPremiumList(null).build());
@ -165,7 +143,7 @@ public class ExportPremiumTermsActionTest {
@Test @Test
void test_exportPremiumTerms_failure_noPremiumList() { void test_exportPremiumTerms_failure_noPremiumList() {
deletePremiumList(new PremiumList.Builder().setName("pl-name").build()); PremiumListDualDao.delete(new PremiumList.Builder().setName("pl-name").build());
assertThrows(RuntimeException.class, () -> runAction("tld")); assertThrows(RuntimeException.class, () -> runAction("tld"));
verifyNoInteractions(driveConnection); verifyNoInteractions(driveConnection);

View file

@ -41,6 +41,7 @@ import google.registry.model.EntityTestCase;
import google.registry.model.registry.Registry.RegistryNotFoundException; import google.registry.model.registry.Registry.RegistryNotFoundException;
import google.registry.model.registry.Registry.TldState; import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.model.registry.label.ReservedList; import google.registry.model.registry.label.ReservedList;
import google.registry.testing.DualDatabaseTest; import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql; import google.registry.testing.TestOfyAndSql;
@ -204,7 +205,7 @@ public class RegistryTest extends EntityTestCase {
Registry registry = Registry.get("tld").asBuilder().setPremiumList(pl2).build(); Registry registry = Registry.get("tld").asBuilder().setPremiumList(pl2).build();
Key<PremiumList> plKey = registry.getPremiumList(); Key<PremiumList> plKey = registry.getPremiumList();
assertThat(plKey).isNotNull(); assertThat(plKey).isNotNull();
PremiumList stored = PremiumList.getUncached(plKey.getName()).get(); PremiumList stored = PremiumListDualDao.getLatestRevision(plKey.getName()).get();
assertThat(stored.getName()).isEqualTo("tld2"); assertThat(stored.getName()).isEqualTo("tld2");
} }

View file

@ -14,6 +14,7 @@
package google.registry.model.registry.label; package google.registry.model.registry.label;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
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 com.google.common.truth.Truth8.assertThat;
import static com.google.monitoring.metrics.contrib.DistributionMetricSubject.assertThat; import static com.google.monitoring.metrics.contrib.DistributionMetricSubject.assertThat;
@ -26,37 +27,33 @@ import static google.registry.model.registry.label.DomainLabelMetrics.PremiumLis
import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.UNCACHED_POSITIVE; import static google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome.UNCACHED_POSITIVE;
import static google.registry.model.registry.label.DomainLabelMetrics.premiumListChecks; import static google.registry.model.registry.label.DomainLabelMetrics.premiumListChecks;
import static google.registry.model.registry.label.DomainLabelMetrics.premiumListProcessingTime; import static google.registry.model.registry.label.DomainLabelMetrics.premiumListProcessingTime;
import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList;
import static google.registry.model.registry.label.PremiumListUtils.doesPremiumListExist;
import static google.registry.model.registry.label.PremiumListUtils.getPremiumPrice;
import static google.registry.model.registry.label.PremiumListUtils.savePremiumListAndEntries;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadPremiumListEntries; import static google.registry.testing.DatabaseHelper.loadPremiumListEntries;
import static google.registry.testing.DatabaseHelper.persistPremiumList; import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.time.Duration.standardDays; import static org.joda.time.Duration.standardDays;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.dns.writer.VoidDnsWriter;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
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.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.TestCacheExtension; import google.registry.testing.TestCacheExtension;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.joda.money.Money; import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link PremiumListUtils}. */ /** Unit tests for {@link PremiumListDatastoreDao}. */
public class PremiumListUtilsTest { public class PremiumListDatastoreDaoTest {
@RegisterExtension @RegisterExtension
public final AppEngineExtension appEngine = public final AppEngineExtension appEngine =
@ -70,11 +67,13 @@ public class PremiumListUtilsTest {
.withPremiumListEntriesCache(standardDays(1)) .withPremiumListEntriesCache(standardDays(1))
.build(); .build();
private PremiumList pl;
@BeforeEach @BeforeEach
void before() { void before() {
// createTld() overwrites the premium list, so call it before persisting pl. // createTld() overwrites the premium list, so call it before persisting pl.
createTld("tld"); createTld("tld");
PremiumList pl = pl =
persistPremiumList( persistPremiumList(
"tld", "tld",
"lol,USD 999 # yup", "lol,USD 999 # yup",
@ -98,66 +97,39 @@ public class PremiumListUtilsTest {
.hasNoOtherValues(); .hasNoOtherValues();
} }
@Test
void testGetPremiumPrice_returnsNoPriceWhenNoPremiumListConfigured() {
createTld("ghost");
persistResource(
new Registry.Builder()
.setTldStr("ghost")
.setPremiumPricingEngine(StaticPremiumListPricingEngine.NAME)
.setDnsWriters(ImmutableSet.of(VoidDnsWriter.NAME))
.build());
assertThat(Registry.get("ghost").getPremiumList()).isNull();
assertThat(getPremiumPrice("blah", Registry.get("ghost"))).isEmpty();
assertThat(premiumListChecks).hasNoOtherValues();
assertThat(premiumListProcessingTime).hasNoOtherValues();
}
@Test
void testGetPremiumPrice_throwsExceptionWhenNonExistentPremiumListConfigured() {
deletePremiumList(PremiumList.getUncached("tld").get());
IllegalStateException thrown =
assertThrows(
IllegalStateException.class, () -> getPremiumPrice("blah", Registry.get("tld")));
assertThat(thrown).hasMessageThat().contains("Could not load premium list 'tld'");
}
@Test @Test
void testSave_largeNumberOfEntries_succeeds() { void testSave_largeNumberOfEntries_succeeds() {
PremiumList premiumList = persistHumongousPremiumList("tld", 2500); PremiumList premiumList = persistHumongousPremiumList("tld", 2500);
assertThat(loadPremiumListEntries(premiumList)).hasSize(2500); assertThat(loadPremiumListEntries(premiumList)).hasSize(2500);
assertThat(getPremiumPrice("7", Registry.get("tld"))).hasValue(Money.parse("USD 100")); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "7", "tld"))
.hasValue(Money.parse("USD 100"));
assertMetricOutcomeCount(1, UNCACHED_POSITIVE); assertMetricOutcomeCount(1, UNCACHED_POSITIVE);
} }
@Test @Test
void testSave_updateTime_isUpdatedOnEverySave() { void testSave_updateTime_isUpdatedOnEverySave() {
PremiumList pl = PremiumList pl = PremiumListDatastoreDao.save("tld3", ImmutableList.of("slime,USD 10"));
savePremiumListAndEntries( PremiumList newPl = PremiumListDatastoreDao.save("tld3", ImmutableList.of("mutants,USD 20"));
new PremiumList.Builder().setName("tld3").build(), ImmutableList.of("slime,USD 10"));
PremiumList newPl =
savePremiumListAndEntries(
new PremiumList.Builder().setName(pl.getName()).build(),
ImmutableList.of("mutants,USD 20"));
assertThat(newPl.getLastUpdateTime()).isGreaterThan(pl.getLastUpdateTime()); assertThat(newPl.getLastUpdateTime()).isGreaterThan(pl.getLastUpdateTime());
} }
@Test @Test
void testSave_creationTime_onlyUpdatedOnFirstCreation() { void testSave_creationTime_onlyUpdatedOnFirstCreation() {
PremiumList pl = persistPremiumList("tld3", "sludge,JPY 1000"); PremiumList pl = persistPremiumList("tld3", "sludge,JPY 1000");
PremiumList newPl = savePremiumListAndEntries(pl, ImmutableList.of("sleighbells,CHF 2000")); PremiumList newPl =
PremiumListDatastoreDao.save("tld3", ImmutableList.of("sleighbells,CHF 2000"));
assertThat(newPl.creationTime).isEqualTo(pl.creationTime); assertThat(newPl.creationTime).isEqualTo(pl.creationTime);
} }
@Test @Test
void testExists() { void testExists() {
assertThat(doesPremiumListExist("tld")).isTrue(); assertThat(PremiumListDatastoreDao.getLatestRevision("tld")).isPresent();
assertThat(doesPremiumListExist("nonExistentPremiumList")).isFalse(); assertThat(PremiumListDatastoreDao.getLatestRevision("nonExistentPremiumList")).isEmpty();
} }
@Test @Test
void testGetPremiumPrice_comesFromBloomFilter() throws Exception { void testGetPremiumPrice_comesFromBloomFilter() throws Exception {
PremiumList pl = PremiumList.getCached("tld").get(); PremiumList pl = PremiumListDatastoreDao.getLatestRevision("tld").get();
PremiumListEntry entry = PremiumListEntry entry =
persistResource( persistResource(
new PremiumListEntry.Builder() new PremiumListEntry.Builder()
@ -167,10 +139,10 @@ public class PremiumListUtilsTest {
.build()); .build());
// "missingno" shouldn't be in the Bloom filter, thus it should return not premium without // "missingno" shouldn't be in the Bloom filter, thus it should return not premium without
// attempting to load the entity that is actually present. // attempting to load the entity that is actually present.
assertThat(getPremiumPrice("missingno", Registry.get("tld"))).isEmpty(); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "missingno", "tld")).isEmpty();
// However, if we manually query the cache to force an entity load, it should be found. // However, if we manually query the cache to force an entity load, it should be found.
assertThat( assertThat(
PremiumList.cachePremiumListEntries.get( PremiumListDatastoreDao.premiumListEntriesCache.get(
Key.create(pl.getRevisionKey(), PremiumListEntry.class, "missingno"))) Key.create(pl.getRevisionKey(), PremiumListEntry.class, "missingno")))
.hasValue(entry); .hasValue(entry);
assertMetricOutcomeCount(1, BLOOM_FILTER_NEGATIVE); assertMetricOutcomeCount(1, BLOOM_FILTER_NEGATIVE);
@ -178,8 +150,10 @@ public class PremiumListUtilsTest {
@Test @Test
void testGetPremiumPrice_cachedSecondTime() { void testGetPremiumPrice_cachedSecondTime() {
assertThat(getPremiumPrice("rich", Registry.get("tld"))).hasValue(Money.parse("USD 1999")); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "rich", "tld"))
assertThat(getPremiumPrice("rich", Registry.get("tld"))).hasValue(Money.parse("USD 1999")); .hasValue(Money.parse("USD 1999"));
assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "rich", "tld"))
.hasValue(Money.parse("USD 1999"));
assertThat(premiumListChecks) assertThat(premiumListChecks)
.hasValueForLabels(1, "tld", "tld", UNCACHED_POSITIVE.toString()) .hasValueForLabels(1, "tld", "tld", UNCACHED_POSITIVE.toString())
.and() .and()
@ -197,20 +171,15 @@ public class PremiumListUtilsTest {
@Test @Test
void testGetPremiumPrice_bloomFilterFalsePositive() { void testGetPremiumPrice_bloomFilterFalsePositive() {
// Remove one of the premium list entries from behind the Bloom filter's back. // Remove one of the premium list entries from behind the Bloom filter's back.
tm() tm().transactNew(
.transactNew(
() -> () ->
ofy() ofy()
.delete() .delete()
.keys( .keys(Key.create(pl.getRevisionKey(), PremiumListEntry.class, "rich")));
Key.create(
PremiumList.getCached("tld").get().getRevisionKey(),
PremiumListEntry.class,
"rich")));
ofy().clearSessionCache(); ofy().clearSessionCache();
assertThat(getPremiumPrice("rich", Registry.get("tld"))).isEmpty(); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "rich", "tld")).isEmpty();
assertThat(getPremiumPrice("rich", Registry.get("tld"))).isEmpty(); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "rich", "tld")).isEmpty();
assertThat(premiumListChecks) assertThat(premiumListChecks)
.hasValueForLabels(1, "tld", "tld", UNCACHED_NEGATIVE.toString()) .hasValueForLabels(1, "tld", "tld", UNCACHED_NEGATIVE.toString())
@ -228,22 +197,26 @@ public class PremiumListUtilsTest {
@Test @Test
void testSave_removedPremiumListEntries_areNoLongerInDatastore() { void testSave_removedPremiumListEntries_areNoLongerInDatastore() {
Registry registry = Registry.get("tld");
PremiumList pl = persistPremiumList("tld", "genius,USD 10", "dolt,JPY 1000"); PremiumList pl = persistPremiumList("tld", "genius,USD 10", "dolt,JPY 1000");
assertThat(getPremiumPrice("genius", registry)).hasValue(Money.parse("USD 10")); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "genius", "tld"))
assertThat(getPremiumPrice("dolt", registry)).hasValue(Money.parse("JPY 1000")); .hasValue(Money.parse("USD 10"));
assertThat(ofy() assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "dolt", "tld"))
.load() .hasValue(Money.parse("JPY 1000"));
.type(PremiumListEntry.class) assertThat(
.parent(pl.getRevisionKey()) ofy()
.id("dolt") .load()
.now() .type(PremiumListEntry.class)
.price) .parent(pl.getRevisionKey())
.id("dolt")
.now()
.price)
.isEqualTo(Money.parse("JPY 1000")); .isEqualTo(Money.parse("JPY 1000"));
savePremiumListAndEntries(pl, ImmutableList.of("genius,USD 10", "savant,USD 90")); PremiumListDatastoreDao.save("tld", ImmutableList.of("genius,USD 10", "savant,USD 90"));
assertThat(getPremiumPrice("genius", registry)).hasValue(Money.parse("USD 10")); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "genius", "tld"))
assertThat(getPremiumPrice("savant", registry)).hasValue(Money.parse("USD 90")); .hasValue(Money.parse("USD 10"));
assertThat(getPremiumPrice("dolt", registry)).isEmpty(); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "savant", "tld"))
.hasValue(Money.parse("USD 90"));
assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "dolt", "tld")).isEmpty();
// TODO(b/79888775): Assert that the old premium list is enqueued for later deletion. // TODO(b/79888775): Assert that the old premium list is enqueued for later deletion.
assertThat(premiumListChecks) assertThat(premiumListChecks)
.hasValueForLabels(4, "tld", "tld", UNCACHED_POSITIVE.toString()) .hasValueForLabels(4, "tld", "tld", UNCACHED_POSITIVE.toString())
@ -261,23 +234,22 @@ public class PremiumListUtilsTest {
@Test @Test
void testGetPremiumPrice_allLabelsAreNonPremium_whenNotInList() { void testGetPremiumPrice_allLabelsAreNonPremium_whenNotInList() {
assertThat(getPremiumPrice("blah", Registry.get("tld"))).isEmpty(); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "blah", "tld")).isEmpty();
assertThat(getPremiumPrice("slinge", Registry.get("tld"))).isEmpty(); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld", "slinge", "tld")).isEmpty();
assertMetricOutcomeCount(2, BLOOM_FILTER_NEGATIVE); assertMetricOutcomeCount(2, BLOOM_FILTER_NEGATIVE);
} }
@Test @Test
void testSave_simple() { void testSave_simple() {
PremiumList pl = PremiumList pl =
savePremiumListAndEntries( PremiumListDatastoreDao.save("tld2", ImmutableList.of("lol , USD 999 # yupper rooni "));
new PremiumList.Builder().setName("tld2").build(),
ImmutableList.of("lol , USD 999 # yupper rooni "));
createTld("tld");
persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build()); persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build());
assertThat(getPremiumPrice("lol", Registry.get("tld"))).hasValue(Money.parse("USD 999")); assertThat(PremiumListDatastoreDao.getPremiumPrice("tld2", "lol", "tld"))
assertThat(getPremiumPrice("lol ", Registry.get("tld"))).isEmpty(); .hasValue(Money.parse("USD 999"));
assertThat(PremiumListDatastoreDao.getPremiumPrice("tld2", "lol ", "tld")).isEmpty();
ImmutableMap<String, PremiumListEntry> entries = ImmutableMap<String, PremiumListEntry> entries =
loadPremiumListEntries(PremiumList.getUncached("tld2").get()); Streams.stream(PremiumListDatastoreDao.loadPremiumListEntriesUncached(pl))
.collect(toImmutableMap(PremiumListEntry::getLabel, Function.identity()));
assertThat(entries.keySet()).containsExactly("lol"); assertThat(entries.keySet()).containsExactly("lol");
assertThat(entries).doesNotContainKey("lol "); assertThat(entries).doesNotContainKey("lol ");
PremiumListEntry entry = entries.get("lol"); PremiumListEntry entry = entries.get("lol");
@ -300,47 +272,47 @@ public class PremiumListUtilsTest {
@Test @Test
void test_saveAndUpdateEntriesTwice() { void test_saveAndUpdateEntriesTwice() {
PremiumList pl = PremiumList firstSave = PremiumListDatastoreDao.save("tld", ImmutableList.of("test,USD 1"));
savePremiumListAndEntries( ImmutableMap<String, PremiumListEntry> entries =
new PremiumList.Builder().setName("pl").build(), ImmutableList.of("test,USD 1")); Streams.stream(PremiumListDatastoreDao.loadPremiumListEntriesUncached(firstSave))
ImmutableMap<String, PremiumListEntry> entries = loadPremiumListEntries(pl); .collect(toImmutableMap(PremiumListEntry::getLabel, Function.identity()));
assertThat(entries.keySet()).containsExactly("test"); assertThat(entries.keySet()).containsExactly("test");
assertThat(loadPremiumListEntries(PremiumList.getUncached("pl").get())).isEqualTo(entries);
// Save again with no changes, and clear the cache to force a re-load from Datastore. // Save again with no changes, and clear the cache to force a re-load from Datastore.
PremiumList resaved = savePremiumListAndEntries(pl, ImmutableList.of("test,USD 1")); PremiumList resaved = PremiumListDatastoreDao.save("tld", ImmutableList.of("test,USD 1"));
ofy().clearSessionCache(); ofy().clearSessionCache();
Map<String, PremiumListEntry> entriesReloaded = Map<String, PremiumListEntry> entriesReloaded =
loadPremiumListEntries(PremiumList.getUncached("pl").get()); Streams.stream(PremiumListDatastoreDao.loadPremiumListEntriesUncached(resaved))
.collect(toImmutableMap(PremiumListEntry::getLabel, Function.identity()));
assertThat(entriesReloaded).hasSize(1); assertThat(entriesReloaded).hasSize(1);
assertThat(entriesReloaded).containsKey("test"); assertThat(entriesReloaded).containsKey("test");
assertThat(entriesReloaded.get("test").parent).isEqualTo(resaved.getRevisionKey()); assertThat(entriesReloaded.get("test").parent).isEqualTo(resaved.getRevisionKey());
} }
@Test
void test_savePremiumListAndEntries_clearsCache() {
assertThat(PremiumList.cachePremiumLists.getIfPresent("tld")).isNull();
PremiumList pl = PremiumList.getCached("tld").get();
assertThat(PremiumList.cachePremiumLists.getIfPresent("tld")).isEqualTo(pl);
savePremiumListAndEntries(
new PremiumList.Builder().setName("tld").build(), ImmutableList.of("test,USD 1"));
assertThat(PremiumList.cachePremiumLists.getIfPresent("tld")).isNull();
}
@Test @Test
void testDelete() { void testDelete() {
persistPremiumList("gtld1", "trombone,USD 10"); persistPremiumList("gtld1", "trombone,USD 10");
assertThat(PremiumList.getUncached("gtld1")).isPresent(); Optional<PremiumList> gtld1 = PremiumListDatastoreDao.getLatestRevision("gtld1");
Key<PremiumListRevision> parent = PremiumList.getUncached("gtld1").get().getRevisionKey(); assertThat(gtld1).isPresent();
deletePremiumList(PremiumList.getUncached("gtld1").get()); Key<PremiumListRevision> parent = gtld1.get().getRevisionKey();
assertThat(PremiumList.getUncached("gtld1")).isEmpty(); PremiumListDatastoreDao.delete(gtld1.get());
assertThat(PremiumListDatastoreDao.getLatestRevision("gtld1")).isEmpty();
assertThat(ofy().load().type(PremiumListEntry.class).ancestor(parent).list()).isEmpty(); assertThat(ofy().load().type(PremiumListEntry.class).ancestor(parent).list()).isEmpty();
} }
@Test @Test
void testDelete_largeNumberOfEntries_succeeds() { void testDelete_largeNumberOfEntries_succeeds() {
persistHumongousPremiumList("ginormous", 2500); PremiumList large = persistHumongousPremiumList("ginormous", 2500);
deletePremiumList(PremiumList.getUncached("ginormous").get()); PremiumListDatastoreDao.delete(large);
assertThat(PremiumList.getUncached("ginormous")).isEmpty(); assertThat(PremiumListDatastoreDao.getLatestRevision("ginormous")).isEmpty();
}
@Test
void test_savePremiumList_clearsCache() {
assertThat(PremiumListDatastoreDao.premiumListCache.getIfPresent("tld")).isNull();
PremiumList pl = PremiumListDatastoreDao.getLatestRevision("tld").get();
assertThat(PremiumListDatastoreDao.premiumListCache.getIfPresent("tld").get()).isEqualTo(pl);
transactIfJpaTm(() -> PremiumListDatastoreDao.save("tld", ImmutableList.of("test,USD 1")));
assertThat(PremiumListDatastoreDao.premiumListCache.getIfPresent("tld")).isNull();
} }
/** Persists a premium list with a specified number of nonsense entries. */ /** Persists a premium list with a specified number of nonsense entries. */

View file

@ -0,0 +1,139 @@
// Copyright 2021 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.model.registry.label;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.time.Duration.standardDays;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.truth.Truth8;
import google.registry.dns.writer.VoidDnsWriter;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registry.Registry;
import google.registry.schema.tld.PremiumListSqlDao;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestCacheExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link PremiumListDualDao}. */
@DualDatabaseTest
public class PremiumListDualDaoTest {
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
// Set long persist times on caches so they can be tested (cache times default to 0 in tests).
@RegisterExtension
public final TestCacheExtension testCacheExtension =
new TestCacheExtension.Builder()
.withPremiumListsCache(standardDays(1))
.withPremiumListEntriesCache(standardDays(1))
.build();
@BeforeEach
void before() {
createTld("tld");
}
@TestOfyOnly
void testGetPremiumPrice_secondaryLoadMissingSql() {
PremiumListSqlDao.delete(PremiumListSqlDao.getLatestRevision("tld").get());
assertThat(
assertThrows(
IllegalStateException.class,
() -> PremiumListDualDao.getPremiumPrice("brass", Registry.get("tld"))))
.hasMessageThat()
.isEqualTo(
"Unequal prices for domain brass.tld from primary Datastore DB "
+ "(Optional[USD 20.00]) and secondary SQL db (Optional.empty).");
}
@TestSqlOnly
void testGetPremiumPrice_secondaryLoadMissingOfy() {
PremiumList premiumList = PremiumListDatastoreDao.getLatestRevision("tld").get();
PremiumListDatastoreDao.delete(premiumList);
assertThat(
assertThrows(
IllegalStateException.class,
() -> PremiumListDualDao.getPremiumPrice("brass", Registry.get("tld"))))
.hasMessageThat()
.isEqualTo(
"Unequal prices for domain brass.tld from primary SQL DB (Optional[USD 20.00]) "
+ "and secondary Datastore db (Optional.empty).");
}
@TestOfyOnly
void testGetPremiumPrice_secondaryDifferentSql() {
PremiumListSqlDao.save("tld", ImmutableList.of("brass,USD 50"));
assertThat(
assertThrows(
IllegalStateException.class,
() -> PremiumListDualDao.getPremiumPrice("brass", Registry.get("tld"))))
.hasMessageThat()
.isEqualTo(
"Unequal prices for domain brass.tld from primary Datastore DB "
+ "(Optional[USD 20.00]) and secondary SQL db (Optional[USD 50.00]).");
}
@TestSqlOnly
void testGetPremiumPrice_secondaryDifferentOfy() {
PremiumListDatastoreDao.save("tld", ImmutableList.of("brass,USD 50"));
assertThat(
assertThrows(
IllegalStateException.class,
() -> PremiumListDualDao.getPremiumPrice("brass", Registry.get("tld"))))
.hasMessageThat()
.isEqualTo(
"Unequal prices for domain brass.tld from primary SQL DB "
+ "(Optional[USD 20.00]) and secondary Datastore db (Optional[USD 50.00]).");
}
@TestOfyAndSql
void testGetPremiumPrice_returnsNoPriceWhenNoPremiumListConfigured() {
createTld("ghost");
persistResource(
new Registry.Builder()
.setTldStr("ghost")
.setPremiumPricingEngine(StaticPremiumListPricingEngine.NAME)
.setDnsWriters(ImmutableSet.of(VoidDnsWriter.NAME))
.build());
assertThat(Registry.get("ghost").getPremiumList()).isNull();
Truth8.assertThat(PremiumListDualDao.getPremiumPrice("blah", Registry.get("ghost"))).isEmpty();
}
@TestOfyAndSql
void testGetPremiumPrice_emptyWhenPremiumListDeleted() {
PremiumList toDelete = PremiumListDualDao.getLatestRevision("tld").get();
PremiumListDualDao.delete(toDelete);
Truth8.assertThat(PremiumListDualDao.getPremiumPrice("blah", Registry.get("tld"))).isEmpty();
}
@TestOfyAndSql
void getPremiumPrice_returnsNoneWhenNoPremiumListConfigured() {
persistResource(newRegistry("foobar", "FOOBAR").asBuilder().setPremiumList(null).build());
Truth8.assertThat(PremiumListDualDao.getPremiumPrice("rich", Registry.get("foobar"))).isEmpty();
}
}

View file

@ -69,7 +69,7 @@ public class PremiumListTest {
@Test @Test
void testProbablePremiumLabels() { void testProbablePremiumLabels() {
PremiumList pl = PremiumList.getUncached("tld").get(); PremiumList pl = PremiumListDualDao.getLatestRevision("tld").get();
PremiumListRevision revision = ofy().load().key(pl.getRevisionKey()).now(); PremiumListRevision revision = ofy().load().key(pl.getRevisionKey()).now();
assertThat(revision.getProbablePremiumLabels().mightContain("notpremium")).isFalse(); assertThat(revision.getProbablePremiumLabels().mightContain("notpremium")).isFalse();
for (String label : ImmutableList.of("rich", "lol", "johnny-be-goode", "icann")) { for (String label : ImmutableList.of("rich", "lol", "johnny-be-goode", "icann")) {
@ -85,7 +85,7 @@ public class PremiumListTest {
assertThrows( assertThrows(
IllegalStateException.class, IllegalStateException.class,
() -> () ->
PremiumList.getUncached("tld") PremiumListDualDao.getLatestRevision("tld")
.get() .get()
.parse( .parse(
ImmutableList.of( ImmutableList.of(

View file

@ -42,7 +42,7 @@ import google.registry.schema.integration.SqlIntegrationTestSuite.BeforeSuiteTes
import google.registry.schema.registrar.RegistrarDaoTest; import google.registry.schema.registrar.RegistrarDaoTest;
import google.registry.schema.replay.SqlReplayCheckpointTest; import google.registry.schema.replay.SqlReplayCheckpointTest;
import google.registry.schema.server.LockDaoTest; import google.registry.schema.server.LockDaoTest;
import google.registry.schema.tld.PremiumListDaoTest; import google.registry.schema.tld.PremiumListSqlDaoTest;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@ -92,7 +92,7 @@ import org.junit.runner.RunWith;
KmsSecretRevisionSqlDaoTest.class, KmsSecretRevisionSqlDaoTest.class,
LockDaoTest.class, LockDaoTest.class,
PollMessageTest.class, PollMessageTest.class,
PremiumListDaoTest.class, PremiumListSqlDaoTest.class,
RdeRevisionTest.class, RdeRevisionTest.class,
RegistrarDaoTest.class, RegistrarDaoTest.class,
RegistryTest.class, RegistryTest.class,

View file

@ -18,30 +18,30 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.newRegistry; import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.money.CurrencyUnit.JPY; import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD; import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.joda.time.Duration.standardDays;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.TestCacheExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Optional; import java.util.Optional;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.money.Money; import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link PremiumListDao}. */ /** Unit tests for {@link PremiumListSqlDao}. */
public class PremiumListDaoTest { public class PremiumListSqlDaoTest {
private final FakeClock fakeClock = new FakeClock(); private final FakeClock fakeClock = new FakeClock();
@ -53,6 +53,14 @@ public class PremiumListDaoTest {
.withClock(fakeClock) .withClock(fakeClock)
.build(); .build();
// Set long persist times on caches so they can be tested (cache times default to 0 in tests).
@RegisterExtension
public final TestCacheExtension testCacheExtension =
new TestCacheExtension.Builder()
.withPremiumListsCache(standardDays(1))
.withPremiumListEntriesCache(standardDays(1))
.build();
private ImmutableMap<String, BigDecimal> testPrices; private ImmutableMap<String, BigDecimal> testPrices;
private PremiumList testList; private PremiumList testList;
@ -78,11 +86,12 @@ public class PremiumListDaoTest {
@Test @Test
void saveNew_worksSuccessfully() { void saveNew_worksSuccessfully() {
PremiumListDao.saveNew(testList); PremiumListSqlDao.save(testList);
jpaTm() jpaTm()
.transact( .transact(
() -> { () -> {
Optional<PremiumList> persistedListOpt = PremiumListDao.getLatestRevision("testname"); Optional<PremiumList> persistedListOpt =
PremiumListSqlDao.getLatestRevision("testname");
assertThat(persistedListOpt).isPresent(); assertThat(persistedListOpt).isPresent();
PremiumList persistedList = persistedListOpt.get(); PremiumList persistedList = persistedListOpt.get();
assertThat(persistedList.getLabelsToPrices()).containsExactlyEntriesIn(testPrices); assertThat(persistedList.getLabelsToPrices()).containsExactlyEntriesIn(testPrices);
@ -92,17 +101,17 @@ public class PremiumListDaoTest {
@Test @Test
void update_worksSuccessfully() { void update_worksSuccessfully() {
PremiumListDao.saveNew(testList); PremiumListSqlDao.save(testList);
Optional<PremiumList> persistedList = PremiumListDao.getLatestRevision("testname"); Optional<PremiumList> persistedList = PremiumListSqlDao.getLatestRevision("testname");
assertThat(persistedList).isPresent(); assertThat(persistedList).isPresent();
long firstRevisionId = persistedList.get().getRevisionId(); long firstRevisionId = persistedList.get().getRevisionId();
PremiumListDao.update( PremiumListSqlDao.save(
new PremiumList.Builder() new PremiumList.Builder()
.setName("testname") .setName("testname")
.setCurrency(USD) .setCurrency(USD)
.setLabelsToPrices( .setLabelsToPrices(
ImmutableMap.of( ImmutableMap.of(
"update", "save",
BigDecimal.valueOf(55343.12), BigDecimal.valueOf(55343.12),
"new", "new",
BigDecimal.valueOf(0.01), BigDecimal.valueOf(0.01),
@ -113,65 +122,46 @@ public class PremiumListDaoTest {
jpaTm() jpaTm()
.transact( .transact(
() -> { () -> {
Optional<PremiumList> updatedListOpt = PremiumListDao.getLatestRevision("testname"); Optional<PremiumList> savedListOpt = PremiumListSqlDao.getLatestRevision("testname");
assertThat(updatedListOpt).isPresent(); assertThat(savedListOpt).isPresent();
PremiumList updatedList = updatedListOpt.get(); PremiumList savedList = savedListOpt.get();
assertThat(updatedList.getLabelsToPrices()) assertThat(savedList.getLabelsToPrices())
.containsExactlyEntriesIn( .containsExactlyEntriesIn(
ImmutableMap.of( ImmutableMap.of(
"update", "save",
BigDecimal.valueOf(55343.12), BigDecimal.valueOf(55343.12),
"new", "new",
BigDecimal.valueOf(0.01), BigDecimal.valueOf(0.01),
"silver", "silver",
BigDecimal.valueOf(30.03))); BigDecimal.valueOf(30.03)));
assertThat(updatedList.getCreationTime()).isEqualTo(fakeClock.nowUtc()); assertThat(savedList.getCreationTime()).isEqualTo(fakeClock.nowUtc());
assertThat(updatedList.getRevisionId()).isGreaterThan(firstRevisionId); assertThat(savedList.getRevisionId()).isGreaterThan(firstRevisionId);
assertThat(updatedList.getCreationTime()).isEqualTo(fakeClock.nowUtc()); assertThat(savedList.getCreationTime()).isEqualTo(fakeClock.nowUtc());
}); });
} }
@Test
void saveNew_throwsWhenPremiumListAlreadyExists() {
PremiumListDao.saveNew(testList);
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> PremiumListDao.saveNew(testList));
assertThat(thrown).hasMessageThat().isEqualTo("Premium list 'testname' already exists");
}
// TODO(b/147246613): Un-ignore this.
@Test
@Disabled
void update_throwsWhenListDoesntExist() {
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> PremiumListDao.update(testList));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Can't update non-existent premium list 'testname'");
}
@Test @Test
void checkExists_worksSuccessfully() { void checkExists_worksSuccessfully() {
assertThat(PremiumListDao.checkExists("testname")).isFalse(); assertThat(PremiumListSqlDao.getLatestRevision("testname")).isEmpty();
PremiumListDao.saveNew(testList); PremiumListSqlDao.save(testList);
assertThat(PremiumListDao.checkExists("testname")).isTrue(); assertThat(PremiumListSqlDao.getLatestRevision("testname")).isPresent();
} }
@Test @Test
void getLatestRevision_returnsEmptyForNonexistentList() { void getLatestRevision_returnsEmptyForNonexistentList() {
assertThat(PremiumListDao.getLatestRevision("nonexistentlist")).isEmpty(); assertThat(PremiumListSqlDao.getLatestRevision("nonexistentlist")).isEmpty();
} }
@Test @Test
void getLatestRevision_worksSuccessfully() { void getLatestRevision_worksSuccessfully() {
PremiumListDao.saveNew( PremiumListSqlDao.save(
new PremiumList.Builder() new PremiumList.Builder()
.setName("list1") .setName("list1")
.setCurrency(JPY) .setCurrency(JPY)
.setLabelsToPrices(ImmutableMap.of("wrong", BigDecimal.valueOf(1000.50))) .setLabelsToPrices(ImmutableMap.of("wrong", BigDecimal.valueOf(1000.50)))
.setCreationTime(fakeClock.nowUtc()) .setCreationTime(fakeClock.nowUtc())
.build()); .build());
PremiumListDao.update( PremiumListSqlDao.save(
new PremiumList.Builder() new PremiumList.Builder()
.setName("list1") .setName("list1")
.setCurrency(JPY) .setCurrency(JPY)
@ -181,7 +171,7 @@ public class PremiumListDaoTest {
jpaTm() jpaTm()
.transact( .transact(
() -> { () -> {
Optional<PremiumList> persistedList = PremiumListDao.getLatestRevision("list1"); Optional<PremiumList> persistedList = PremiumListSqlDao.getLatestRevision("list1");
assertThat(persistedList).isPresent(); assertThat(persistedList).isPresent();
assertThat(persistedList.get().getName()).isEqualTo("list1"); assertThat(persistedList.get().getName()).isEqualTo("list1");
assertThat(persistedList.get().getCurrency()).isEqualTo(JPY); assertThat(persistedList.get().getCurrency()).isEqualTo(JPY);
@ -190,12 +180,6 @@ public class PremiumListDaoTest {
}); });
} }
@Test
void getPremiumPrice_returnsNoneWhenNoPremiumListConfigured() {
persistResource(newRegistry("foobar", "FOOBAR").asBuilder().setPremiumList(null).build());
assertThat(PremiumListDao.getPremiumPrice("rich", Registry.get("foobar"))).isEmpty();
}
@Test @Test
void getPremiumPrice_worksSuccessfully() { void getPremiumPrice_worksSuccessfully() {
persistResource( persistResource(
@ -207,28 +191,18 @@ public class PremiumListDaoTest {
google.registry.model.registry.label.PremiumList.class, google.registry.model.registry.label.PremiumList.class,
"premlist")) "premlist"))
.build()); .build());
PremiumListDao.saveNew( PremiumListSqlDao.save(
new PremiumList.Builder() new PremiumList.Builder()
.setName("premlist") .setName("premlist")
.setCurrency(USD) .setCurrency(USD)
.setLabelsToPrices(testPrices) .setLabelsToPrices(testPrices)
.setCreationTime(fakeClock.nowUtc()) .setCreationTime(fakeClock.nowUtc())
.build()); .build());
assertThat(PremiumListDao.getPremiumPrice("silver", Registry.get("foobar"))) assertThat(PremiumListSqlDao.getPremiumPrice("premlist", "silver"))
.hasValue(Money.of(USD, 10.23)); .hasValue(Money.of(USD, 10.23));
assertThat(PremiumListDao.getPremiumPrice("gold", Registry.get("foobar"))) assertThat(PremiumListSqlDao.getPremiumPrice("premlist", "gold"))
.hasValue(Money.of(USD, 1305.47)); .hasValue(Money.of(USD, 1305.47));
assertThat(PremiumListDao.getPremiumPrice("zirconium", Registry.get("foobar"))).isEmpty(); assertThat(PremiumListSqlDao.getPremiumPrice("premlist", "zirconium")).isEmpty();
}
@Test
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'");
} }
@Test @Test
@ -242,7 +216,7 @@ public class PremiumListDaoTest {
google.registry.model.registry.label.PremiumList.class, google.registry.model.registry.label.PremiumList.class,
"premlist")) "premlist"))
.build()); .build());
PremiumListDao.saveNew( PremiumListSqlDao.save(
new PremiumList.Builder() new PremiumList.Builder()
.setName("premlist") .setName("premlist")
.setCurrency(JPY) .setCurrency(JPY)
@ -256,14 +230,22 @@ public class PremiumListDaoTest {
BigDecimal.valueOf(15000))) BigDecimal.valueOf(15000)))
.setCreationTime(fakeClock.nowUtc()) .setCreationTime(fakeClock.nowUtc())
.build()); .build());
assertThat(PremiumListDao.getPremiumPrice("silver", Registry.get("foobar"))) assertThat(PremiumListSqlDao.getPremiumPrice("premlist", "silver")).hasValue(moneyOf(JPY, 10));
.hasValue(moneyOf(JPY, 10)); assertThat(PremiumListSqlDao.getPremiumPrice("premlist", "gold")).hasValue(moneyOf(JPY, 1000));
assertThat(PremiumListDao.getPremiumPrice("gold", Registry.get("foobar"))) assertThat(PremiumListSqlDao.getPremiumPrice("premlist", "palladium"))
.hasValue(moneyOf(JPY, 1000));
assertThat(PremiumListDao.getPremiumPrice("palladium", Registry.get("foobar")))
.hasValue(moneyOf(JPY, 15000)); .hasValue(moneyOf(JPY, 15000));
} }
@Test
void test_savePremiumList_clearsCache() {
assertThat(PremiumListSqlDao.premiumListCache.getIfPresent("testname")).isNull();
PremiumListSqlDao.save(testList);
PremiumList pl = PremiumListSqlDao.getLatestRevision("testname").get();
assertThat(PremiumListSqlDao.premiumListCache.getIfPresent("testname").get()).isEqualTo(pl);
transactIfJpaTm(() -> PremiumListSqlDao.save("testname", ImmutableList.of("test,USD 1")));
assertThat(PremiumListSqlDao.premiumListCache.getIfPresent("testname")).isNull();
}
private static Money moneyOf(CurrencyUnit unit, double amount) { private static Money moneyOf(CurrencyUnit unit, double amount) {
return Money.of(unit, BigDecimal.valueOf(amount).setScale(unit.getDecimalPlaces())); return Money.of(unit, BigDecimal.valueOf(amount).setScale(unit.getDecimalPlaces()));
} }

View file

@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.schema.tld.PremiumListUtils.parseToPremiumList; import static google.registry.schema.tld.PremiumListUtils.parseToPremiumList;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -34,7 +35,8 @@ class PremiumListUtilsTest {
@Test @Test
void parseInputToPremiumList_works() { void parseInputToPremiumList_works() {
PremiumList premiumList = PremiumList premiumList =
parseToPremiumList("testlist", "foo,USD 99.50\n" + "bar,USD 30\n" + "baz,USD 10\n"); parseToPremiumList(
"testlist", ImmutableList.of("foo,USD 99.50", "bar,USD 30", "baz,USD 10"));
assertThat(premiumList.getName()).isEqualTo("testlist"); assertThat(premiumList.getName()).isEqualTo("testlist");
assertThat(premiumList.getLabelsToPrices()) assertThat(premiumList.getLabelsToPrices())
.containsExactly("foo", twoDigits(99.50), "bar", twoDigits(30), "baz", twoDigits(10)); .containsExactly("foo", twoDigits(99.50), "bar", twoDigits(30), "baz", twoDigits(10));
@ -47,7 +49,7 @@ class PremiumListUtilsTest {
IllegalArgumentException.class, IllegalArgumentException.class,
() -> () ->
parseToPremiumList( parseToPremiumList(
"testlist", "foo,USD 99.50\n" + "bar,USD 30\n" + "baz,JPY 990\n")); "testlist", ImmutableList.of("foo,USD 99.50", "bar,USD 30", "baz,JPY 990")));
assertThat(thrown) assertThat(thrown)
.hasMessageThat() .hasMessageThat()
.isEqualTo("The Cloud SQL schema requires exactly one currency, but got: [JPY, USD]"); .isEqualTo("The Cloud SQL schema requires exactly one currency, but got: [JPY, USD]");

View file

@ -32,7 +32,9 @@ import static google.registry.model.ImmutableObjectSubject.immutableObjectCorres
import static google.registry.model.ResourceTransferUtils.createTransferResponse; import static google.registry.model.ResourceTransferUtils.createTransferResponse;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY; import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.registry.label.PremiumListUtils.parentPremiumListEntriesOnRevision; import static google.registry.model.registry.label.PremiumListDatastoreDao.parentPremiumListEntriesOnRevision;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.ofyTmOrDoNothing; import static google.registry.persistence.transaction.TransactionManagerUtil.ofyTmOrDoNothing;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm; import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
@ -57,6 +59,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.net.InetAddresses; import com.google.common.net.InetAddresses;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Saver; import com.googlecode.objectify.cmd.Saver;
@ -100,6 +103,7 @@ import google.registry.model.registry.Registry.TldType;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
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.model.registry.label.PremiumListDualDao;
import google.registry.model.registry.label.ReservedList; import google.registry.model.registry.label.ReservedList;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.ContactTransferData; import google.registry.model.transfer.ContactTransferData;
@ -108,11 +112,11 @@ import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus; import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
import google.registry.tmch.LordnTaskUtils; import google.registry.tmch.LordnTaskUtils;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.money.Money; import org.joda.money.Money;
@ -387,25 +391,23 @@ public class DatabaseHelper {
.build(); .build();
PremiumListRevision revision = PremiumListRevision.create(premiumList, entries.keySet()); PremiumListRevision revision = PremiumListRevision.create(premiumList, entries.keySet());
if (tm().isOfy()) { ImmutableList<Object> premiumListOfyObjects =
ImmutableList<Object> premiumLists = ImmutableList.of(
ImmutableList.of( premiumList.asBuilder().setRevision(Key.create(revision)).build(), revision);
premiumList.asBuilder().setRevision(Key.create(revision)).build(), revision); ImmutableSet<PremiumListEntry> entriesOnRevision =
ImmutableSet<PremiumListEntry> entriesOnRevision = parentPremiumListEntriesOnRevision(entries.values(), Key.create(revision));
parentPremiumListEntriesOnRevision(entries.values(), Key.create(revision)); if (alwaysSaveWithBackup) {
if (alwaysSaveWithBackup) { ofyTm()
tm().transact( .transact(
() -> { () -> {
tm().putAll(premiumLists); ofyTm().putAll(premiumListOfyObjects);
tm().putAll(entriesOnRevision); ofyTm().putAll(entriesOnRevision);
}); });
} else {
tm().putAllWithoutBackup(premiumLists);
tm().putAllWithoutBackup(entriesOnRevision);
}
} else { } else {
tm().transact(() -> tm().insert(premiumList)); ofyTm().putAllWithoutBackup(premiumListOfyObjects);
ofyTm().putAllWithoutBackup(entriesOnRevision);
} }
jpaTm().transact(() -> jpaTm().insert(premiumList));
maybeAdvanceClock(); maybeAdvanceClock();
// The above premiumList is in the session cache and it is different from the corresponding // The above premiumList is in the session cache and it is different from the corresponding
// entity stored in Datastore because it has some @Ignore fields set dedicated for SQL. This // entity stored in Datastore because it has some @Ignore fields set dedicated for SQL. This
@ -807,7 +809,7 @@ public class DatabaseHelper {
/** Assert that the expected billing events are exactly the ones found in the fake Datastore. */ /** Assert that the expected billing events are exactly the ones found in the fake Datastore. */
public static void assertBillingEvents(BillingEvent... expected) { public static void assertBillingEvents(BillingEvent... expected) {
assertBillingEventsEqual(getBillingEvents(), Arrays.asList(expected)); assertBillingEventsEqual(getBillingEvents(), asList(expected));
} }
/** Assert that the expected billing events set is exactly the one found in the fake Datastore. */ /** Assert that the expected billing events set is exactly the one found in the fake Datastore. */
@ -820,7 +822,7 @@ public class DatabaseHelper {
*/ */
public static void assertBillingEventsForResource( public static void assertBillingEventsForResource(
EppResource resource, BillingEvent... expected) { EppResource resource, BillingEvent... expected) {
assertBillingEventsEqual(getBillingEvents(resource), Arrays.asList(expected)); assertBillingEventsEqual(getBillingEvents(resource), asList(expected));
} }
/** Assert that there are no billing events. */ /** Assert that there are no billing events. */
@ -851,15 +853,15 @@ public class DatabaseHelper {
} }
public static void assertPollMessages(String clientId, PollMessage... expected) { public static void assertPollMessages(String clientId, PollMessage... expected) {
assertPollMessagesEqual(getPollMessages(clientId), Arrays.asList(expected)); assertPollMessagesEqual(getPollMessages(clientId), asList(expected));
} }
public static void assertPollMessages(PollMessage... expected) { public static void assertPollMessages(PollMessage... expected) {
assertPollMessagesEqual(getPollMessages(), Arrays.asList(expected)); assertPollMessagesEqual(getPollMessages(), asList(expected));
} }
public static void assertPollMessagesForResource(DomainContent domain, PollMessage... expected) { public static void assertPollMessagesForResource(DomainContent domain, PollMessage... expected) {
assertPollMessagesEqual(getPollMessages(domain), Arrays.asList(expected)); assertPollMessagesEqual(getPollMessages(domain), asList(expected));
} }
public static ImmutableList<PollMessage> getPollMessages() { public static ImmutableList<PollMessage> getPollMessages() {
@ -1232,19 +1234,8 @@ public class DatabaseHelper {
/** Returns the entire map of {@link PremiumListEntry}s for the given {@link PremiumList}. */ /** Returns the entire map of {@link PremiumListEntry}s for the given {@link PremiumList}. */
public static ImmutableMap<String, PremiumListEntry> loadPremiumListEntries( public static ImmutableMap<String, PremiumListEntry> loadPremiumListEntries(
PremiumList premiumList) { PremiumList premiumList) {
try { return Streams.stream(PremiumListDualDao.loadAllPremiumListEntries(premiumList.getName()))
ImmutableMap.Builder<String, PremiumListEntry> entriesMap = new ImmutableMap.Builder<>(); .collect(toImmutableMap(PremiumListEntry::getLabel, Function.identity()));
if (premiumList.getRevisionKey() != null) {
for (PremiumListEntry entry :
ofy().load().type(PremiumListEntry.class).ancestor(premiumList.getRevisionKey())) {
entriesMap.put(entry.getLabel(), entry);
}
}
return entriesMap.build();
} catch (Exception e) {
throw new RuntimeException(
"Could not retrieve entries for premium list " + premiumList.getName(), e);
}
} }
/** Loads and returns the registrar with the given client ID, or throws IAE if not present. */ /** Loads and returns the registrar with the given client ID, or throws IAE if not present. */

View file

@ -18,7 +18,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import google.registry.model.EppResource; import google.registry.model.EppResource;
import google.registry.model.index.ForeignKeyIndex; import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumListDatastoreDao;
import google.registry.schema.tld.PremiumListSqlDao;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.joda.time.Duration; import org.joda.time.Duration;
@ -70,15 +71,18 @@ public class TestCacheExtension implements BeforeEachCallback, AfterEachCallback
public Builder withPremiumListsCache(Duration expiry) { public Builder withPremiumListsCache(Duration expiry) {
cacheHandlerMap.put( cacheHandlerMap.put(
"PremiumList.cachePremiumLists", "PremiumListSqlDao.premiumListCache",
new TestCacheHandler(PremiumList::setPremiumListCacheForTest, expiry)); new TestCacheHandler(PremiumListSqlDao::setPremiumListCacheForTest, expiry));
cacheHandlerMap.put(
"PremiumListDatastoreDao.premiumListCache",
new TestCacheHandler(PremiumListDatastoreDao::setPremiumListCacheForTest, expiry));
return this; return this;
} }
public Builder withPremiumListEntriesCache(Duration expiry) { public Builder withPremiumListEntriesCache(Duration expiry) {
cacheHandlerMap.put( cacheHandlerMap.put(
"PremiumList.cachePremiumListEntries", "PremiumList.cachePremiumListEntries",
new TestCacheHandler(PremiumList::setPremiumListEntriesCacheForTest, expiry)); new TestCacheHandler(PremiumListDatastoreDao::setPremiumListEntriesCacheForTest, expiry));
return this; return this;
} }

View file

@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException; import com.beust.jcommander.ParameterException;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.testing.DeterministicStringGenerator; import google.registry.testing.DeterministicStringGenerator;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -101,7 +101,7 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
persistResource( persistResource(
Registry.get("baar") Registry.get("baar")
.asBuilder() .asBuilder()
.setPremiumList(PremiumList.getUncached("baar").get()) .setPremiumList(PremiumListDualDao.getLatestRevision("baar").get())
.build()); .build());
runCommandForced( runCommandForced(
"--client=NewRegistrar", "--client=NewRegistrar",

View file

@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumList.PremiumListEntry; import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.model.registry.label.PremiumListDualDao;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** Unit tests for {@link DeletePremiumListCommand}. */ /** Unit tests for {@link DeletePremiumListCommand}. */
@ -36,7 +37,7 @@ class DeletePremiumListCommandTest extends CommandTestCase<DeletePremiumListComm
PremiumList premiumList = persistPremiumList("xn--q9jyb4c", "blah,USD 100"); PremiumList premiumList = persistPremiumList("xn--q9jyb4c", "blah,USD 100");
assertThat(loadPremiumListEntries(premiumList)).hasSize(1); assertThat(loadPremiumListEntries(premiumList)).hasSize(1);
runCommand("--force", "--name=xn--q9jyb4c"); runCommand("--force", "--name=xn--q9jyb4c");
assertThat(PremiumList.getUncached("xn--q9jyb4c")).isEmpty(); assertThat(PremiumListDualDao.getLatestRevision("xn--q9jyb4c")).isEmpty();
// Ensure that the Datastore premium list entry entities were deleted correctly. // Ensure that the Datastore premium list entry entities were deleted correctly.
assertThat(ofy().load() assertThat(ofy().load()
@ -64,7 +65,7 @@ class DeletePremiumListCommandTest extends CommandTestCase<DeletePremiumListComm
assertThrows( assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> runCommandForced("--name=" + premiumList.getName())); () -> runCommandForced("--name=" + premiumList.getName()));
assertThat(PremiumList.getUncached(premiumList.getName())).isPresent(); assertThat(PremiumListDualDao.getLatestRevision(premiumList.getName())).isPresent();
assertThat(thrown) assertThat(thrown)
.hasMessageThat() .hasMessageThat()
.isEqualTo("Cannot delete premium list because it is used on these tld(s): xn--q9jyb4c"); .isEqualTo("Cannot delete premium list because it is used on these tld(s): xn--q9jyb4c");

View file

@ -16,14 +16,13 @@ package google.registry.tools.server;
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 com.google.common.truth.Truth8.assertThat;
import static google.registry.model.registry.label.PremiumListUtils.deletePremiumList;
import static google.registry.model.registry.label.PremiumListUtils.getPremiumPrice;
import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadPremiumListEntries; import static google.registry.testing.DatabaseHelper.loadPremiumListEntries;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeJsonResponse; import google.registry.testing.FakeJsonResponse;
import org.joda.money.Money; import org.joda.money.Money;
@ -46,7 +45,7 @@ public class CreatePremiumListActionTest {
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
createTlds("foo", "xn--q9jyb4c", "how"); createTlds("foo", "xn--q9jyb4c", "how");
deletePremiumList(PremiumList.getUncached("foo").get()); PremiumListDualDao.delete(PremiumListDualDao.getLatestRevision("foo").get());
action = new CreatePremiumListAction(); action = new CreatePremiumListAction();
response = new FakeJsonResponse(); response = new FakeJsonResponse();
action.response = response; action.response = response;
@ -78,7 +77,8 @@ public class CreatePremiumListActionTest {
action.override = true; action.override = true;
action.run(); action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(loadPremiumListEntries(PremiumList.getUncached("zanzibar").get())).hasSize(1); assertThat(loadPremiumListEntries(PremiumListDualDao.getLatestRevision("zanzibar").get()))
.hasSize(1);
} }
@Test @Test
@ -87,8 +87,10 @@ public class CreatePremiumListActionTest {
action.inputData = "rich,USD 25\nricher,USD 1000\n"; action.inputData = "rich,USD 25\nricher,USD 1000\n";
action.run(); action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(loadPremiumListEntries(PremiumList.getUncached("foo").get())).hasSize(2); PremiumList premiumList = PremiumListDualDao.getLatestRevision("foo").get();
assertThat(getPremiumPrice("rich", Registry.get("foo"))).hasValue(Money.parse("USD 25")); assertThat(loadPremiumListEntries(premiumList)).hasSize(2);
assertThat(getPremiumPrice("diamond", Registry.get("foo"))).isEmpty(); assertThat(PremiumListDualDao.getPremiumPrice("rich", Registry.get("foo")))
.hasValue(Money.parse("USD 25"));
assertThat(PremiumListDualDao.getPremiumPrice("diamond", Registry.get("foo"))).isEmpty();
} }
} }

View file

@ -15,22 +15,23 @@
package google.registry.tools.server; package google.registry.tools.server;
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.registry.label.PremiumListUtils.getPremiumPrice;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.schema.tld.PremiumListUtils.parseToPremiumList;
import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadPremiumListEntries; import static google.registry.testing.DatabaseHelper.loadPremiumListEntries;
import static google.registry.util.ResourceUtils.readResourceUtf8; import static google.registry.util.ResourceUtils.readResourceUtf8;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Splitter;
import com.google.common.truth.Truth8;
import google.registry.model.registry.Registry; import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.PremiumList;
import google.registry.schema.tld.PremiumListDao; import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.schema.tld.PremiumListSqlDao;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.DatabaseHelper; import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeJsonResponse; import google.registry.testing.FakeJsonResponse;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import org.joda.money.Money; import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -75,24 +76,31 @@ class UpdatePremiumListActionTest {
@Test @Test
void test_success() { void test_success() {
PremiumListDao.saveNew( List<String> inputLines =
parseToPremiumList( Splitter.on('\n')
"foo", readResourceUtf8(DatabaseHelper.class, "default_premium_list_testdata.csv"))); .omitEmptyStrings()
.splitToList(
readResourceUtf8(DatabaseHelper.class, "default_premium_list_testdata.csv"));
PremiumListDualDao.save("foo", inputLines);
action.name = "foo"; action.name = "foo";
action.inputData = "rich,USD 75\nricher,USD 5000\npoor, USD 0.99"; action.inputData = "rich,USD 75\nricher,USD 5000\npoor, USD 0.99";
action.run(); action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(response.getStatus()).isEqualTo(SC_OK);
Registry registry = Registry.get("foo"); Registry registry = Registry.get("foo");
assertThat(loadPremiumListEntries(PremiumList.getUncached("foo").get())).hasSize(3); assertThat(loadPremiumListEntries(PremiumListDualDao.getLatestRevision("foo").get()))
assertThat(getPremiumPrice("rich", registry)).hasValue(Money.parse("USD 75")); .hasSize(3);
assertThat(getPremiumPrice("richer", registry)).hasValue(Money.parse("USD 5000")); Truth8.assertThat(PremiumListDualDao.getPremiumPrice("rich", registry))
assertThat(getPremiumPrice("poor", registry)).hasValue(Money.parse("USD 0.99")); .hasValue(Money.parse("USD 75"));
assertThat(getPremiumPrice("diamond", registry)).isEmpty(); Truth8.assertThat(PremiumListDualDao.getPremiumPrice("richer", registry))
.hasValue(Money.parse("USD 5000"));
Truth8.assertThat(PremiumListDualDao.getPremiumPrice("poor", registry))
.hasValue(Money.parse("USD 0.99"));
Truth8.assertThat(PremiumListDualDao.getPremiumPrice("diamond", registry)).isEmpty();
jpaTm() jpaTm()
.transact( .transact(
() -> { () -> {
PremiumList persistedList = PremiumListDao.getLatestRevision("foo").get(); PremiumList persistedList = PremiumListSqlDao.getLatestRevision("foo").get();
assertThat(persistedList.getLabelsToPrices()) assertThat(persistedList.getLabelsToPrices())
.containsEntry("rich", new BigDecimal("75.00")); .containsEntry("rich", new BigDecimal("75.00"));
assertThat(persistedList.getLabelsToPrices()) assertThat(persistedList.getLabelsToPrices())