mirror of
https://github.com/google/nomulus.git
synced 2025-07-08 12:13:19 +02:00
Use a TimedTransitionProperty for the DB migration schedule (#1186)
This includes the following changes: - Convert the single-valued database migration state to a timed transition property, meaning that we can switch all instances over at the same time and schedule it in advance - Use a "cache" (technically an expiring memoized supplier) when retrieving the database migration state value - Delete the old DatabaseTransitionSchedule because it is no longer necessary. We took the idea from that and used it for the new DatabaseMigrationStateSchedule, though we cannot reuse the entity itself because the structure is fundamentally different. - Removed references to the DatabaseTransitionSchedule, mainly in the getter/setter commands+tests and a few odd references elsewhere.
This commit is contained in:
parent
4b6811d4c3
commit
e75d45d45d
23 changed files with 429 additions and 1000 deletions
|
@ -1,55 +0,0 @@
|
|||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
|
||||
/** Utility methods related to migrating dual-read/dual-write entities. */
|
||||
public class DatabaseMigrationUtils {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** Throws exceptions only in unit tests, otherwise only logs exceptions. */
|
||||
public static void suppressExceptionUnlessInTest(Runnable work, String message) {
|
||||
try {
|
||||
work.run();
|
||||
} catch (Exception e) {
|
||||
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)) {
|
||||
throw e;
|
||||
}
|
||||
logger.atWarning().withCause(e).log(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the value for the database currently considered primary. */
|
||||
public static PrimaryDatabase getPrimaryDatabase(TransitionId transitionId) {
|
||||
return DatabaseTransitionSchedule.getCached(transitionId)
|
||||
.map(DatabaseTransitionSchedule::getPrimaryDatabase)
|
||||
.orElse(PrimaryDatabase.DATASTORE);
|
||||
}
|
||||
|
||||
public static boolean isDatastore(TransitionId transitionId) {
|
||||
return tm().transactNew(() -> DatabaseMigrationUtils.getPrimaryDatabase(transitionId))
|
||||
.equals(PrimaryDatabase.DATASTORE);
|
||||
}
|
||||
|
||||
private DatabaseMigrationUtils() {}
|
||||
}
|
|
@ -17,8 +17,7 @@ package google.registry.model;
|
|||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.common.DatabaseMigrationStateWrapper;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
||||
import google.registry.model.common.EntityGroupRoot;
|
||||
import google.registry.model.common.GaeUserIdConverter;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
|
@ -76,8 +75,7 @@ public final class EntityClasses {
|
|||
ContactHistory.class,
|
||||
ContactResource.class,
|
||||
Cursor.class,
|
||||
DatabaseMigrationStateWrapper.class,
|
||||
DatabaseTransitionSchedule.class,
|
||||
DatabaseMigrationStateSchedule.class,
|
||||
DomainBase.class,
|
||||
DomainHistory.class,
|
||||
EntityGroupRoot.class,
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.common;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Mapify;
|
||||
import google.registry.model.annotations.InCrossTld;
|
||||
import google.registry.model.common.TimedTransitionProperty.TimeMapper;
|
||||
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
|
||||
import google.registry.schema.replay.DatastoreOnlyEntity;
|
||||
import java.time.Duration;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* A wrapper object representing the stage-to-time mapping of the Registry 3.0 Cloud SQL migration.
|
||||
*
|
||||
* <p>The entity is stored in Datastore throughout the entire migration so as to have a single point
|
||||
* of access.
|
||||
*/
|
||||
@Entity
|
||||
@InCrossTld
|
||||
public class DatabaseMigrationStateSchedule extends CrossTldSingleton
|
||||
implements DatastoreOnlyEntity {
|
||||
|
||||
public enum PrimaryDatabase {
|
||||
CLOUD_SQL,
|
||||
DATASTORE
|
||||
}
|
||||
|
||||
/**
|
||||
* The current phase of the migration plus information about which database to use and whether or
|
||||
* not the phase is read-only.
|
||||
*/
|
||||
public enum MigrationState {
|
||||
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false),
|
||||
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false),
|
||||
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true),
|
||||
SQL_PRIMARY_READ_ONLY(PrimaryDatabase.CLOUD_SQL, true),
|
||||
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false),
|
||||
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false);
|
||||
|
||||
private final PrimaryDatabase primaryDatabase;
|
||||
private final boolean isReadOnly;
|
||||
|
||||
public PrimaryDatabase getPrimaryDatabase() {
|
||||
return primaryDatabase;
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return isReadOnly;
|
||||
}
|
||||
|
||||
MigrationState(PrimaryDatabase primaryDatabase, boolean isReadOnly) {
|
||||
this.primaryDatabase = primaryDatabase;
|
||||
this.isReadOnly = isReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@Embed
|
||||
public static class MigrationStateTransition extends TimedTransition<MigrationState> {
|
||||
private MigrationState migrationState;
|
||||
|
||||
@Override
|
||||
protected MigrationState getValue() {
|
||||
return migrationState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValue(MigrationState migrationState) {
|
||||
this.migrationState = migrationState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache of the current migration schedule. The key is meaningless; this is essentially a memoized
|
||||
* Supplier that can be reset for testing purposes and after writes.
|
||||
*/
|
||||
private static final LoadingCache<
|
||||
Class<DatabaseMigrationStateSchedule>,
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition>>
|
||||
// Each instance should cache the migration schedule for five minutes before reloading
|
||||
CACHE =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMinutes(5))
|
||||
.build(
|
||||
new CacheLoader<
|
||||
Class<DatabaseMigrationStateSchedule>,
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition>>() {
|
||||
@Override
|
||||
public TimedTransitionProperty<MigrationState, MigrationStateTransition> load(
|
||||
Class<DatabaseMigrationStateSchedule> unused) {
|
||||
return DatabaseMigrationStateSchedule.getUncached();
|
||||
}
|
||||
});
|
||||
|
||||
// Restrictions on the state transitions, e.g. no going from DATASTORE_ONLY to SQL_ONLY
|
||||
private static final ImmutableMultimap<MigrationState, MigrationState> VALID_STATE_TRANSITIONS =
|
||||
createValidStateTransitions();
|
||||
|
||||
/**
|
||||
* The valid state transitions. Generally, one can advance the state one step or move backward any
|
||||
* number of steps, as long as the step we're moving back to has the same primary database as the
|
||||
* one we're in. Otherwise, we must move to the corresponding READ_ONLY stage first.
|
||||
*/
|
||||
private static ImmutableMultimap<MigrationState, MigrationState> createValidStateTransitions() {
|
||||
ImmutableMultimap.Builder<MigrationState, MigrationState> builder =
|
||||
new ImmutableMultimap.Builder<MigrationState, MigrationState>()
|
||||
.put(MigrationState.DATASTORE_ONLY, MigrationState.DATASTORE_PRIMARY)
|
||||
.putAll(
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
MigrationState.DATASTORE_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY)
|
||||
.putAll(
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
MigrationState.DATASTORE_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
MigrationState.SQL_PRIMARY_READ_ONLY,
|
||||
MigrationState.SQL_PRIMARY)
|
||||
.putAll(
|
||||
MigrationState.SQL_PRIMARY_READ_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
MigrationState.SQL_PRIMARY)
|
||||
.putAll(
|
||||
MigrationState.SQL_PRIMARY,
|
||||
MigrationState.SQL_PRIMARY_READ_ONLY,
|
||||
MigrationState.SQL_ONLY)
|
||||
.putAll(
|
||||
MigrationState.SQL_ONLY,
|
||||
MigrationState.SQL_PRIMARY_READ_ONLY,
|
||||
MigrationState.SQL_PRIMARY);
|
||||
// In addition, we can always transition from a state to itself (useful when updating the map).
|
||||
for (MigrationState migrationState : MigrationState.values()) {
|
||||
builder.put(migrationState, migrationState);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// Default map to return if we have never saved any -- only use Datastore.
|
||||
@VisibleForTesting
|
||||
public static final TimedTransitionProperty<MigrationState, MigrationStateTransition>
|
||||
DEFAULT_TRANSITION_MAP =
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, MigrationState.DATASTORE_ONLY),
|
||||
MigrationStateTransition.class);
|
||||
|
||||
@Mapify(TimeMapper.class)
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition> migrationTransitions =
|
||||
TimedTransitionProperty.forMapify(
|
||||
MigrationState.DATASTORE_ONLY, MigrationStateTransition.class);
|
||||
|
||||
// Required for Objectify initialization
|
||||
private DatabaseMigrationStateSchedule() {}
|
||||
|
||||
@VisibleForTesting
|
||||
DatabaseMigrationStateSchedule(
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition> migrationTransitions) {
|
||||
this.migrationTransitions = migrationTransitions;
|
||||
}
|
||||
|
||||
/** Sets and persists to Datastore the provided migration transition schedule. */
|
||||
public static void set(ImmutableSortedMap<DateTime, MigrationState> migrationTransitionMap) {
|
||||
ofyTm().assertInTransaction();
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition> transitions =
|
||||
TimedTransitionProperty.make(
|
||||
migrationTransitionMap,
|
||||
MigrationStateTransition.class,
|
||||
VALID_STATE_TRANSITIONS,
|
||||
"validStateTransitions",
|
||||
MigrationState.DATASTORE_ONLY,
|
||||
"migrationTransitionMap must start with DATASTORE_ONLY");
|
||||
validateTransitionAtCurrentTime(transitions);
|
||||
ofyTm().put(new DatabaseMigrationStateSchedule(transitions));
|
||||
CACHE.invalidateAll();
|
||||
}
|
||||
|
||||
/** Loads the currently-set migration schedule from the cache, or the default if none exists. */
|
||||
public static TimedTransitionProperty<MigrationState, MigrationStateTransition> get() {
|
||||
return CACHE.getUnchecked(DatabaseMigrationStateSchedule.class);
|
||||
}
|
||||
|
||||
/** Loads the currently-set migration schedule from Datastore, or the default if none exists. */
|
||||
@VisibleForTesting
|
||||
static TimedTransitionProperty<MigrationState, MigrationStateTransition> getUncached() {
|
||||
return ofyTm()
|
||||
.transact(
|
||||
() ->
|
||||
ofyTm()
|
||||
.loadSingleton(DatabaseMigrationStateSchedule.class)
|
||||
.map(s -> s.migrationTransitions)
|
||||
.orElse(DEFAULT_TRANSITION_MAP));
|
||||
}
|
||||
|
||||
/**
|
||||
* A provided map of transitions may be valid by itself (i.e. it shifts states properly, doesn't
|
||||
* skip states, and doesn't backtrack incorrectly) while still being invalid. In addition to the
|
||||
* transitions in the map being valid, the single transition from the current map at the current
|
||||
* time to the new map at the current time time must also be valid.
|
||||
*/
|
||||
private static void validateTransitionAtCurrentTime(
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition> newTransitions) {
|
||||
MigrationState currentValue = getUncached().getValueAtTime(ofyTm().getTransactionTime());
|
||||
MigrationState nextCurrentValue = newTransitions.getValueAtTime(ofyTm().getTransactionTime());
|
||||
checkArgument(
|
||||
VALID_STATE_TRANSITIONS.get(currentValue).contains(nextCurrentValue),
|
||||
"Cannot transition from current state-as-of-now %s to new state-as-of-now %s",
|
||||
currentValue,
|
||||
nextCurrentValue);
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.common;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import google.registry.model.annotations.InCrossTld;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.schema.replay.DatastoreOnlyEntity;
|
||||
|
||||
/**
|
||||
* A wrapper object representing the current stage of the Registry 3.0 Cloud SQL migration.
|
||||
*
|
||||
* <p>The entity is stored in Datastore throughout the entire migration so as to have a single point
|
||||
* of access (avoiding a two-phase commit problem).
|
||||
*/
|
||||
@Entity
|
||||
@InCrossTld
|
||||
public class DatabaseMigrationStateWrapper extends CrossTldSingleton
|
||||
implements DatastoreOnlyEntity {
|
||||
|
||||
/**
|
||||
* The current phase of the migration plus information about which database to use and whether or
|
||||
* not the phase is read-only.
|
||||
*/
|
||||
public enum MigrationState {
|
||||
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false),
|
||||
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false),
|
||||
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true),
|
||||
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false),
|
||||
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false);
|
||||
|
||||
private final PrimaryDatabase primaryDatabase;
|
||||
private final boolean readOnly;
|
||||
|
||||
public PrimaryDatabase getPrimaryDatabase() {
|
||||
return primaryDatabase;
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
MigrationState(PrimaryDatabase primaryDatabase, boolean readOnly) {
|
||||
this.primaryDatabase = primaryDatabase;
|
||||
this.readOnly = readOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private MigrationState migrationState;
|
||||
|
||||
// Required for Objectify initialization
|
||||
private DatabaseMigrationStateWrapper() {}
|
||||
|
||||
DatabaseMigrationStateWrapper(MigrationState migrationState) {
|
||||
this.migrationState = migrationState;
|
||||
}
|
||||
|
||||
// The valid state transitions. Basically, at state N, state N+1 is valid as well as all previous
|
||||
// states, with one type of exception: when in either of the SQL states, we can only move back
|
||||
// one step so that we can make sure that any modifications have been replayed back to Datastore.
|
||||
private static final ImmutableMap<MigrationState, ImmutableSet<MigrationState>>
|
||||
VALID_STATE_TRANSITIONS =
|
||||
ImmutableMap.of(
|
||||
MigrationState.DATASTORE_ONLY,
|
||||
ImmutableSet.of(MigrationState.DATASTORE_PRIMARY),
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
ImmutableSet.of(
|
||||
MigrationState.DATASTORE_ONLY, MigrationState.DATASTORE_PRIMARY_READ_ONLY),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
ImmutableSet.of(
|
||||
MigrationState.DATASTORE_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
MigrationState.SQL_PRIMARY),
|
||||
MigrationState.SQL_PRIMARY,
|
||||
ImmutableSet.of(MigrationState.DATASTORE_PRIMARY_READ_ONLY, MigrationState.SQL_ONLY),
|
||||
MigrationState.SQL_ONLY,
|
||||
ImmutableSet.of(MigrationState.SQL_PRIMARY));
|
||||
|
||||
private static boolean isValidStateTransition(MigrationState from, MigrationState to) {
|
||||
return VALID_STATE_TRANSITIONS.get(from).contains(to);
|
||||
}
|
||||
|
||||
/** Sets and persists to Datastore the current database migration state. */
|
||||
public static void set(MigrationState newState) {
|
||||
MigrationState currentState = get();
|
||||
checkArgument(
|
||||
isValidStateTransition(currentState, newState),
|
||||
"Moving from migration state %s to %s is not a valid transition",
|
||||
currentState,
|
||||
newState);
|
||||
DatabaseMigrationStateWrapper wrapper = new DatabaseMigrationStateWrapper(newState);
|
||||
ofyTm().transact(() -> ofyTm().put(wrapper));
|
||||
}
|
||||
|
||||
/** Retrieves the current state of the migration (or DATASTORE_ONLY if it hasn't started). */
|
||||
public static MigrationState get() {
|
||||
return ofyTm()
|
||||
.transact(
|
||||
() ->
|
||||
ofyTm()
|
||||
.loadSingleton(DatabaseMigrationStateWrapper.class)
|
||||
.map(s -> s.migrationState)
|
||||
.orElse(MigrationState.DATASTORE_ONLY));
|
||||
}
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.common;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Mapify;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.model.annotations.InCrossTld;
|
||||
import google.registry.model.common.TimedTransitionProperty.TimeMapper;
|
||||
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.model.registry.label.ReservedList;
|
||||
import google.registry.model.smd.SignedMarkRevocationList;
|
||||
import google.registry.model.tmch.ClaimsList;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreOnlyEntity;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@Entity
|
||||
@Immutable
|
||||
@InCrossTld
|
||||
public class DatabaseTransitionSchedule extends ImmutableObject implements DatastoreOnlyEntity {
|
||||
|
||||
/**
|
||||
* The name of the database to be treated as the primary database. The first entry in the schedule
|
||||
* will always be Datastore.
|
||||
*/
|
||||
public enum PrimaryDatabase {
|
||||
CLOUD_SQL,
|
||||
DATASTORE
|
||||
}
|
||||
|
||||
/** The id of the transition schedule. */
|
||||
public enum TransitionId {
|
||||
/** The schedule for migration of {@link ClaimsList} entities. */
|
||||
CLAIMS_LIST,
|
||||
/** The schedule for the migration of {@link PremiumList} and {@link ReservedList}. */
|
||||
DOMAIN_LABEL_LISTS,
|
||||
/** The schedule for the migration of the {@link SignedMarkRevocationList} entity. */
|
||||
SIGNED_MARK_REVOCATION_LIST,
|
||||
/** The schedule for all asynchronously-replayed entities, ones not dually-written. */
|
||||
REPLAYED_ENTITIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* The transition to a specified primary database at a specific point in time, for use in a
|
||||
* TimedTransitionProperty.
|
||||
*/
|
||||
@Embed
|
||||
public static class PrimaryDatabaseTransition extends TimedTransition<PrimaryDatabase> {
|
||||
private PrimaryDatabase primaryDatabase;
|
||||
|
||||
@Override
|
||||
protected PrimaryDatabase getValue() {
|
||||
return primaryDatabase;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValue(PrimaryDatabase primaryDatabase) {
|
||||
this.primaryDatabase = primaryDatabase;
|
||||
}
|
||||
}
|
||||
|
||||
@Parent Key<EntityGroupRoot> parent = getCrossTldKey();
|
||||
|
||||
@Id String transitionId;
|
||||
|
||||
/** An automatically managed timestamp of when this schedule was last written to Datastore. */
|
||||
UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
|
||||
|
||||
/** A property that tracks the primary database for a dual-read/dual-write database migration. */
|
||||
@Mapify(TimeMapper.class)
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
|
||||
TimedTransitionProperty.forMapify(PrimaryDatabase.DATASTORE, PrimaryDatabaseTransition.class);
|
||||
|
||||
/** A cache that loads the {@link DatabaseTransitionSchedule} for a given id. */
|
||||
private static final LoadingCache<TransitionId, Optional<DatabaseTransitionSchedule>> CACHE =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(
|
||||
java.time.Duration.ofMillis(getSingletonCacheRefreshDuration().getMillis()))
|
||||
.build(
|
||||
new CacheLoader<TransitionId, Optional<DatabaseTransitionSchedule>>() {
|
||||
@Override
|
||||
public Optional<DatabaseTransitionSchedule> load(TransitionId transitionId) {
|
||||
return DatabaseTransitionSchedule.get(transitionId);
|
||||
}
|
||||
});
|
||||
|
||||
public static DatabaseTransitionSchedule create(
|
||||
TransitionId transitionId,
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions) {
|
||||
checkNotNull(transitionId, "Id cannot be null");
|
||||
checkNotNull(databaseTransitions, "databaseTransitions cannot be null");
|
||||
databaseTransitions.checkValidity();
|
||||
DatabaseTransitionSchedule instance = new DatabaseTransitionSchedule();
|
||||
instance.transitionId = transitionId.name();
|
||||
instance.databaseTransitions = databaseTransitions;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Returns the database that is indicated as primary at the given time. */
|
||||
public PrimaryDatabase getPrimaryDatabase() {
|
||||
return databaseTransitions.getValueAtTime(tm().getTransactionTime());
|
||||
}
|
||||
|
||||
/** Returns the database transitions as a map of start time to primary database. */
|
||||
public ImmutableSortedMap<DateTime, PrimaryDatabase> getDatabaseTransitions() {
|
||||
return databaseTransitions.toValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current cached schedule for the given id.
|
||||
*
|
||||
* <p>WARNING: The schedule returned by this method could be up to 10 minutes out of date.
|
||||
*/
|
||||
public static Optional<DatabaseTransitionSchedule> getCached(TransitionId id) {
|
||||
return CACHE.getUnchecked(id);
|
||||
}
|
||||
|
||||
/** Returns the schedule for a given id. */
|
||||
public static Optional<DatabaseTransitionSchedule> get(TransitionId transitionId) {
|
||||
VKey<DatabaseTransitionSchedule> key =
|
||||
VKey.create(
|
||||
DatabaseTransitionSchedule.class,
|
||||
transitionId,
|
||||
Key.create(getCrossTldKey(), DatabaseTransitionSchedule.class, transitionId.name()));
|
||||
|
||||
return ofyTm().transact(() -> ofyTm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"%s(last updated at %s): %s",
|
||||
transitionId, lastUpdateTime.getTimestamp(), databaseTransitions.toValueMap());
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ package google.registry.rdap;
|
|||
import static com.google.common.base.Charsets.UTF_8;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Actions.getPathForAction;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
|
@ -29,10 +28,7 @@ import com.google.common.net.MediaType;
|
|||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.DatabaseMigrationUtils;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.rdap.RdapMetrics.EndpointType;
|
||||
import google.registry.rdap.RdapObjectClasses.ErrorResponse;
|
||||
|
@ -261,10 +257,4 @@ public abstract class RdapActionBase implements Runnable {
|
|||
return rdapJsonFormatter.getRequestTime();
|
||||
}
|
||||
|
||||
static boolean isDatastore() {
|
||||
return tm().transact(
|
||||
() ->
|
||||
DatabaseMigrationUtils.getPrimaryDatabase(TransitionId.REPLAYED_ENTITIES)
|
||||
.equals(PrimaryDatabase.DATASTORE));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
|||
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.HEAD;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
@ -204,7 +205,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
// need it.
|
||||
int querySizeLimit = RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize;
|
||||
RdapResultSet<DomainBase> resultSet;
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<DomainBase> query =
|
||||
auditedOfy()
|
||||
.load()
|
||||
|
@ -260,7 +261,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
// Don't use queryItems, because it doesn't handle pending deletes.
|
||||
int querySizeLimit = RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize;
|
||||
RdapResultSet<DomainBase> resultSet;
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<DomainBase> query = auditedOfy().load().type(DomainBase.class).filter("tld", tld);
|
||||
if (cursorString.isPresent()) {
|
||||
query = query.filter("fullyQualifiedDomainName >", cursorString.get());
|
||||
|
@ -337,7 +338,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
// incomplete result set if a search asks for something like "ns*", but we need to enforce a
|
||||
// limit in order to avoid arbitrarily long-running queries.
|
||||
Optional<String> desiredRegistrar = getDesiredRegistrar();
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<HostResource> query =
|
||||
queryItems(
|
||||
HostResource.class,
|
||||
|
@ -472,7 +473,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
private DomainSearchResponse searchByNameserverIp(final InetAddress inetAddress) {
|
||||
Optional<String> desiredRegistrar = getDesiredRegistrar();
|
||||
ImmutableSet<VKey<HostResource>> hostKeys;
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<HostResource> query =
|
||||
queryItems(
|
||||
HostResource.class,
|
||||
|
@ -544,7 +545,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
int numHostKeysSearched = 0;
|
||||
for (List<VKey<HostResource>> chunk : Iterables.partition(hostKeys, 30)) {
|
||||
numHostKeysSearched += chunk.size();
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<DomainBase> query =
|
||||
auditedOfy()
|
||||
.load()
|
||||
|
|
|
@ -262,7 +262,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|
|||
|| (cursorType == CursorType.REGISTRAR)) {
|
||||
resultSet = RdapResultSet.create(ImmutableList.of());
|
||||
} else {
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<ContactResource> query =
|
||||
queryItems(
|
||||
ContactResource.class,
|
||||
|
@ -386,7 +386,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|
|||
if (subtype == Subtype.REGISTRARS) {
|
||||
contactResultSet = RdapResultSet.create(ImmutableList.of());
|
||||
} else {
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
contactResultSet =
|
||||
getMatchingResources(
|
||||
queryItemsByKey(
|
||||
|
|
|
@ -16,6 +16,7 @@ package google.registry.rdap;
|
|||
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.HEAD;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
@ -220,7 +221,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
|||
private NameserverSearchResponse searchByNameUsingPrefix(RdapSearchPattern partialStringQuery) {
|
||||
// Add 1 so we can detect truncation.
|
||||
int querySizeLimit = getStandardQuerySizeLimit();
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<HostResource> query =
|
||||
queryItems(
|
||||
HostResource.class,
|
||||
|
@ -254,7 +255,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
|||
// Add 1 so we can detect truncation.
|
||||
int querySizeLimit = getStandardQuerySizeLimit();
|
||||
RdapResultSet<HostResource> rdapResultSet;
|
||||
if (isDatastore()) {
|
||||
if (tm().isOfy()) {
|
||||
Query<HostResource> query =
|
||||
queryItems(
|
||||
HostResource.class,
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import java.util.List;
|
||||
|
||||
/** Command to show the {@link DatabaseTransitionSchedule} for a transition id. */
|
||||
@Parameters(separators = " =", commandDescription = "Show database transition schedule")
|
||||
final class GetDatabaseTransitionScheduleCommand implements CommandWithRemoteApi {
|
||||
|
||||
@Parameter(description = "Transition id(s) for the schedules to get", required = true)
|
||||
private List<TransitionId> mainParameters;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (TransitionId transitionId : mainParameters) {
|
||||
DatabaseTransitionSchedule schedule =
|
||||
checkArgumentPresent(
|
||||
DatabaseTransitionSchedule.get(transitionId),
|
||||
"A database transition schedule for %s does not exist",
|
||||
transitionId);
|
||||
|
||||
System.out.println(schedule);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -69,7 +69,6 @@ public final class RegistryTool {
|
|||
.put("get_allocation_token", GetAllocationTokenCommand.class)
|
||||
.put("get_claims_list", GetClaimsListCommand.class)
|
||||
.put("get_contact", GetContactCommand.class)
|
||||
.put("get_database_transition_schedule", GetDatabaseTransitionScheduleCommand.class)
|
||||
.put("get_domain", GetDomainCommand.class)
|
||||
.put("get_history_entries", GetHistoryEntriesCommand.class)
|
||||
.put("get_host", GetHostCommand.class)
|
||||
|
@ -111,7 +110,6 @@ public final class RegistryTool {
|
|||
.put("resave_epp_resource", ResaveEppResourceCommand.class)
|
||||
.put("save_sql_credential", SaveSqlCredentialCommand.class)
|
||||
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
|
||||
.put("set_database_transition_schedule", SetDatabaseTransitionScheduleCommand.class)
|
||||
.put("set_num_instances", SetNumInstancesCommand.class)
|
||||
.put("set_sql_replay_checkpoint", SetSqlReplayCheckpointCommand.class)
|
||||
.put("setup_ote", SetupOteCommand.class)
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.tools.params.TransitionListParameter.PrimaryDatabaseTransitions;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Command to update {@link DatabaseTransitionSchedule}. */
|
||||
@Parameters(
|
||||
separators = " =",
|
||||
commandDescription = "Set the database transition schedule for transition id.")
|
||||
public class SetDatabaseTransitionScheduleCommand extends ConfirmingCommand
|
||||
implements CommandWithRemoteApi {
|
||||
|
||||
@Parameter(
|
||||
names = "--transition_schedule",
|
||||
converter = PrimaryDatabaseTransitions.class,
|
||||
validateWith = PrimaryDatabaseTransitions.class,
|
||||
description =
|
||||
"Comma-delimited list of database transitions, of the form"
|
||||
+ " <time>=<primary-database>[,<time>=<primary-database>]*")
|
||||
ImmutableSortedMap<DateTime, PrimaryDatabase> transitionSchedule;
|
||||
|
||||
@Parameter(
|
||||
names = "--transition_id",
|
||||
required = true,
|
||||
description = "Transition id string for the schedule being updated")
|
||||
private TransitionId transitionId;
|
||||
|
||||
@Override
|
||||
protected String prompt() {
|
||||
return String.format(
|
||||
"Insert new schedule %s for transition ID %s?", transitionSchedule, transitionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String execute() {
|
||||
DatabaseTransitionSchedule newSchedule =
|
||||
DatabaseTransitionSchedule.create(
|
||||
transitionId,
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
transitionSchedule, PrimaryDatabaseTransition.class));
|
||||
ofyTm().transact(() -> ofyTm().put(newSchedule));
|
||||
return String.format(
|
||||
"Inserted new schedule %s for transition ID %s.", transitionSchedule, transitionId);
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools.params;
|
||||
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
|
||||
/**
|
||||
* {@link TransitionId} CLI parameter converter/validator. Required to support multi-value
|
||||
* TransitionId parameters.
|
||||
*/
|
||||
public final class TransitionIdParameter extends EnumParameter<TransitionId> {}
|
|
@ -19,7 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Ordering;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
|
||||
import google.registry.model.domain.token.AllocationToken.TokenStatus;
|
||||
import google.registry.model.registry.Registry.TldState;
|
||||
import org.joda.money.Money;
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.common;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_ONLY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_READ_ONLY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_ONLY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY_READ_ONLY;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
fakeClock.setAutoIncrementByOneMilli();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmpty_returnsDatastoreOnlyMap() {
|
||||
assertThat(DatabaseMigrationStateSchedule.getUncached())
|
||||
.isEqualTo(DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidTransitions() {
|
||||
// First, verify that no-ops are safe
|
||||
for (MigrationState migrationState : MigrationState.values()) {
|
||||
runValidTransition(migrationState, migrationState);
|
||||
}
|
||||
|
||||
// Next, the transitions that will actually cause a change
|
||||
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
|
||||
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY);
|
||||
|
||||
runValidTransition(SQL_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runValidTransition(SQL_PRIMARY_READ_ONLY, SQL_PRIMARY);
|
||||
|
||||
runValidTransition(SQL_PRIMARY, SQL_PRIMARY_READ_ONLY);
|
||||
runValidTransition(SQL_PRIMARY, SQL_ONLY);
|
||||
|
||||
runValidTransition(SQL_ONLY, SQL_PRIMARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidTransitions() {
|
||||
runInvalidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY_READ_ONLY);
|
||||
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY);
|
||||
runInvalidTransition(DATASTORE_ONLY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY_READ_ONLY);
|
||||
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY);
|
||||
runInvalidTransition(DATASTORE_PRIMARY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(SQL_PRIMARY_READ_ONLY, DATASTORE_ONLY);
|
||||
runInvalidTransition(SQL_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
|
||||
runInvalidTransition(SQL_PRIMARY_READ_ONLY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(SQL_PRIMARY, DATASTORE_ONLY);
|
||||
runInvalidTransition(SQL_PRIMARY, DATASTORE_PRIMARY);
|
||||
runInvalidTransition(SQL_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
|
||||
runInvalidTransition(SQL_ONLY, DATASTORE_ONLY);
|
||||
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY);
|
||||
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_newMapImpliesInvalidChangeNow() {
|
||||
DateTime startTime = fakeClock.nowUtc();
|
||||
fakeClock.advanceBy(Duration.standardHours(6));
|
||||
|
||||
// The new map is valid by itself, but not with the current state of DATASTORE_ONLY because the
|
||||
// new map implies that the current state is DATASTORE_PRIMARY_READ_ONLY
|
||||
ImmutableSortedMap<DateTime, MigrationState> nowInvalidMap =
|
||||
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
|
||||
.put(START_OF_TIME, DATASTORE_ONLY)
|
||||
.put(startTime.plusHours(1), DATASTORE_PRIMARY)
|
||||
.put(startTime.plusHours(2), DATASTORE_PRIMARY_READ_ONLY)
|
||||
.build();
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(nowInvalidMap))))
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Cannot transition from current state-as-of-now DATASTORE_ONLY "
|
||||
+ "to new state-as-of-now DATASTORE_PRIMARY_READ_ONLY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_notInTransaction() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
DatabaseMigrationStateSchedule.set(
|
||||
DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP.toValueMap())))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Must be called in a transaction");
|
||||
}
|
||||
|
||||
private void runValidTransition(MigrationState from, MigrationState to) {
|
||||
ImmutableSortedMap<DateTime, MigrationState> transitions =
|
||||
createMapEndingWithTransition(from, to);
|
||||
ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
|
||||
assertThat(DatabaseMigrationStateSchedule.getUncached().toValueMap())
|
||||
.containsExactlyEntriesIn(transitions);
|
||||
}
|
||||
|
||||
private void runInvalidTransition(MigrationState from, MigrationState to) {
|
||||
ImmutableSortedMap<DateTime, MigrationState> transitions =
|
||||
createMapEndingWithTransition(from, to);
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions))))
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
String.format("validStateTransitions map cannot transition from %s to %s.", from, to));
|
||||
}
|
||||
|
||||
// Create a transition map that is valid up to the "from" transition, then add the "to" transition
|
||||
private ImmutableSortedMap<DateTime, MigrationState> createMapEndingWithTransition(
|
||||
MigrationState from, MigrationState to) {
|
||||
ImmutableSortedMap.Builder<DateTime, MigrationState> builder =
|
||||
ImmutableSortedMap.naturalOrder();
|
||||
builder.put(START_OF_TIME, DATASTORE_ONLY);
|
||||
MigrationState[] allMigrationStates = MigrationState.values();
|
||||
for (int i = 0; i < allMigrationStates.length; i++) {
|
||||
builder.put(fakeClock.nowUtc().plusMinutes(i), allMigrationStates[i]);
|
||||
if (allMigrationStates[i].equals(from)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
builder.put(fakeClock.nowUtc().plusDays(1), to);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.common;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.DATASTORE_ONLY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.DATASTORE_PRIMARY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.DATASTORE_PRIMARY_READ_ONLY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.SQL_ONLY;
|
||||
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.SQL_PRIMARY;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
public class DatabaseMigrationStateWrapperTest {
|
||||
|
||||
@RegisterExtension
|
||||
public final AppEngineExtension appEngine =
|
||||
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
|
||||
|
||||
@Test
|
||||
void testEmpty_returnsDatastore() {
|
||||
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(DATASTORE_ONLY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmpty_canChangeToDatastorePrimary() {
|
||||
DatabaseMigrationStateWrapper.set(DATASTORE_PRIMARY);
|
||||
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(DATASTORE_PRIMARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidTransitions() {
|
||||
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
|
||||
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY);
|
||||
|
||||
runValidTransition(SQL_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runValidTransition(SQL_PRIMARY, SQL_ONLY);
|
||||
|
||||
runValidTransition(SQL_ONLY, SQL_PRIMARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidTransitions() {
|
||||
runInvalidTransition(DATASTORE_ONLY, DATASTORE_ONLY);
|
||||
runInvalidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY);
|
||||
runInvalidTransition(DATASTORE_ONLY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY);
|
||||
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY);
|
||||
runInvalidTransition(DATASTORE_PRIMARY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runInvalidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_ONLY);
|
||||
|
||||
runInvalidTransition(SQL_PRIMARY, DATASTORE_ONLY);
|
||||
runInvalidTransition(SQL_PRIMARY, DATASTORE_PRIMARY);
|
||||
runInvalidTransition(SQL_PRIMARY, SQL_PRIMARY);
|
||||
|
||||
runInvalidTransition(SQL_ONLY, DATASTORE_ONLY);
|
||||
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY);
|
||||
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runInvalidTransition(SQL_ONLY, SQL_ONLY);
|
||||
}
|
||||
|
||||
private static void runValidTransition(MigrationState from, MigrationState to) {
|
||||
setStateForced(from);
|
||||
DatabaseMigrationStateWrapper.set(to);
|
||||
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(to);
|
||||
}
|
||||
|
||||
private static void runInvalidTransition(MigrationState from, MigrationState to) {
|
||||
setStateForced(from);
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> DatabaseMigrationStateWrapper.set(to)))
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
String.format(
|
||||
"Moving from migration state %s to %s is not a valid transition", from, to));
|
||||
}
|
||||
|
||||
private static void setStateForced(MigrationState migrationState) {
|
||||
DatabaseMigrationStateWrapper wrapper = new DatabaseMigrationStateWrapper(migrationState);
|
||||
ofyTm().transact(() -> ofyTm().put(wrapper));
|
||||
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(migrationState);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.common;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.EntityTestCase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link DatabaseTransitionSchedule}. */
|
||||
public class DatabaseTransitionScheduleTest extends EntityTestCase {
|
||||
|
||||
@Test
|
||||
void testSuccess_persistence() {
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
|
||||
PrimaryDatabaseTransition.class);
|
||||
DatabaseTransitionSchedule schedule =
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST, databaseTransitions);
|
||||
ofyTm().transactNew(() -> ofyTm().put(schedule));
|
||||
|
||||
assertThat(
|
||||
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
|
||||
.get()
|
||||
.databaseTransitions)
|
||||
.isEqualTo(databaseTransitions);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_scheduleWithNoStartOfTime() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST,
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(fakeClock.nowUtc(), PrimaryDatabase.DATASTORE),
|
||||
PrimaryDatabaseTransition.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_getPrimaryDatabase() {
|
||||
DatabaseTransitionSchedule schedule =
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST,
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(
|
||||
START_OF_TIME,
|
||||
PrimaryDatabase.DATASTORE,
|
||||
fakeClock.nowUtc().plusDays(1),
|
||||
PrimaryDatabase.CLOUD_SQL),
|
||||
PrimaryDatabaseTransition.class));
|
||||
assertThat(ofyTm().transact(schedule::getPrimaryDatabase)).isEqualTo(PrimaryDatabase.DATASTORE);
|
||||
fakeClock.advanceBy(Duration.standardDays(5));
|
||||
assertThat(ofyTm().transact(schedule::getPrimaryDatabase)).isEqualTo(PrimaryDatabase.CLOUD_SQL);
|
||||
}
|
||||
}
|
|
@ -17,12 +17,10 @@ package google.registry.testing;
|
|||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.io.Files.asCharSink;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.persistSimpleResources;
|
||||
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.injectTmForDualDatabaseTest;
|
||||
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.restoreTmAfterDualDatabaseTest;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
@ -42,16 +40,10 @@ import com.google.common.base.Joiner;
|
|||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.io.Files;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.ObjectifyFilter;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.model.ofy.ObjectifyService;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.Registrar.State;
|
||||
|
@ -402,18 +394,8 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
|
|||
if (withDatastore && !withoutCannedData) {
|
||||
loadInitialData();
|
||||
}
|
||||
} else {
|
||||
// If we're using SQL, set replayed entities to use SQL
|
||||
DatabaseTransitionSchedule schedule =
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.REPLAYED_ENTITIES,
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.CLOUD_SQL),
|
||||
PrimaryDatabaseTransition.class));
|
||||
tm().transactNew(() -> auditedOfy().saveWithoutBackup().entity(schedule).now());
|
||||
if (withCloudSql && !withJpaUnitTest && !withoutCannedData) {
|
||||
loadInitialData();
|
||||
}
|
||||
} else if (withCloudSql && !withJpaUnitTest && !withoutCannedData) {
|
||||
loadInitialData();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.beust.jcommander.ParameterException;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.testing.InjectExtension;
|
||||
import google.registry.testing.SetClockExtension;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link GetDatabaseTransitionScheduleCommand} */
|
||||
public class GetDatabaseTransitionScheduleCommandTest
|
||||
extends CommandTestCase<GetDatabaseTransitionScheduleCommand> {
|
||||
|
||||
@Order(value = Order.DEFAULT - 3)
|
||||
@RegisterExtension
|
||||
final SetClockExtension setClockExtension =
|
||||
new SetClockExtension(fakeClock, "1984-12-21T06:07:08.789Z");
|
||||
|
||||
@RegisterExtension public final InjectExtension inject = new InjectExtension();
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
inject.setStaticField(Ofy.class, "clock", fakeClock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess() throws Exception {
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
|
||||
PrimaryDatabaseTransition.class);
|
||||
DatabaseTransitionSchedule schedule =
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST, databaseTransitions);
|
||||
ofyTm().transactNew(() -> ofyTm().put(schedule));
|
||||
runCommand("SIGNED_MARK_REVOCATION_LIST");
|
||||
assertStdoutIs(
|
||||
"SIGNED_MARK_REVOCATION_LIST(last updated at 1984-12-21T06:07:08.789Z):"
|
||||
+ " {1970-01-01T00:00:00.000Z=DATASTORE}\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_multipleArguments() throws Exception {
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
|
||||
PrimaryDatabaseTransition.class);
|
||||
DatabaseTransitionSchedule schedule =
|
||||
DatabaseTransitionSchedule.create(TransitionId.DOMAIN_LABEL_LISTS, databaseTransitions);
|
||||
ofyTm().transactNew(() -> ofyTm().put(schedule));
|
||||
fakeClock.advanceOneMilli(); // Now 1984-12-21T06:07:08.790Z
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions2 =
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(
|
||||
START_OF_TIME,
|
||||
PrimaryDatabase.DATASTORE,
|
||||
DateTime.parse("2020-10-01T00:00:00Z"),
|
||||
PrimaryDatabase.CLOUD_SQL),
|
||||
PrimaryDatabaseTransition.class);
|
||||
DatabaseTransitionSchedule schedule2 =
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST, databaseTransitions2);
|
||||
ofyTm().transactNew(() -> ofyTm().put(schedule2));
|
||||
runCommand("DOMAIN_LABEL_LISTS", "SIGNED_MARK_REVOCATION_LIST");
|
||||
assertStdoutIs(
|
||||
"DOMAIN_LABEL_LISTS(last updated at 1984-12-21T06:07:08.789Z):"
|
||||
+ " {1970-01-01T00:00:00.000Z=DATASTORE}\n"
|
||||
+ "SIGNED_MARK_REVOCATION_LIST(last updated at 1984-12-21T06:07:08.790Z):"
|
||||
+ " {1970-01-01T00:00:00.000Z=DATASTORE, 2020-10-01T00:00:00.000Z=CLOUD_SQL}\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_scheduleDoesNotExist() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> runCommand("SIGNED_MARK_REVOCATION_LIST"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("A database transition schedule for SIGNED_MARK_REVOCATION_LIST does not exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_noIdGiven() {
|
||||
assertThrows(ParameterException.class, this::runCommand);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_oneScheduleDoesNotExist() {
|
||||
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
|
||||
TimedTransitionProperty.fromValueMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
|
||||
PrimaryDatabaseTransition.class);
|
||||
DatabaseTransitionSchedule schedule =
|
||||
DatabaseTransitionSchedule.create(TransitionId.DOMAIN_LABEL_LISTS, databaseTransitions);
|
||||
ofyTm().transactNew(() -> ofyTm().put(schedule));
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> runCommand("DOMAIN_LABEL_LISTS", "SIGNED_MARK_REVOCATION_LIST"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("A database transition schedule for SIGNED_MARK_REVOCATION_LIST does not exist");
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.beust.jcommander.ParameterException;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.truth.Truth8;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.persistence.VKey;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link SetDatabaseTransitionScheduleCommand}. */
|
||||
public class SetDatabaseTransitionScheduleCommandTest
|
||||
extends CommandTestCase<SetDatabaseTransitionScheduleCommand> {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
fakeClock.setTo(DateTime.parse("2020-12-01T00:00:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_noTransitionId() throws Exception {
|
||||
ParameterException thrown =
|
||||
assertThrows(
|
||||
ParameterException.class,
|
||||
() -> runCommandForced("--transition_schedule=2021-04-14T00:00:00.000Z=DATASTORE"));
|
||||
assertThat(thrown).hasMessageThat().contains("--transition_id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_currentScheduleIsEmpty() throws Exception {
|
||||
Truth8.assertThat(
|
||||
ofyTm()
|
||||
.loadByKeyIfPresent(
|
||||
VKey.createOfy(
|
||||
DatabaseTransitionSchedule.class,
|
||||
Key.create(getCrossTldKey(), DatabaseTransitionSchedule.class, "test"))))
|
||||
.isEmpty();
|
||||
runCommandForced(
|
||||
"--transition_id=SIGNED_MARK_REVOCATION_LIST",
|
||||
"--transition_schedule=1970-01-01T00:00:00.000Z=DATASTORE");
|
||||
assertThat(
|
||||
ofyTm()
|
||||
.transact(
|
||||
() ->
|
||||
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
|
||||
.get()
|
||||
.getPrimaryDatabase()))
|
||||
.isEqualTo(PrimaryDatabase.DATASTORE);
|
||||
assertThat(command.prompt())
|
||||
.isEqualTo(
|
||||
"Insert new schedule {1970-01-01T00:00:00.000Z=DATASTORE} "
|
||||
+ "for transition ID SIGNED_MARK_REVOCATION_LIST?");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess() throws Exception {
|
||||
ImmutableSortedMap<DateTime, PrimaryDatabase> transitionMap =
|
||||
ImmutableSortedMap.of(
|
||||
START_OF_TIME,
|
||||
PrimaryDatabase.DATASTORE,
|
||||
fakeClock.nowUtc().minusDays(1),
|
||||
PrimaryDatabase.CLOUD_SQL);
|
||||
persistResource(
|
||||
DatabaseTransitionSchedule.create(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST,
|
||||
TimedTransitionProperty.fromValueMap(transitionMap, PrimaryDatabaseTransition.class)));
|
||||
assertThat(
|
||||
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
|
||||
.get()
|
||||
.getDatabaseTransitions())
|
||||
.isEqualTo(transitionMap);
|
||||
runCommandForced(
|
||||
"--transition_id=SIGNED_MARK_REVOCATION_LIST",
|
||||
"--transition_schedule=1970-01-01T00:00:00.000Z=DATASTORE,"
|
||||
+ "2020-11-30T00:00:00.000Z=CLOUD_SQL,2020-12-06T00:00:00.000Z=DATASTORE");
|
||||
ImmutableSortedMap<DateTime, PrimaryDatabase> retrievedTransitionMap =
|
||||
ofyTm()
|
||||
.transact(
|
||||
() ->
|
||||
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
|
||||
.get()
|
||||
.getDatabaseTransitions());
|
||||
assertThat(retrievedTransitionMap)
|
||||
.containsExactly(
|
||||
START_OF_TIME,
|
||||
PrimaryDatabase.DATASTORE,
|
||||
fakeClock.nowUtc().minusDays(1),
|
||||
PrimaryDatabase.CLOUD_SQL,
|
||||
fakeClock.nowUtc().plusDays(5),
|
||||
PrimaryDatabase.DATASTORE);
|
||||
fakeClock.advanceBy(Duration.standardDays(5));
|
||||
assertThat(
|
||||
ofyTm()
|
||||
.transact(
|
||||
() ->
|
||||
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
|
||||
.get()
|
||||
.getPrimaryDatabase()))
|
||||
.isEqualTo(PrimaryDatabase.DATASTORE);
|
||||
assertThat(command.prompt())
|
||||
.isEqualTo(
|
||||
"Insert new schedule {1970-01-01T00:00:00.000Z=DATASTORE, "
|
||||
+ "2020-11-30T00:00:00.000Z=CLOUD_SQL, 2020-12-06T00:00:00.000Z=DATASTORE} "
|
||||
+ "for transition ID SIGNED_MARK_REVOCATION_LIST?");
|
||||
}
|
||||
}
|
|
@ -2,8 +2,7 @@ AllocationToken
|
|||
Cancellation
|
||||
ContactResource
|
||||
Cursor
|
||||
DatabaseMigrationStateWrapper
|
||||
DatabaseTransitionSchedule
|
||||
DatabaseMigrationStateSchedule
|
||||
DomainBase
|
||||
EntityGroupRoot
|
||||
EppResourceIndex
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
ClaimsList
|
||||
ClaimsListSingleton
|
||||
Cursor
|
||||
DatabaseMigrationStateWrapper
|
||||
DatabaseTransitionSchedule
|
||||
DatabaseMigrationStateSchedule
|
||||
KmsSecret
|
||||
KmsSecretRevision
|
||||
PremiumList
|
||||
|
|
|
@ -78,30 +78,21 @@ class google.registry.model.common.Cursor {
|
|||
google.registry.model.UpdateAutoTimestamp lastUpdateTime;
|
||||
org.joda.time.DateTime cursorTime;
|
||||
}
|
||||
class google.registry.model.common.DatabaseMigrationStateWrapper {
|
||||
class google.registry.model.common.DatabaseMigrationStateSchedule {
|
||||
@Id long id;
|
||||
@Parent com.googlecode.objectify.Key<google.registry.model.common.EntityGroupRoot> parent;
|
||||
google.registry.model.common.DatabaseMigrationStateWrapper$MigrationState migrationState;
|
||||
google.registry.model.common.TimedTransitionProperty<google.registry.model.common.DatabaseMigrationStateSchedule$MigrationState, google.registry.model.common.DatabaseMigrationStateSchedule$MigrationStateTransition> migrationTransitions;
|
||||
}
|
||||
enum google.registry.model.common.DatabaseMigrationStateWrapper$MigrationState {
|
||||
enum google.registry.model.common.DatabaseMigrationStateSchedule$MigrationState {
|
||||
DATASTORE_ONLY;
|
||||
DATASTORE_PRIMARY;
|
||||
DATASTORE_PRIMARY_READ_ONLY;
|
||||
SQL_ONLY;
|
||||
SQL_PRIMARY;
|
||||
SQL_PRIMARY_READ_ONLY;
|
||||
}
|
||||
class google.registry.model.common.DatabaseTransitionSchedule {
|
||||
@Id java.lang.String transitionId;
|
||||
@Parent com.googlecode.objectify.Key<google.registry.model.common.EntityGroupRoot> parent;
|
||||
google.registry.model.UpdateAutoTimestamp lastUpdateTime;
|
||||
google.registry.model.common.TimedTransitionProperty<google.registry.model.common.DatabaseTransitionSchedule$PrimaryDatabase, google.registry.model.common.DatabaseTransitionSchedule$PrimaryDatabaseTransition> databaseTransitions;
|
||||
}
|
||||
enum google.registry.model.common.DatabaseTransitionSchedule$PrimaryDatabase {
|
||||
CLOUD_SQL;
|
||||
DATASTORE;
|
||||
}
|
||||
class google.registry.model.common.DatabaseTransitionSchedule$PrimaryDatabaseTransition {
|
||||
google.registry.model.common.DatabaseTransitionSchedule$PrimaryDatabase primaryDatabase;
|
||||
class google.registry.model.common.DatabaseMigrationStateSchedule$MigrationStateTransition {
|
||||
google.registry.model.common.DatabaseMigrationStateSchedule$MigrationState migrationState;
|
||||
org.joda.time.DateTime transitionTime;
|
||||
}
|
||||
class google.registry.model.common.EntityGroupRoot {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue