google-nomulus/java/google/registry/model/common/Cursor.java
ctingue 87961fbb12 Add global-scoped (and other type-scoped) cursors
Expanding recurring billing events will require a global cursor as opposed to a Registry-scoped cursor, so this CL creates a more generic Cursor type and adds a dual-write for the old RegistryCursor (for both old and new styles) on save. We can then touch any stragglers using the UpdateCursorsCommand and simply drop the old RegistryCursor once all have been migrated.

See [] for migration tracking.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=121706885
2016-05-13 23:19:34 -04:00

166 lines
6.7 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.common;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.registry.Registry;
import org.joda.time.DateTime;
/**
* Shared entity for date cursors. This type supports both "scoped" cursors (i.e. per resource
* of a given type, such as a TLD) and global (i.e. one per environment) cursors, defined internally
* as scoped on {@link EntityGroupRoot}.
*/
@Entity
public class Cursor extends ImmutableObject {
/** The types of cursors, used as the string id field for each cursor in datastore. */
public enum CursorType {
/** Cursor for ensuring rolling transactional isolation of BRDA staging operation. */
BRDA(Registry.class),
/** Cursor for ensuring rolling transactional isolation of RDE report operation. */
RDE_REPORT(Registry.class),
/** Cursor for ensuring rolling transactional isolation of RDE staging operation. */
RDE_STAGING(Registry.class),
/** Cursor for ensuring rolling transactional isolation of RDE upload operation. */
RDE_UPLOAD(Registry.class),
/**
* Cursor that tracks the last time we talked to the escrow provider's SFTP server for a given
* TLD.
*
* <p>Our escrow provider has an odd feature where separate deposits uploaded within two hours
* of each other will be merged into a single deposit. This is problematic in situations where
* the cursor might be a few days behind and is trying to catch up.
*
* <p>The way we solve this problem is by having {@code RdeUploadAction} check this cursor
* before performing an upload for a given TLD. If the cursor is less than two hours old, the
* action will fail with a status code above 300 and App Engine will keep retrying the action
* until it's ready.
*/
RDE_UPLOAD_SFTP(Registry.class),
/** Cursor for ensuring rolling transactional isolation of recurring billing expansion. */
RECURRING_BILLING(EntityGroupRoot.class);
/** See the definition of scope on {@link #getScopeClass}. */
private final Class<? extends ImmutableObject> scope;
private CursorType(Class<? extends ImmutableObject> scope) {
this.scope = scope;
}
/**
* If there are multiple cursors for a given cursor type, a cursor must also have a scope
* defined (distinct from a parent, which is always the EntityGroupRoot key). For instance,
* for a cursor that is defined at the registry level, the scope type will be Registry.class.
* For a cursor (theoretically) defined for each EPP resource, the scope type will be
* EppResource.class. For a global cursor, i.e. one that applies per environment, this will be
* {@link EntityGroupRoot}.
*/
public Class<?> getScopeClass() {
return scope;
}
}
@Parent
Key<EntityGroupRoot> parent = getCrossTldKey();
@Id
String id;
DateTime cursorTime = START_OF_TIME;
/**
* Checks that the type of the scoped object (or null) matches the required type for the specified
* cursor (or null, if the cursor is a global cursor).
*/
private static void checkValidCursorTypeForScope(
CursorType cursorType, Key<? extends ImmutableObject> scope) {
checkArgument(
cursorType.getScopeClass().equals(
scope.equals(EntityGroupRoot.getCrossTldKey())
? EntityGroupRoot.class
: ofy().factory().getMetadata(scope).getEntityClass()),
"Class required for cursor does not match scope class");
}
/** Generates a unique ID for a given scope key and cursor type. */
private static String generateId(CursorType cursorType, Key<? extends ImmutableObject> scope) {
return String.format("%s_%s", scope.getString(), cursorType.name());
}
/** Creates a unique key for a given scope and cursor type. */
@VisibleForTesting
static Key<Cursor> createKey(CursorType cursorType, ImmutableObject scope) {
Key<? extends ImmutableObject> scopeKey = Key.create(scope);
checkValidCursorTypeForScope(cursorType, scopeKey);
return Key.create(getCrossTldKey(), Cursor.class, generateId(cursorType, scopeKey));
}
/** Creates a unique key for a given global cursor type. */
@VisibleForTesting
static Key<Cursor> createGlobalKey(CursorType cursorType) {
checkArgument(
cursorType.getScopeClass().equals(EntityGroupRoot.class),
"Cursor type is not a global cursor.");
return Key.create(
getCrossTldKey(), Cursor.class, generateId(cursorType, EntityGroupRoot.getCrossTldKey()));
}
/** Creates a new global cursor instance. */
public static Cursor createGlobal(CursorType cursorType, DateTime cursorTime) {
return create(cursorType, cursorTime, EntityGroupRoot.getCrossTldKey());
}
/** Creates a new cursor instance with a given {@link Key} scope. */
private static Cursor create(
CursorType cursorType, DateTime cursorTime, Key<? extends ImmutableObject> scope) {
Cursor instance = new Cursor();
instance.cursorTime = checkNotNull(cursorTime, "Cursor time cannot be null");
checkNotNull(scope, "Cursor scope cannot be null");
checkNotNull(cursorType, "Cursor type cannot be null");
checkValidCursorTypeForScope(cursorType, scope);
instance.id = generateId(cursorType, scope);
return instance;
}
/** Creates a new cursor instance with a given {@link ImmutableObject} scope. */
public static Cursor create(CursorType cursorType, DateTime cursorTime, ImmutableObject scope) {
checkNotNull(scope, "Cursor scope cannot be null");
return create(cursorType, cursorTime, Key.create(scope));
}
public DateTime getCursorTime() {
return cursorTime;
}
}