mirror of
https://github.com/google/nomulus.git
synced 2025-07-21 18:26:12 +02:00
mv com/google/domain/registry google/registry
This change renames directories in preparation for the great package rename. The repository is now in a broken state because the code itself hasn't been updated. However this should ensure that git correctly preserves history for each file.
This commit is contained in:
parent
a41677aea1
commit
5012893c1d
2396 changed files with 0 additions and 0 deletions
90
java/google/registry/model/ofy/AugmentedDeleter.java
Normal file
90
java/google/registry/model/ofy/AugmentedDeleter.java
Normal file
|
@ -0,0 +1,90 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.domain.registry.util.ObjectifyUtils.OBJECTS_TO_KEYS;
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import com.google.common.base.Functions;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.Result;
|
||||
import com.googlecode.objectify.cmd.DeleteType;
|
||||
import com.googlecode.objectify.cmd.Deleter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A Deleter that forwards to {@code ofy().delete()}, but can be augmented via subclassing to
|
||||
* do custom processing on the keys to be deleted prior to their deletion.
|
||||
*/
|
||||
abstract class AugmentedDeleter implements Deleter {
|
||||
private final Deleter delegate = ofy().delete();
|
||||
|
||||
/** Extension method to allow this Deleter to do extra work prior to the actual delete. */
|
||||
protected abstract void handleDeletion(Iterable<Key<?>> keys);
|
||||
|
||||
@Override
|
||||
public Result<Void> entities(Iterable<?> entities) {
|
||||
handleDeletion(Iterables.transform(entities, OBJECTS_TO_KEYS));
|
||||
return delegate.entities(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> entities(Object... entities) {
|
||||
handleDeletion(FluentIterable.from(asList(entities)).transform(OBJECTS_TO_KEYS));
|
||||
return delegate.entities(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> entity(Object entity) {
|
||||
handleDeletion(Arrays.<Key<?>>asList(Key.create(entity)));
|
||||
return delegate.entity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> key(Key<?> key) {
|
||||
handleDeletion(Arrays.<Key<?>>asList(key));
|
||||
return delegate.keys(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> keys(Iterable<? extends Key<?>> keys) {
|
||||
// Magic to convert the type Iterable<? extends Key<?>> (a family of types which allows for
|
||||
// homogeneous iterables of a fixed Key<T> type, e.g. List<Key<Lock>>, and is convenient for
|
||||
// callers) into the type Iterable<Key<?>> (a concrete type of heterogeneous keys, which is
|
||||
// convenient for users). We do this by passing each key through the identity function
|
||||
// parameterized for Key<?>, which erases any homogeneous typing on the iterable.
|
||||
// See: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
|
||||
Iterable<Key<?>> retypedKeys = Iterables.transform(keys, Functions.<Key<?>>identity());
|
||||
handleDeletion(retypedKeys);
|
||||
return delegate.keys(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> keys(Key<?>... keys) {
|
||||
handleDeletion(Arrays.asList(keys));
|
||||
return delegate.keys(keys);
|
||||
}
|
||||
|
||||
/** Augmenting this gets ugly; you can always just use keys(Key.create(...)) instead. */
|
||||
@Override
|
||||
public DeleteType type(Class<?> clazz) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
63
java/google/registry/model/ofy/AugmentedSaver.java
Normal file
63
java/google/registry/model/ofy/AugmentedSaver.java
Normal file
|
@ -0,0 +1,63 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
|
||||
import com.google.appengine.api.datastore.Entity;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.Result;
|
||||
import com.googlecode.objectify.cmd.Saver;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A Saver that forwards to {@code ofy().save()}, but can be augmented via subclassing to
|
||||
* do custom processing on the entities to be saved prior to their saving.
|
||||
*/
|
||||
abstract class AugmentedSaver implements Saver {
|
||||
private final Saver delegate = ofy().save();
|
||||
|
||||
/** Extension method to allow this Saver to do extra work prior to the actual save. */
|
||||
protected abstract void handleSave(Iterable<?> entities);
|
||||
|
||||
@Override
|
||||
public <E> Result<Map<Key<E>, E>> entities(Iterable<E> entities) {
|
||||
handleSave(entities);
|
||||
return delegate.entities(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SafeVarargs
|
||||
public final <E> Result<Map<Key<E>, E>> entities(E... entities) {
|
||||
handleSave(Arrays.asList(entities));
|
||||
return delegate.entities(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> Result<Key<E>> entity(E entity) {
|
||||
handleSave(ImmutableList.of(entity));
|
||||
return delegate.entity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity toEntity(Object pojo) {
|
||||
// No call to the extension method, since toEntity() doesn't do any actual saving.
|
||||
return delegate.toEntity(pojo);
|
||||
}
|
||||
}
|
177
java/google/registry/model/ofy/CommitLogBucket.java
Normal file
177
java/google/registry/model/ofy/CommitLogBucket.java
Normal file
|
@ -0,0 +1,177 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.DiscreteDomain.integers;
|
||||
import static com.google.domain.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ContiguousSet;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.domain.registry.config.RegistryEnvironment;
|
||||
import com.google.domain.registry.model.Buildable;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp.Reason;
|
||||
import com.google.domain.registry.util.NonFinalForTesting;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Root for a random commit log bucket.
|
||||
*
|
||||
* <p>This is used to shard {@link CommitLogManifest} objects into
|
||||
* {@link com.google.domain.registry.config.RegistryConfig#getCommitLogBucketCount() N} entity
|
||||
* groups. This increases transaction throughput, while maintaining the ability to perform
|
||||
* strongly-consistent ancestor queries.
|
||||
*
|
||||
* @see "https://cloud.google.com/appengine/articles/scaling/contention"
|
||||
*/
|
||||
@Entity
|
||||
@NotBackedUp(reason = Reason.COMMIT_LOGS)
|
||||
public class CommitLogBucket extends ImmutableObject implements Buildable {
|
||||
|
||||
private static final RegistryEnvironment ENVIRONMENT = RegistryEnvironment.get();
|
||||
|
||||
/** Ranges from 1 to {@link #getNumBuckets()}, inclusive; starts at 1 since IDs can't be 0. */
|
||||
@Id
|
||||
long bucketNum;
|
||||
|
||||
/** The timestamp of the last {@link CommitLogManifest} written to this bucket. */
|
||||
DateTime lastWrittenTime = START_OF_TIME;
|
||||
|
||||
public int getBucketNum() {
|
||||
return (int) bucketNum;
|
||||
}
|
||||
|
||||
public DateTime getLastWrittenTime() {
|
||||
return lastWrittenTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key for the specified bucket ID.
|
||||
*
|
||||
* <p>Always use this method in preference to manually creating bucket keys, since manual keys
|
||||
* are not guaranteed to have a valid bucket ID number.
|
||||
*/
|
||||
public static Key<CommitLogBucket> getBucketKey(int num) {
|
||||
checkArgument(getBucketIdRange().contains(num), "%s not in %s", num, getBucketIdRange());
|
||||
return getBucketKeyUnsafe(num);
|
||||
}
|
||||
|
||||
private static Key<CommitLogBucket> getBucketKeyUnsafe(int num) {
|
||||
return Key.create(CommitLogBucket.class, num);
|
||||
}
|
||||
|
||||
/** Returns a sorted set of all the possible numeric bucket IDs. */
|
||||
public static ImmutableSortedSet<Integer> getBucketIds() {
|
||||
return ContiguousSet.create(getBucketIdRange(), integers());
|
||||
}
|
||||
|
||||
private static int getNumBuckets() {
|
||||
return ENVIRONMENT.config().getCommitLogBucketCount();
|
||||
}
|
||||
|
||||
private static Range<Integer> getBucketIdRange() {
|
||||
return Range.closed(1, getNumBuckets());
|
||||
}
|
||||
|
||||
/** Returns an arbitrary numeric bucket ID. Default behavior is randomly chosen IDs. */
|
||||
public static int getArbitraryBucketId() {
|
||||
return bucketIdSupplier.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplier of valid bucket IDs to use for {@link #getArbitraryBucketId()}.
|
||||
*
|
||||
* <p>Default supplier is one that returns bucket IDs via uniform random selection, but can be
|
||||
* overridden in tests that rely on predictable bucket assignment for commit logs.
|
||||
*/
|
||||
@NonFinalForTesting
|
||||
private static Supplier<Integer> bucketIdSupplier =
|
||||
new Supplier<Integer>() {
|
||||
private final Random random = new Random();
|
||||
|
||||
@Override
|
||||
public Integer get() {
|
||||
return random.nextInt(getNumBuckets()) + 1; // Add 1 since IDs can't be 0.
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns the loaded bucket for the given key, or a new object if the bucket doesn't exist. */
|
||||
public static CommitLogBucket loadBucket(Key<CommitLogBucket> bucketKey) {
|
||||
CommitLogBucket bucket = ofy().load().key(bucketKey).now();
|
||||
return bucket == null
|
||||
? new CommitLogBucket.Builder().setBucketNum(bucketKey.getId()).build()
|
||||
: bucket;
|
||||
}
|
||||
|
||||
/** Returns the set of all loaded commit log buckets, filling in missing buckets with new ones. */
|
||||
public static ImmutableSet<CommitLogBucket> loadAllBuckets() {
|
||||
ofy().load().keys(getAllBucketKeys()); // Load all buckets into session cache at once.
|
||||
ImmutableSet.Builder<CommitLogBucket> allBuckets = new ImmutableSet.Builder<>();
|
||||
for (Key<CommitLogBucket> key : getAllBucketKeys()) {
|
||||
allBuckets.add(loadBucket(key));
|
||||
}
|
||||
return allBuckets.build();
|
||||
}
|
||||
|
||||
/** Returns all commit log bucket keys, in ascending order by bucket ID. */
|
||||
public static ImmutableSet<Key<CommitLogBucket>> getAllBucketKeys() {
|
||||
return FluentIterable.from(getBucketIds())
|
||||
.transform(new Function<Integer, Key<CommitLogBucket>>() {
|
||||
@Override
|
||||
public Key<CommitLogBucket> apply(Integer bucketId) {
|
||||
return getBucketKeyUnsafe(bucketId);
|
||||
}})
|
||||
.toSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** A builder for {@link CommitLogBucket} since it is immutable. */
|
||||
public static class Builder extends Buildable.Builder<CommitLogBucket> {
|
||||
public Builder() {}
|
||||
|
||||
public Builder(CommitLogBucket instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setBucketNum(long bucketNum) {
|
||||
getInstance().bucketNum = bucketNum;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLastWrittenTime(DateTime lastWrittenTime) {
|
||||
getInstance().lastWrittenTime = lastWrittenTime;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
98
java/google/registry/model/ofy/CommitLogCheckpoint.java
Normal file
98
java/google/registry/model/ofy/CommitLogCheckpoint.java
Normal file
|
@ -0,0 +1,98 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp.Reason;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Entity representing a point-in-time consistent view of datastore, based on commit logs.
|
||||
*
|
||||
* <p>Conceptually, this entity consists of two pieces of information: the checkpoint "wall" time
|
||||
* and a set of bucket checkpoint times. The former is the ID for this checkpoint (constrained
|
||||
* to be unique upon checkpoint creation) and also represents the approximate wall time of the
|
||||
* consistent datastore view this checkpoint represents. The latter is really a mapping from
|
||||
* bucket ID to timestamp, where the timestamp dictates the upper bound (inclusive) on commit logs
|
||||
* from that bucket to include when restoring the datastore to this checkpoint.
|
||||
*/
|
||||
@Entity
|
||||
@NotBackedUp(reason = Reason.COMMIT_LOGS)
|
||||
public class CommitLogCheckpoint extends ImmutableObject {
|
||||
|
||||
/** Shared singleton parent entity for commit log checkpoints. */
|
||||
@Parent
|
||||
Key<CommitLogCheckpointRoot> parent = CommitLogCheckpointRoot.getKey();
|
||||
|
||||
/** The checkpoint's approximate "wall" time (in millis since the epoch). */
|
||||
@Id
|
||||
long checkpointTime;
|
||||
|
||||
/** Bucket checkpoint times for this checkpoint, ordered to match up with buckets 1-N. */
|
||||
List<DateTime> bucketTimestamps = new ArrayList<>();
|
||||
|
||||
public DateTime getCheckpointTime() {
|
||||
return new DateTime(checkpointTime, UTC);
|
||||
}
|
||||
|
||||
/** Returns the bucket checkpoint times as a map from bucket ID to commit timestamp. */
|
||||
public ImmutableMap<Integer, DateTime> getBucketTimestamps() {
|
||||
ImmutableMap.Builder<Integer, DateTime> builder = new ImmutableMap.Builder<>();
|
||||
for (int i = 0; i < bucketTimestamps.size(); ++i) {
|
||||
// Add 1 to map the bucket timestamps properly to buckets indexed from 1-N.
|
||||
builder.put(i + 1, bucketTimestamps.get(i));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CommitLogCheckpoint for the given wall time and bucket checkpoint times, specified
|
||||
* as a map from bucket ID to bucket commit timestamp.
|
||||
*/
|
||||
public static CommitLogCheckpoint create(
|
||||
DateTime checkpointTime,
|
||||
ImmutableMap<Integer, DateTime> bucketTimestamps) {
|
||||
checkArgument(
|
||||
Objects.equals(CommitLogBucket.getBucketIds().asList(), bucketTimestamps.keySet().asList()),
|
||||
"Bucket ids are incorrect: %s",
|
||||
bucketTimestamps.keySet());
|
||||
CommitLogCheckpoint instance = new CommitLogCheckpoint();
|
||||
instance.checkpointTime = checkpointTime.getMillis();
|
||||
instance.bucketTimestamps = ImmutableList.copyOf(bucketTimestamps.values());
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Creates a key for the CommitLogCheckpoint for the given wall time. */
|
||||
public static Key<CommitLogCheckpoint> createKey(DateTime checkpointTime) {
|
||||
return Key.create(
|
||||
CommitLogCheckpointRoot.getKey(), CommitLogCheckpoint.class, checkpointTime.getMillis());
|
||||
}
|
||||
}
|
64
java/google/registry/model/ofy/CommitLogCheckpointRoot.java
Normal file
64
java/google/registry/model/ofy/CommitLogCheckpointRoot.java
Normal file
|
@ -0,0 +1,64 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.domain.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp.Reason;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Singleton parent entity for all commit log checkpoints.
|
||||
*/
|
||||
@Entity
|
||||
@NotBackedUp(reason = Reason.COMMIT_LOGS)
|
||||
public class CommitLogCheckpointRoot extends ImmutableObject {
|
||||
|
||||
public static final long SINGLETON_ID = 1; // There is always exactly one of these.
|
||||
|
||||
@Id
|
||||
long id = SINGLETON_ID;
|
||||
|
||||
/** Singleton key for CommitLogCheckpointParent. */
|
||||
public static Key<CommitLogCheckpointRoot> getKey() {
|
||||
return Key.create(CommitLogCheckpointRoot.class, SINGLETON_ID);
|
||||
}
|
||||
|
||||
/** The timestamp of the last {@link CommitLogCheckpoint} written. */
|
||||
DateTime lastWrittenTime = START_OF_TIME;
|
||||
|
||||
public DateTime getLastWrittenTime() {
|
||||
return lastWrittenTime;
|
||||
}
|
||||
|
||||
public static CommitLogCheckpointRoot loadRoot() {
|
||||
CommitLogCheckpointRoot root = ofy().load().key(getKey()).now();
|
||||
return root == null ? new CommitLogCheckpointRoot() : root;
|
||||
}
|
||||
|
||||
public static CommitLogCheckpointRoot create(DateTime lastWrittenTime) {
|
||||
CommitLogCheckpointRoot instance = new CommitLogCheckpointRoot();
|
||||
instance.lastWrittenTime = lastWrittenTime;
|
||||
return instance;
|
||||
}
|
||||
}
|
90
java/google/registry/model/ofy/CommitLogManifest.java
Normal file
90
java/google/registry/model/ofy/CommitLogManifest.java
Normal file
|
@ -0,0 +1,90 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.domain.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp.Reason;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Archived datastore transaction that can be replayed.
|
||||
*
|
||||
* <p>Entities of this kind are entity group sharded using a {@link CommitLogBucket} parent. Each
|
||||
* object that was saved during this transaction is stored in a {@link CommitLogMutation} child
|
||||
* entity.
|
||||
*/
|
||||
@Entity
|
||||
@NotBackedUp(reason = Reason.COMMIT_LOGS)
|
||||
public class CommitLogManifest extends ImmutableObject {
|
||||
|
||||
/** Commit log manifests are parented on a random bucket. */
|
||||
@Parent
|
||||
Key<CommitLogBucket> parent;
|
||||
|
||||
/**
|
||||
* The commit time (in millis since the epoch).
|
||||
*
|
||||
* <p>This will be unique among siblings sharing the same parent {@link CommitLogBucket}.
|
||||
*/
|
||||
@Id
|
||||
long commitTime;
|
||||
|
||||
/** Keys that were deleted in this commit. (Saves are recorded in child entities.) */
|
||||
Set<Key<?>> deletions = new LinkedHashSet<>();
|
||||
|
||||
public DateTime getCommitTime() {
|
||||
return new DateTime(commitTime, UTC);
|
||||
}
|
||||
|
||||
public int getBucketId() {
|
||||
return (int) parent.getId();
|
||||
}
|
||||
|
||||
public ImmutableSet<Key<?>> getDeletions() {
|
||||
return nullToEmptyImmutableCopy(deletions);
|
||||
}
|
||||
|
||||
public static CommitLogManifest create(
|
||||
Key<CommitLogBucket> parent, DateTime commitTime, Set<Key<?>> deletions) {
|
||||
CommitLogManifest instance = new CommitLogManifest();
|
||||
instance.parent = parent;
|
||||
instance.commitTime = commitTime.getMillis();
|
||||
instance.deletions = nullToEmptyImmutableCopy(deletions);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static Key<CommitLogManifest> createKey(Key<CommitLogBucket> parent, DateTime commitTime) {
|
||||
return Key.create(parent, CommitLogManifest.class, commitTime.getMillis());
|
||||
}
|
||||
|
||||
/** Returns the commit time encoded into a CommitLogManifest key. */
|
||||
public static DateTime extractCommitTime(Key<CommitLogManifest> manifestKey) {
|
||||
return new DateTime(manifestKey.getId(), UTC);
|
||||
}
|
||||
}
|
95
java/google/registry/model/ofy/CommitLogMutation.java
Normal file
95
java/google/registry/model/ofy/CommitLogMutation.java
Normal file
|
@ -0,0 +1,95 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.appengine.api.datastore.EntityTranslator.convertToPb;
|
||||
import static com.google.appengine.api.datastore.EntityTranslator.createFromPbBytes;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
|
||||
|
||||
import com.google.appengine.api.datastore.KeyFactory;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp.Reason;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
|
||||
/** Representation of a saved entity in a {@link CommitLogManifest} (not deletes). */
|
||||
@Entity
|
||||
@NotBackedUp(reason = Reason.COMMIT_LOGS)
|
||||
public class CommitLogMutation extends ImmutableObject {
|
||||
|
||||
/** The manifest this belongs to. */
|
||||
@Parent
|
||||
Key<CommitLogManifest> parent;
|
||||
|
||||
/** Serialized web-safe string representation of saved entity key. */
|
||||
@Id
|
||||
String entityKey;
|
||||
|
||||
/**
|
||||
* Raw entity that was saved during the transaction, serialized as a protocol buffer.
|
||||
*
|
||||
* <p>This value will be written to a GCS file by an export task.
|
||||
*/
|
||||
byte[] entityProtoBytes;
|
||||
|
||||
public byte[] getEntityProtoBytes() {
|
||||
return entityProtoBytes.clone();
|
||||
}
|
||||
|
||||
/** Deserializes embedded entity bytes and returns it. */
|
||||
public com.google.appengine.api.datastore.Entity getEntity() {
|
||||
return createFromPbBytes(entityProtoBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new mutation entity created from an @Entity ImmutableObject instance.
|
||||
*
|
||||
* <p>The mutation key is generated deterministically from the {@code entity} key. The object is
|
||||
* converted to a raw datastore Entity, serialized to bytes, and stored within the mutation.
|
||||
*/
|
||||
public static CommitLogMutation create(Key<CommitLogManifest> parent, Object entity) {
|
||||
return createFromRaw(parent, ofy().save().toEntity(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new mutation entity created from a raw datastore Entity instance.
|
||||
*
|
||||
* <p>The mutation key is generated deterministically from the {@code entity} key. The Entity
|
||||
* itself is serialized to bytes and stored within the returned mutation.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static CommitLogMutation createFromRaw(
|
||||
Key<CommitLogManifest> parent,
|
||||
com.google.appengine.api.datastore.Entity rawEntity) {
|
||||
CommitLogMutation instance = new CommitLogMutation();
|
||||
instance.parent = checkNotNull(parent);
|
||||
// Creates a web-safe key string.
|
||||
instance.entityKey = KeyFactory.keyToString(rawEntity.getKey());
|
||||
instance.entityProtoBytes = convertToPb(rawEntity).toByteArray();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Returns the key of a mutation based on the {@code entityKey} of the entity it stores. */
|
||||
public static
|
||||
Key<CommitLogMutation> createKey(Key<CommitLogManifest> parent, Key<?> entityKey) {
|
||||
return Key.create(parent, CommitLogMutation.class, entityKey.getString());
|
||||
}
|
||||
}
|
213
java/google/registry/model/ofy/CommitLoggedWork.java
Normal file
213
java/google/registry/model/ofy/CommitLoggedWork.java
Normal file
|
@ -0,0 +1,213 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Predicates.in;
|
||||
import static com.google.common.base.Predicates.not;
|
||||
import static com.google.common.collect.Maps.filterKeys;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static com.google.domain.registry.model.ofy.CommitLogBucket.loadBucket;
|
||||
import static com.google.domain.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.domain.registry.model.BackupGroupRoot;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.util.Clock;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.VoidWork;
|
||||
import com.googlecode.objectify.Work;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
/** Wrapper for {@link Work} that associates a time with each attempt. */
|
||||
class CommitLoggedWork<R> extends VoidWork {
|
||||
|
||||
private final Work<R> work;
|
||||
private final Clock clock;
|
||||
|
||||
/**
|
||||
* Temporary place to store the result of a non-void work.
|
||||
*
|
||||
* <p>We don't want to return the result directly because we are going to try to recover from a
|
||||
* {@link com.google.appengine.api.datastore.DatastoreTimeoutException} deep inside Objectify
|
||||
* when it tries to commit the transaction. When an exception is thrown the return value would be
|
||||
* lost, but sometimes we will be able to determine that we actually succeeded despite the
|
||||
* timeout, and we'll want to get the result.
|
||||
*/
|
||||
private R result;
|
||||
|
||||
/**
|
||||
* Temporary place to store the key of the commit log manifest.
|
||||
*
|
||||
* <p>We can use this to determine whether a transaction that failed with a
|
||||
* {@link com.google.appengine.api.datastore.DatastoreTimeoutException} actually succeeded. If
|
||||
* the manifest exists, and if the contents of the commit log are what we expected to have saved,
|
||||
* then the transaction committed. If the manifest does not exist, then the transaction failed and
|
||||
* is retryable.
|
||||
*/
|
||||
protected CommitLogManifest manifest;
|
||||
|
||||
/**
|
||||
* Temporary place to store the mutations belonging to the commit log manifest.
|
||||
*
|
||||
* <p>These are used along with the manifest to determine whether a transaction succeeded.
|
||||
*/
|
||||
protected ImmutableSet<ImmutableObject> mutations = ImmutableSet.of();
|
||||
|
||||
/** Lifecycle marker to track whether {@link #vrun} has been called. */
|
||||
private boolean vrunCalled;
|
||||
|
||||
CommitLoggedWork(Work<R> work, Clock clock) {
|
||||
this.work = work;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
protected TransactionInfo createNewTransactionInfo() {
|
||||
return new TransactionInfo(clock.nowUtc());
|
||||
}
|
||||
|
||||
boolean hasRun() {
|
||||
return vrunCalled;
|
||||
}
|
||||
|
||||
R getResult() {
|
||||
checkState(vrunCalled, "Cannot call getResult() before vrun()");
|
||||
return result;
|
||||
}
|
||||
|
||||
CommitLogManifest getManifest() {
|
||||
checkState(vrunCalled, "Cannot call getManifest() before vrun()");
|
||||
return manifest;
|
||||
}
|
||||
|
||||
ImmutableSet<ImmutableObject> getMutations() {
|
||||
checkState(vrunCalled, "Cannot call getMutations() before vrun()");
|
||||
return mutations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void vrun() {
|
||||
// The previous time will generally be null, except when using transactNew.
|
||||
TransactionInfo previous = Ofy.TRANSACTION_INFO.get();
|
||||
// Set the time to be used for "now" within the transaction.
|
||||
try {
|
||||
Ofy.TRANSACTION_INFO.set(createNewTransactionInfo());
|
||||
result = work.run();
|
||||
saveCommitLog(Ofy.TRANSACTION_INFO.get());
|
||||
} finally {
|
||||
Ofy.TRANSACTION_INFO.set(previous);
|
||||
}
|
||||
vrunCalled = true;
|
||||
}
|
||||
|
||||
/** Records all mutations enrolled by this transaction to a {@link CommitLogManifest} entry. */
|
||||
private void saveCommitLog(TransactionInfo info) {
|
||||
ImmutableSet<Key<?>> touchedKeys = info.getTouchedKeys();
|
||||
if (touchedKeys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
CommitLogBucket bucket = loadBucket(info.bucketKey);
|
||||
// Enforce unique monotonic property on CommitLogBucket.getLastWrittenTime().
|
||||
if (isBeforeOrAt(info.transactionTime, bucket.getLastWrittenTime())) {
|
||||
throw new TimestampInversionException(info.transactionTime, bucket.getLastWrittenTime());
|
||||
}
|
||||
Map<Key<BackupGroupRoot>, BackupGroupRoot> rootsForTouchedKeys =
|
||||
getBackupGroupRoots(touchedKeys);
|
||||
Map<Key<BackupGroupRoot>, BackupGroupRoot> rootsForUntouchedKeys =
|
||||
getBackupGroupRoots(difference(getObjectifySessionCacheKeys(), touchedKeys));
|
||||
// Check the update timestamps of all keys in the transaction, whether touched or merely read.
|
||||
checkBackupGroupRootTimestamps(
|
||||
info.transactionTime,
|
||||
union(rootsForUntouchedKeys.entrySet(), rootsForTouchedKeys.entrySet()));
|
||||
// Find any BGRs that have children which were touched but were not themselves touched.
|
||||
Set<BackupGroupRoot> untouchedRootsWithTouchedChildren =
|
||||
ImmutableSet.copyOf(filterKeys(rootsForTouchedKeys, not(in(touchedKeys))).values());
|
||||
manifest = CommitLogManifest.create(info.bucketKey, info.transactionTime, info.getDeletes());
|
||||
final Key<CommitLogManifest> manifestKey = Key.create(manifest);
|
||||
mutations = FluentIterable
|
||||
.from(union(info.getSaves(), untouchedRootsWithTouchedChildren))
|
||||
.transform(new Function<Object, ImmutableObject>() {
|
||||
@Override
|
||||
public CommitLogMutation apply(Object saveEntity) {
|
||||
return CommitLogMutation.create(manifestKey, saveEntity);
|
||||
}})
|
||||
.toSet();
|
||||
ofy().save()
|
||||
.entities(new ImmutableSet.Builder<>()
|
||||
.add(manifest)
|
||||
.add(bucket.asBuilder().setLastWrittenTime(info.transactionTime).build())
|
||||
.addAll(mutations)
|
||||
.addAll(untouchedRootsWithTouchedChildren)
|
||||
.build())
|
||||
.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns keys read by Objectify during this transaction.
|
||||
*
|
||||
* <p>This won't include the keys of asynchronous save and delete operations that haven't been
|
||||
* reaped. But that's ok because we already logged all of those keys in {@link TransactionInfo}
|
||||
* and only need this method to figure out what was loaded.
|
||||
*/
|
||||
private ImmutableSet<Key<?>> getObjectifySessionCacheKeys() {
|
||||
return ((SessionKeyExposingObjectify) ofy()).getSessionKeys();
|
||||
}
|
||||
|
||||
/** Check that the timestamp of each BackupGroupRoot is in the past. */
|
||||
private void checkBackupGroupRootTimestamps(
|
||||
DateTime transactionTime, Set<Entry<Key<BackupGroupRoot>, BackupGroupRoot>> bgrEntries) {
|
||||
ImmutableMap.Builder<Key<BackupGroupRoot>, DateTime> builder = new ImmutableMap.Builder<>();
|
||||
for (Entry<Key<BackupGroupRoot>, BackupGroupRoot> entry : bgrEntries) {
|
||||
DateTime updateTime = entry.getValue().getUpdateAutoTimestamp().getTimestamp();
|
||||
if (!updateTime.isBefore(transactionTime)) {
|
||||
builder.put(entry.getKey(), updateTime);
|
||||
}
|
||||
}
|
||||
ImmutableMap<Key<BackupGroupRoot>, DateTime> problematicRoots = builder.build();
|
||||
if (!problematicRoots.isEmpty()) {
|
||||
throw new TimestampInversionException(transactionTime, problematicRoots);
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the set of {@link BackupGroupRoot} ancestors of the given keys. */
|
||||
private Map<Key<BackupGroupRoot>, BackupGroupRoot> getBackupGroupRoots(Iterable<Key<?>> keys) {
|
||||
Set<Key<BackupGroupRoot>> rootKeys = new HashSet<>();
|
||||
for (Key<?> key : keys) {
|
||||
while (key != null
|
||||
&& !BackupGroupRoot.class
|
||||
.isAssignableFrom(ofy().factory().getMetadata(key).getEntityClass())) {
|
||||
key = key.getParent();
|
||||
}
|
||||
if (key != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Key<BackupGroupRoot> rootKey = (Key<BackupGroupRoot>) key;
|
||||
rootKeys.add(rootKey);
|
||||
}
|
||||
}
|
||||
return ImmutableMap.copyOf(ofy().load().keys(rootKeys));
|
||||
}
|
||||
}
|
182
java/google/registry/model/ofy/ObjectifyService.java
Normal file
182
java/google/registry/model/ofy/ObjectifyService.java
Normal file
|
@ -0,0 +1,182 @@
|
|||
// 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 com.google.domain.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.google.domain.registry.util.TypeUtils.hasAnnotation;
|
||||
import static com.googlecode.objectify.ObjectifyService.factory;
|
||||
|
||||
import com.google.appengine.api.datastore.DatastoreServiceFactory;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.domain.registry.config.RegistryEnvironment;
|
||||
import com.google.domain.registry.model.EntityClasses;
|
||||
import com.google.domain.registry.model.ImmutableObject;
|
||||
import com.google.domain.registry.model.translators.CidrAddressBlockTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.CommitLogRevisionsTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.CreateAutoTimestampTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.CurrencyUnitTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.DurationTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.InetAddressTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.ReadableInstantUtcTranslatorFactory;
|
||||
import com.google.domain.registry.model.translators.UpdateAutoTimestampTranslatorFactory;
|
||||
|
||||
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 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);
|
||||
}});
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
360
java/google/registry/model/ofy/Ofy.java
Normal file
360
java/google/registry/model/ofy/Ofy.java
Normal file
|
@ -0,0 +1,360 @@
|
|||
// 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 com.google.domain.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.google.domain.registry.util.CollectionUtils.union;
|
||||
import static com.google.domain.registry.util.ObjectifyUtils.OBJECTS_TO_KEYS;
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
|
||||
import com.google.appengine.api.datastore.DatastoreFailureException;
|
||||
import com.google.appengine.api.datastore.DatastoreTimeoutException;
|
||||
import com.google.appengine.api.datastore.ReadPolicy.Consistency;
|
||||
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.google.domain.registry.config.RegistryEnvironment;
|
||||
import com.google.domain.registry.model.annotations.NotBackedUp;
|
||||
import com.google.domain.registry.model.annotations.VirtualEntity;
|
||||
import com.google.domain.registry.model.ofy.ReadOnlyWork.KillTransactionException;
|
||||
import com.google.domain.registry.util.Clock;
|
||||
import com.google.domain.registry.util.FormattingLogger;
|
||||
import com.google.domain.registry.util.NonFinalForTesting;
|
||||
import com.google.domain.registry.util.Sleeper;
|
||||
import com.google.domain.registry.util.SystemClock;
|
||||
import com.google.domain.registry.util.SystemSleeper;
|
||||
|
||||
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 org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* A wrapper around ofy().
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>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<TransactionInfo> TRANSACTION_INFO = new ThreadLocal<>();
|
||||
|
||||
/** Returns the wrapped Objectify's ObjectifyFactory. */
|
||||
public ObjectifyFactory factory() {
|
||||
return ofy().factory();
|
||||
}
|
||||
|
||||
/** Clears the session cache. */
|
||||
public void clearSessionCache() {
|
||||
ofy().clear();
|
||||
}
|
||||
|
||||
public boolean inTransaction() {
|
||||
return ofy().getTransaction() != null;
|
||||
}
|
||||
|
||||
public void assertInTransaction() {
|
||||
checkState(inTransaction(), "Must be called in a transaction");
|
||||
}
|
||||
|
||||
public Loader load() {
|
||||
return ofy().load();
|
||||
}
|
||||
|
||||
public Loader loadEventuallyConsistent() {
|
||||
return ofy().consistency(Consistency.EVENTUAL).load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete, augmented to enroll the deleted entities in a commit log.
|
||||
*
|
||||
* <p>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<Key<?>> keys) {
|
||||
assertInTransaction();
|
||||
checkState(Iterables.all(keys, notNull()), "Can't delete a null key.");
|
||||
checkProhibitedAnnotations(keys, NotBackedUp.class, VirtualEntity.class);
|
||||
TRANSACTION_INFO.get().putDeletes(keys);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete, without any augmentations.
|
||||
*
|
||||
* <p>No backups get written.
|
||||
*/
|
||||
public Deleter deleteWithoutBackup() {
|
||||
return ofy().delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save, augmented to enroll the saved entities in a commit log and to check that we're not saving
|
||||
* virtual entities.
|
||||
*
|
||||
* <p>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<Key<?>, ?> keysToEntities = uniqueIndex(entities, OBJECTS_TO_KEYS);
|
||||
TRANSACTION_INFO.get().putSaves(keysToEntities);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save, without any augmentations except to check that we're not saving any virtual entities.
|
||||
* <p>
|
||||
* 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 <R> R transact(Work<R> work) {
|
||||
// If we are already in a transaction, don't wrap in a CommitLoggedWork.
|
||||
return inTransaction() ? work.run() : transactNew(work);
|
||||
}
|
||||
|
||||
/** Pause the current transaction (if any) and complete this one before returning to it. */
|
||||
public <R> R transactNew(Work<R> work) {
|
||||
// Wrap the Work in a CommitLoggedWork so that we can give transactions a frozen view of time
|
||||
// and maintain commit logs for them.
|
||||
return transactCommitLoggedWork(new CommitLoggedWork<>(work, getClock()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transact with commit logs and retry with exponential backoff.
|
||||
*
|
||||
* <p>This method is broken out from {@link #transactNew(Work)} for testing purposes.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
<R> R transactCommitLoggedWork(CommitLoggedWork<R> work) {
|
||||
long baseRetryMillis = RegistryEnvironment.get().config().getBaseOfyRetryDuration().getMillis();
|
||||
for (long attempt = 0, sleepMillis = baseRetryMillis;
|
||||
true;
|
||||
attempt++, sleepMillis *= 2) {
|
||||
try {
|
||||
ofy().transactNew(work);
|
||||
return work.getResult();
|
||||
} catch (TransientFailureException
|
||||
| TimestampInversionException
|
||||
| DatastoreTimeoutException
|
||||
| DatastoreFailureException e) {
|
||||
// TransientFailureExceptions come from task queues and always mean nothing committed.
|
||||
// TimestampInversionExceptions are thrown by our code and are always retryable as well.
|
||||
// However, datastore exceptions might get thrown even if the transaction succeeded.
|
||||
if ((e instanceof DatastoreTimeoutException || e instanceof DatastoreFailureException)
|
||||
&& checkIfAlreadySucceeded(work)) {
|
||||
return work.getResult();
|
||||
}
|
||||
if (attempt == NUM_RETRIES) {
|
||||
throw e; // Give up.
|
||||
}
|
||||
sleeper.sleepUninterruptibly(Duration.millis(sleepMillis));
|
||||
logger.infofmt(e, "Retrying %s, attempt %s", e.getClass().getSimpleName(), attempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can determine whether a transaction has succeded by trying to read the commit log back in
|
||||
* its own retryable read-only transaction.
|
||||
*/
|
||||
private <R> Boolean checkIfAlreadySucceeded(final CommitLoggedWork<R> work) {
|
||||
return work.hasRun() && transactNewReadOnly(new Work<Boolean>() {
|
||||
@Override
|
||||
public Boolean run() {
|
||||
CommitLogManifest manifest = work.getManifest();
|
||||
if (manifest == null) {
|
||||
// Work ran but no commit log was created. This might mean that the transaction did not
|
||||
// write anything to datastore. We can safely retry because it only reads. (Although the
|
||||
// transaction might have written a task to a queue, we consider that safe to retry too
|
||||
// since we generally assume that tasks might be doubly executed.) Alternatively it
|
||||
// might mean that the transaction wrote to datastore but turned off commit logs by
|
||||
// exclusively using save/deleteWithoutBackups() rather than save/delete(). Although we
|
||||
// have no hard proof that retrying is safe, we use these methods judiciously and it is
|
||||
// reasonable to assume that if the transaction really did succeed that the retry will
|
||||
// either be idempotent or will fail with a non-transient error.
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(
|
||||
union(work.getMutations(), manifest),
|
||||
ImmutableSet.copyOf(ofy().load().ancestor(manifest)));
|
||||
}});
|
||||
}
|
||||
|
||||
/** A read-only transaction is useful to get strongly consistent reads at a shared timestamp. */
|
||||
public <R> R transactNewReadOnly(Work<R> work) {
|
||||
ReadOnlyWork<R> readOnlyWork = new ReadOnlyWork<>(work, getClock());
|
||||
try {
|
||||
ofy().transactNew(readOnlyWork);
|
||||
} catch (TransientFailureException | DatastoreTimeoutException | DatastoreFailureException e) {
|
||||
// These are always retryable for a read-only operation.
|
||||
return transactNewReadOnly(work);
|
||||
} catch (KillTransactionException e) {
|
||||
// Expected; we killed the transaction as a safety measure, and now we can return the result.
|
||||
return readOnlyWork.getResult();
|
||||
}
|
||||
throw new AssertionError(); // How on earth did we get here?
|
||||
}
|
||||
|
||||
/** Execute some work in a transactionless context. */
|
||||
public <R> R doTransactionless(Work<R> work) {
|
||||
try {
|
||||
com.googlecode.objectify.ObjectifyService.push(
|
||||
com.googlecode.objectify.ObjectifyService.ofy().transactionless());
|
||||
return work.run();
|
||||
} finally {
|
||||
com.googlecode.objectify.ObjectifyService.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute some work with a fresh session cache.
|
||||
*
|
||||
* <p>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 <R> R doWithFreshSessionCache(Work<R> work) {
|
||||
try {
|
||||
com.googlecode.objectify.ObjectifyService.push(
|
||||
com.googlecode.objectify.ObjectifyService.factory().begin());
|
||||
return work.run();
|
||||
} finally {
|
||||
com.googlecode.objectify.ObjectifyService.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the time associated with the start of this particular transaction attempt. */
|
||||
public DateTime getTransactionTime() {
|
||||
assertInTransaction();
|
||||
return TRANSACTION_INFO.get().transactionTime;
|
||||
}
|
||||
|
||||
/** Returns key of {@link CommitLogManifest} that will be saved when the transaction ends. */
|
||||
public Key<CommitLogManifest> getCommitLogManifestKey() {
|
||||
assertInTransaction();
|
||||
TransactionInfo info = TRANSACTION_INFO.get();
|
||||
return Key.create(info.bucketKey, CommitLogManifest.class, info.transactionTime.getMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the @Entity-annotated base class for an object that is either an {@code Key<?>} or an
|
||||
* object of an entity class registered with Objectify.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static Class<?> getBaseEntityClassFromEntityOrKey(Object entityOrKey) {
|
||||
// Convert both keys and entities into keys, so that we get consistent behavior in either case.
|
||||
Key<?> key = (entityOrKey instanceof Key<?> ? (Key<?>) entityOrKey : Key.create(entityOrKey));
|
||||
// Get the entity class associated with this key's kind, which should be the base @Entity class
|
||||
// from which the kind name is derived. Don't be tempted to use getMetadata(String kind) or
|
||||
// getMetadataForEntity(T pojo) instead; the former won't throw an exception for an unknown
|
||||
// kind (it just returns null) and the latter will return the @EntitySubclass if there is one.
|
||||
return ofy().factory().getMetadata(key).getEntityClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the base @Entity classes for the provided entities or keys don't have any of the
|
||||
* specified forbidden annotations.
|
||||
*/
|
||||
@SafeVarargs
|
||||
private static void checkProhibitedAnnotations(
|
||||
Iterable<?> entitiesOrKeys, Class<? extends Annotation>... annotations) {
|
||||
for (Object entityOrKey : entitiesOrKeys) {
|
||||
Class<?> entityClass = getBaseEntityClassFromEntityOrKey(entityOrKey);
|
||||
for (Class<? extends Annotation> annotation : annotations) {
|
||||
checkArgument(!entityClass.isAnnotationPresent(annotation),
|
||||
"Can't save/delete a @%s entity: %s", annotation.getSimpleName(), entityClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
java/google/registry/model/ofy/OfyFilter.java
Normal file
44
java/google/registry/model/ofy/OfyFilter.java
Normal file
|
@ -0,0 +1,44 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
|
||||
/** A filter that statically registers types with Objectify. */
|
||||
public class OfyFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) throws ServletException {
|
||||
// Make sure that we've registered all types before we do anything else with Objectify.
|
||||
ObjectifyService.initOfy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {}
|
||||
}
|
41
java/google/registry/model/ofy/ReadOnlyWork.java
Normal file
41
java/google/registry/model/ofy/ReadOnlyWork.java
Normal file
|
@ -0,0 +1,41 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import com.google.domain.registry.util.Clock;
|
||||
|
||||
import com.googlecode.objectify.Work;
|
||||
|
||||
/** Wrapper for {@link Work} that disallows mutations and fails the transaction at the end. */
|
||||
class ReadOnlyWork<R> extends CommitLoggedWork<R> {
|
||||
|
||||
ReadOnlyWork(Work<R> work, Clock clock) {
|
||||
super(work, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionInfo createNewTransactionInfo() {
|
||||
return super.createNewTransactionInfo().setReadOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void vrun() {
|
||||
super.vrun();
|
||||
throw new KillTransactionException();
|
||||
}
|
||||
|
||||
/** Exception used to exit a transaction. */
|
||||
static class KillTransactionException extends RuntimeException {}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.ObjectifyFactory;
|
||||
import com.googlecode.objectify.impl.ObjectifyImpl;
|
||||
|
||||
/** Registry-specific Objectify subclass that exposes the keys used in the current session. */
|
||||
public class SessionKeyExposingObjectify extends ObjectifyImpl<SessionKeyExposingObjectify> {
|
||||
|
||||
public SessionKeyExposingObjectify(ObjectifyFactory factory) {
|
||||
super(factory);
|
||||
}
|
||||
|
||||
/** Expose the protected method that provides the keys read, saved or deleted in a session. */
|
||||
ImmutableSet<Key<?>> getSessionKeys() {
|
||||
return ImmutableSet.copyOf(getSession().keys());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.domain.registry.model.BackupGroupRoot;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.Objectify;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Exception when trying to write to the datastore with a timestamp that is inconsistent with
|
||||
* a partial ordering on transactions that touch the same entities.
|
||||
*/
|
||||
class TimestampInversionException extends RuntimeException {
|
||||
|
||||
static String getFileAndLine(StackTraceElement callsite) {
|
||||
return callsite.getFileName() + ":" + callsite.getLineNumber();
|
||||
}
|
||||
|
||||
TimestampInversionException(
|
||||
DateTime transactionTime, Map<Key<BackupGroupRoot>, DateTime> problematicRoots) {
|
||||
this(transactionTime, "entities rooted under:\n" + problematicRoots);
|
||||
}
|
||||
|
||||
TimestampInversionException(DateTime transactionTime, DateTime updateTimestamp) {
|
||||
this(transactionTime, String.format("update timestamp (%s)", updateTimestamp));
|
||||
}
|
||||
|
||||
private TimestampInversionException(DateTime transactionTime, String problem) {
|
||||
super(Joiner.on('\n').join(
|
||||
String.format(
|
||||
"Timestamp inversion between transaction time (%s) and %s",
|
||||
transactionTime,
|
||||
problem),
|
||||
getFileAndLine(FluentIterable.from(asList(new Exception().getStackTrace()))
|
||||
.firstMatch(new Predicate<StackTraceElement>() {
|
||||
@Override
|
||||
public boolean apply(StackTraceElement element) {
|
||||
return !element.getClassName().startsWith(Objectify.class.getPackage().getName())
|
||||
&& !element.getClassName().startsWith(Ofy.class.getName());
|
||||
}}).get())));
|
||||
}
|
||||
}
|
99
java/google/registry/model/ofy/TransactionInfo.java
Normal file
99
java/google/registry/model/ofy/TransactionInfo.java
Normal file
|
@ -0,0 +1,99 @@
|
|||
// 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 com.google.domain.registry.model.ofy;
|
||||
|
||||
import static com.google.common.base.Functions.constant;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.Maps.filterValues;
|
||||
import static com.google.common.collect.Maps.toMap;
|
||||
import static com.google.domain.registry.model.ofy.CommitLogBucket.getArbitraryBucketId;
|
||||
import static com.googlecode.objectify.ObjectifyService.ofy;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/** Metadata for an {@link Ofy} transaction that saves commit logs. */
|
||||
class TransactionInfo {
|
||||
|
||||
private static final Predicate<Object> IS_DELETE = Predicates.<Object>equalTo(Delete.SENTINEL);
|
||||
|
||||
private enum Delete { SENTINEL }
|
||||
|
||||
/** Logical "now" of the transaction. */
|
||||
DateTime transactionTime;
|
||||
|
||||
/** Whether this is a read-only transaction. */
|
||||
private boolean readOnly;
|
||||
|
||||
/** Bucket shard to under which commit log will be stored, chosen at random (in production). */
|
||||
final Key<CommitLogBucket> bucketKey = CommitLogBucket.getBucketKey(getArbitraryBucketId());
|
||||
|
||||
/**
|
||||
* Accumulator of save/delete operations performed in transaction.
|
||||
*
|
||||
* <p>The {@link ImmutableMap} builder provides us the benefit of not permitting duplicates.
|
||||
* This allows us to avoid potential race conditions where the same key is mutated twice in a
|
||||
* transaction.
|
||||
*/
|
||||
private final ImmutableMap.Builder<Key<?>, Object> changesBuilder = new ImmutableMap.Builder<>();
|
||||
|
||||
TransactionInfo(DateTime now) {
|
||||
this.transactionTime = now;
|
||||
ofy().load().key(bucketKey); // Asynchronously load value into session cache.
|
||||
}
|
||||
|
||||
TransactionInfo setReadOnly() {
|
||||
this.readOnly = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
void assertNotReadOnly() {
|
||||
checkState(!readOnly, "This is a read only transaction.");
|
||||
}
|
||||
|
||||
void putSaves(Map<Key<?>, ?> keysToEntities) {
|
||||
assertNotReadOnly();
|
||||
changesBuilder.putAll(keysToEntities);
|
||||
}
|
||||
|
||||
void putDeletes(Iterable<Key<?>> keys) {
|
||||
assertNotReadOnly();
|
||||
changesBuilder.putAll(toMap(keys, constant(TransactionInfo.Delete.SENTINEL)));
|
||||
}
|
||||
|
||||
ImmutableSet<Key<?>> getTouchedKeys() {
|
||||
return ImmutableSet.copyOf(changesBuilder.build().keySet());
|
||||
}
|
||||
|
||||
ImmutableSet<Key<?>> getDeletes() {
|
||||
return ImmutableSet.copyOf(filterValues(changesBuilder.build(), IS_DELETE).keySet());
|
||||
}
|
||||
|
||||
ImmutableSet<Object> getSaves() {
|
||||
return FluentIterable
|
||||
.from(changesBuilder.build().values())
|
||||
.filter(Predicates.not(IS_DELETE))
|
||||
.toSet();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue