mirror of
https://github.com/google/nomulus.git
synced 2025-05-21 11:49:37 +02:00
This change replaces all Ref objects in the code with Key objects. These are stored in datastore as the same object (raw datastore keys), so this is not a model change. Our best practices doc says to use Keys not Refs because: * The .get() method obscures what's actually going on - Much harder to visually audit the code for datastore loads - Hard to distinguish Ref<T> get()'s from Optional get()'s and Supplier get()'s * Implicit ofy().load() offers much less control - Antipattern for ultimate goal of making Ofy injectable - Can't control cache use or batch loading without making ofy() explicit anyway * Serialization behavior is surprising and could be quite dangerous/incorrect - Can lead to serialization errors. If it actually worked "as intended", it would lead to a Ref<> on a serialized object being replaced upon deserialization with a stale copy of the old value, which could potentially break all kinds of transactional expectations * Having both Ref<T> and Key<T> introduces extra boilerplate everywhere - E.g. helper methods all need to have Ref and Key overloads, or you need to call .key() to get the Key<T> for every Ref<T> you want to pass in - Creating a Ref<T> is more cumbersome, since it doesn't have all the create() overloads that Key<T> has, only create(Key<T>) and create(Entity) - no way to create directly from kind+ID/name, raw Key, websafe key string, etc. (Note that Refs are treated specially by Objectify's @Load method and Keys are not; we don't use that feature, but it is the one advantage Refs have over Keys.) The direct impetus for this change is that I am trying to audit our use of memcache, and the implicit .get() calls to datastore were making that very hard. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=131965491
387 lines
14 KiB
Java
387 lines
14 KiB
Java
// Copyright 2016 The Domain Registry 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.appengine.api.datastore.DatastoreServiceFactory.getDatastoreService;
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
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.allocateId;
|
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
|
import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION;
|
|
import static google.registry.util.CollectionUtils.nullToEmpty;
|
|
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|
|
|
import com.google.appengine.api.datastore.EntityNotFoundException;
|
|
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.ImmutableMap;
|
|
import com.google.common.collect.Maps;
|
|
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;
|
|
import com.googlecode.objectify.annotation.Id;
|
|
import com.googlecode.objectify.annotation.Ignore;
|
|
import com.googlecode.objectify.annotation.OnLoad;
|
|
import com.googlecode.objectify.annotation.Parent;
|
|
import com.googlecode.objectify.cmd.Query;
|
|
import google.registry.config.RegistryEnvironment;
|
|
import google.registry.model.Buildable;
|
|
import google.registry.model.ImmutableObject;
|
|
import google.registry.model.annotations.VirtualEntity;
|
|
import google.registry.model.registry.Registry;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
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.
|
|
*/
|
|
@Entity
|
|
@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;
|
|
|
|
@Ignore
|
|
Map<String, PremiumListEntry> premiumListMap;
|
|
|
|
/** Virtual parent entity for premium list entry entities associated with a single revision. */
|
|
@Entity
|
|
@VirtualEntity
|
|
public static class PremiumListRevision extends ImmutableObject {
|
|
@Parent
|
|
Key<PremiumList> parent;
|
|
|
|
@Id
|
|
long revisionId;
|
|
|
|
static Key<PremiumListRevision> createKey(PremiumList parent) {
|
|
PremiumListRevision revision = new PremiumListRevision();
|
|
revision.parent = Key.create(parent);
|
|
revision.revisionId = allocateId();
|
|
return Key.create(revision);
|
|
}
|
|
}
|
|
|
|
private static LoadingCache<String, PremiumList> cache = CacheBuilder
|
|
.newBuilder()
|
|
.expireAfterWrite(
|
|
RegistryEnvironment.get().config().getDomainLabelListCacheDuration().getMillis(),
|
|
MILLISECONDS)
|
|
.build(new CacheLoader<String, PremiumList>() {
|
|
@Override
|
|
public PremiumList load(final String listName) {
|
|
return ofy().doTransactionless(new Work<PremiumList>() {
|
|
@Override
|
|
public PremiumList run() {
|
|
return ofy().load()
|
|
.type(PremiumList.class)
|
|
.parent(getCrossTldKey())
|
|
.id(listName)
|
|
.now();
|
|
}});
|
|
}});
|
|
|
|
/**
|
|
* Gets the premium price for the specified label on the specified tld, or returns Optional.absent
|
|
* if there is no premium price.
|
|
*/
|
|
public static Optional<Money> getPremiumPrice(String label, String tld) {
|
|
Registry registry = Registry.get(checkNotNull(tld, "tld"));
|
|
if (registry.getPremiumList() == null) {
|
|
return Optional.<Money> absent();
|
|
}
|
|
String listName = registry.getPremiumList().getName();
|
|
Optional<PremiumList> premiumList = get(listName);
|
|
if (!premiumList.isPresent()) {
|
|
throw new IllegalStateException("Could not load premium list named " + listName);
|
|
}
|
|
return premiumList.get().getPremiumPrice(label);
|
|
}
|
|
|
|
@OnLoad
|
|
private void loadPremiumListMap() {
|
|
try {
|
|
ImmutableMap.Builder<String, PremiumListEntry> entriesMap = new ImmutableMap.Builder<>();
|
|
if (revisionKey != null) {
|
|
for (PremiumListEntry entry : loadEntriesForCurrentRevision()) {
|
|
entriesMap.put(entry.getLabel(), entry);
|
|
}
|
|
}
|
|
premiumListMap = entriesMap.build();
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Could not retrieve entries for premium list " + name, e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the premium price for the specified label in the current PremiumList, or returns
|
|
* Optional.absent if there is no premium price.
|
|
*/
|
|
public Optional<Money> getPremiumPrice(String label) {
|
|
return Optional.fromNullable(
|
|
premiumListMap.containsKey(label) ? premiumListMap.get(label).getValue() : null);
|
|
}
|
|
|
|
public Map<String, PremiumListEntry> getPremiumListEntries() {
|
|
return nullToEmptyImmutableCopy(premiumListMap);
|
|
}
|
|
|
|
public Key<PremiumListRevision> getRevisionKey() {
|
|
return revisionKey;
|
|
}
|
|
|
|
/** Returns the PremiumList with the specified name. */
|
|
public static Optional<PremiumList> get(String name) {
|
|
try {
|
|
return Optional.of(cache.get(name));
|
|
} catch (InvalidCacheLoadException e) {
|
|
return Optional.<PremiumList> absent();
|
|
} catch (ExecutionException e) {
|
|
throw new UncheckedExecutionException("Could not retrieve premium list named " + name, e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether a PremiumList of the given name exists, without going through the overhead
|
|
* of loading up all of the premium list entities. Also does not hit the cache.
|
|
*/
|
|
public static boolean exists(String name) {
|
|
try {
|
|
// Use DatastoreService to bypass the @OnLoad method that loads the premium list entries.
|
|
getDatastoreService().get(Key.create(getCrossTldKey(), PremiumList.class, name).getRaw());
|
|
return true;
|
|
} catch (EntityNotFoundException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A premium list entry entity, persisted to Datastore. Each instance represents the price of a
|
|
* single label on a given TLD.
|
|
*/
|
|
@Entity
|
|
@Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION)
|
|
public static class PremiumListEntry extends DomainLabelEntry<Money, PremiumListEntry>
|
|
implements Buildable {
|
|
|
|
@Parent
|
|
Key<PremiumListRevision> parent;
|
|
|
|
Money price;
|
|
|
|
@Override
|
|
public Money getValue() {
|
|
return price;
|
|
}
|
|
|
|
@Override
|
|
public Builder asBuilder() {
|
|
return new Builder(clone(this));
|
|
}
|
|
|
|
/**
|
|
* A builder for constructing {@link PremiumList} objects, since they are immutable.
|
|
*/
|
|
public static class Builder extends DomainLabelEntry.Builder<PremiumListEntry, Builder> {
|
|
public Builder() {}
|
|
|
|
private Builder(PremiumListEntry instance) {
|
|
super(instance);
|
|
}
|
|
|
|
public Builder setParent(Key<PremiumListRevision> parentKey) {
|
|
getInstance().parent = parentKey;
|
|
return this;
|
|
}
|
|
|
|
public Builder setPrice(Money price) {
|
|
getInstance().price = price;
|
|
return this;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
PremiumListEntry createFromLine(String originalLine) {
|
|
List<String> lineAndComment = splitOnComment(originalLine);
|
|
if (lineAndComment.isEmpty()) {
|
|
return null;
|
|
}
|
|
String line = lineAndComment.get(0);
|
|
String comment = lineAndComment.get(1);
|
|
List<String> parts = Splitter.on(',').trimResults().splitToList(line);
|
|
checkArgument(parts.size() == 2, "Could not parse line in premium list: %s", originalLine);
|
|
return new PremiumListEntry.Builder()
|
|
.setLabel(parts.get(0))
|
|
.setPrice(Money.parse(parts.get(1)))
|
|
.setComment(comment)
|
|
.build();
|
|
}
|
|
|
|
/**
|
|
* Persists a PremiumList object 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.
|
|
*/
|
|
public PremiumList saveAndUpdateEntries() {
|
|
final Optional<PremiumList> oldPremiumList = get(name);
|
|
// Only update entries if there's actually a new revision of the list to save (which there will
|
|
// be if the list content changes, vs just the description/metadata).
|
|
boolean entriesToUpdate =
|
|
!oldPremiumList.isPresent()
|
|
|| !Objects.equals(oldPremiumList.get().revisionKey, this.revisionKey);
|
|
// If needed, save the new child entities in a series of transactions.
|
|
if (entriesToUpdate) {
|
|
for (final List<PremiumListEntry> batch
|
|
: partition(premiumListMap.values(), TRANSACTION_BATCH_SIZE)) {
|
|
ofy().transactNew(new VoidWork() {
|
|
@Override
|
|
public void vrun() {
|
|
ofy().save().entities(batch);
|
|
}});
|
|
}
|
|
}
|
|
// Save the new PremiumList 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.
|
|
checkState(
|
|
Objects.equals(
|
|
ofy().load().type(PremiumList.class).parent(getCrossTldKey()).id(name).now(),
|
|
oldPremiumList.orNull()),
|
|
"PremiumList was concurrently edited");
|
|
PremiumList newList = PremiumList.this.asBuilder()
|
|
.setLastUpdateTime(now)
|
|
.setCreationTime(
|
|
oldPremiumList.isPresent() ? oldPremiumList.get().creationTime : now)
|
|
.build();
|
|
ofy().save().entity(newList);
|
|
return newList;
|
|
}});
|
|
// Update the cache.
|
|
PremiumList.cache.put(name, updated);
|
|
// If needed and there are any, delete the entities under the old PremiumList.
|
|
if (entriesToUpdate && oldPremiumList.isPresent()) {
|
|
oldPremiumList.get().deleteEntries();
|
|
}
|
|
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);
|
|
}});
|
|
deleteEntries();
|
|
cache.invalidate(name);
|
|
}
|
|
|
|
private void deleteEntries() {
|
|
if (revisionKey == null) {
|
|
return;
|
|
}
|
|
for (final List<Key<PremiumListEntry>> batch : partition(
|
|
loadEntriesForCurrentRevision().keys(),
|
|
TRANSACTION_BATCH_SIZE)) {
|
|
ofy().transactNew(new VoidWork() {
|
|
@Override
|
|
public void vrun() {
|
|
ofy().delete().keys(batch);
|
|
}});
|
|
}
|
|
}
|
|
|
|
private Query<PremiumListEntry> loadEntriesForCurrentRevision() {
|
|
return ofy().load().type(PremiumListEntry.class).ancestor(revisionKey);
|
|
}
|
|
|
|
@Override
|
|
public Builder asBuilder() {
|
|
return new Builder(clone(this));
|
|
}
|
|
|
|
/** A builder for constructing {@link PremiumList} objects, since they are immutable. */
|
|
public static class Builder extends BaseDomainLabelList.Builder<PremiumList, Builder> {
|
|
|
|
public Builder() {}
|
|
|
|
private Builder(PremiumList instance) {
|
|
super(instance);
|
|
}
|
|
|
|
private boolean entriesWereUpdated;
|
|
|
|
public Builder setPremiumListMap(ImmutableMap<String, PremiumListEntry> premiumListMap) {
|
|
entriesWereUpdated = true;
|
|
getInstance().premiumListMap = premiumListMap;
|
|
return this;
|
|
}
|
|
|
|
/** Updates the premiumListMap from input lines. */
|
|
public Builder setPremiumListMapFromLines(Iterable<String> lines) {
|
|
return setPremiumListMap(getInstance().parse(lines));
|
|
}
|
|
|
|
@Override
|
|
public PremiumList build() {
|
|
final PremiumList instance = getInstance();
|
|
if (getInstance().revisionKey == null || entriesWereUpdated) {
|
|
getInstance().revisionKey = PremiumListRevision.createKey(instance);
|
|
}
|
|
// When we build an instance, make sure all entries are parented on its revisionKey.
|
|
instance.premiumListMap = Maps.transformValues(
|
|
nullToEmpty(instance.premiumListMap),
|
|
new Function<PremiumListEntry, PremiumListEntry>() {
|
|
@Override
|
|
public PremiumListEntry apply(PremiumListEntry entry) {
|
|
return entry.asBuilder().setParent(instance.revisionKey).build();
|
|
}});
|
|
return super.build();
|
|
}
|
|
}
|
|
}
|