mirror of
https://github.com/google/nomulus
synced 2026-09-10 18:16:24 +00:00
Implement read-only transaction manager modes for R3.0 migration (#1241)
This involves: - Altering both transaction managers to check for a read-only mode at the start of standard write actions (e.g. delete, put). - Altering both raw layers (entity manager, ofy) to throw exceptions on write actions as well - Implementing bypass routes for reading / setting / removing the schedule itself so that we don't get "stuck"
This commit is contained in:
@@ -40,6 +40,7 @@ import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.truth.Truth8;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.googlecode.objectify.Key;
|
||||
@@ -357,10 +358,10 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||
// even though the domain came first in the file
|
||||
// 2. that the allocation token delete occurred after the insertions
|
||||
InOrder inOrder = Mockito.inOrder(spy);
|
||||
inOrder.verify(spy).put(any(ContactResource.class));
|
||||
inOrder.verify(spy).put(any(DomainBase.class));
|
||||
inOrder.verify(spy).delete(toDelete.createVKey());
|
||||
inOrder.verify(spy).put(any(SqlReplayCheckpoint.class));
|
||||
inOrder.verify(spy).putIgnoringReadOnly(any(ContactResource.class));
|
||||
inOrder.verify(spy).putIgnoringReadOnly(any(DomainBase.class));
|
||||
inOrder.verify(spy).deleteIgnoringReadOnly(toDelete.createVKey());
|
||||
inOrder.verify(spy).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -399,8 +400,8 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||
// deletes have higher weight
|
||||
ArgumentCaptor<Object> putCaptor = ArgumentCaptor.forClass(Object.class);
|
||||
InOrder inOrder = Mockito.inOrder(spy);
|
||||
inOrder.verify(spy).delete(contact.createVKey());
|
||||
inOrder.verify(spy).put(putCaptor.capture());
|
||||
inOrder.verify(spy).deleteIgnoringReadOnly(contact.createVKey());
|
||||
inOrder.verify(spy).putIgnoringReadOnly(putCaptor.capture());
|
||||
assertThat(putCaptor.getValue().getClass()).isEqualTo(ContactResource.class);
|
||||
assertThat(jpaTm().transact(() -> jpaTm().loadByKey(contact.createVKey()).getEmailAddress()))
|
||||
.isEqualTo("replay@example.tld");
|
||||
@@ -441,9 +442,9 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||
}
|
||||
});
|
||||
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
|
||||
// jpaTm()::put should only have been called with the checkpoint
|
||||
verify(spy, times(2)).put(any(SqlReplayCheckpoint.class));
|
||||
verify(spy, times(2)).put(any());
|
||||
// jpaTm()::putIgnoringReadOnly should only have been called with the checkpoint
|
||||
verify(spy, times(2)).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
|
||||
verify(spy, times(2)).putIgnoringReadOnly(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -556,6 +557,34 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReplay_duringReadOnly() throws Exception {
|
||||
DateTime now = fakeClock.nowUtc();
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
jpaTm().insertWithoutBackup(TestObject.create("previous to delete"));
|
||||
SqlReplayCheckpoint.set(now.minusMinutes(2));
|
||||
});
|
||||
Key<CommitLogManifest> manifestKey =
|
||||
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
|
||||
saveDiffFile(
|
||||
gcsUtils,
|
||||
createCheckpoint(now.minusMinutes(1)),
|
||||
CommitLogManifest.create(
|
||||
getBucketKey(1),
|
||||
now.minusMinutes(1),
|
||||
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))),
|
||||
CommitLogMutation.create(manifestKey, TestObject.create("a")));
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(fakeClock);
|
||||
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
assertThat(Iterables.getOnlyElement(jpaTm().loadAllOf(TestObject.class)).getId())
|
||||
.isEqualTo("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReplay_deleteAndResaveCascade_withOtherDeletion_noErrors() throws Exception {
|
||||
createTld("tld");
|
||||
|
||||
+19
@@ -23,12 +23,17 @@ import static google.registry.model.common.DatabaseMigrationStateSchedule.Migrat
|
||||
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY_READ_ONLY;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
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.model.domain.token.AllocationToken;
|
||||
import google.registry.model.domain.token.AllocationToken.TokenType;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
@@ -152,6 +157,20 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
|
||||
assertThat(tm().isOfy()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_factoryUsesReadOnly() {
|
||||
createTld("tld");
|
||||
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);
|
||||
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
|
||||
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
|
||||
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
|
||||
runValidTransition(SQL_PRIMARY_READ_ONLY, SQL_PRIMARY);
|
||||
persistResource(token);
|
||||
}
|
||||
|
||||
private void runValidTransition(MigrationState from, MigrationState to) {
|
||||
ImmutableSortedMap<DateTime, MigrationState> transitions =
|
||||
createMapEndingWithTransition(from, to);
|
||||
|
||||
@@ -47,6 +47,7 @@ import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.replay.EntityTest.EntityForTesting;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
@@ -61,9 +62,11 @@ import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
/** Tests for our wrapper around Objectify. */
|
||||
public class OfyTest {
|
||||
|
||||
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2000-01-01TZ"));
|
||||
|
||||
@RegisterExtension
|
||||
public final AppEngineExtension appEngine =
|
||||
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
|
||||
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
|
||||
|
||||
/** An entity to use in save and delete tests. */
|
||||
private HistoryEntry someObject;
|
||||
@@ -434,4 +437,12 @@ public class OfyTest {
|
||||
// Test the normal loading again to verify that we've restored the original session unchanged.
|
||||
assertThat(auditedOfy().load().entity(someObject).now()).isEqualTo(someObject.asHistoryEntry());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadOnly_failsWrite() {
|
||||
Ofy ofy = new Ofy(fakeClock);
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(fakeClock);
|
||||
assertThrows(ReadOnlyModeException.class, () -> ofy.save().entity(someObject).now());
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ public class ReplicateToDatastoreActionTest {
|
||||
|
||||
@Test
|
||||
void testNotInMigrationState_doesNothing() {
|
||||
// set a schedule that backtracks the current status to DATASTORE_PRIMARY_READ_ONLY
|
||||
// set a schedule that backtracks the current status to DATASTORE_PRIMARY
|
||||
DateTime now = fakeClock.nowUtc();
|
||||
jpaTm()
|
||||
.transact(
|
||||
@@ -225,6 +225,7 @@ public class ReplicateToDatastoreActionTest {
|
||||
.put(START_OF_TIME.plusHours(3), 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)
|
||||
.build()));
|
||||
fakeClock.advanceBy(Duration.standardDays(1));
|
||||
|
||||
@@ -237,6 +238,6 @@ public class ReplicateToDatastoreActionTest {
|
||||
.hasLogAtLevelWithMessage(
|
||||
Level.INFO,
|
||||
"Skipping ReplicateToDatastoreAction because we are in migration phase "
|
||||
+ "DATASTORE_PRIMARY_READ_ONLY.");
|
||||
+ "DATASTORE_PRIMARY.");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -638,7 +638,9 @@ class JpaTransactionManagerImplTest {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm().query("FROM TestEntity", TestEntity.class).getResultList().stream()
|
||||
jpaTm()
|
||||
.query("FROM TestEntity", TestEntity.class)
|
||||
.getResultList()
|
||||
.forEach(e -> assertThat(jpaTm().getEntityManager().contains(e)).isFalse()));
|
||||
jpaTm()
|
||||
.transact(
|
||||
|
||||
@@ -31,7 +31,9 @@ import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.ofy.DatastoreTransactionManager;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.InjectExtension;
|
||||
@@ -406,6 +408,13 @@ public class TransactionManagerTest {
|
||||
assertThat(tm().transact(() -> tm().loadByKey(theEntity.key())).data).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@TestOfyAndSql
|
||||
void testReadOnly_writeFails() {
|
||||
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(fakeClock);
|
||||
assertThrows(ReadOnlyModeException.class, () -> tm().transact(() -> tm().put(theEntity)));
|
||||
DatabaseHelper.removeDatabaseMigrationSchedule();
|
||||
}
|
||||
|
||||
private static void assertEntityExists(TestEntity entity) {
|
||||
assertThat(tm().transact(() -> tm().exists(entity))).isTrue();
|
||||
}
|
||||
|
||||
@@ -1360,6 +1360,33 @@ public class DatabaseHelper {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a DATASTORE_PRIMARY_READ_ONLY 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 setMigrationScheduleToDatastorePrimaryReadOnly(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_READ_ONLY)));
|
||||
fakeClock.advanceBy(Duration.standardSeconds(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a SQL_PRIMARY state on the {@link DatabaseMigrationStateSchedule}.
|
||||
*
|
||||
@@ -1395,8 +1422,9 @@ public class DatabaseHelper {
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.loadSingleton(DatabaseMigrationStateSchedule.class)
|
||||
.ifPresent(jpaTm()::delete));
|
||||
.putIgnoringReadOnly(
|
||||
new DatabaseMigrationStateSchedule(
|
||||
DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP)));
|
||||
DatabaseMigrationStateSchedule.CACHE.invalidateAll();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user