Combine the two Lock classes into one class (#1126)

* Combine the two Lock classes into one class

This allows us to remove the DAO and to just treat locks the same as we
would treat any other object -- generically grabbing them from the
transaction manager.

We do not need to be concerned about the changeover between Datastore
and SQL because we assume that any such changeover will require
sufficient downtime that any currently-valid acquired locks will expire
during the downtime. Otherwise, we could get into a situation where an
action has acquired a particular lock in Datastore but not SQL.
This commit is contained in:
gbrodman 2021-05-11 16:37:40 -04:00 committed by GitHub
parent 3dc4cd8e4f
commit 4641781d36
22 changed files with 170 additions and 579 deletions

View file

@ -137,7 +137,7 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
/** Status values associated with this resource. */
@Column(name = "statuses")
// TODO(mmuller): rename to "statuses" once we're off datastore.
// TODO(b/177567432): rename to "statuses" once we're off datastore.
Set<StatusValue> status;
/**

View file

@ -127,7 +127,7 @@ public class DomainContent extends EppResource
*
* @invariant fullyQualifiedDomainName == fullyQualifiedDomainName.toLowerCase(Locale.ENGLISH)
*/
// TODO(b/158858642): Rename this to domainName when we are off Datastore
// TODO(b/177567432): Rename this to domainName when we are off Datastore
@Column(name = "domainName")
@Index
String fullyQualifiedDomainName;

View file

@ -64,7 +64,7 @@ public class HostBase extends EppResource {
* from (creationTime, deletionTime) there can only be one host in Datastore with this name.
* However, there can be many hosts with the same name and non-overlapping lifetimes.
*/
// TODO(b/158858642): Rename this to hostName when we are off Datastore
// TODO(b/177567432): Rename this to hostName when we are off Datastore
@Index
@Column(name = "hostName")
String fullyQualifiedHostName;

View file

@ -236,7 +236,7 @@ public class Registrar extends ImmutableObject
* Unique registrar client id. Must conform to "clIDType" as defined in RFC5730.
*
* @see <a href="http://tools.ietf.org/html/rfc5730#section-4.2">Shared Structure Schema</a>
* <p>TODO(b/177568946): Rename this field to registrarId.
* <p>TODO(b/177567432): Rename this field to registrarId.
*/
@Id
@javax.persistence.Id
@ -301,7 +301,7 @@ public class Registrar extends ImmutableObject
String failoverClientCertificateHash;
/** An allow list of netmasks (in CIDR notation) which the client is allowed to connect from. */
// TODO: Rename to ipAddressAllowList once Cloud SQL migration is complete.
// TODO(b/177567432): Rename to ipAddressAllowList once Cloud SQL migration is complete.
@Column(name = "ip_address_allow_list")
List<CidrAddressBlock> ipAddressWhitelist;

View file

@ -69,7 +69,7 @@ import javax.persistence.Transient;
* *MUST* also modify the persisted Registrar entity with {@link Registrar#contactsRequireSyncing}
* set to true.
*
* <p>TODO(b/163366543): Rename the class name to RegistrarPoc after database migration
* <p>TODO(b/177567432): Rename the class name to RegistrarPoc after database migration
*/
@ReportedOn
@Entity

View file

@ -23,7 +23,7 @@ import java.util.Optional;
/**
* A {@link ReservedList} DAO for Cloud SQL.
*
* <p>TODO(b/160993806): Rename this class to ReservedListDao after migrating to Cloud SQL.
* <p>TODO(b/177567432): Rename this class to ReservedListDao after migrating to Cloud SQL.
*/
public class ReservedListSqlDao {

View file

@ -49,7 +49,8 @@ import javax.persistence.Transient;
* succeeds, we will end up with having two exact same revisions that differ only by revisionKey.
* This is fine though, because we only use the revision with the highest revisionKey.
*
* <p>TODO: remove Datastore-specific fields post-Registry-3.0-migration and rename to KmsSecret.
* <p>TODO(b/177567432): remove Datastore-specific fields post-Registry-3.0-migration and rename to
* KmsSecret.
*
* @see <a href="https://cloud.google.com/kms/docs/">Google Cloud Key Management Service
* Documentation</a>
@ -71,7 +72,7 @@ public class KmsSecretRevision extends ImmutableObject implements NonReplicatedE
/**
* The revision of this secret.
*
* <p>TODO: change name of the variable to revisionId once we're off Datastore
* <p>TODO(b/177567432): change name of the variable to revisionId once we're off Datastore
*/
@Id
@javax.persistence.Id

View file

@ -24,7 +24,7 @@ import java.util.Optional;
/**
* A {@link KmsSecretRevision} DAO for Cloud SQL.
*
* <p>TODO: Rename this class to KmsSecretDao after migrating to Cloud SQL.
* <p>TODO(b/177567432): Rename this class to KmsSecretDao after migrating to Cloud SQL.
*/
public class KmsSecretRevisionSqlDao {

View file

@ -15,8 +15,9 @@
package google.registry.model.server;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@ -29,40 +30,59 @@ import google.registry.model.ImmutableObject;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreOnlyEntity;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import google.registry.util.RequestStatusChecker;
import google.registry.util.RequestStatusCheckerImpl;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.IdClass;
import javax.persistence.PostLoad;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* A lock on some shared resource.
*
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of null
* is used.
* <p>Locks are either specific to a tld or global to the entire system, in which case a scope of
* GLOBAL is used.
*
* <p>This is the "barebone" lock implementation, that requires manual locking and unlocking. For
* safe calls that automatically lock and unlock, see LockHandler.
*/
@Entity
@NotBackedUp(reason = Reason.TRANSIENT)
public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serializable {
@javax.persistence.Entity
@Table
@IdClass(Lock.LockId.class)
public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Serializable {
private static final long serialVersionUID = 756397280691684645L;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Disposition of locking, for monitoring. */
enum LockState { IN_USE, FREE, TIMED_OUT, OWNER_DIED }
/**
* The scope of a lock that is not specific to a single tld.
*
* <p>Note: we'd use a null "tld" here for global locks, except Hibernate/Postgres don't allow for
* null values in primary-key columns.
*/
static final String GLOBAL = "GLOBAL";
@VisibleForTesting
static LockMetrics lockMetrics = new LockMetrics();
/** Disposition of locking, for monitoring. */
enum LockState {
IN_USE,
FREE,
TIMED_OUT,
OWNER_DIED
}
@VisibleForTesting static LockMetrics lockMetrics = new LockMetrics();
/** The name of the locked resource. */
@Id
String lockId;
@Transient @Id String lockId;
/**
* Unique log ID of the request that owns this lock.
@ -73,58 +93,62 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
* <p>See {@link RequestStatusCheckerImpl#getLogId} for details about how it's created in
* practice.
*/
@Column(nullable = false)
String requestLogId;
/** When the lock can be considered implicitly released. */
@Column(nullable = false)
DateTime expirationTime;
public String getRequestLogId() {
return requestLogId;
}
/** When was the lock acquired. Used for logging. */
@Column(nullable = false)
DateTime acquiredTime;
/** The resource name used to create the lock. */
@Column(nullable = false)
@javax.persistence.Id
String resourceName;
/** The tld used to create the lock, or GLOBAL if it's cross-TLD. */
// TODO(b/177567432): rename to "scope" post-Datastore
@Column(nullable = false, name = "scope")
@javax.persistence.Id
String tld;
public DateTime getExpirationTime() {
return expirationTime;
}
public DateTime getAcquiredTime() {
return acquiredTime;
@PostLoad
void postLoad() {
lockId = makeLockId(resourceName, tld);
}
/** When was the lock acquired. Used for logging. */
DateTime acquiredTime;
/** The resource name used to create lockId. */
String resourceName;
/** The tld used to create lockId. */
@Nullable
String tld;
/**
* Create a new {@link Lock} for the given resource name in the specified tld (which can be null
* for cross-tld locks).
* Create a new {@link Lock} for the given resource name in the specified tld (or in the GLOBAL
* namespace).
*/
public static Lock create(
private static Lock create(
String resourceName,
@Nullable String tld,
String scope,
String requestLogId,
DateTime acquiredTime,
Duration leaseLength) {
checkArgument(!Strings.isNullOrEmpty(resourceName), "resourceName cannot be null or empty");
Lock instance = new Lock();
// Add the tld to the Lock's id so that it is unique for locks acquiring the same resource
// Add the scope to the Lock's id so that it is unique for locks acquiring the same resource
// across different TLDs.
instance.lockId = makeLockId(resourceName, tld);
instance.lockId = makeLockId(resourceName, scope);
instance.requestLogId = requestLogId;
instance.expirationTime = acquiredTime.plus(leaseLength);
instance.acquiredTime = acquiredTime;
instance.resourceName = resourceName;
instance.tld = tld;
instance.tld = scope;
return instance;
}
private static String makeLockId(String resourceName, @Nullable String tld) {
return String.format("%s-%s", tld, resourceName);
private static String makeLockId(String resourceName, String scope) {
return String.format("%s-%s", scope, resourceName);
}
@AutoValue
@ -186,21 +210,23 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
Duration leaseLength,
RequestStatusChecker requestStatusChecker,
boolean checkThreadRunning) {
String lockId = makeLockId(resourceName, tld);
String scope = (tld != null) ? tld : GLOBAL;
String lockId = makeLockId(resourceName, scope);
// It's important to use transactNew rather than transact, because a Lock can be used to control
// access to resources like GCS that can't be transactionally rolled back. Therefore, the lock
// must be definitively acquired before it is used, even when called inside another transaction.
AcquireResult acquireResult =
ofyTm()
.transactNew(
tm().transactNew(
() -> {
DateTime now = ofyTm().getTransactionTime();
DateTime now = tm().getTransactionTime();
// Checking if an unexpired lock still exists - if so, the lock can't be acquired.
Lock lock =
ofyTm()
.loadByKeyIfPresent(
VKey.createOfy(Lock.class, Key.create(Lock.class, lockId)))
tm().loadByKeyIfPresent(
VKey.create(
Lock.class,
new LockId(resourceName, scope),
Key.create(Lock.class, lockId)))
.orElse(null);
if (lock != null) {
logger.atInfo().log(
@ -220,43 +246,44 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
}
Lock newLock =
create(resourceName, tld, requestStatusChecker.getLogId(), now, leaseLength);
create(
resourceName, scope, requestStatusChecker.getLogId(), now, leaseLength);
// Locks are not parented under an EntityGroupRoot (so as to avoid write
// contention) and
// don't need to be backed up.
ofyTm().putWithoutBackup(newLock);
// contention) and don't need to be backed up.
tm().putWithoutBackup(newLock);
return AcquireResult.create(now, lock, newLock, lockState);
});
logAcquireResult(acquireResult);
lockMetrics.recordAcquire(resourceName, tld, acquireResult.lockState());
lockMetrics.recordAcquire(resourceName, scope, acquireResult.lockState());
return Optional.ofNullable(acquireResult.newLock());
}
/** Release the lock. */
public void release() {
// Just use the default clock because we aren't actually doing anything that will use the clock.
ofyTm()
.transact(
tm().transact(
() -> {
// To release a lock, check that no one else has already obtained it and if not
// delete it. If the lock in Datastore was different then this lock is gone already;
// this can happen if release() is called around the expiration time and the lock
// expires underneath us.
Lock loadedLock =
ofyTm()
.loadByKeyIfPresent(
VKey.createOfy(Lock.class, Key.create(Lock.class, lockId)))
tm().loadByKeyIfPresent(
VKey.create(
Lock.class,
new LockId(resourceName, tld),
Key.create(Lock.class, lockId)))
.orElse(null);
if (Lock.this.equals(loadedLock)) {
// Use noBackupOfy() so that we don't create a commit log entry for deleting the
// lock.
// Use deleteWithoutBackup() so that we don't create a commit log entry for deleting
// the lock.
logger.atInfo().log("Deleting lock: %s", lockId);
ofyTm().deleteWithoutBackup(Lock.this);
tm().deleteWithoutBackup(Lock.this);
lockMetrics.recordRelease(
resourceName, tld, new Duration(acquiredTime, ofyTm().getTransactionTime()));
resourceName, tld, new Duration(acquiredTime, tm().getTransactionTime()));
} else {
logger.atSevere().log(
"The lock we acquired was transferred to someone else before we"
@ -268,4 +295,21 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
}
});
}
static class LockId extends ImmutableObject implements Serializable {
String resourceName;
// TODO(b/177567432): rename to "scope" post-Datastore
@Column(name = "scope")
String tld;
// Required for Hibernate
private LockId() {}
LockId(String resourceName, String tld) {
this.resourceName = checkArgumentNotNull(resourceName, "The resource name cannot be null");
this.tld = checkArgumentNotNull(tld, "Scope of a lock cannot be null");
}
}
}

View file

@ -117,7 +117,7 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
* the DNL List creation datetime from the rfc. Since this field has been used by Datastore, we
* cannot change its name until we finish the migration.
*
* <p>TODO(b/166784536): Rename this field to tmdbGenerationTime.
* <p>TODO(b/177567432): Rename this field to tmdbGenerationTime.
*/
@Column(name = "tmdb_generation_time", nullable = false)
DateTime creationTime;

View file

@ -1,140 +0,0 @@
// Copyright 2020 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.schema.server;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.ImmutableObject;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import google.registry.schema.server.Lock.LockId;
import google.registry.util.DateTimeUtils;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* A lock on some shared resource.
*
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of
* {@link GLOBAL} is used.
*
* <p>This uses a compound primary key as defined in {@link LockId}.
*/
@Entity
@Table
@IdClass(LockId.class)
public class Lock implements SqlEntity {
/** The resource name used to create the lock. */
@Column(nullable = false)
@Id
String resourceName;
/** The tld used to create the lock. */
@Column(nullable = false)
@Id
String tld;
/**
* Unique log ID of the request that owns this lock.
*
* <p>When that request is no longer running (is finished), the lock can be considered implicitly
* released.
*
* <p>See {@link RequestStatusCheckerImpl#getLogId} for details about how it's created in
* practice.
*/
@Column(nullable = false)
String requestLogId;
/** When the lock was acquired. Used for logging. */
@Column(nullable = false)
ZonedDateTime acquiredTime;
/** When the lock can be considered implicitly released. */
@Column(nullable = false)
ZonedDateTime expirationTime;
/** The scope of a lock that is not specific to a single tld. */
static final String GLOBAL = "GLOBAL";
/**
* Validate input and create a new {@link Lock} for the given resource name in the specified tld.
*/
private Lock(
String resourceName,
String tld,
String requestLogId,
DateTime acquiredTime,
Duration leaseLength) {
this.resourceName = checkArgumentNotNull(resourceName, "The resource name cannot be null");
this.tld = checkArgumentNotNull(tld, "The tld cannot be null. For a global lock, use GLOBAL");
this.requestLogId =
checkArgumentNotNull(requestLogId, "The requestLogId of the lock cannot be null");
this.acquiredTime =
DateTimeUtils.toZonedDateTime(
checkArgumentNotNull(acquiredTime, "The acquired time of the lock cannot be null"));
checkArgumentNotNull(leaseLength, "The lease length of the lock cannot be null");
this.expirationTime = DateTimeUtils.toZonedDateTime(acquiredTime.plus(leaseLength));
}
// Hibernate requires a default constructor.
private Lock() {}
/** Constructs a {@link Lock} object. */
public static Lock create(
String resourceName,
String tld,
String requestLogId,
DateTime acquiredTime,
Duration leaseLength) {
checkArgumentNotNull(
tld, "The tld cannot be null. To create a global lock, use the createGlobal method");
return new Lock(resourceName, tld, requestLogId, acquiredTime, leaseLength);
}
/** Constructs a {@link Lock} object with a {@link GLOBAL} scope. */
public static Lock createGlobal(
String resourceName, String requestLogId, DateTime acquiredTime, Duration leaseLength) {
return new Lock(resourceName, GLOBAL, requestLogId, acquiredTime, leaseLength);
}
@Override
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Locks are not converted since they are ephemeral
}
static class LockId extends ImmutableObject implements Serializable {
String resourceName;
String tld;
private LockId() {}
LockId(String resourceName, String tld) {
this.resourceName = checkArgumentNotNull(resourceName, "The resource name cannot be null");
this.tld = tld;
}
}
}

View file

@ -1,136 +0,0 @@
// Copyright 2020 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.schema.server;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.schema.server.Lock.GLOBAL;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.flogger.FluentLogger;
import google.registry.schema.server.Lock.LockId;
import google.registry.util.DateTimeUtils;
import java.util.Optional;
/** Data access object class for {@link Lock}. */
public class LockDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Saves the {@link Lock} object to Cloud SQL. */
public static void save(Lock lock) {
jpaTm()
.transact(
() -> {
jpaTm().put(lock);
});
}
/**
* Loads and returns a {@link Lock} object with the given resourceName and tld from Cloud SQL if
* it exists, else empty.
*/
public static Optional<Lock> load(String resourceName, String tld) {
checkArgumentNotNull(resourceName, "The resource name of the lock to load cannot be null");
checkArgumentNotNull(tld, "The tld of the lock to load cannot be null");
return Optional.ofNullable(
jpaTm()
.transact(
() -> jpaTm().getEntityManager().find(Lock.class, new LockId(resourceName, tld))));
}
/**
* Loads a global {@link Lock} object with the given resourceName from Cloud SQL if it exists,
* else empty.
*/
public static Optional<Lock> load(String resourceName) {
return load(resourceName, GLOBAL);
}
/**
* Deletes the {@link Lock} object with the given resourceName and tld from Cloud SQL. This method
* is idempotent and will simply return if the lock has already been deleted.
*/
public static void delete(String resourceName, String tld) {
jpaTm()
.transact(
() -> {
Optional<Lock> loadedLock = load(resourceName, tld);
if (loadedLock.isPresent()) {
jpaTm().delete(loadedLock.get());
}
});
}
/**
* Deletes the global {@link Lock} object with the given resourceName from Cloud SQL. This method
* is idempotent and will simply return if the lock has already been deleted.
*/
public static void delete(String resourceName) {
delete(resourceName, GLOBAL);
}
/**
* Compares a {@link google.registry.model.server.Lock} object with a {@link Lock} object, logging
* a warning if there are any differences.
*/
public static void compare(
Optional<google.registry.model.server.Lock> datastoreLockOptional,
Optional<Lock> cloudSqlLockOptional) {
if (!datastoreLockOptional.isPresent()) {
cloudSqlLockOptional.ifPresent(
value ->
logger.atWarning().log(
String.format(
"Cloud SQL lock for %s with tld %s should be null",
value.resourceName, value.tld)));
return;
}
google.registry.schema.server.Lock cloudSqlLock;
google.registry.model.server.Lock datastoreLock = datastoreLockOptional.get();
if (cloudSqlLockOptional.isPresent()) {
cloudSqlLock = cloudSqlLockOptional.get();
if (!datastoreLock.getRequestLogId().equals(cloudSqlLock.requestLogId)) {
logger.atWarning().log(
String.format(
"Datastore lock requestLogId of %s does not equal Cloud SQL lock requestLogId of"
+ " %s",
datastoreLock.getRequestLogId(), cloudSqlLock.requestLogId));
}
if (!datastoreLock
.getAcquiredTime()
.equals(DateTimeUtils.toJodaDateTime(cloudSqlLock.acquiredTime))) {
logger.atWarning().log(
String.format(
"Datastore lock acquiredTime of %s does not equal Cloud SQL lock acquiredTime of"
+ " %s",
datastoreLock.getAcquiredTime(),
DateTimeUtils.toJodaDateTime(cloudSqlLock.acquiredTime)));
}
if (!datastoreLock
.getExpirationTime()
.equals(DateTimeUtils.toJodaDateTime(cloudSqlLock.expirationTime))) {
logger.atWarning().log(
String.format(
"Datastore lock expirationTime of %s does not equal Cloud SQL lock expirationTime"
+ " of %s",
datastoreLock.getExpirationTime(),
DateTimeUtils.toJodaDateTime(cloudSqlLock.expirationTime)));
}
} else {
logger.atWarning().log(
String.format("Datastore lock: %s was not found in Cloud SQL", datastoreLock));
}
}
}

View file

@ -65,6 +65,7 @@
<class>google.registry.model.reporting.DomainTransactionRecord</class>
<class>google.registry.model.reporting.Spec11ThreatMatch</class>
<class>google.registry.model.server.KmsSecretRevision</class>
<class>google.registry.model.server.Lock</class>
<class>google.registry.model.server.ServerSecret</class>
<class>google.registry.model.smd.SignedMarkRevocationList</class>
<class>google.registry.model.tmch.ClaimsListShard</class>
@ -72,7 +73,6 @@
<class>google.registry.persistence.transaction.TransactionEntity</class>
<class>google.registry.schema.domain.RegistryLock</class>
<class>google.registry.schema.replay.SqlReplayCheckpoint</class>
<class>google.registry.schema.server.Lock</class>
<class>google.registry.schema.tld.PremiumEntry</class>
<!-- Customized type converters -->

View file

@ -26,35 +26,30 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import google.registry.model.ofy.Ofy;
import google.registry.model.EntityTestCase;
import google.registry.model.server.Lock.LockState;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.util.RequestStatusChecker;
import java.util.Optional;
import org.joda.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link Lock}. */
public class LockTest {
@DualDatabaseTest
public class LockTest extends EntityTestCase {
private static final String RESOURCE_NAME = "foo";
private static final Duration ONE_DAY = Duration.standardDays(1);
private static final Duration TWO_MILLIS = Duration.millis(2);
private static final RequestStatusChecker requestStatusChecker = mock(RequestStatusChecker.class);
private static final FakeClock clock = new FakeClock();
private LockMetrics origLockMetrics;
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(clock).build();
@RegisterExtension public final InjectExtension inject = new InjectExtension();
public LockTest() {
super(JpaEntityCoverageCheck.ENABLED);
}
private Optional<Lock> acquire(String tld, Duration leaseLength, LockState expectedLockState) {
Lock.lockMetrics = mock(LockMetrics.class);
@ -76,7 +71,6 @@ public class LockTest {
@BeforeEach
void beforeEach() {
inject.setStaticField(Ofy.class, "clock", clock);
origLockMetrics = Lock.lockMetrics;
Lock.lockMetrics = null;
when(requestStatusChecker.getLogId()).thenReturn("current-request-id");
@ -88,32 +82,32 @@ public class LockTest {
Lock.lockMetrics = origLockMetrics;
}
@Test
@TestOfyAndSql
void testReleasedExplicitly() {
Optional<Lock> lock = acquire("", ONE_DAY, FREE);
assertThat(lock).isPresent();
// We can't get it again at the same time.
assertThat(acquire("", ONE_DAY, IN_USE)).isEmpty();
// But if we release it, it's available.
clock.advanceBy(Duration.millis(123));
fakeClock.advanceBy(Duration.millis(123));
release(lock.get(), "", 123);
assertThat(acquire("", ONE_DAY, FREE)).isPresent();
}
@Test
@TestOfyAndSql
void testReleasedAfterTimeout() {
assertThat(acquire("", TWO_MILLIS, FREE)).isPresent();
// We can't get it again at the same time.
assertThat(acquire("", TWO_MILLIS, IN_USE)).isEmpty();
// A second later we still can't get the lock.
clock.advanceOneMilli();
fakeClock.advanceOneMilli();
assertThat(acquire("", TWO_MILLIS, IN_USE)).isEmpty();
// But two seconds later we can get it.
clock.advanceOneMilli();
fakeClock.advanceOneMilli();
assertThat(acquire("", TWO_MILLIS, TIMED_OUT)).isPresent();
}
@Test
@TestOfyAndSql
void testReleasedAfterRequestFinish() {
assertThat(acquire("", ONE_DAY, FREE)).isPresent();
// We can't get it again while request is active
@ -123,7 +117,7 @@ public class LockTest {
assertThat(acquire("", ONE_DAY, OWNER_DIED)).isPresent();
}
@Test
@TestOfyAndSql
void testTldsAreIndependent() {
Optional<Lock> lockA = acquire("a", ONE_DAY, FREE);
assertThat(lockA).isPresent();
@ -133,12 +127,12 @@ public class LockTest {
// We can't get lockB again at the same time.
assertThat(acquire("b", ONE_DAY, IN_USE)).isEmpty();
// Releasing lockA has no effect on lockB (even though we are still using the "b" tld).
clock.advanceOneMilli();
fakeClock.advanceOneMilli();
release(lockA.get(), "a", 1);
assertThat(acquire("b", ONE_DAY, IN_USE)).isEmpty();
}
@Test
@TestOfyAndSql
void testFailure_emptyResourceName() {
IllegalArgumentException thrown =
assertThrows(

View file

@ -31,6 +31,7 @@ import google.registry.model.registry.RegistryTest;
import google.registry.model.registry.label.ReservedListSqlDaoTest;
import google.registry.model.reporting.Spec11ThreatMatchTest;
import google.registry.model.server.KmsSecretRevisionSqlDaoTest;
import google.registry.model.server.LockTest;
import google.registry.model.server.ServerSecretTest;
import google.registry.model.smd.SignedMarkRevocationListDaoTest;
import google.registry.model.tmch.ClaimsListSqlDaoTest;
@ -41,7 +42,6 @@ import google.registry.schema.integration.SqlIntegrationTestSuite.AfterSuiteTest
import google.registry.schema.integration.SqlIntegrationTestSuite.BeforeSuiteTest;
import google.registry.schema.registrar.RegistrarDaoTest;
import google.registry.schema.replay.SqlReplayCheckpointTest;
import google.registry.schema.server.LockDaoTest;
import google.registry.schema.tld.PremiumListSqlDaoTest;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.AfterAll;
@ -90,7 +90,7 @@ import org.junit.runner.RunWith;
DomainHistoryTest.class,
HostHistoryTest.class,
KmsSecretRevisionSqlDaoTest.class,
LockDaoTest.class,
LockTest.class,
PollMessageTest.class,
PremiumListSqlDaoTest.class,
RdeRevisionTest.class,

View file

@ -1,188 +0,0 @@
// Copyright 2020 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.schema.server;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import com.google.common.testing.TestLogHandler;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.joda.time.Duration;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link Lock}. */
public class LockDaoTest {
private final FakeClock fakeClock = new FakeClock();
private final TestLogHandler logHandler = new TestLogHandler();
private final Logger loggerToIntercept = Logger.getLogger(LockDao.class.getCanonicalName());
@RegisterExtension
@Order(value = 1)
DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@Test
void save_worksSuccessfully() {
Lock lock =
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
}
@Test
void save_succeedsWhenLockAlreadyExists() {
Lock lock =
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Lock lock2 =
Lock.create("testResource", "tld", "testLogId2", fakeClock.nowUtc(), Duration.millis(4));
LockDao.save(lock2);
assertThat(LockDao.load("testResource", "tld").get().requestLogId).isEqualTo("testLogId2");
}
@Test
void save_worksSuccesfullyGlobalLock() {
Lock lock =
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Optional<Lock> returnedLock = LockDao.load("testResource");
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
}
@Test
void load_worksSuccessfully() {
Lock lock =
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
}
@Test
void load_worksSuccessfullyGlobalLock() {
Lock lock =
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Optional<Lock> returnedLock = LockDao.load("testResource");
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
}
@Test
void load_worksSuccesfullyLockDoesNotExist() {
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
assertThat(returnedLock.isPresent()).isFalse();
}
@Test
void delete_worksSuccesfully() {
Lock lock =
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
LockDao.delete("testResource", "tld");
returnedLock = LockDao.load("testResource", "tld");
assertThat(returnedLock.isPresent()).isFalse();
}
@Test
void delete_worksSuccessfullyGlobalLock() {
Lock lock =
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.save(lock);
Optional<Lock> returnedLock = LockDao.load("testResource");
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
LockDao.delete("testResource");
returnedLock = LockDao.load("testResource");
assertThat(returnedLock.isPresent()).isFalse();
}
@Test
void delete_succeedsLockDoesntExist() {
LockDao.delete("testResource");
}
@Test
void compare_logsWarningWhenCloudSqlLockMissing() {
loggerToIntercept.addHandler(logHandler);
google.registry.model.server.Lock datastoreLock =
google.registry.model.server.Lock.create(
"resourceName", "tld", "id", fakeClock.nowUtc(), Duration.millis(2));
LockDao.compare(Optional.of(datastoreLock), Optional.empty());
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
String.format("Datastore lock: %s was not found in Cloud SQL", datastoreLock));
}
@Test
void compare_logsWarningWhenCloudSqlLockExistsWhenItShouldNot() {
loggerToIntercept.addHandler(logHandler);
Lock lock =
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
LockDao.compare(Optional.ofNullable(null), Optional.of(lock));
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING, "Cloud SQL lock for testResource with tld GLOBAL should be null");
}
@Test
void compare_logsWarningWhenLocksDontMatch() {
loggerToIntercept.addHandler(logHandler);
Lock cloudSqlLock =
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
google.registry.model.server.Lock datastoreLock =
google.registry.model.server.Lock.create(
"testResource", "tld", "wrong", fakeClock.nowUtc().minusDays(1), Duration.millis(3));
LockDao.compare(Optional.of(datastoreLock), Optional.of(cloudSqlLock));
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Datastore lock requestLogId of wrong does not equal Cloud SQL lock requestLogId"
+ " of testLogId");
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Datastore lock acquiredTime of 1969-12-31T00:00:00.000Z does not equal Cloud SQL"
+ " lock acquiredTime of 1970-01-01T00:00:00.000Z");
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Datastore lock expirationTime of 1969-12-31T00:00:00.003Z does not equal Cloud"
+ " SQL lock expirationTime of 1970-01-01T00:00:00.002Z");
}
}

View file

@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2021-04-21 21:56:39.575987</td>
<td class="property_value">2021-05-07 18:02:12.304005</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V93__defer_all_fkeys.sql</td>
<td id="lastFlywayFile" class="property_value">V94__rename_lock_scope.sql</td>
</tr>
</tbody>
</table>
@ -284,7 +284,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="4027.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2021-04-21 21:56:39.575987
2021-05-07 18:02:12.304005
</text>
<polygon fill="none" stroke="#888888" points="3940.44,-4 3940.44,-44 4205.44,-44 4205.44,-4 3940.44,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
@ -2535,7 +2535,7 @@ td.section {
text not null
</text>
<text text-anchor="start" x="3957.5" y="-1541.48" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">
tld
"scope"
</text>
<text text-anchor="start" x="4062.5" y="-1540.48" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
@ -5415,7 +5415,7 @@ td.section {
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth"><b><i>tld</i></b></td>
<td class="minwidth"><b><i>"scope"</i></b></td>
<td class="minwidth">text not null</td>
</tr>
<tr>
@ -5438,7 +5438,7 @@ td.section {
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">tld</td>
<td class="minwidth">"scope"</td>
<td class="minwidth"></td>
</tr>
</tbody>

View file

@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2021-04-21 21:56:37.513728</td>
<td class="property_value">2021-05-07 18:02:10.32026</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V93__defer_all_fkeys.sql</td>
<td id="lastFlywayFile" class="property_value">V94__rename_lock_scope.sql</td>
</tr>
</tbody>
</table>
@ -274,19 +274,19 @@ td.section {
<svg viewbox="0.00 0.00 4850.02 4900.91" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 4896.91)">
<title>SchemaCrawler_Diagram</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-4896.91 4846.02,-4896.91 4846.02,4 -4,4" />
<text text-anchor="start" x="4573.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4581.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
generated by
</text>
<text text-anchor="start" x="4656.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4664.52" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
SchemaCrawler 16.10.1
</text>
<text text-anchor="start" x="4572.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4580.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
generated on
</text>
<text text-anchor="start" x="4656.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2021-04-21 21:56:37.513728
<text text-anchor="start" x="4664.52" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2021-05-07 18:02:10.32026
</text>
<polygon fill="none" stroke="#888888" points="4569.02,-4 4569.02,-44 4834.02,-44 4834.02,-4 4569.02,-4" /> <!-- allocationtoken_a08ccbef -->
<polygon fill="none" stroke="#888888" points="4577.02,-4 4577.02,-44 4834.02,-44 4834.02,-4 4577.02,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>allocationtoken_a08ccbef</title>
<polygon fill="#ebcef2" stroke="transparent" points="3001.5,-2741.91 3001.5,-2760.91 3200.5,-2760.91 3200.5,-2741.91 3001.5,-2741.91" />
@ -5655,7 +5655,7 @@ td.section {
text not null
</text>
<text text-anchor="start" x="4535.5" y="-3401.71" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">
tld
"scope"
</text>
<text text-anchor="start" x="4640.5" y="-3400.71" font-family="Helvetica,sans-Serif" font-size="14.00">
</text>
@ -11297,7 +11297,7 @@ td.section {
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth"><b><i>tld</i></b></td>
<td class="minwidth"><b><i>"scope"</i></b></td>
<td class="minwidth">text not null</td>
</tr>
<tr>
@ -11335,7 +11335,7 @@ td.section {
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">tld</td>
<td class="minwidth">"scope"</td>
<td class="minwidth"></td>
</tr>
<tr>
@ -11358,7 +11358,7 @@ td.section {
</tr>
<tr>
<td class="spacer"></td>
<td class="minwidth">tld</td>
<td class="minwidth">"scope"</td>
<td class="minwidth">ascending</td>
</tr>
</tbody>

View file

@ -91,3 +91,4 @@ V90__update_timestamp.sql
V91__defer_fkeys.sql
V92__singletons.sql
V93__defer_all_fkeys.sql
V94__rename_lock_scope.sql

View file

@ -0,0 +1,15 @@
-- Copyright 2021 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.
ALTER TABLE IF EXISTS "Lock" RENAME tld TO scope;

View file

@ -501,11 +501,11 @@
create table "Lock" (
resource_name text not null,
tld text not null,
scope text not null,
acquired_time timestamptz not null,
expiration_time timestamptz not null,
request_log_id text not null,
primary key (resource_name, tld)
primary key (resource_name, scope)
);
create table "PollMessage" (

View file

@ -653,7 +653,7 @@ CREATE TABLE public."KmsSecret" (
CREATE TABLE public."Lock" (
resource_name text NOT NULL,
tld text NOT NULL,
scope text NOT NULL,
acquired_time timestamp with time zone NOT NULL,
expiration_time timestamp with time zone NOT NULL,
request_log_id text NOT NULL
@ -1303,7 +1303,7 @@ ALTER TABLE ONLY public."KmsSecret"
--
ALTER TABLE ONLY public."Lock"
ADD CONSTRAINT "Lock_pkey" PRIMARY KEY (resource_name, tld);
ADD CONSTRAINT "Lock_pkey" PRIMARY KEY (resource_name, scope);
--