google-nomulus/java/google/registry/model/ofy/ObjectifyService.java
cgoldfeder aa10792f73 Test that marshaling a domain only does one datastore fetch.
This is true even though the domain has three fields (a contact,
a host, and the registrant) whose foreign keys need to be loaded.

This CL also adds the generic ability to do these sort of tests
elsewhere in the code, by instrumenting the datastore instance
used by Objectify to store static counts of method calls.

TESTED=patched in a rollback of [] and confirmed that the
test failed because there were three reads.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=123885768
2016-06-06 13:31:16 -04:00

195 lines
8.8 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.ofy;
import static com.google.appengine.api.memcache.ErrorHandlers.getConsistentLogAndContinue;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.not;
import static com.googlecode.objectify.ObjectifyService.factory;
import static google.registry.util.TypeUtils.hasAnnotation;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceConfig;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.common.annotations.VisibleForTesting;
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.annotation.Entity;
import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.impl.translate.TranslatorFactory;
import com.googlecode.objectify.impl.translate.opt.joda.MoneyStringTranslatorFactory;
import google.registry.config.RegistryEnvironment;
import google.registry.model.EntityClasses;
import google.registry.model.ImmutableObject;
import google.registry.model.translators.CidrAddressBlockTranslatorFactory;
import google.registry.model.translators.CommitLogRevisionsTranslatorFactory;
import google.registry.model.translators.CreateAutoTimestampTranslatorFactory;
import google.registry.model.translators.CurrencyUnitTranslatorFactory;
import google.registry.model.translators.DurationTranslatorFactory;
import google.registry.model.translators.InetAddressTranslatorFactory;
import google.registry.model.translators.ReadableInstantUtcTranslatorFactory;
import google.registry.model.translators.UpdateAutoTimestampTranslatorFactory;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
/**
* An instance of Ofy, obtained via {@code #ofy()}, should be used to access all persistable
* objects. The class contains a static initializer to call factory().register(...) on all
* persistable objects in this package.
*/
public class ObjectifyService {
/**
* A placeholder String passed into DatastoreService.allocateIds that ensures that all ids are
* initialized from the same id pool.
*/
public static final String APP_WIDE_ALLOCATION_KIND = "common";
/** A singleton instance of our Ofy wrapper. */
private static final Ofy OFY = new Ofy(null);
/**
* Returns a singleton {@link Ofy} instance.
*
* <p><b>Deprecated:</b> This will go away once everything injects {@code Ofy}.
*/
public static Ofy ofy() {
return OFY;
}
static {
initOfyOnce();
}
/** Ensures that Objectify has been fully initialized. */
public static void initOfy() {
// This method doesn't actually do anything; it's here so that callers have something to call
// to ensure that the static initialization of ObjectifyService has been performed (which Java
// guarantees will happen exactly once, before any static methods are invoked).
//
// See JLS section 12.4: http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4
}
/**
* Performs static initialization for Objectify to register types and do other setup.
*
* <p>This method is non-idempotent, so it should only be called exactly once, which is achieved
* by calling it from this class's static initializer block.
*/
private static void initOfyOnce() {
// Set an ObjectifyFactory that uses our extended ObjectifyImpl.
// The "false" argument means that we are not using the v5-style Objectify embedded entities.
com.googlecode.objectify.ObjectifyService.setFactory(new ObjectifyFactory(false) {
@Override
public Objectify begin() {
return new SessionKeyExposingObjectify(this);
}
@Override
protected AsyncDatastoreService createRawAsyncDatastoreService(DatastoreServiceConfig cfg) {
// In the unit test environment, wrap the datastore service in a proxy that can be used to
// examine the number of requests sent to datastore.
AsyncDatastoreService service = super.createRawAsyncDatastoreService(cfg);
return RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
? new RequestCountingAsyncDatastoreService(service)
: service;
}});
// Translators must be registered before any entities can be registered.
registerTranslators();
registerEntityClasses(EntityClasses.ALL_CLASSES);
// Set the memcache error handler so that we don't see internally logged errors.
factory().setMemcacheErrorHandler(getConsistentLogAndContinue(Level.INFO));
}
/** Register translators that allow less common types to be stored directly in Datastore. */
private static void registerTranslators() {
for (TranslatorFactory<?> translatorFactory : Arrays.asList(
new ReadableInstantUtcTranslatorFactory(),
new CidrAddressBlockTranslatorFactory(),
new CurrencyUnitTranslatorFactory(),
new DurationTranslatorFactory(),
new InetAddressTranslatorFactory(),
new MoneyStringTranslatorFactory(),
new CreateAutoTimestampTranslatorFactory(),
new UpdateAutoTimestampTranslatorFactory(),
new CommitLogRevisionsTranslatorFactory())) {
factory().getTranslators().add(translatorFactory);
}
}
/** Register classes that can be persisted via Objectify as datastore entities. */
private static void registerEntityClasses(
Iterable<Class<? extends ImmutableObject>> entityClasses) {
// Register all the @Entity classes before any @EntitySubclass classes so that we can check
// that every @Entity registration is a new kind and every @EntitySubclass registration is not.
// This is future-proofing for Objectify 5.x where the registration logic gets less lenient.
for (Class<?> clazz : Iterables.concat(
Iterables.filter(entityClasses, hasAnnotation(Entity.class)),
Iterables.filter(entityClasses, not(hasAnnotation(Entity.class))))) {
String kind = Key.getKind(clazz);
boolean registered = factory().getMetadata(kind) != null;
if (clazz.isAnnotationPresent(Entity.class)) {
// Objectify silently ignores re-registrations for a given kind string, even if the classes
// being registered are distinct. Throw an exception if that would happen here.
checkState(!registered,
"Kind '%s' already registered, cannot register new @Entity %s",
kind, clazz.getCanonicalName());
} else if (clazz.isAnnotationPresent(EntitySubclass.class)) {
// Ensure that any @EntitySubclass classes have also had their parent @Entity registered,
// which Objectify nominally requires but doesn't enforce in 4.x (though it may in 5.x).
checkState(registered,
"No base entity for kind '%s' registered yet, cannot register new @EntitySubclass %s",
kind, clazz.getCanonicalName());
}
com.googlecode.objectify.ObjectifyService.register(clazz);
// Autogenerated ids make the commit log code very difficult since we won't always be able
// to create a key for an entity immediately when requesting a save. Disallow that here.
checkState(
!factory().getMetadata(clazz).getKeyMetadata().isIdGeneratable(),
"Can't register %s: Autogenerated ids (@Id on a Long) are not supported.", kind);
}
}
/** Counts of used ids for use in unit tests. Outside tests this is never used. */
private static final AtomicLong nextTestId = new AtomicLong(1); // ids cannot be zero
/** Allocates an id. */
public static long allocateId() {
return RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get())
? nextTestId.getAndIncrement()
: DatastoreServiceFactory.getDatastoreService()
.allocateIds(APP_WIDE_ALLOCATION_KIND, 1)
.iterator()
.next()
.getId();
}
/** Resets the global test id counter (i.e. sets the next id to 1). */
@VisibleForTesting
public static void resetNextTestId() {
checkState(RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get()),
"Can't call resetTestIdCounts() from RegistryEnvironment.%s",
RegistryEnvironment.get());
nextTestId.set(1); // ids cannot be zero
}
}