Move premium list static helper methods into their own class

It was kind of messy having all of that logic living alongside the
entities themselves.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=148498024
This commit is contained in:
mcilwain 2017-02-24 13:38:59 -08:00 committed by Ben McIlwain
parent 388dd1055e
commit ea4e471c04
12 changed files with 523 additions and 421 deletions

View file

@ -17,7 +17,7 @@ package google.registry.model.pricing;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;
import static google.registry.model.registry.Registry.TldState.SUNRISE;
import static google.registry.model.registry.label.PremiumList.getPremiumPrice;
import static google.registry.model.registry.label.PremiumListUtils.getPremiumPrice;
import static google.registry.model.registry.label.ReservationType.NAME_COLLISION;
import static google.registry.model.registry.label.ReservedList.getReservation;
import static google.registry.util.DomainNameUtils.getTldFromDomainName;

View file

@ -13,10 +13,7 @@
// limitations under the License.
package google.registry.model.registry.label;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.partition;
import static com.google.common.hash.Funnels.unencodedCharsFunnel;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.config.RegistryConfig.getSingletonCachePersistDuration;
@ -28,20 +25,15 @@ import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
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.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.BloomFilter;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.VoidWork;
import com.googlecode.objectify.Work;
import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Entity;
@ -55,13 +47,11 @@ import google.registry.model.registry.Registry;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
/**
* A premium list entity, persisted to Datastore, that is used to check domain label prices.
@ -71,9 +61,6 @@ import org.joda.time.DateTime;
@Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION)
public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.PremiumListEntry> {
/** The number of premium list entry entities that are created and deleted per batch. */
private static final int TRANSACTION_BATCH_SIZE = 200;
/** Stores the revision key for the set of currently used premium list entry entities. */
Key<PremiumListRevision> revisionKey;
@ -139,7 +126,7 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
* <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.
*/
private static final LoadingCache<String, PremiumList> cachePremiumLists =
static final LoadingCache<String, PremiumList> cachePremiumLists =
CacheBuilder.newBuilder()
.expireAfterWrite(getDomainLabelListCacheDuration().getMillis(), MILLISECONDS)
.build(new CacheLoader<String, PremiumList>() {
@ -163,7 +150,7 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
* {@link PremiumListRevision} is immutable and cannot ever be changed once created, so its cache
* need not ever expire.
*/
private static final LoadingCache<Key<PremiumListRevision>, PremiumListRevision>
static final LoadingCache<Key<PremiumListRevision>, PremiumListRevision>
cachePremiumListRevisions =
CacheBuilder.newBuilder()
.expireAfterWrite(getSingletonCachePersistDuration().getMillis(), MILLISECONDS)
@ -214,67 +201,6 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
}});
}});
/**
* 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.<Money> absent();
}
String listName = registry.getPremiumList().getName();
Optional<PremiumList> optionalPremiumList = get(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.probablePremiumLabels != null,
"Probable premium labels bloom filter is null on revision '%s'",
premiumList.getRevisionKey());
if (revision.probablePremiumLabels.mightContain(label)) {
Key<PremiumListEntry> entryKey =
Key.create(premiumList.getRevisionKey(), PremiumListEntry.class, label);
try {
Optional<PremiumListEntry> entry = cachePremiumListEntries.get(entryKey);
return (entry.isPresent()) ? Optional.of(entry.get().getValue()) : Optional.<Money>absent();
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException("Could not load premium list entry " + entryKey, e);
}
} else {
return Optional.<Money>absent();
}
}
/**
* Loads and returns the entire premium list map.
*
* <p>This load operation is quite expensive for large premium lists because each premium list
* entry is a separate Datastore entity, and loading them this way bypasses the in-memory caches.
* Do not use this method if all you need to do is check the price of a small number of labels!
*/
@VisibleForTesting
public Map<String, PremiumListEntry> loadPremiumListEntries() {
try {
ImmutableMap.Builder<String, PremiumListEntry> entriesMap = new ImmutableMap.Builder<>();
if (revisionKey != null) {
for (PremiumListEntry entry : queryEntriesForCurrentRevision()) {
entriesMap.put(entry.getLabel(), entry);
}
}
return entriesMap.build();
} catch (Exception e) {
throw new RuntimeException("Could not retrieve entries for premium list " + name, e);
}
}
@VisibleForTesting
public Key<PremiumListRevision> getRevisionKey() {
return revisionKey;
@ -291,11 +217,6 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
}
}
/** Returns whether a PremiumList of the given name exists, bypassing the cache. */
public static boolean exists(String name) {
return ofy().load().key(Key.create(getCrossTldKey(), PremiumList.class, name)).now() != null;
}
/**
* A premium list entry entity, persisted to Datastore. Each instance represents the price of a
* single label on a given TLD.
@ -361,124 +282,12 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
.build();
}
public static PremiumList saveWithEntries(
PremiumList premiumList, Iterable<String> premiumListLines) {
return saveWithEntries(premiumList, premiumList.parse(premiumListLines));
}
/** Re-parents the given {@link PremiumListEntry}s on the given {@link PremiumListRevision}. */
public static ImmutableSet<PremiumListEntry> parentEntriesOnRevision(
Iterable<PremiumListEntry> entries, final Key<PremiumListRevision> revisionKey) {
return FluentIterable.from(firstNonNull(entries, ImmutableSet.of()))
.transform(
new Function<PremiumListEntry, PremiumListEntry>() {
@Override
public PremiumListEntry apply(PremiumListEntry entry) {
return entry.asBuilder().setParent(revisionKey).build();
}
})
.toSet();
}
/**
* 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 saveWithEntries(
final PremiumList premiumList, ImmutableMap<String, PremiumListEntry> premiumListEntries) {
final Optional<PremiumList> oldPremiumList = get(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 =
parentEntriesOnRevision(
firstNonNull(premiumListEntries.values(), ImmutableSet.of()), newRevisionKey);
// Save the new child entities in a series of transactions.
for (final List<PremiumListEntry> batch : partition(parentedEntries, TRANSACTION_BATCH_SIZE)) {
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().save().entities(batch);
}});
}
// Save the new PremiumList and revision itself.
PremiumList updated = ofy().transactNew(new Work<PremiumList>() {
@Override
public PremiumList run() {
DateTime now = ofy().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.orNull()),
"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;
}});
// Update the cache.
cachePremiumLists.put(premiumList.getName(), updated);
// Delete the entities under the old PremiumList.
if (oldPremiumList.isPresent()) {
oldPremiumList.get().deleteRevisionAndEntries();
}
return updated;
}
@Override
public boolean refersToKey(Registry registry, Key<? extends BaseDomainLabelList<?, ?>> key) {
return Objects.equals(registry.getPremiumList(), key);
}
/** Deletes the PremiumList and all of its child entities. */
public void delete() {
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().delete().entity(PremiumList.this);
}});
deleteRevisionAndEntries();
cachePremiumLists.invalidate(name);
}
private void deleteRevisionAndEntries() {
if (revisionKey == null) {
return;
}
for (final List<Key<PremiumListEntry>> batch : partition(
queryEntriesForCurrentRevision().keys(),
TRANSACTION_BATCH_SIZE)) {
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().delete().keys(batch);
}});
}
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().delete().key(revisionKey);
}});
}
private Query<PremiumListEntry> queryEntriesForCurrentRevision() {
Query<PremiumListEntry> queryEntriesForCurrentRevision() {
return ofy().load().type(PremiumListEntry.class).ancestor(revisionKey);
}

View file

@ -0,0 +1,234 @@
// 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.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
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.PremiumList.cachePremiumListEntries;
import static google.registry.model.registry.label.PremiumList.cachePremiumListRevisions;
import static google.registry.model.registry.label.PremiumList.cachePremiumLists;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.VoidWork;
import com.googlecode.objectify.Work;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.model.registry.label.PremiumList.PremiumListRevision;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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. */
static final int TRANSACTION_BATCH_SIZE = 200;
/**
* 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.<Money> absent();
}
String listName = registry.getPremiumList().getName();
Optional<PremiumList> optionalPremiumList = PremiumList.get(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.probablePremiumLabels != null,
"Probable premium labels bloom filter is null on revision '%s'",
premiumList.getRevisionKey());
if (revision.probablePremiumLabels.mightContain(label)) {
Key<PremiumListEntry> entryKey =
Key.create(premiumList.getRevisionKey(), PremiumListEntry.class, label);
try {
Optional<PremiumListEntry> entry = cachePremiumListEntries.get(entryKey);
return (entry.isPresent()) ? Optional.of(entry.get().getValue()) : Optional.<Money>absent();
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException("Could not load premium list entry " + entryKey, e);
}
} else {
return Optional.<Money>absent();
}
}
/**
* 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.get(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(
firstNonNull(premiumListEntries.values(), ImmutableSet.of()), newRevisionKey);
// Save the new child entities in a series of transactions.
for (final List<PremiumListEntry> batch :
partition(parentedEntries, TRANSACTION_BATCH_SIZE)) {
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().save().entities(batch);
}});
}
// Save the new PremiumList and revision itself.
PremiumList updated = ofy().transactNew(new Work<PremiumList>() {
@Override
public PremiumList run() {
DateTime now = ofy().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.orNull()),
"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;
}});
// Update the cache.
cachePremiumLists.put(premiumList.getName(), updated);
// Delete the entities under the old PremiumList.
if (oldPremiumList.isPresent()) {
deleteRevisionAndEntriesOfPremiumList(oldPremiumList.get());
}
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}. */
public static ImmutableSet<PremiumListEntry> parentPremiumListEntriesOnRevision(
Iterable<PremiumListEntry> entries, final Key<PremiumListRevision> revisionKey) {
return FluentIterable.from(firstNonNull(entries, ImmutableSet.of()))
.transform(
new Function<PremiumListEntry, PremiumListEntry>() {
@Override
public PremiumListEntry apply(PremiumListEntry entry) {
return entry.asBuilder().setParent(revisionKey).build();
}
})
.toSet();
}
/** Deletes the PremiumList and all of its child entities. */
public static void deletePremiumList(final PremiumList premiumList) {
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
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(
premiumList.queryEntriesForCurrentRevision().keys(),
TRANSACTION_BATCH_SIZE)) {
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().delete().keys(batch);
}});
}
ofy().transactNew(new VoidWork() {
@Override
public void vrun() {
ofy().delete().key(premiumList.getRevisionKey());
}});
}
/** 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;
}
/**
* Loads and returns the entire premium list map.
*
* <p>This load operation is quite expensive for large premium lists because each premium list
* entry is a separate Datastore entity, and loading them this way bypasses the in-memory caches.
* Do not use this method if all you need to do is check the price of a small number of labels!
*/
@VisibleForTesting
public static Map<String, PremiumListEntry> loadPremiumListEntries(PremiumList premiumList) {
try {
ImmutableMap.Builder<String, PremiumListEntry> entriesMap = new ImmutableMap.Builder<>();
if (premiumList.getRevisionKey() != null) {
for (PremiumListEntry entry : premiumList.queryEntriesForCurrentRevision()) {
entriesMap.put(entry.getLabel(), entry);
}
}
return entriesMap.build();
} catch (Exception e) {
throw new RuntimeException(
"Could not retrieve entries for premium list " + premiumList.getName(), e);
}
}
private PremiumListUtils() {}
}