mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 03:57:51 +02:00
Delete DatabaseMigrationStateSchedule (#2001)
We have been using it as a poor man's timed flag that triggers a system behavior change after a certain time. We have no foreseeable future use for it now that the DNS pull queue related code is deleted. If in the future a need for such a flag arises, we are better off implementing a proper flag system than hijacking this class any way.
This commit is contained in:
parent
f173b4fb4e
commit
1c0d507bc3
14 changed files with 0 additions and 1009 deletions
|
@ -1,275 +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.tm;
|
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.collect.ImmutableMultimap;
|
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import google.registry.config.RegistryEnvironment;
|
|
||||||
import google.registry.model.CacheUtils;
|
|
||||||
import google.registry.model.annotations.DeleteAfterMigration;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.PersistenceException;
|
|
||||||
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 SQL throughout the entire migration so as to have a single point of
|
|
||||||
* access.
|
|
||||||
*/
|
|
||||||
@DeleteAfterMigration
|
|
||||||
@Entity
|
|
||||||
public class DatabaseMigrationStateSchedule extends CrossTldSingleton {
|
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
private static boolean useUncachedForTest = false;
|
|
||||||
|
|
||||||
public enum PrimaryDatabase {
|
|
||||||
CLOUD_SQL,
|
|
||||||
DATASTORE
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ReplayDirection {
|
|
||||||
NO_REPLAY,
|
|
||||||
DATASTORE_TO_SQL,
|
|
||||||
SQL_TO_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 is the only DB being used. */
|
|
||||||
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false, ReplayDirection.NO_REPLAY),
|
|
||||||
|
|
||||||
/** Datastore is the primary DB, with changes replicated to Cloud SQL. */
|
|
||||||
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
|
|
||||||
|
|
||||||
/** Datastore is the primary DB, with replication, and async actions are disallowed. */
|
|
||||||
DATASTORE_PRIMARY_NO_ASYNC(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
|
|
||||||
|
|
||||||
/** Datastore is the primary DB, with replication, and all mutating actions are disallowed. */
|
|
||||||
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true, ReplayDirection.DATASTORE_TO_SQL),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cloud SQL is the primary DB, with replication back to Datastore, and all mutating actions are
|
|
||||||
* disallowed.
|
|
||||||
*/
|
|
||||||
SQL_PRIMARY_READ_ONLY(PrimaryDatabase.CLOUD_SQL, true, ReplayDirection.SQL_TO_DATASTORE),
|
|
||||||
|
|
||||||
/** Cloud SQL is the primary DB, with changes replicated to Datastore. */
|
|
||||||
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.SQL_TO_DATASTORE),
|
|
||||||
|
|
||||||
/** Cloud SQL is the only DB being used. */
|
|
||||||
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY),
|
|
||||||
|
|
||||||
/** Toggles SQL Sequence based allocateId */
|
|
||||||
SEQUENCE_BASED_ALLOCATE_ID(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY),
|
|
||||||
|
|
||||||
/** Use SQL-based Nordn upload flow instead of the pull queue-based one. */
|
|
||||||
NORDN_SQL(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY),
|
|
||||||
|
|
||||||
/** Use SQL-based DNS update flow instead of the pull queue-based one. */
|
|
||||||
DNS_SQL(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY);
|
|
||||||
|
|
||||||
private final PrimaryDatabase primaryDatabase;
|
|
||||||
private final boolean isReadOnly;
|
|
||||||
private final ReplayDirection replayDirection;
|
|
||||||
|
|
||||||
public PrimaryDatabase getPrimaryDatabase() {
|
|
||||||
return primaryDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReadOnly() {
|
|
||||||
return isReadOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplayDirection getReplayDirection() {
|
|
||||||
return replayDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
MigrationState(
|
|
||||||
PrimaryDatabase primaryDatabase, boolean isReadOnly, ReplayDirection replayDirection) {
|
|
||||||
this.primaryDatabase = primaryDatabase;
|
|
||||||
this.isReadOnly = isReadOnly;
|
|
||||||
this.replayDirection = replayDirection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public static final LoadingCache<
|
|
||||||
Class<DatabaseMigrationStateSchedule>, TimedTransitionProperty<MigrationState>>
|
|
||||||
// Each instance should cache the migration schedule for five minutes before reloading
|
|
||||||
CACHE =
|
|
||||||
CacheUtils.newCacheBuilder(Duration.ofMinutes(5))
|
|
||||||
.build(singletonClazz -> 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_NO_ASYNC)
|
|
||||||
.putAll(
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY)
|
|
||||||
.putAll(
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
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)
|
|
||||||
.putAll(MigrationState.SQL_ONLY, MigrationState.SEQUENCE_BASED_ALLOCATE_ID)
|
|
||||||
.putAll(MigrationState.SEQUENCE_BASED_ALLOCATE_ID, MigrationState.NORDN_SQL)
|
|
||||||
.putAll(
|
|
||||||
MigrationState.NORDN_SQL,
|
|
||||||
MigrationState.SEQUENCE_BASED_ALLOCATE_ID,
|
|
||||||
MigrationState.DNS_SQL)
|
|
||||||
.putAll(MigrationState.DNS_SQL, MigrationState.NORDN_SQL);
|
|
||||||
|
|
||||||
// In addition, we can always transition from a state to itself (useful when updating the map).
|
|
||||||
Arrays.stream(MigrationState.values()).forEach(state -> builder.put(state, state));
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default map to return if we have never saved any -- only use Datastore.
|
|
||||||
@VisibleForTesting
|
|
||||||
public static final TimedTransitionProperty<MigrationState> DEFAULT_TRANSITION_MAP =
|
|
||||||
TimedTransitionProperty.fromValueMap(
|
|
||||||
ImmutableSortedMap.of(START_OF_TIME, MigrationState.DATASTORE_ONLY));
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public TimedTransitionProperty<MigrationState> migrationTransitions =
|
|
||||||
TimedTransitionProperty.withInitialValue(MigrationState.DATASTORE_ONLY);
|
|
||||||
|
|
||||||
// Required for Hibernate initialization
|
|
||||||
protected DatabaseMigrationStateSchedule() {}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public DatabaseMigrationStateSchedule(
|
|
||||||
TimedTransitionProperty<MigrationState> migrationTransitions) {
|
|
||||||
this.migrationTransitions = migrationTransitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets and persists to SQL the provided migration transition schedule. */
|
|
||||||
public static void set(ImmutableSortedMap<DateTime, MigrationState> migrationTransitionMap) {
|
|
||||||
tm().assertInTransaction();
|
|
||||||
TimedTransitionProperty<MigrationState> transitions =
|
|
||||||
TimedTransitionProperty.make(
|
|
||||||
migrationTransitionMap,
|
|
||||||
VALID_STATE_TRANSITIONS,
|
|
||||||
"validStateTransitions",
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
"migrationTransitionMap must start with DATASTORE_ONLY");
|
|
||||||
validateTransitionAtCurrentTime(transitions);
|
|
||||||
tm().put(new DatabaseMigrationStateSchedule(transitions));
|
|
||||||
CACHE.invalidateAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public static void useUncachedForTest() {
|
|
||||||
useUncachedForTest = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loads the currently-set migration schedule from the cache, or the default if none exists. */
|
|
||||||
public static TimedTransitionProperty<MigrationState> get() {
|
|
||||||
return CACHE.get(DatabaseMigrationStateSchedule.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the database migration status at the given time. */
|
|
||||||
public static MigrationState getValueAtTime(DateTime dateTime) {
|
|
||||||
return useUncachedForTest
|
|
||||||
? getUncached().getValueAtTime(dateTime)
|
|
||||||
: get().getValueAtTime(dateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loads the currently-set migration schedule from SQL, or the default if none exists. */
|
|
||||||
@VisibleForTesting
|
|
||||||
static TimedTransitionProperty<MigrationState> getUncached() {
|
|
||||||
return tm().transact(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
return tm().loadSingleton(DatabaseMigrationStateSchedule.class)
|
|
||||||
.map(s -> s.migrationTransitions)
|
|
||||||
.orElse(DEFAULT_TRANSITION_MAP);
|
|
||||||
} catch (PersistenceException e) {
|
|
||||||
if (!RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
logger.atWarning().withCause(e).log(
|
|
||||||
"Error when retrieving migration schedule; this should only happen in tests.");
|
|
||||||
return 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 must also be valid.
|
|
||||||
*/
|
|
||||||
private static void validateTransitionAtCurrentTime(
|
|
||||||
TimedTransitionProperty<MigrationState> newTransitions) {
|
|
||||||
MigrationState currentValue = getUncached().getValueAtTime(tm().getTransactionTime());
|
|
||||||
MigrationState nextCurrentValue = newTransitions.getValueAtTime(tm().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,37 +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.persistence.converter;
|
|
||||||
|
|
||||||
import google.registry.model.annotations.DeleteAfterMigration;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import javax.persistence.Converter;
|
|
||||||
|
|
||||||
/** JPA converter for {@link DatabaseMigrationStateSchedule} transitions. */
|
|
||||||
@DeleteAfterMigration
|
|
||||||
@Converter(autoApply = true)
|
|
||||||
public class DatabaseMigrationScheduleTransitionConverter
|
|
||||||
extends TimedTransitionPropertyConverterBase<MigrationState> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String convertValueToString(MigrationState value) {
|
|
||||||
return value.name();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected MigrationState convertStringToValue(String string) {
|
|
||||||
return MigrationState.valueOf(string);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +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 com.beust.jcommander.Parameters;
|
|
||||||
import google.registry.model.annotations.DeleteAfterMigration;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.model.common.TimedTransitionProperty;
|
|
||||||
|
|
||||||
/** A command to check the current Registry 3.0 migration state of the database. */
|
|
||||||
@DeleteAfterMigration
|
|
||||||
@Parameters(separators = " =", commandDescription = "Check current Registry 3.0 migration state")
|
|
||||||
public class GetDatabaseMigrationStateCommand implements Command {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() throws Exception {
|
|
||||||
TimedTransitionProperty<MigrationState> migrationSchedule =
|
|
||||||
DatabaseMigrationStateSchedule.get();
|
|
||||||
System.out.printf("Current migration schedule: %s%n", migrationSchedule.toValueMap());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -68,7 +68,6 @@ public final class RegistryTool {
|
||||||
.put("get_allocation_token", GetAllocationTokenCommand.class)
|
.put("get_allocation_token", GetAllocationTokenCommand.class)
|
||||||
.put("get_claims_list", GetClaimsListCommand.class)
|
.put("get_claims_list", GetClaimsListCommand.class)
|
||||||
.put("get_contact", GetContactCommand.class)
|
.put("get_contact", GetContactCommand.class)
|
||||||
.put("get_database_migration_state", GetDatabaseMigrationStateCommand.class)
|
|
||||||
.put("get_domain", GetDomainCommand.class)
|
.put("get_domain", GetDomainCommand.class)
|
||||||
.put("get_history_entries", GetHistoryEntriesCommand.class)
|
.put("get_history_entries", GetHistoryEntriesCommand.class)
|
||||||
.put("get_host", GetHostCommand.class)
|
.put("get_host", GetHostCommand.class)
|
||||||
|
@ -98,7 +97,6 @@ public final class RegistryTool {
|
||||||
.put("renew_domain", RenewDomainCommand.class)
|
.put("renew_domain", RenewDomainCommand.class)
|
||||||
.put("save_sql_credential", SaveSqlCredentialCommand.class)
|
.put("save_sql_credential", SaveSqlCredentialCommand.class)
|
||||||
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
|
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
|
||||||
.put("set_database_migration_state", SetDatabaseMigrationStateCommand.class)
|
|
||||||
.put("setup_ote", SetupOteCommand.class)
|
.put("setup_ote", SetupOteCommand.class)
|
||||||
.put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class)
|
.put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class)
|
||||||
.put("unlock_domain", UnlockDomainCommand.class)
|
.put("unlock_domain", UnlockDomainCommand.class)
|
||||||
|
|
|
@ -1,70 +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.tm;
|
|
||||||
|
|
||||||
import com.beust.jcommander.Parameter;
|
|
||||||
import com.beust.jcommander.Parameters;
|
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
|
||||||
import google.registry.model.annotations.DeleteAfterMigration;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.tools.params.TransitionListParameter.MigrationStateTransitions;
|
|
||||||
import org.joda.time.DateTime;
|
|
||||||
|
|
||||||
/** Command to set the Registry 3.0 database migration state schedule. */
|
|
||||||
@DeleteAfterMigration
|
|
||||||
@Parameters(
|
|
||||||
separators = " =",
|
|
||||||
commandDescription = "Set the current database migration state schedule.")
|
|
||||||
public class SetDatabaseMigrationStateCommand extends ConfirmingCommand {
|
|
||||||
|
|
||||||
private static final String WARNING_MESSAGE =
|
|
||||||
"Attempting to change the schedule with an effect that would take place within the next 10 "
|
|
||||||
+ "minutes. The cache expiration duration is 5 minutes so this MAY BE DANGEROUS.\n";
|
|
||||||
|
|
||||||
@Parameter(
|
|
||||||
names = "--migration_schedule",
|
|
||||||
converter = MigrationStateTransitions.class,
|
|
||||||
validateWith = MigrationStateTransitions.class,
|
|
||||||
required = true,
|
|
||||||
description =
|
|
||||||
"Comma-delimited list of database transitions, of the form"
|
|
||||||
+ " <time>=<migration-state>[,<time>=<migration-state>]*")
|
|
||||||
ImmutableSortedMap<DateTime, MigrationState> transitionSchedule;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String prompt() {
|
|
||||||
return tm().transact(
|
|
||||||
() -> {
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
DateTime now = tm().getTransactionTime();
|
|
||||||
DateTime nextTransition = transitionSchedule.ceilingKey(now);
|
|
||||||
if (nextTransition != null && nextTransition.isBefore(now.plusMinutes(10))) {
|
|
||||||
result.append(WARNING_MESSAGE);
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
.append(String.format("Set new migration state schedule %s?", transitionSchedule))
|
|
||||||
.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String execute() {
|
|
||||||
tm().transact(() -> DatabaseMigrationStateSchedule.set(transitionSchedule));
|
|
||||||
return String.format("Successfully set new migration state schedule %s", transitionSchedule);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
import com.google.common.collect.ImmutableSortedMap;
|
||||||
import com.google.common.collect.Ordering;
|
import com.google.common.collect.Ordering;
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.model.domain.token.AllocationToken.TokenStatus;
|
import google.registry.model.domain.token.AllocationToken.TokenStatus;
|
||||||
import google.registry.model.tld.Tld.TldState;
|
import google.registry.model.tld.Tld.TldState;
|
||||||
import org.joda.money.Money;
|
import org.joda.money.Money;
|
||||||
|
@ -73,12 +72,4 @@ public abstract class TransitionListParameter<V> extends KeyValueMapParameter<Da
|
||||||
return TokenStatus.valueOf(value);
|
return TokenStatus.valueOf(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Converter-validator for states of the Registry 3.0 database migration. */
|
|
||||||
public static class MigrationStateTransitions extends TransitionListParameter<MigrationState> {
|
|
||||||
@Override
|
|
||||||
protected MigrationState parseValue(String value) {
|
|
||||||
return MigrationState.valueOf(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
<class>google.registry.model.billing.BillingEvent</class>
|
<class>google.registry.model.billing.BillingEvent</class>
|
||||||
<class>google.registry.model.billing.BillingRecurrence</class>
|
<class>google.registry.model.billing.BillingRecurrence</class>
|
||||||
<class>google.registry.model.common.Cursor</class>
|
<class>google.registry.model.common.Cursor</class>
|
||||||
<class>google.registry.model.common.DatabaseMigrationStateSchedule</class>
|
|
||||||
<class>google.registry.model.common.DnsRefreshRequest</class>
|
<class>google.registry.model.common.DnsRefreshRequest</class>
|
||||||
<class>google.registry.model.console.User</class>
|
<class>google.registry.model.console.User</class>
|
||||||
<class>google.registry.model.contact.ContactHistory</class>
|
<class>google.registry.model.contact.ContactHistory</class>
|
||||||
|
@ -88,7 +87,6 @@
|
||||||
<class>google.registry.persistence.converter.CommandNameSetConverter</class>
|
<class>google.registry.persistence.converter.CommandNameSetConverter</class>
|
||||||
<class>google.registry.persistence.converter.CurrencyToBillingConverter</class>
|
<class>google.registry.persistence.converter.CurrencyToBillingConverter</class>
|
||||||
<class>google.registry.persistence.converter.CurrencyUnitConverter</class>
|
<class>google.registry.persistence.converter.CurrencyUnitConverter</class>
|
||||||
<class>google.registry.persistence.converter.DatabaseMigrationScheduleTransitionConverter</class>
|
|
||||||
<class>google.registry.persistence.converter.DateTimeConverter</class>
|
<class>google.registry.persistence.converter.DateTimeConverter</class>
|
||||||
<class>google.registry.persistence.converter.DurationConverter</class>
|
<class>google.registry.persistence.converter.DurationConverter</class>
|
||||||
<class>google.registry.persistence.converter.IdnTableEnumSetConverter</class>
|
<class>google.registry.persistence.converter.IdnTableEnumSetConverter</class>
|
||||||
|
|
|
@ -1,187 +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.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_ONLY;
|
|
||||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY;
|
|
||||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_NO_ASYNC;
|
|
||||||
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.tm;
|
|
||||||
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 google.registry.testing.DatabaseHelper;
|
|
||||||
import org.joda.time.DateTime;
|
|
||||||
import org.joda.time.Duration;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
/** Tests for {@link DatabaseMigrationStateSchedule}. */
|
|
||||||
public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void beforeEach() {
|
|
||||||
fakeClock.setAutoIncrementByOneMilli();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void afterEach() {
|
|
||||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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_NO_ASYNC);
|
|
||||||
runValidTransition(DATASTORE_PRIMARY_NO_ASYNC, DATASTORE_PRIMARY_READ_ONLY);
|
|
||||||
|
|
||||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
|
|
||||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
|
|
||||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_NO_ASYNC);
|
|
||||||
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, DATASTORE_PRIMARY_READ_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_NO_ASYNC)
|
|
||||||
.put(startTime.plusHours(3), DATASTORE_PRIMARY_READ_ONLY)
|
|
||||||
.build();
|
|
||||||
IllegalArgumentException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() -> tm().transact(() -> DatabaseMigrationStateSchedule.set(nowInvalidMap)));
|
|
||||||
assertThat(thrown)
|
|
||||||
.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() {
|
|
||||||
IllegalStateException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalStateException.class,
|
|
||||||
() ->
|
|
||||||
DatabaseMigrationStateSchedule.set(
|
|
||||||
DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP.toValueMap()));
|
|
||||||
assertThat(thrown).hasMessageThat().isEqualTo("Not in a transaction");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runValidTransition(MigrationState from, MigrationState to) {
|
|
||||||
ImmutableSortedMap<DateTime, MigrationState> transitions =
|
|
||||||
createMapEndingWithTransition(from, to);
|
|
||||||
tm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
|
|
||||||
assertThat(DatabaseMigrationStateSchedule.getUncached().toValueMap())
|
|
||||||
.containsExactlyEntriesIn(transitions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runInvalidTransition(MigrationState from, MigrationState to) {
|
|
||||||
ImmutableSortedMap<DateTime, MigrationState> transitions =
|
|
||||||
createMapEndingWithTransition(from, to);
|
|
||||||
IllegalArgumentException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() -> tm().transact(() -> DatabaseMigrationStateSchedule.set(transitions)));
|
|
||||||
assertThat(thrown)
|
|
||||||
.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,88 +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.persistence.converter;
|
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
|
||||||
import static google.registry.testing.DatabaseHelper.insertInDb;
|
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
|
||||||
import google.registry.model.ImmutableObject;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.model.common.TimedTransitionProperty;
|
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import org.joda.time.DateTime;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
|
||||||
|
|
||||||
/** Unit tests for {@link DatabaseMigrationScheduleTransitionConverter}. */
|
|
||||||
public class DatabaseMigrationScheduleTransitionConverterTest {
|
|
||||||
|
|
||||||
@RegisterExtension
|
|
||||||
public final JpaUnitTestExtension jpa =
|
|
||||||
new JpaTestExtensions.Builder()
|
|
||||||
.withEntityClass(DatabaseMigrationScheduleTransitionConverterTestEntity.class)
|
|
||||||
.buildUnitTestExtension();
|
|
||||||
|
|
||||||
private static final ImmutableSortedMap<DateTime, MigrationState> values =
|
|
||||||
ImmutableSortedMap.of(
|
|
||||||
START_OF_TIME,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
DateTime.parse("2001-01-01T00:00:00.0Z"),
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
DateTime.parse("2002-01-01T01:00:00.0Z"),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
DateTime.parse("2002-01-01T02:00:00.0Z"),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
|
||||||
DateTime.parse("2002-01-02T00:00:00.0Z"),
|
|
||||||
MigrationState.SQL_PRIMARY,
|
|
||||||
DateTime.parse("2002-01-03T00:00:00.0Z"),
|
|
||||||
MigrationState.SQL_ONLY);
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void roundTripConversion_returnsSameTimedTransitionProperty() {
|
|
||||||
TimedTransitionProperty<MigrationState> timedTransitionProperty =
|
|
||||||
TimedTransitionProperty.fromValueMap(values);
|
|
||||||
DatabaseMigrationScheduleTransitionConverterTestEntity testEntity =
|
|
||||||
new DatabaseMigrationScheduleTransitionConverterTestEntity(timedTransitionProperty);
|
|
||||||
insertInDb(testEntity);
|
|
||||||
DatabaseMigrationScheduleTransitionConverterTestEntity persisted =
|
|
||||||
tm().transact(
|
|
||||||
() ->
|
|
||||||
tm().getEntityManager()
|
|
||||||
.find(DatabaseMigrationScheduleTransitionConverterTestEntity.class, "id"));
|
|
||||||
assertThat(persisted.timedTransitionProperty).containsExactlyEntriesIn(timedTransitionProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
private static class DatabaseMigrationScheduleTransitionConverterTestEntity
|
|
||||||
extends ImmutableObject {
|
|
||||||
|
|
||||||
@Id String name = "id";
|
|
||||||
|
|
||||||
TimedTransitionProperty<MigrationState> timedTransitionProperty;
|
|
||||||
|
|
||||||
private DatabaseMigrationScheduleTransitionConverterTestEntity() {}
|
|
||||||
|
|
||||||
private DatabaseMigrationScheduleTransitionConverterTestEntity(
|
|
||||||
TimedTransitionProperty<MigrationState> timedTransitionProperty) {
|
|
||||||
this.timedTransitionProperty = timedTransitionProperty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,17 +39,8 @@ import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
*/
|
*/
|
||||||
public class JpaEntityCoverageExtension implements BeforeEachCallback, AfterEachCallback {
|
public class JpaEntityCoverageExtension implements BeforeEachCallback, AfterEachCallback {
|
||||||
|
|
||||||
private static final ImmutableSet<String> IGNORE_ENTITIES =
|
|
||||||
ImmutableSet.of(
|
|
||||||
// DatabaseMigrationStateSchedule is persisted in tests, however any test that sets it
|
|
||||||
// needs to remove it in order to avoid affecting any other tests running in the same JVM.
|
|
||||||
// TODO(gbrodman): remove this when we implement proper read-only modes for the
|
|
||||||
// transaction managers.
|
|
||||||
"DatabaseMigrationStateSchedule");
|
|
||||||
|
|
||||||
public static final ImmutableSet<Class<?>> ALL_JPA_ENTITIES =
|
public static final ImmutableSet<Class<?>> ALL_JPA_ENTITIES =
|
||||||
PersistenceXmlUtility.getManagedClasses().stream()
|
PersistenceXmlUtility.getManagedClasses().stream()
|
||||||
.filter(e -> !IGNORE_ENTITIES.contains(e.getSimpleName()))
|
|
||||||
.filter(e -> e.isAnnotationPresent(Entity.class))
|
.filter(e -> e.isAnnotationPresent(Entity.class))
|
||||||
.filter(e -> !e.isAnnotationPresent(DiscriminatorValue.class))
|
.filter(e -> !e.isAnnotationPresent(DiscriminatorValue.class))
|
||||||
.collect(ImmutableSet.toImmutableSet());
|
.collect(ImmutableSet.toImmutableSet());
|
||||||
|
|
|
@ -71,8 +71,6 @@ import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||||
import google.registry.model.billing.BillingCancellation;
|
import google.registry.model.billing.BillingCancellation;
|
||||||
import google.registry.model.billing.BillingEvent;
|
import google.registry.model.billing.BillingEvent;
|
||||||
import google.registry.model.billing.BillingRecurrence;
|
import google.registry.model.billing.BillingRecurrence;
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.model.common.DnsRefreshRequest;
|
import google.registry.model.common.DnsRefreshRequest;
|
||||||
import google.registry.model.contact.Contact;
|
import google.registry.model.contact.Contact;
|
||||||
import google.registry.model.contact.ContactAuthInfo;
|
import google.registry.model.contact.ContactAuthInfo;
|
||||||
|
@ -123,7 +121,6 @@ import org.joda.money.CurrencyUnit;
|
||||||
import org.joda.money.Money;
|
import org.joda.money.Money;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.joda.time.DateTimeZone;
|
import org.joda.time.DateTimeZone;
|
||||||
import org.joda.time.Duration;
|
|
||||||
|
|
||||||
/** Static utils for setting up test resources. */
|
/** Static utils for setting up test resources. */
|
||||||
public final class DatabaseHelper {
|
public final class DatabaseHelper {
|
||||||
|
@ -1316,46 +1313,6 @@ public final class DatabaseHelper {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a SQL_PRIMARY state on the {@link DatabaseMigrationStateSchedule}.
|
|
||||||
*
|
|
||||||
* <p>In order to allow for tests to manipulate the clock how they need, we start the transitions
|
|
||||||
* one millisecond after the clock's current time (in case the clock's current value is
|
|
||||||
* START_OF_TIME). We then advance the clock one second so that we're in the SQL_PRIMARY phase.
|
|
||||||
*
|
|
||||||
* <p>We must use the current time, otherwise the setting of the migration state will fail due to
|
|
||||||
* an invalid transition.
|
|
||||||
*/
|
|
||||||
public static void setMigrationScheduleToSqlPrimary(FakeClock fakeClock) {
|
|
||||||
DateTime now = fakeClock.nowUtc();
|
|
||||||
tm().transact(
|
|
||||||
() ->
|
|
||||||
DatabaseMigrationStateSchedule.set(
|
|
||||||
ImmutableSortedMap.of(
|
|
||||||
START_OF_TIME,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
now.plusMillis(1),
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
now.plusMillis(2),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
now.plusMillis(3),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
|
||||||
now.plusMillis(4),
|
|
||||||
MigrationState.SQL_PRIMARY)));
|
|
||||||
fakeClock.advanceBy(Duration.standardSeconds(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes the database migration schedule, in essence transitioning to DATASTORE_ONLY. */
|
|
||||||
public static void removeDatabaseMigrationSchedule() {
|
|
||||||
// use the raw calls because going SQL_PRIMARY -> DATASTORE_ONLY is not valid
|
|
||||||
tm().transact(
|
|
||||||
() ->
|
|
||||||
tm().put(
|
|
||||||
new DatabaseMigrationStateSchedule(
|
|
||||||
DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP)));
|
|
||||||
DatabaseMigrationStateSchedule.CACHE.invalidateAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImmutableList<String> getDnsRefreshRequests(TargetType type, String... names) {
|
private static ImmutableList<String> getDnsRefreshRequests(TargetType type, String... names) {
|
||||||
return tm().transact(
|
return tm().transact(
|
||||||
() ->
|
() ->
|
||||||
|
|
|
@ -1,66 +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.model.common.DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP;
|
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.testing.DatabaseHelper;
|
|
||||||
import org.joda.time.DateTime;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
/** Tests for {@link GetDatabaseMigrationStateCommand}. */
|
|
||||||
public class GetDatabaseMigrationStateCommandTest
|
|
||||||
extends CommandTestCase<GetDatabaseMigrationStateCommand> {
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void afterEach() {
|
|
||||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testInitial_returnsDatastoreOnly() throws Exception {
|
|
||||||
runCommand();
|
|
||||||
assertStdoutIs(
|
|
||||||
String.format("Current migration schedule: %s\n", DEFAULT_TRANSITION_MAP.toValueMap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFullSchedule() throws Exception {
|
|
||||||
DateTime now = fakeClock.nowUtc();
|
|
||||||
ImmutableSortedMap<DateTime, MigrationState> transitions =
|
|
||||||
ImmutableSortedMap.of(
|
|
||||||
START_OF_TIME,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
now.plusHours(1),
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
now.plusHours(2),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
now.plusHours(3),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
|
||||||
now.plusHours(4),
|
|
||||||
MigrationState.SQL_PRIMARY,
|
|
||||||
now.plusHours(5),
|
|
||||||
MigrationState.SQL_ONLY);
|
|
||||||
tm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
|
|
||||||
runCommand();
|
|
||||||
assertStdoutIs(String.format("Current migration schedule: %s\n", transitions));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,181 +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 com.google.common.truth.Truth8.assertThat;
|
|
||||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP;
|
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
|
||||||
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.DatabaseMigrationStateSchedule;
|
|
||||||
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
|
|
||||||
import google.registry.testing.DatabaseHelper;
|
|
||||||
import org.joda.time.DateTime;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
/** Tests for {@link SetDatabaseMigrationStateCommand}. */
|
|
||||||
public class SetDatabaseMigrationStateCommandTest
|
|
||||||
extends CommandTestCase<SetDatabaseMigrationStateCommand> {
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void afterEach() {
|
|
||||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSuccess_setsBasicSchedule() throws Exception {
|
|
||||||
assertThat(DatabaseMigrationStateSchedule.get()).isEqualTo(DEFAULT_TRANSITION_MAP);
|
|
||||||
assertThat(tm().transact(() -> tm().loadSingleton(DatabaseMigrationStateSchedule.class)))
|
|
||||||
.isEmpty();
|
|
||||||
runCommandForced("--migration_schedule=1970-01-01T00:00:00.000Z=DATASTORE_ONLY");
|
|
||||||
tm().transact(
|
|
||||||
() ->
|
|
||||||
assertThat(
|
|
||||||
tm().loadSingleton(DatabaseMigrationStateSchedule.class)
|
|
||||||
.get()
|
|
||||||
.migrationTransitions)
|
|
||||||
.isEqualTo(DEFAULT_TRANSITION_MAP));
|
|
||||||
assertThat(DatabaseMigrationStateSchedule.get()).isEqualTo(DEFAULT_TRANSITION_MAP);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSuccess_fullSchedule() throws Exception {
|
|
||||||
DateTime now = fakeClock.nowUtc();
|
|
||||||
DateTime datastorePrimary = now.plusHours(1);
|
|
||||||
DateTime datastorePrimaryNoAsync = now.plusHours(2);
|
|
||||||
DateTime datastorePrimaryReadOnly = now.plusHours(3);
|
|
||||||
DateTime sqlPrimary = now.plusHours(4);
|
|
||||||
DateTime sqlOnly = now.plusHours(5);
|
|
||||||
runCommandForced(
|
|
||||||
String.format(
|
|
||||||
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
|
|
||||||
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY,"
|
|
||||||
+ "%s=SQL_PRIMARY,%s=SQL_ONLY",
|
|
||||||
START_OF_TIME,
|
|
||||||
datastorePrimary,
|
|
||||||
datastorePrimaryNoAsync,
|
|
||||||
datastorePrimaryReadOnly,
|
|
||||||
sqlPrimary,
|
|
||||||
sqlOnly));
|
|
||||||
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
|
|
||||||
.containsExactlyEntriesIn(
|
|
||||||
ImmutableSortedMap.of(
|
|
||||||
START_OF_TIME,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
datastorePrimary,
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
datastorePrimaryNoAsync,
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
datastorePrimaryReadOnly,
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
|
||||||
sqlPrimary,
|
|
||||||
MigrationState.SQL_PRIMARY,
|
|
||||||
sqlOnly,
|
|
||||||
MigrationState.SQL_ONLY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSuccess_warnsOnChangeSoon() throws Exception {
|
|
||||||
DateTime now = fakeClock.nowUtc();
|
|
||||||
runCommandForced(
|
|
||||||
String.format(
|
|
||||||
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY",
|
|
||||||
START_OF_TIME, now.plusMinutes(1)));
|
|
||||||
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
|
|
||||||
.containsExactlyEntriesIn(
|
|
||||||
ImmutableSortedMap.of(
|
|
||||||
START_OF_TIME,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
now.plusMinutes(1),
|
|
||||||
MigrationState.DATASTORE_PRIMARY));
|
|
||||||
assertInStdout("MAY BE DANGEROUS");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSuccess_goesBackward() throws Exception {
|
|
||||||
DateTime now = fakeClock.nowUtc();
|
|
||||||
runCommandForced(
|
|
||||||
String.format(
|
|
||||||
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
|
|
||||||
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY,"
|
|
||||||
+ "%s=DATASTORE_PRIMARY",
|
|
||||||
START_OF_TIME, now.plusHours(1), now.plusHours(2), now.plusHours(3), now.plusHours(4)));
|
|
||||||
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
|
|
||||||
.containsExactlyEntriesIn(
|
|
||||||
ImmutableSortedMap.of(
|
|
||||||
START_OF_TIME,
|
|
||||||
MigrationState.DATASTORE_ONLY,
|
|
||||||
now.plusHours(1),
|
|
||||||
MigrationState.DATASTORE_PRIMARY,
|
|
||||||
now.plusHours(2),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
|
||||||
now.plusHours(3),
|
|
||||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
|
||||||
now.plusHours(4),
|
|
||||||
MigrationState.DATASTORE_PRIMARY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFailure_invalidTransition() {
|
|
||||||
IllegalArgumentException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() ->
|
|
||||||
runCommandForced(
|
|
||||||
String.format(
|
|
||||||
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY_READ_ONLY",
|
|
||||||
START_OF_TIME, START_OF_TIME.plusHours(1))));
|
|
||||||
assertThat(thrown)
|
|
||||||
.hasMessageThat()
|
|
||||||
.isEqualTo(
|
|
||||||
"validStateTransitions map cannot transition from DATASTORE_ONLY "
|
|
||||||
+ "to DATASTORE_PRIMARY_READ_ONLY.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFailure_invalidTransitionFromOldToNew() {
|
|
||||||
// The map we pass in is valid by itself, but we can't go from DATASTORE_ONLY now to
|
|
||||||
// DATASTORE_PRIMARY_READ_ONLY now
|
|
||||||
DateTime now = fakeClock.nowUtc();
|
|
||||||
IllegalArgumentException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() ->
|
|
||||||
runCommandForced(
|
|
||||||
String.format(
|
|
||||||
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
|
|
||||||
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY",
|
|
||||||
START_OF_TIME, now.minusHours(3), now.minusHours(2), now.minusHours(1))));
|
|
||||||
assertThat(thrown)
|
|
||||||
.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_invalidParams() {
|
|
||||||
assertThrows(ParameterException.class, this::runCommandForced);
|
|
||||||
assertThrows(ParameterException.class, () -> runCommandForced("--migration_schedule=FOOBAR"));
|
|
||||||
assertThrows(
|
|
||||||
ParameterException.class,
|
|
||||||
() -> runCommandForced("--migration_schedule=1970-01-01T00:00:00.000Z=FOOBAR"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -241,12 +241,6 @@
|
||||||
primary key (scope, type)
|
primary key (scope, type)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table "DatabaseMigrationStateSchedule" (
|
|
||||||
id int8 not null,
|
|
||||||
migration_transitions hstore,
|
|
||||||
primary key (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table "DelegationSignerData" (
|
create table "DelegationSignerData" (
|
||||||
algorithm int4 not null,
|
algorithm int4 not null,
|
||||||
digest bytea not null,
|
digest bytea not null,
|
||||||
|
|
Loading…
Add table
Reference in a new issue