Embed a ZonedDateTime as the UpdateAutoTimestamp in SQL (#1033)

* Embed a ZonedDateTime as the UpdateAutoTimestamp in SQL

This means we can get rid of the converter and more importantly, means
that reading the object from SQL does not affect the last-read time (the
test added to UpdateAutoTimestampTest failed prior to the production
code change).

For now we keep both time fields in UpdateAutoTimestamp however
post-migration, we can remove the joda-time field if we wish.

Note: I'm not sure why <now> is the time that we started getting
LazyInitializationExceptions in the LegacyHistoryObject and
ReplayExtension tests but we can solve that by just examining /
initializing the object within the transaction.
This commit is contained in:
gbrodman
2021-03-29 11:59:08 -04:00
committed by GitHub
parent 1e650bd0a1
commit a4e078305d
28 changed files with 2875 additions and 2932 deletions
@@ -16,6 +16,8 @@ package google.registry.model;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.xml.bind.annotation.XmlTransient;
@@ -40,6 +42,7 @@ public abstract class BackupGroupRoot extends ImmutableObject {
// Prevents subclasses from unexpectedly accessing as property (e.g., HostResource), which would
// require an unnecessary non-private setter method.
@Access(AccessType.FIELD)
@AttributeOverride(name = "lastUpdateTime", column = @Column(name = "updateTimestamp"))
UpdateAutoTimestamp updateTimestamp = UpdateAutoTimestamp.create(null);
/** Get the {@link UpdateAutoTimestamp} for this entity. */
@@ -14,11 +14,22 @@
package google.registry.model;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.OnLoad;
import google.registry.model.translators.UpdateAutoTimestampTranslatorFactory;
import google.registry.util.DateTimeUtils;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Transient;
import org.joda.time.DateTime;
/**
@@ -26,14 +37,44 @@ import org.joda.time.DateTime;
*
* @see UpdateAutoTimestampTranslatorFactory
*/
@Embeddable
public class UpdateAutoTimestamp extends ImmutableObject {
// When set to true, database converters/translators should do tha auto update. When set to
// When set to true, database converters/translators should do the auto update. When set to
// false, auto update should be suspended (this exists to allow us to preserve the original value
// during a replay).
private static ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
DateTime timestamp;
@Transient DateTime timestamp;
@Ignore
@Column(nullable = false)
ZonedDateTime lastUpdateTime;
// Unfortunately, we cannot use the @UpdateTimestamp annotation on "lastUpdateTime" in this class
// because Hibernate does not allow it to be used on @Embeddable classes, see
// https://hibernate.atlassian.net/browse/HHH-13235. This is a workaround.
@PrePersist
@PreUpdate
void setTimestamp() {
if (autoUpdateEnabled() || lastUpdateTime == null) {
lastUpdateTime = DateTimeUtils.toZonedDateTime(jpaTm().getTransactionTime());
}
}
@OnLoad
void onLoad() {
if (timestamp != null) {
lastUpdateTime = DateTimeUtils.toZonedDateTime(timestamp);
}
}
@PostLoad
void postLoad() {
if (lastUpdateTime != null) {
timestamp = DateTimeUtils.toJodaDateTime(lastUpdateTime);
}
}
/** Returns the timestamp, or {@code START_OF_TIME} if it's null. */
public DateTime getTimestamp() {
@@ -43,6 +84,7 @@ public class UpdateAutoTimestamp extends ImmutableObject {
public static UpdateAutoTimestamp create(@Nullable DateTime timestamp) {
UpdateAutoTimestamp instance = new UpdateAutoTimestamp();
instance.timestamp = timestamp;
instance.lastUpdateTime = timestamp == null ? null : DateTimeUtils.toZonedDateTime(timestamp);
return instance;
}
@@ -51,10 +51,8 @@ public final class RegistryLockDao {
return ImmutableList.copyOf(
jpaTm()
.query(
"SELECT lock FROM RegistryLock lock"
+ " WHERE lock.registrarId = :registrarId"
+ " AND lock.unlockCompletionTimestamp IS NULL"
+ " ORDER BY lock.domainName ASC",
"SELECT lock FROM RegistryLock lock WHERE lock.registrarId = :registrarId"
+ " AND lock.unlockCompletionTime IS NULL ORDER BY lock.domainName ASC",
RegistryLock.class)
.setParameter("registrarId", registrarId)
.getResultList());
@@ -89,9 +87,8 @@ public final class RegistryLockDao {
return jpaTm()
.query(
"SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId AND"
+ " lock.lockCompletionTimestamp IS NOT NULL AND"
+ " lock.unlockCompletionTimestamp IS NULL ORDER BY lock.revisionId"
+ " DESC",
+ " lock.lockCompletionTime IS NOT NULL AND lock.unlockCompletionTime IS NULL"
+ " ORDER BY lock.revisionId DESC",
RegistryLock.class)
.setParameter("repoId", repoId)
.setMaxResults(1)
@@ -110,8 +107,7 @@ public final class RegistryLockDao {
return jpaTm()
.query(
"SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId AND"
+ " lock.unlockCompletionTimestamp IS NOT NULL ORDER BY lock.revisionId"
+ " DESC",
+ " lock.unlockCompletionTime IS NOT NULL ORDER BY lock.revisionId DESC",
RegistryLock.class)
.setParameter("repoId", repoId)
.setMaxResults(1)
@@ -1,53 +0,0 @@
// Copyright 2019 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 google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.util.DateTimeUtils;
import java.sql.Timestamp;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA converter for storing/retrieving UpdateAutoTimestamp objects. */
@Converter(autoApply = true)
public class UpdateAutoTimestampConverter
implements AttributeConverter<UpdateAutoTimestamp, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(UpdateAutoTimestamp entity) {
return Timestamp.from(
DateTimeUtils.toZonedDateTime(
UpdateAutoTimestamp.autoUpdateEnabled()
|| entity == null
|| entity.getTimestamp() == null
? jpaTm().getTransactionTime()
: entity.getTimestamp())
.toInstant());
}
@Override
@Nullable
public UpdateAutoTimestamp convertToEntityAttribute(@Nullable Timestamp columnValue) {
if (columnValue == null) {
return null;
}
ZonedDateTime zdt = ZonedDateTime.ofInstant(columnValue.toInstant(), ZoneOffset.UTC);
return UpdateAutoTimestamp.create(DateTimeUtils.toJodaDateTime(zdt));
}
}
@@ -102,22 +102,22 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
/** When the lock is first requested. */
@Column(nullable = false)
private CreateAutoTimestamp lockRequestTimestamp = CreateAutoTimestamp.create(null);
private CreateAutoTimestamp lockRequestTime = CreateAutoTimestamp.create(null);
/** When the unlock is first requested. */
private ZonedDateTime unlockRequestTimestamp;
private ZonedDateTime unlockRequestTime;
/**
* When the user has verified the lock. If this field is null, it means the lock has not been
* verified yet (and thus not been put into effect).
*/
private ZonedDateTime lockCompletionTimestamp;
private ZonedDateTime lockCompletionTime;
/**
* When the user has verified the unlock of this lock. If this field is null, it means the unlock
* action has not been verified yet (and has not been put into effect).
*/
private ZonedDateTime unlockCompletionTimestamp;
private ZonedDateTime unlockCompletionTime;
/** The user must provide the random verification code in order to complete the action. */
@Column(nullable = false)
@@ -140,7 +140,7 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
private Duration relockDuration;
/** Time that this entity was last updated. */
private UpdateAutoTimestamp lastUpdateTimestamp;
private UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
public String getRepoId() {
return repoId;
@@ -158,25 +158,25 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
return registrarPocId;
}
public DateTime getLockRequestTimestamp() {
return lockRequestTimestamp.getTimestamp();
public DateTime getLockRequestTime() {
return lockRequestTime.getTimestamp();
}
/** Returns the unlock request timestamp or null if an unlock has not been requested yet. */
public Optional<DateTime> getUnlockRequestTimestamp() {
return Optional.ofNullable(unlockRequestTimestamp).map(DateTimeUtils::toJodaDateTime);
public Optional<DateTime> getUnlockRequestTime() {
return Optional.ofNullable(unlockRequestTime).map(DateTimeUtils::toJodaDateTime);
}
/** Returns the completion timestamp, or empty if this lock has not been completed yet. */
public Optional<DateTime> getLockCompletionTimestamp() {
return Optional.ofNullable(lockCompletionTimestamp).map(DateTimeUtils::toJodaDateTime);
public Optional<DateTime> getLockCompletionTime() {
return Optional.ofNullable(lockCompletionTime).map(DateTimeUtils::toJodaDateTime);
}
/**
* Returns the unlock completion timestamp, or empty if this unlock has not been completed yet.
*/
public Optional<DateTime> getUnlockCompletionTimestamp() {
return Optional.ofNullable(unlockCompletionTimestamp).map(DateTimeUtils::toJodaDateTime);
public Optional<DateTime> getUnlockCompletionTime() {
return Optional.ofNullable(unlockCompletionTime).map(DateTimeUtils::toJodaDateTime);
}
public String getVerificationCode() {
@@ -187,8 +187,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
return isSuperuser;
}
public DateTime getLastUpdateTimestamp() {
return lastUpdateTimestamp.getTimestamp();
public DateTime getLastUpdateTime() {
return lastUpdateTime.getTimestamp();
}
public Long getRevisionId() {
@@ -211,20 +211,20 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
}
public boolean isLocked() {
return lockCompletionTimestamp != null && unlockCompletionTimestamp == null;
return lockCompletionTime != null && unlockCompletionTime == null;
}
/** Returns true iff the lock was requested &gt;= 1 hour ago and has not been verified. */
public boolean isLockRequestExpired(DateTime now) {
return !getLockCompletionTimestamp().isPresent()
&& isBeforeOrAt(getLockRequestTimestamp(), now.minusHours(1));
return !getLockCompletionTime().isPresent()
&& isBeforeOrAt(getLockRequestTime(), now.minusHours(1));
}
/** Returns true iff the unlock was requested &gt;= 1 hour ago and has not been verified. */
public boolean isUnlockRequestExpired(DateTime now) {
Optional<DateTime> unlockRequestTimestamp = getUnlockRequestTimestamp();
Optional<DateTime> unlockRequestTimestamp = getUnlockRequestTime();
return unlockRequestTimestamp.isPresent()
&& !getUnlockCompletionTimestamp().isPresent()
&& !getUnlockCompletionTime().isPresent()
&& isBeforeOrAt(unlockRequestTimestamp.get(), now.minusHours(1));
}
@@ -278,18 +278,18 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
return this;
}
public Builder setUnlockRequestTimestamp(DateTime unlockRequestTimestamp) {
getInstance().unlockRequestTimestamp = toZonedDateTime(unlockRequestTimestamp);
public Builder setUnlockRequestTime(DateTime unlockRequestTime) {
getInstance().unlockRequestTime = toZonedDateTime(unlockRequestTime);
return this;
}
public Builder setLockCompletionTimestamp(DateTime lockCompletionTimestamp) {
getInstance().lockCompletionTimestamp = toZonedDateTime(lockCompletionTimestamp);
public Builder setLockCompletionTime(DateTime lockCompletionTime) {
getInstance().lockCompletionTime = toZonedDateTime(lockCompletionTime);
return this;
}
public Builder setUnlockCompletionTimestamp(DateTime unlockCompletionTimestamp) {
getInstance().unlockCompletionTimestamp = toZonedDateTime(unlockCompletionTimestamp);
public Builder setUnlockCompletionTime(DateTime unlockCompletionTime) {
getInstance().unlockCompletionTime = toZonedDateTime(unlockCompletionTime);
return this;
}
@@ -103,7 +103,7 @@ public final class DomainLockUtils {
RegistryLock lock = getByVerificationCode(verificationCode);
checkArgument(
!lock.getLockCompletionTimestamp().isPresent(),
!lock.getLockCompletionTime().isPresent(),
"Domain %s is already locked",
lock.getDomainName());
@@ -115,7 +115,7 @@ public final class DomainLockUtils {
!lock.isSuperuser() || isAdmin, "Non-admin user cannot complete admin lock");
RegistryLock newLock =
RegistryLockDao.save(lock.asBuilder().setLockCompletionTimestamp(now).build());
RegistryLockDao.save(lock.asBuilder().setLockCompletionTime(now).build());
setAsRelock(newLock);
tm().transact(() -> applyLockStatuses(newLock, now, isAdmin));
return newLock;
@@ -131,7 +131,7 @@ public final class DomainLockUtils {
DateTime now = jpaTm().getTransactionTime();
RegistryLock previousLock = getByVerificationCode(verificationCode);
checkArgument(
!previousLock.getUnlockCompletionTimestamp().isPresent(),
!previousLock.getUnlockCompletionTime().isPresent(),
"Domain %s is already unlocked",
previousLock.getDomainName());
@@ -145,7 +145,7 @@ public final class DomainLockUtils {
RegistryLock newLock =
RegistryLockDao.save(
previousLock.asBuilder().setUnlockCompletionTimestamp(now).build());
previousLock.asBuilder().setUnlockCompletionTime(now).build());
tm().transact(() -> removeLockStatuses(newLock, isAdmin, now));
return newLock;
});
@@ -169,7 +169,7 @@ public final class DomainLockUtils {
RegistryLock newLock =
RegistryLockDao.save(
createLockBuilder(domainName, registrarId, registrarPocId, isAdmin)
.setLockCompletionTimestamp(now)
.setLockCompletionTime(now)
.build());
tm().transact(() -> applyLockStatuses(newLock, now, isAdmin));
setAsRelock(newLock);
@@ -192,7 +192,7 @@ public final class DomainLockUtils {
RegistryLock result =
RegistryLockDao.save(
createUnlockBuilder(domainName, registrarId, isAdmin, relockDuration)
.setUnlockCompletionTimestamp(now)
.setUnlockCompletionTime(now)
.build());
tm().transact(() -> removeLockStatuses(result, isAdmin, now));
return result;
@@ -230,7 +230,7 @@ public final class DomainLockUtils {
previousLock ->
checkArgument(
previousLock.isLockRequestExpired(now)
|| previousLock.getUnlockCompletionTimestamp().isPresent()
|| previousLock.getUnlockCompletionTime().isPresent()
|| isAdmin,
"A pending or completed lock action already exists for %s",
previousLock.getDomainName()));
@@ -264,7 +264,7 @@ public final class DomainLockUtils {
new RegistryLock.Builder()
.setRepoId(domainBase.getRepoId())
.setDomainName(domainName)
.setLockCompletionTimestamp(now)
.setLockCompletionTime(now)
.setRegistrarId(registrarId));
} else {
RegistryLock lock =
@@ -275,7 +275,7 @@ public final class DomainLockUtils {
checkArgument(
lock.isLocked(), "Lock object for domain %s is not currently locked", domainName);
checkArgument(
!lock.getUnlockRequestTimestamp().isPresent() || lock.isUnlockRequestExpired(now),
!lock.getUnlockRequestTime().isPresent() || lock.isUnlockRequestExpired(now),
"A pending unlock action already exists for %s",
domainName);
checkArgument(
@@ -290,7 +290,7 @@ public final class DomainLockUtils {
return newLockBuilder
.setVerificationCode(stringGenerator.createString(VERIFICATION_CODE_LENGTH))
.isSuperuser(isAdmin)
.setUnlockRequestTimestamp(now)
.setUnlockRequestTime(now)
.setRegistrarId(registrarId);
}
@@ -107,7 +107,7 @@ public class BackfillRegistryLocksCommand extends ConfirmingCommand
.setRegistrarId(registryAdminClientId)
.setRepoId(domainBase.getRepoId())
.setDomainName(domainBase.getDomainName())
.setLockCompletionTimestamp(
.setLockCompletionTime(
getLockCompletionTimestamp(domainBase, jpaTm().getTransactionTime()))
.setVerificationCode(
stringGenerator.createString(VERIFICATION_CODE_LENGTH))
@@ -191,14 +191,13 @@ public final class RegistryLockGetAction implements JsonGetAction {
DateTime now = jpaTm().getTransactionTime();
return new ImmutableMap.Builder<String, Object>()
.put(DOMAIN_NAME_PARAM, lock.getDomainName())
.put(
LOCKED_TIME_PARAM, lock.getLockCompletionTimestamp().map(DateTime::toString).orElse(""))
.put(LOCKED_TIME_PARAM, lock.getLockCompletionTime().map(DateTime::toString).orElse(""))
.put(LOCKED_BY_PARAM, lock.isSuperuser() ? "admin" : lock.getRegistrarPocId())
.put(IS_LOCK_PENDING_PARAM, !lock.getLockCompletionTimestamp().isPresent())
.put(IS_LOCK_PENDING_PARAM, !lock.getLockCompletionTime().isPresent())
.put(
IS_UNLOCK_PENDING_PARAM,
lock.getUnlockRequestTimestamp().isPresent()
&& !lock.getUnlockCompletionTimestamp().isPresent()
lock.getUnlockRequestTime().isPresent()
&& !lock.getUnlockCompletionTime().isPresent()
&& !lock.isUnlockRequestExpired(now))
.put(USER_CAN_UNLOCK_PARAM, isAdmin || !lock.isSuperuser())
.build();
@@ -98,7 +98,6 @@
<class>google.registry.persistence.converter.StringSetConverter</class>
<class>google.registry.persistence.converter.TldStateTransitionConverter</class>
<class>google.registry.persistence.converter.TransferServerApproveEntitySetConverter</class>
<class>google.registry.persistence.converter.UpdateAutoTimestampConverter</class>
<class>google.registry.persistence.converter.ZonedDateTimeConverter</class>
<!-- Generated converters for VKey -->