Allow disabling UpdateAutoTimestamp updates (#906)

* Allow disabling UpdateAutoTimestamp updates

Allow us to disable timestamp updates within a try-with-resources block for a
given thread.  This functionality will be needed for transaction replays both
to and from datastore.

As part of this, also upgrade the UpdateAutoTimestampTest to a
DualDatabaseTest so we can verify that the functionality works both on
Datastore and Cloud SQL.
This commit is contained in:
Michael Muller 2020-12-15 10:34:52 -05:00 committed by GitHub
parent 08738c54c8
commit 090eeaa618
4 changed files with 95 additions and 12 deletions

View file

@ -28,6 +28,11 @@ import org.joda.time.DateTime;
*/ */
public class UpdateAutoTimestamp extends ImmutableObject { public class UpdateAutoTimestamp extends ImmutableObject {
// When set to true, database converters/translators should do tha auto update. When set to
// false, auto update should be suspended (this exists to allow us to preserve the original value
// during a replay).
static ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
DateTime timestamp; DateTime timestamp;
/** Returns the timestamp, or {@code START_OF_TIME} if it's null. */ /** Returns the timestamp, or {@code START_OF_TIME} if it's null. */
@ -40,4 +45,30 @@ public class UpdateAutoTimestamp extends ImmutableObject {
instance.timestamp = timestamp; instance.timestamp = timestamp;
return instance; return instance;
} }
// TODO(b/175610935): Remove the auto-update disabling code below after migration.
/** Class to allow us to safely disable auto-update in a try-with-resources block. */
public static class DisableAutoUpdateResource implements AutoCloseable {
DisableAutoUpdateResource() {
autoUpdateEnabled.set(false);
}
@Override
public void close() {
autoUpdateEnabled.set(true);
}
}
/**
* Resturns a resource that disables auto-updates on all {@link UpdateAutoTimestamp}s in the
* current thread, suitable for use with in a try-with-resources block.
*/
public static DisableAutoUpdateResource disableAutoUpdate() {
return new DisableAutoUpdateResource();
}
public static boolean autoUpdateEnabled() {
return autoUpdateEnabled.get();
}
} }

View file

@ -46,7 +46,10 @@ public class UpdateAutoTimestampTranslatorFactory
/** Save a timestamp, setting it to the current time. */ /** Save a timestamp, setting it to the current time. */
@Override @Override
public Date saveValue(UpdateAutoTimestamp pojoValue) { public Date saveValue(UpdateAutoTimestamp pojoValue) {
return tm().getTransactionTime().toDate(); return UpdateAutoTimestamp.autoUpdateEnabled()
}}; ? tm().getTransactionTime().toDate()
: pojoValue.getTimestamp().toDate();
}
};
} }
} }

View file

@ -31,7 +31,14 @@ public class UpdateAutoTimestampConverter
@Override @Override
public Timestamp convertToDatabaseColumn(UpdateAutoTimestamp entity) { public Timestamp convertToDatabaseColumn(UpdateAutoTimestamp entity) {
return Timestamp.from(DateTimeUtils.toZonedDateTime(jpaTm().getTransactionTime()).toInstant()); return Timestamp.from(
DateTimeUtils.toZonedDateTime(
UpdateAutoTimestamp.autoUpdateEnabled()
|| entity == null
|| entity.getTimestamp() == null
? jpaTm().getTransactionTime()
: entity.getTimestamp())
.toInstant());
} }
@Override @Override

View file

@ -15,64 +15,106 @@
package google.registry.model; package google.registry.model;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.common.CrossTldSingleton; import google.registry.model.common.CrossTldSingleton;
import google.registry.persistence.VKey;
import google.registry.schema.replay.EntityTest.EntityForTesting; import google.registry.schema.replay.EntityTest.EntityForTesting;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link UpdateAutoTimestamp}. */ /** Unit tests for {@link UpdateAutoTimestamp}. */
@DualDatabaseTest
public class UpdateAutoTimestampTest { public class UpdateAutoTimestampTest {
FakeClock clock = new FakeClock();
@RegisterExtension @RegisterExtension
public final AppEngineExtension appEngine = public final AppEngineExtension appEngine =
AppEngineExtension.builder() AppEngineExtension.builder()
.withDatastoreAndCloudSql() .withDatastoreAndCloudSql()
.withJpaUnitTestEntities(UpdateAutoTimestampTestObject.class)
.withOfyTestEntities(UpdateAutoTimestampTestObject.class) .withOfyTestEntities(UpdateAutoTimestampTestObject.class)
.withClock(clock)
.build(); .build();
/** Timestamped class. */ /** Timestamped class. */
@Entity(name = "UatTestEntity") @Entity(name = "UatTestEntity")
@javax.persistence.Entity
@EntityForTesting @EntityForTesting
public static class UpdateAutoTimestampTestObject extends CrossTldSingleton { public static class UpdateAutoTimestampTestObject extends CrossTldSingleton {
@Ignore @javax.persistence.Id long id = SINGLETON_ID;
UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null); UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null);
} }
private UpdateAutoTimestampTestObject reload() { private UpdateAutoTimestampTestObject reload() {
return ofy().load().entity(new UpdateAutoTimestampTestObject()).now(); return tm().transact(
() ->
tm().load(
VKey.create(
UpdateAutoTimestampTestObject.class,
1L,
Key.create(new UpdateAutoTimestampTestObject()))));
} }
@Test @TestOfyAndSql
void testSaveSetsTime() { void testSaveSetsTime() {
DateTime transactionTime = DateTime transactionTime =
tm().transact( tm().transact(
() -> { () -> {
UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject(); UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject();
assertThat(object.updateTime.timestamp).isNull(); assertThat(object.updateTime.timestamp).isNull();
ofy().save().entity(object); tm().insert(object);
return tm().getTransactionTime(); return tm().getTransactionTime();
}); });
ofy().clearSessionCache(); tm().clearSessionCache();
assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime); assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime);
} }
@Test @TestOfyAndSql
void testDisabledUpdates() throws Exception {
DateTime initialTime =
tm().transact(
() -> {
tm().insert(new UpdateAutoTimestampTestObject());
return tm().getTransactionTime();
});
UpdateAutoTimestampTestObject object = reload();
clock.advanceOneMilli();
try (UpdateAutoTimestamp.DisableAutoUpdateResource disabler =
new UpdateAutoTimestamp.DisableAutoUpdateResource()) {
DateTime secondTransactionTime =
tm().transact(
() -> {
tm().put(object);
return tm().getTransactionTime();
});
assertThat(secondTransactionTime).isGreaterThan(initialTime);
}
assertThat(reload().updateTime.timestamp).isEqualTo(initialTime);
}
@TestOfyAndSql
void testResavingOverwritesOriginalTime() { void testResavingOverwritesOriginalTime() {
DateTime transactionTime = DateTime transactionTime =
tm().transact( tm().transact(
() -> { () -> {
UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject(); UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject();
object.updateTime = UpdateAutoTimestamp.create(DateTime.now(UTC).minusDays(1)); object.updateTime = UpdateAutoTimestamp.create(DateTime.now(UTC).minusDays(1));
ofy().save().entity(object); tm().insert(object);
return tm().getTransactionTime(); return tm().getTransactionTime();
}); });
ofy().clearSessionCache(); tm().clearSessionCache();
assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime); assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime);
} }
} }