mirror of
https://github.com/google/nomulus.git
synced 2025-07-24 03:30:46 +02:00
Add a no-async actions DB migration phase (#1579)
* Add a no-async actions DB migration phase This needs to be set several hours prior to entering the READONLY stage. This is not a read-only stage; all synchronous actions under Datastore (such as domain creates) will continue to succeed. The only thing that will fail is host deletes, host renames, and contact deletes, as these three actions require a mapreduce to run before they are complete, and we don't want mapreduces hanging around and executing during what is supposed to be a short duration READONLY period.
This commit is contained in:
parent
86226d6ffa
commit
396e1fb0cf
16 changed files with 213 additions and 25 deletions
|
@ -25,6 +25,7 @@ import static google.registry.model.ResourceTransferUtils.handlePendingTransferO
|
|||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
@ -94,6 +95,7 @@ public final class ContactDeleteFlow implements TransactionalFlow {
|
|||
extensionManager.register(MetadataExtension.class);
|
||||
validateRegistrarIsLoggedIn(registrarId);
|
||||
extensionManager.validate();
|
||||
assertAsyncActionsAreAllowed();
|
||||
DateTime now = tm().getTransactionTime();
|
||||
checkLinkedDomains(targetId, now, ContactResource.class, DomainBase::getReferencedContacts);
|
||||
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
|
||||
|
|
|
@ -22,6 +22,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
|||
import static google.registry.flows.host.HostFlowUtils.validateHostName;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
@ -96,6 +97,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
|
|||
extensionManager.register(MetadataExtension.class);
|
||||
validateRegistrarIsLoggedIn(registrarId);
|
||||
extensionManager.validate();
|
||||
assertAsyncActionsAreAllowed();
|
||||
DateTime now = tm().getTransactionTime();
|
||||
validateHostName(targetId);
|
||||
checkLinkedDomains(targetId, now, HostResource.class, DomainBase::getNameservers);
|
||||
|
|
|
@ -28,6 +28,7 @@ import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomain
|
|||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
|
||||
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
|
||||
|
@ -136,6 +137,9 @@ public final class HostUpdateFlow implements TransactionalFlow {
|
|||
validateHostName(targetId);
|
||||
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
|
||||
boolean isHostRename = suppliedNewHostName != null;
|
||||
if (isHostRename) {
|
||||
assertAsyncActionsAreAllowed();
|
||||
}
|
||||
String oldHostName = targetId;
|
||||
String newHostName = firstNonNull(suppliedNewHostName, oldHostName);
|
||||
DomainBase oldSuperordinateDomain =
|
||||
|
|
|
@ -30,6 +30,7 @@ import google.registry.model.annotations.DeleteAfterMigration;
|
|||
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
|
||||
import google.registry.model.replay.SqlOnlyEntity;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.PersistenceException;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -62,11 +63,28 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
|
|||
* 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);
|
||||
|
||||
private final PrimaryDatabase primaryDatabase;
|
||||
|
@ -146,11 +164,17 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
|
|||
.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(
|
||||
|
@ -165,10 +189,9 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
|
|||
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);
|
||||
}
|
||||
Arrays.stream(MigrationState.values()).forEach(state -> builder.put(state, state));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -246,7 +269,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
|
|||
* 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.
|
||||
* time to the new map at the current time must also be valid.
|
||||
*/
|
||||
private static void validateTransitionAtCurrentTime(
|
||||
TimedTransitionProperty<MigrationState, MigrationStateTransition> newTransitions) {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package google.registry.persistence.transaction;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_NO_ASYNC;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
|
@ -23,6 +24,7 @@ import com.google.appengine.api.utils.SystemProperty.Environment.Value;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Suppliers;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.annotations.DeleteAfterMigration;
|
||||
import google.registry.model.common.DatabaseMigrationStateSchedule;
|
||||
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
|
||||
import google.registry.model.ofy.DatastoreTransactionManager;
|
||||
|
@ -198,6 +200,22 @@ public final class TransactionManagerFactory {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that async actions (contact/host deletes and host renames) are allowed.
|
||||
*
|
||||
* <p>These are allowed at all times except during the {@link
|
||||
* DatabaseMigrationStateSchedule.MigrationState#DATASTORE_PRIMARY_NO_ASYNC} stage. Note that
|
||||
* {@link ReadOnlyModeException} may well be thrown during other read-only stages inside the
|
||||
* transaction manager; this method specifically checks only async actions.
|
||||
*/
|
||||
@DeleteAfterMigration
|
||||
public static void assertAsyncActionsAreAllowed() {
|
||||
if (DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC))
|
||||
.equals(DATASTORE_PRIMARY_NO_ASYNC)) {
|
||||
throw new ReadOnlyModeException();
|
||||
}
|
||||
}
|
||||
|
||||
/** Registry is currently undergoing maintenance and is in read-only mode. */
|
||||
public static class ReadOnlyModeException extends IllegalStateException {
|
||||
public ReadOnlyModeException() {
|
||||
|
|
|
@ -254,6 +254,15 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
|
|||
assertIcannReportingActivityFieldLogged("srs-cont-delete");
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testModification_duringNoAsyncPhase() throws Exception {
|
||||
persistActiveContact(getUniqueIdFromCommand());
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
|
||||
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testModification_duringReadOnlyPhase() throws Exception {
|
||||
persistActiveContact(getUniqueIdFromCommand());
|
||||
|
|
|
@ -840,6 +840,15 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
|||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testSuccess_inNoAsyncPhase() throws Exception {
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
|
||||
persistContactsAndHosts();
|
||||
runFlowAssertResponse(
|
||||
loadFile("domain_create_response_noasync.xml", ImmutableMap.of("DOMAIN", "example.tld")));
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testSuccess_maxNumberOfNameservers() throws Exception {
|
||||
setEppInput("domain_create_13_nameservers.xml");
|
||||
|
|
|
@ -358,6 +358,15 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
|
|||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testModification_duringNoAsyncPhase() {
|
||||
persistActiveHost("ns1.example.tld");
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
|
||||
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
private void assertOfyDeleteSuccess(String registrarId, String clientTrid, boolean isSuperuser)
|
||||
throws Exception {
|
||||
HostResource deletedHost = reloadResourceByForeignKey();
|
||||
|
|
|
@ -1355,7 +1355,43 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, HostResour
|
|||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testModification_duringReadOnlyPhase() throws Exception {
|
||||
void testSuccess_nonHostRename_inNoAsyncPhase_succeeds() throws Exception {
|
||||
setEppInput("host_update_name_unchanged.xml");
|
||||
createTld("tld");
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
|
||||
DomainBase domain = persistActiveDomain("example.tld");
|
||||
HostResource oldHost = persistActiveSubordinateHost(oldHostName(), domain);
|
||||
clock.advanceOneMilli();
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
// The example xml doesn't do a host rename, so reloading the host should work.
|
||||
assertAboutHosts()
|
||||
.that(reloadResourceByForeignKey())
|
||||
.hasLastSuperordinateChange(oldHost.getLastSuperordinateChange())
|
||||
.and()
|
||||
.hasSuperordinateDomain(domain.createVKey())
|
||||
.and()
|
||||
.hasPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.and()
|
||||
.hasLastTransferTime(null)
|
||||
.and()
|
||||
.hasOnlyOneHistoryEntryWhich()
|
||||
.hasType(HistoryEntry.Type.HOST_UPDATE);
|
||||
assertDnsTasksEnqueued("ns1.example.tld");
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testRename_duringNoAsyncPhase_fails() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
|
||||
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testModification_duringReadOnlyPhase_fails() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(clock);
|
||||
|
|
|
@ -17,6 +17,7 @@ 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;
|
||||
|
@ -71,10 +72,12 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
|||
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
|
||||
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_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);
|
||||
|
||||
|
@ -94,6 +97,7 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
|||
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);
|
||||
|
@ -124,7 +128,8 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
|||
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
|
||||
.put(START_OF_TIME, DATASTORE_ONLY)
|
||||
.put(startTime.plusHours(1), DATASTORE_PRIMARY)
|
||||
.put(startTime.plusHours(2), DATASTORE_PRIMARY_READ_ONLY)
|
||||
.put(startTime.plusHours(2), DATASTORE_PRIMARY_NO_ASYNC)
|
||||
.put(startTime.plusHours(3), DATASTORE_PRIMARY_READ_ONLY)
|
||||
.build();
|
||||
assertThat(
|
||||
assertThrows(
|
||||
|
@ -163,7 +168,8 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
|||
fakeClock.setTo(START_OF_TIME.plusDays(1));
|
||||
AllocationToken token =
|
||||
new AllocationToken.Builder().setToken("token").setTokenType(TokenType.SINGLE_USE).build();
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
|
||||
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_NO_ASYNC);
|
||||
runValidTransition(DATASTORE_PRIMARY_NO_ASYNC, DATASTORE_PRIMARY_READ_ONLY);
|
||||
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
|
||||
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
|
||||
|
|
|
@ -366,8 +366,9 @@ public class ReplicateToDatastoreActionTest {
|
|||
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
|
||||
.put(START_OF_TIME, MigrationState.DATASTORE_ONLY)
|
||||
.put(START_OF_TIME.plusHours(1), MigrationState.DATASTORE_PRIMARY)
|
||||
.put(START_OF_TIME.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
|
||||
.put(START_OF_TIME.plusHours(3), MigrationState.SQL_PRIMARY)
|
||||
.put(START_OF_TIME.plusHours(2), MigrationState.DATASTORE_PRIMARY_NO_ASYNC)
|
||||
.put(START_OF_TIME.plusHours(3), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
|
||||
.put(START_OF_TIME.plusHours(4), MigrationState.SQL_PRIMARY)
|
||||
.put(now.plusHours(1), MigrationState.SQL_PRIMARY_READ_ONLY)
|
||||
.put(now.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
|
||||
.put(now.plusHours(3), MigrationState.DATASTORE_PRIMARY)
|
||||
|
|
|
@ -47,7 +47,9 @@ public class DatabaseMigrationScheduleTransitionConverterTest {
|
|||
MigrationState.DATASTORE_ONLY,
|
||||
DateTime.parse("2001-01-01T00:00:00.0Z"),
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
DateTime.parse("2002-01-01T00:00:00.0Z"),
|
||||
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,
|
||||
|
|
|
@ -1429,6 +1429,33 @@ public class DatabaseHelper {
|
|||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a DATASTORE_PRIMARY_NO_ASYNC 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
|
||||
* DATASTORE_PRIMARY_READ_ONLY 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 setMigrationScheduleToDatastorePrimaryNoAsync(FakeClock fakeClock) {
|
||||
DateTime now = fakeClock.nowUtc();
|
||||
jpaTm()
|
||||
.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)));
|
||||
fakeClock.advanceBy(Duration.standardSeconds(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a DATASTORE_PRIMARY_READ_ONLY state on the {@link DatabaseMigrationStateSchedule}.
|
||||
*
|
||||
|
@ -1452,6 +1479,8 @@ public class DatabaseHelper {
|
|||
now.plusMillis(1),
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
now.plusMillis(2),
|
||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
||||
now.plusMillis(3),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY)));
|
||||
fakeClock.advanceBy(Duration.standardSeconds(1));
|
||||
}
|
||||
|
@ -1478,8 +1507,10 @@ public class DatabaseHelper {
|
|||
now.plusMillis(1),
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
now.plusMillis(2),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
||||
now.plusMillis(3),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
now.plusMillis(4),
|
||||
MigrationState.SQL_PRIMARY)));
|
||||
fakeClock.advanceBy(Duration.standardSeconds(1));
|
||||
}
|
||||
|
|
|
@ -54,10 +54,12 @@ public class GetDatabaseMigrationStateCommandTest
|
|||
now.plusHours(1),
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
now.plusHours(2),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
||||
now.plusHours(3),
|
||||
MigrationState.SQL_PRIMARY,
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
now.plusHours(4),
|
||||
MigrationState.SQL_PRIMARY,
|
||||
now.plusHours(5),
|
||||
MigrationState.SQL_ONLY);
|
||||
jpaTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
|
||||
runCommand();
|
||||
|
|
|
@ -64,14 +64,21 @@ public class SetDatabaseMigrationStateCommandTest
|
|||
void testSuccess_fullSchedule() throws Exception {
|
||||
DateTime now = fakeClock.nowUtc();
|
||||
DateTime datastorePrimary = now.plusHours(1);
|
||||
DateTime datastorePrimaryReadOnly = now.plusHours(2);
|
||||
DateTime sqlPrimary = now.plusHours(3);
|
||||
DateTime sqlOnly = now.plusHours(4);
|
||||
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_READ_ONLY,%s=SQL_PRIMARY,%s=SQL_ONLY",
|
||||
START_OF_TIME, datastorePrimary, datastorePrimaryReadOnly, sqlPrimary, sqlOnly));
|
||||
+ "%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(
|
||||
|
@ -79,6 +86,8 @@ public class SetDatabaseMigrationStateCommandTest
|
|||
MigrationState.DATASTORE_ONLY,
|
||||
datastorePrimary,
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
datastorePrimaryNoAsync,
|
||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
||||
datastorePrimaryReadOnly,
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
sqlPrimary,
|
||||
|
@ -110,8 +119,9 @@ public class SetDatabaseMigrationStateCommandTest
|
|||
runCommandForced(
|
||||
String.format(
|
||||
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
|
||||
+ "%s=DATASTORE_PRIMARY_READ_ONLY,%s=DATASTORE_PRIMARY",
|
||||
START_OF_TIME, now.plusHours(1), now.plusHours(2), now.plusHours(3)));
|
||||
+ "%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(
|
||||
|
@ -120,8 +130,10 @@ public class SetDatabaseMigrationStateCommandTest
|
|||
now.plusHours(1),
|
||||
MigrationState.DATASTORE_PRIMARY,
|
||||
now.plusHours(2),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
|
||||
now.plusHours(3),
|
||||
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
|
||||
now.plusHours(4),
|
||||
MigrationState.DATASTORE_PRIMARY));
|
||||
}
|
||||
|
||||
|
@ -152,9 +164,12 @@ public class SetDatabaseMigrationStateCommandTest
|
|||
() ->
|
||||
runCommandForced(
|
||||
String.format(
|
||||
"--migration_schedule=%s=DATASTORE_ONLY,"
|
||||
+ "%s=DATASTORE_PRIMARY,%s=DATASTORE_PRIMARY_READ_ONLY",
|
||||
START_OF_TIME, now.minusHours(2), now.minusHours(1)))))
|
||||
"--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)))))
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Cannot transition from current state-as-of-now DATASTORE_ONLY "
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:creData
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>%DOMAIN%</domain:name>
|
||||
<domain:crDate>1999-04-03T22:00:01.0Z</domain:crDate>
|
||||
<domain:exDate>2001-04-03T22:00:01.0Z</domain:exDate>
|
||||
</domain:creData>
|
||||
</resData>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
Loading…
Add table
Add a link
Reference in a new issue