diff --git a/core/src/main/java/google/registry/model/UpdateAutoTimestamp.java b/core/src/main/java/google/registry/model/UpdateAutoTimestamp.java index f9f69456e..c77c98fc8 100644 --- a/core/src/main/java/google/registry/model/UpdateAutoTimestamp.java +++ b/core/src/main/java/google/registry/model/UpdateAutoTimestamp.java @@ -28,6 +28,11 @@ import org.joda.time.DateTime; */ 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 autoUpdateEnabled = ThreadLocal.withInitial(() -> true); + DateTime timestamp; /** Returns the timestamp, or {@code START_OF_TIME} if it's null. */ @@ -40,4 +45,30 @@ public class UpdateAutoTimestamp extends ImmutableObject { instance.timestamp = timestamp; 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(); + } } diff --git a/core/src/main/java/google/registry/model/translators/UpdateAutoTimestampTranslatorFactory.java b/core/src/main/java/google/registry/model/translators/UpdateAutoTimestampTranslatorFactory.java index fd3518351..21b461339 100644 --- a/core/src/main/java/google/registry/model/translators/UpdateAutoTimestampTranslatorFactory.java +++ b/core/src/main/java/google/registry/model/translators/UpdateAutoTimestampTranslatorFactory.java @@ -46,7 +46,10 @@ public class UpdateAutoTimestampTranslatorFactory /** Save a timestamp, setting it to the current time. */ @Override public Date saveValue(UpdateAutoTimestamp pojoValue) { - return tm().getTransactionTime().toDate(); - }}; + return UpdateAutoTimestamp.autoUpdateEnabled() + ? tm().getTransactionTime().toDate() + : pojoValue.getTimestamp().toDate(); + } + }; } } diff --git a/core/src/main/java/google/registry/persistence/converter/UpdateAutoTimestampConverter.java b/core/src/main/java/google/registry/persistence/converter/UpdateAutoTimestampConverter.java index 5e6964176..ebd709693 100644 --- a/core/src/main/java/google/registry/persistence/converter/UpdateAutoTimestampConverter.java +++ b/core/src/main/java/google/registry/persistence/converter/UpdateAutoTimestampConverter.java @@ -31,7 +31,14 @@ public class UpdateAutoTimestampConverter @Override 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 diff --git a/core/src/test/java/google/registry/model/UpdateAutoTimestampTest.java b/core/src/test/java/google/registry/model/UpdateAutoTimestampTest.java index cfd49a987..6f6edc4db 100644 --- a/core/src/test/java/google/registry/model/UpdateAutoTimestampTest.java +++ b/core/src/test/java/google/registry/model/UpdateAutoTimestampTest.java @@ -15,64 +15,106 @@ package google.registry.model; 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 org.joda.time.DateTimeZone.UTC; +import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Ignore; import google.registry.model.common.CrossTldSingleton; +import google.registry.persistence.VKey; import google.registry.schema.replay.EntityTest.EntityForTesting; 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.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; /** Unit tests for {@link UpdateAutoTimestamp}. */ +@DualDatabaseTest public class UpdateAutoTimestampTest { + FakeClock clock = new FakeClock(); + @RegisterExtension public final AppEngineExtension appEngine = AppEngineExtension.builder() .withDatastoreAndCloudSql() + .withJpaUnitTestEntities(UpdateAutoTimestampTestObject.class) .withOfyTestEntities(UpdateAutoTimestampTestObject.class) + .withClock(clock) .build(); /** Timestamped class. */ @Entity(name = "UatTestEntity") + @javax.persistence.Entity @EntityForTesting public static class UpdateAutoTimestampTestObject extends CrossTldSingleton { + @Ignore @javax.persistence.Id long id = SINGLETON_ID; UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null); } 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() { DateTime transactionTime = tm().transact( () -> { UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject(); assertThat(object.updateTime.timestamp).isNull(); - ofy().save().entity(object); + tm().insert(object); return tm().getTransactionTime(); }); - ofy().clearSessionCache(); + tm().clearSessionCache(); 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() { DateTime transactionTime = tm().transact( () -> { UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject(); object.updateTime = UpdateAutoTimestamp.create(DateTime.now(UTC).minusDays(1)); - ofy().save().entity(object); + tm().insert(object); return tm().getTransactionTime(); }); - ofy().clearSessionCache(); + tm().clearSessionCache(); assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime); } }