// 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.ofy; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Predicates.notNull; import static com.google.common.collect.Maps.uniqueIndex; import static com.googlecode.objectify.ObjectifyService.ofy; import static google.registry.config.RegistryConfig.getBaseOfyRetryDuration; import static google.registry.util.CollectionUtils.union; import static google.registry.util.ObjectifyUtils.OBJECTS_TO_KEYS; import com.google.appengine.api.datastore.DatastoreFailureException; import com.google.appengine.api.datastore.DatastoreTimeoutException; import com.google.appengine.api.taskqueue.TransientFailureException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.googlecode.objectify.Key; import com.googlecode.objectify.Objectify; import com.googlecode.objectify.ObjectifyFactory; import com.googlecode.objectify.Work; import com.googlecode.objectify.cmd.Deleter; import com.googlecode.objectify.cmd.Loader; import com.googlecode.objectify.cmd.Saver; import google.registry.model.annotations.NotBackedUp; import google.registry.model.annotations.VirtualEntity; import google.registry.model.ofy.ReadOnlyWork.KillTransactionException; import google.registry.util.Clock; import google.registry.util.FormattingLogger; import google.registry.util.NonFinalForTesting; import google.registry.util.Sleeper; import google.registry.util.SystemClock; import google.registry.util.SystemSleeper; import java.lang.annotation.Annotation; import java.util.Objects; import javax.inject.Inject; import org.joda.time.DateTime; import org.joda.time.Duration; /** * A wrapper around ofy(). * *
The primary purpose of this class is to add functionality to support commit logs. It is * simpler to wrap {@link Objectify} rather than extend it because this way we can remove some * methods that we don't really want exposed and add some shortcuts. */ public class Ofy { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); /** * Recommended memcache expiration time, which is one hour, specified in seconds. * *
This value should used as a cache expiration time for any entities annotated with an * Objectify {@code @Cache} annotation, to put an upper bound on unlikely-but-possible divergence * between memcache and Datastore when a memcache write fails. */ public static final int RECOMMENDED_MEMCACHE_EXPIRATION = 3600; /** Default clock for transactions that don't provide one. */ @NonFinalForTesting static Clock clock = new SystemClock(); /** Default sleeper for transactions that don't provide one. */ @NonFinalForTesting static Sleeper sleeper = new SystemSleeper(); /** * An injected clock that overrides the static clock. * *
Eventually the static clock should go away when we are 100% injected, but for now we need to
* preserve the old way of overriding the clock in tests by changing the static field.
*/
private final Clock injectedClock;
/** Retry for 8^2 * 100ms = ~25 seconds. */
private static final int NUM_RETRIES = 8;
@Inject
public Ofy(Clock injectedClock) {
this.injectedClock = injectedClock;
}
/**
* Thread local transaction info. There can only be one active transaction on a thread at a given
* time, and this will hold metadata for it.
*/
static final ThreadLocal In general, this is the correct method to use for loads. Loading from memcache can, in rare
* instances, produce a stale result (when a memcache write fails and the previous result is not
* cleared out) and so using memcache should be avoided unless the caller can tolerate staleness
* until the memcache expiration time and there is a specific need for very low latency that is
* worth the extra complexity of reasoning about caching.
*/
public Loader load() {
// TODO(b/27424173): change to false when memcache audit changes are implemented.
return ofy().cache(true).load();
}
/**
* Load from Datastore, bypassing memcache even when the results might be there.
*
* In general, prefer {@link #load} over this method. Loading from memcache can, in rare
* instances, produce a stale result (when a memcache write fails and the previous result is not
* cleared out) and so using memcache should be avoided unless the caller can tolerate staleness
* until the memcache expiration time and there is a specific need for very low latency that is
* worth the extra complexity of reasoning about caching.
*/
public Loader loadWithMemcache() {
return ofy().cache(true).load();
}
/**
* Delete, augmented to enroll the deleted entities in a commit log.
*
* We only allow this in transactions so commit logs can be written in tandem with the delete.
*/
public Deleter delete() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable No backups get written.
*/
public Deleter deleteWithoutBackup() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable We only allow this in transactions so commit logs can be written in tandem with the save.
*/
public Saver save() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable> entities) {
assertInTransaction();
checkState(Iterables.all(entities, notNull()), "Can't save a null entity.");
checkProhibitedAnnotations(entities, NotBackedUp.class, VirtualEntity.class);
ImmutableMap No backups get written.
*/
public Saver saveWithoutBackup() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable> entities) {
checkProhibitedAnnotations(entities, VirtualEntity.class);
}
};
}
private Clock getClock() {
return injectedClock == null ? clock : injectedClock;
}
/** Execute a transaction. */
public This method is broken out from {@link #transactNew(Work)} for testing purposes.
*/
@VisibleForTesting
This is useful in cases where we want to load the latest possible data from Datastore but
* don't need point-in-time consistency across loads and consequently don't need a transaction.
* Note that unlike a transaction's fresh session cache, the contents of this cache will be
* discarded once the work completes, rather than being propagated into the enclosing session.
*/
public