mirror of
https://github.com/google/nomulus.git
synced 2025-08-11 20:19:37 +02:00
Create a mechanism for storing / using locks explicitly only in SQL (#1392)
This is used for the replay locks so that Beam pipelines (which will be used for database comparison) can acquire / release locks as necessary to avoid database contention. If we're comparing contents of Datastore and SQL databases, we shouldn't have replay actively running during the comparison, so the pipeline will grab the locks. Beam doesn't always play nicely with loading from / saving to Datastore, so we need to make sure that we store the replay locks in SQL at all times, even when Datastore is the primary DB.
This commit is contained in:
parent
201b6e8e0b
commit
1e7aae26a3
10 changed files with 205 additions and 41 deletions
|
@ -112,7 +112,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Optional<Lock> lock =
|
Optional<Lock> lock =
|
||||||
Lock.acquire(
|
Lock.acquireSql(
|
||||||
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
|
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
|
||||||
if (!lock.isPresent()) {
|
if (!lock.isPresent()) {
|
||||||
String message = "Can't acquire SQL commit log replay lock, aborting.";
|
String message = "Can't acquire SQL commit log replay lock, aborting.";
|
||||||
|
|
|
@ -169,7 +169,7 @@ public class ReplicateToDatastoreAction implements Runnable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Optional<Lock> lock =
|
Optional<Lock> lock =
|
||||||
Lock.acquire(
|
Lock.acquireSql(
|
||||||
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
|
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
|
||||||
if (!lock.isPresent()) {
|
if (!lock.isPresent()) {
|
||||||
String message = "Can't acquire ReplicateToDatastoreAction lock, aborting.";
|
String message = "Can't acquire ReplicateToDatastoreAction lock, aborting.";
|
||||||
|
|
|
@ -32,6 +32,8 @@ import google.registry.model.annotations.NotBackedUp;
|
||||||
import google.registry.model.annotations.NotBackedUp.Reason;
|
import google.registry.model.annotations.NotBackedUp.Reason;
|
||||||
import google.registry.model.replay.DatastoreAndSqlEntity;
|
import google.registry.model.replay.DatastoreAndSqlEntity;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
|
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||||
|
import google.registry.persistence.transaction.TransactionManager;
|
||||||
import google.registry.util.RequestStatusChecker;
|
import google.registry.util.RequestStatusChecker;
|
||||||
import google.registry.util.RequestStatusCheckerImpl;
|
import google.registry.util.RequestStatusCheckerImpl;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -212,6 +214,47 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
|
||||||
Duration leaseLength,
|
Duration leaseLength,
|
||||||
RequestStatusChecker requestStatusChecker,
|
RequestStatusChecker requestStatusChecker,
|
||||||
boolean checkThreadRunning) {
|
boolean checkThreadRunning) {
|
||||||
|
return acquireWithTransactionManager(
|
||||||
|
resourceName, tld, leaseLength, requestStatusChecker, checkThreadRunning, tm());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to acquire a lock in SQL. Returns absent if it can't be acquired.
|
||||||
|
*
|
||||||
|
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
|
||||||
|
*/
|
||||||
|
public static Optional<Lock> acquireSql(
|
||||||
|
String resourceName,
|
||||||
|
@Nullable String tld,
|
||||||
|
Duration leaseLength,
|
||||||
|
RequestStatusChecker requestStatusChecker,
|
||||||
|
boolean checkThreadRunning) {
|
||||||
|
return acquireWithTransactionManager(
|
||||||
|
resourceName, tld, leaseLength, requestStatusChecker, checkThreadRunning, jpaTm());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release the lock. */
|
||||||
|
public void release() {
|
||||||
|
releaseWithTransactionManager(tm());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the lock from SQL.
|
||||||
|
*
|
||||||
|
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
|
||||||
|
*/
|
||||||
|
public void releaseSql() {
|
||||||
|
releaseWithTransactionManager(jpaTm());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to acquire a lock. Returns absent if it can't be acquired. */
|
||||||
|
private static Optional<Lock> acquireWithTransactionManager(
|
||||||
|
String resourceName,
|
||||||
|
@Nullable String tld,
|
||||||
|
Duration leaseLength,
|
||||||
|
RequestStatusChecker requestStatusChecker,
|
||||||
|
boolean checkThreadRunning,
|
||||||
|
TransactionManager transactionManager) {
|
||||||
String scope = (tld != null) ? tld : GLOBAL;
|
String scope = (tld != null) ? tld : GLOBAL;
|
||||||
String lockId = makeLockId(resourceName, scope);
|
String lockId = makeLockId(resourceName, scope);
|
||||||
// It's important to use transactNew rather than transact, because a Lock can be used to control
|
// It's important to use transactNew rather than transact, because a Lock can be used to control
|
||||||
|
@ -219,11 +262,12 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
|
||||||
// must be definitively acquired before it is used, even when called inside another transaction.
|
// must be definitively acquired before it is used, even when called inside another transaction.
|
||||||
Supplier<AcquireResult> lockAcquirer =
|
Supplier<AcquireResult> lockAcquirer =
|
||||||
() -> {
|
() -> {
|
||||||
DateTime now = tm().getTransactionTime();
|
DateTime now = transactionManager.getTransactionTime();
|
||||||
|
|
||||||
// Checking if an unexpired lock still exists - if so, the lock can't be acquired.
|
// Checking if an unexpired lock still exists - if so, the lock can't be acquired.
|
||||||
Lock lock =
|
Lock lock =
|
||||||
tm().loadByKeyIfPresent(
|
transactionManager
|
||||||
|
.loadByKeyIfPresent(
|
||||||
VKey.create(
|
VKey.create(
|
||||||
Lock.class,
|
Lock.class,
|
||||||
new LockId(resourceName, scope),
|
new LockId(resourceName, scope),
|
||||||
|
@ -249,13 +293,15 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
|
||||||
create(resourceName, scope, requestStatusChecker.getLogId(), now, leaseLength);
|
create(resourceName, scope, requestStatusChecker.getLogId(), now, leaseLength);
|
||||||
// Locks are not parented under an EntityGroupRoot (so as to avoid write
|
// Locks are not parented under an EntityGroupRoot (so as to avoid write
|
||||||
// contention) and don't need to be backed up.
|
// contention) and don't need to be backed up.
|
||||||
tm().putIgnoringReadOnly(newLock);
|
transactionManager.putIgnoringReadOnly(newLock);
|
||||||
|
|
||||||
return AcquireResult.create(now, lock, newLock, lockState);
|
return AcquireResult.create(now, lock, newLock, lockState);
|
||||||
};
|
};
|
||||||
// In ofy, backup is determined per-action, but in SQL it's determined per-transaction
|
// In ofy, backup is determined per-action, but in SQL it's determined per-transaction
|
||||||
AcquireResult acquireResult =
|
AcquireResult acquireResult =
|
||||||
tm().isOfy() ? tm().transactNew(lockAcquirer) : jpaTm().transactWithoutBackup(lockAcquirer);
|
transactionManager.isOfy()
|
||||||
|
? transactionManager.transactNew(lockAcquirer)
|
||||||
|
: ((JpaTransactionManager) transactionManager).transactWithoutBackup(lockAcquirer);
|
||||||
|
|
||||||
logAcquireResult(acquireResult);
|
logAcquireResult(acquireResult);
|
||||||
lockMetrics.recordAcquire(resourceName, scope, acquireResult.lockState());
|
lockMetrics.recordAcquire(resourceName, scope, acquireResult.lockState());
|
||||||
|
@ -263,7 +309,7 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Release the lock. */
|
/** Release the lock. */
|
||||||
public void release() {
|
private void releaseWithTransactionManager(TransactionManager transactionManager) {
|
||||||
// Just use the default clock because we aren't actually doing anything that will use the clock.
|
// Just use the default clock because we aren't actually doing anything that will use the clock.
|
||||||
Supplier<Void> lockReleaser =
|
Supplier<Void> lockReleaser =
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -274,15 +320,17 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
|
||||||
VKey<Lock> key =
|
VKey<Lock> key =
|
||||||
VKey.create(
|
VKey.create(
|
||||||
Lock.class, new LockId(resourceName, tld), Key.create(Lock.class, lockId));
|
Lock.class, new LockId(resourceName, tld), Key.create(Lock.class, lockId));
|
||||||
Lock loadedLock = tm().loadByKeyIfPresent(key).orElse(null);
|
Lock loadedLock = transactionManager.loadByKeyIfPresent(key).orElse(null);
|
||||||
if (Lock.this.equals(loadedLock)) {
|
if (Lock.this.equals(loadedLock)) {
|
||||||
// Use deleteIgnoringReadOnly() so that we don't create a commit log entry for deleting
|
// Use deleteIgnoringReadOnly() so that we don't create a commit log entry for deleting
|
||||||
// the lock.
|
// the lock.
|
||||||
logger.atInfo().log("Deleting lock: %s", lockId);
|
logger.atInfo().log("Deleting lock: %s", lockId);
|
||||||
tm().deleteIgnoringReadOnly(key);
|
transactionManager.deleteIgnoringReadOnly(key);
|
||||||
|
|
||||||
lockMetrics.recordRelease(
|
lockMetrics.recordRelease(
|
||||||
resourceName, tld, new Duration(acquiredTime, tm().getTransactionTime()));
|
resourceName,
|
||||||
|
tld,
|
||||||
|
new Duration(acquiredTime, transactionManager.getTransactionTime()));
|
||||||
} else {
|
} else {
|
||||||
logger.atSevere().log(
|
logger.atSevere().log(
|
||||||
"The lock we acquired was transferred to someone else before we"
|
"The lock we acquired was transferred to someone else before we"
|
||||||
|
@ -294,11 +342,12 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// In ofy, backup is determined per-action, but in SQL it's determined per-transaction
|
// In ofy, backup is determined per-action, but in SQL it's determined per-transaction
|
||||||
if (tm().isOfy()) {
|
if (transactionManager.isOfy()) {
|
||||||
tm().transact(lockReleaser);
|
transactionManager.transact(lockReleaser);
|
||||||
} else {
|
} else {
|
||||||
jpaTm().transactWithoutBackup(lockReleaser);
|
((JpaTransactionManager) transactionManager).transactWithoutBackup(lockReleaser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,8 @@ import org.joda.time.Duration;
|
||||||
/**
|
/**
|
||||||
* Code execution locked on some shared resource.
|
* Code execution locked on some shared resource.
|
||||||
*
|
*
|
||||||
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of
|
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of null
|
||||||
* null is used.
|
* is used.
|
||||||
*/
|
*/
|
||||||
public interface LockHandler extends Serializable {
|
public interface LockHandler extends Serializable {
|
||||||
|
|
||||||
|
@ -42,4 +42,22 @@ public interface LockHandler extends Serializable {
|
||||||
@Nullable String tld,
|
@Nullable String tld,
|
||||||
Duration leaseLength,
|
Duration leaseLength,
|
||||||
String... lockNames);
|
String... lockNames);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire one or more locks using only Cloud SQL and execute a Void {@link Callable}.
|
||||||
|
*
|
||||||
|
* <p>Runs on a thread that will be killed if it doesn't complete before the lease expires.
|
||||||
|
*
|
||||||
|
* <p>Note that locks are specific either to a given tld or to the entire system (in which case
|
||||||
|
* tld should be passed as null).
|
||||||
|
*
|
||||||
|
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
|
||||||
|
*
|
||||||
|
* @return true if all locks were acquired and the callable was run; false otherwise.
|
||||||
|
*/
|
||||||
|
boolean executeWithSqlLocks(
|
||||||
|
final Callable<Void> callable,
|
||||||
|
@Nullable String tld,
|
||||||
|
Duration leaseLength,
|
||||||
|
String... lockNames);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,12 +73,42 @@ public class LockHandlerImpl implements LockHandler {
|
||||||
@Nullable String tld,
|
@Nullable String tld,
|
||||||
Duration leaseLength,
|
Duration leaseLength,
|
||||||
String... lockNames) {
|
String... lockNames) {
|
||||||
|
return executeWithLockAcquirer(callable, tld, leaseLength, this::acquire, lockNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire one or more locks using only Cloud SQL and execute a Void {@link Callable}.
|
||||||
|
*
|
||||||
|
* <p>Thread will be killed if it doesn't complete before the lease expires.
|
||||||
|
*
|
||||||
|
* <p>Note that locks are specific either to a given tld or to the entire system (in which case
|
||||||
|
* tld should be passed as null).
|
||||||
|
*
|
||||||
|
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
|
||||||
|
*
|
||||||
|
* @return whether all locks were acquired and the callable was run.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean executeWithSqlLocks(
|
||||||
|
final Callable<Void> callable,
|
||||||
|
@Nullable String tld,
|
||||||
|
Duration leaseLength,
|
||||||
|
String... lockNames) {
|
||||||
|
return executeWithLockAcquirer(callable, tld, leaseLength, this::acquireSql, lockNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean executeWithLockAcquirer(
|
||||||
|
final Callable<Void> callable,
|
||||||
|
@Nullable String tld,
|
||||||
|
Duration leaseLength,
|
||||||
|
LockAcquirer lockAcquirer,
|
||||||
|
String... lockNames) {
|
||||||
DateTime startTime = clock.nowUtc();
|
DateTime startTime = clock.nowUtc();
|
||||||
String sanitizedTld = Strings.emptyToNull(tld);
|
String sanitizedTld = Strings.emptyToNull(tld);
|
||||||
try {
|
try {
|
||||||
return AppEngineTimeLimiter.create()
|
return AppEngineTimeLimiter.create()
|
||||||
.callWithTimeout(
|
.callWithTimeout(
|
||||||
new LockingCallable(callable, sanitizedTld, leaseLength, lockNames),
|
new LockingCallable(callable, lockAcquirer, sanitizedTld, leaseLength, lockNames),
|
||||||
leaseLength.minus(LOCK_TIMEOUT_FUDGE).getMillis(),
|
leaseLength.minus(LOCK_TIMEOUT_FUDGE).getMillis(),
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
} catch (ExecutionException | UncheckedExecutionException e) {
|
} catch (ExecutionException | UncheckedExecutionException e) {
|
||||||
|
@ -108,17 +138,32 @@ public class LockHandlerImpl implements LockHandler {
|
||||||
return Lock.acquire(lockName, tld, leaseLength, requestStatusChecker, true);
|
return Lock.acquire(lockName, tld, leaseLength, requestStatusChecker, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Optional<Lock> acquireSql(String lockName, @Nullable String tld, Duration leaseLength) {
|
||||||
|
return Lock.acquireSql(lockName, tld, leaseLength, requestStatusChecker, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface LockAcquirer {
|
||||||
|
Optional<Lock> acquireLock(String lockName, @Nullable String tld, Duration leaseLength);
|
||||||
|
}
|
||||||
|
|
||||||
/** A {@link Callable} that acquires and releases a lock around a delegate {@link Callable}. */
|
/** A {@link Callable} that acquires and releases a lock around a delegate {@link Callable}. */
|
||||||
private class LockingCallable implements Callable<Boolean> {
|
private static class LockingCallable implements Callable<Boolean> {
|
||||||
final Callable<Void> delegate;
|
final Callable<Void> delegate;
|
||||||
|
final LockAcquirer lockAcquirer;
|
||||||
@Nullable final String tld;
|
@Nullable final String tld;
|
||||||
final Duration leaseLength;
|
final Duration leaseLength;
|
||||||
final Set<String> lockNames;
|
final Set<String> lockNames;
|
||||||
|
|
||||||
LockingCallable(
|
LockingCallable(
|
||||||
Callable<Void> delegate, String tld, Duration leaseLength, String... lockNames) {
|
Callable<Void> delegate,
|
||||||
|
LockAcquirer lockAcquirer,
|
||||||
|
String tld,
|
||||||
|
Duration leaseLength,
|
||||||
|
String... lockNames) {
|
||||||
checkArgument(leaseLength.isLongerThan(LOCK_TIMEOUT_FUDGE));
|
checkArgument(leaseLength.isLongerThan(LOCK_TIMEOUT_FUDGE));
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
|
this.lockAcquirer = lockAcquirer;
|
||||||
this.tld = tld;
|
this.tld = tld;
|
||||||
this.leaseLength = leaseLength;
|
this.leaseLength = leaseLength;
|
||||||
// Make sure we join locks in a fixed (lexicographical) order to avoid deadlock.
|
// Make sure we join locks in a fixed (lexicographical) order to avoid deadlock.
|
||||||
|
@ -130,7 +175,7 @@ public class LockHandlerImpl implements LockHandler {
|
||||||
Set<Lock> acquiredLocks = new HashSet<>();
|
Set<Lock> acquiredLocks = new HashSet<>();
|
||||||
try {
|
try {
|
||||||
for (String lockName : lockNames) {
|
for (String lockName : lockNames) {
|
||||||
Optional<Lock> lock = acquire(lockName, tld, leaseLength);
|
Optional<Lock> lock = lockAcquirer.acquireLock(lockName, tld, leaseLength);
|
||||||
if (!lock.isPresent()) {
|
if (!lock.isPresent()) {
|
||||||
logger.atInfo().log("Couldn't acquire lock named: %s for TLD %s.", lockName, tld);
|
logger.atInfo().log("Couldn't acquire lock named: %s for TLD %s.", lockName, tld);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -111,6 +112,7 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||||
DelegationSignerData.class,
|
DelegationSignerData.class,
|
||||||
DomainBase.class,
|
DomainBase.class,
|
||||||
GracePeriod.class,
|
GracePeriod.class,
|
||||||
|
Lock.class,
|
||||||
PremiumList.class,
|
PremiumList.class,
|
||||||
PremiumEntry.class,
|
PremiumEntry.class,
|
||||||
RegistrarContact.class,
|
RegistrarContact.class,
|
||||||
|
@ -135,6 +137,7 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeEach() {
|
void beforeEach() {
|
||||||
inject.setStaticField(Ofy.class, "clock", fakeClock);
|
inject.setStaticField(Ofy.class, "clock", fakeClock);
|
||||||
|
lenient().when(requestStatusChecker.getLogId()).thenReturn("requestLogId");
|
||||||
action.gcsUtils = gcsUtils;
|
action.gcsUtils = gcsUtils;
|
||||||
action.response = response;
|
action.response = response;
|
||||||
action.requestStatusChecker = requestStatusChecker;
|
action.requestStatusChecker = requestStatusChecker;
|
||||||
|
@ -464,9 +467,10 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
|
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
|
||||||
// jpaTm()::putIgnoringReadOnly should only have been called with the checkpoint
|
// jpaTm()::putIgnoringReadOnly should only have been called with the checkpoint and the lock
|
||||||
verify(spy, times(2)).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
|
verify(spy, times(2)).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
|
||||||
verify(spy, times(2)).putIgnoringReadOnly(any());
|
verify(spy).putIgnoringReadOnly(any(Lock.class));
|
||||||
|
verify(spy, times(3)).putIgnoringReadOnly(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -506,7 +510,7 @@ public class ReplayCommitLogsToSqlActionTest {
|
||||||
@Test
|
@Test
|
||||||
void testFailure_cannotAcquireLock() {
|
void testFailure_cannotAcquireLock() {
|
||||||
Truth8.assertThat(
|
Truth8.assertThat(
|
||||||
Lock.acquire(
|
Lock.acquireSql(
|
||||||
ReplayCommitLogsToSqlAction.class.getSimpleName(),
|
ReplayCommitLogsToSqlAction.class.getSimpleName(),
|
||||||
null,
|
null,
|
||||||
Duration.standardHours(1),
|
Duration.standardHours(1),
|
||||||
|
|
|
@ -294,7 +294,7 @@ public class ReplicateToDatastoreActionTest {
|
||||||
RequestStatusChecker requestStatusChecker = mock(RequestStatusChecker.class);
|
RequestStatusChecker requestStatusChecker = mock(RequestStatusChecker.class);
|
||||||
when(requestStatusChecker.getLogId()).thenReturn("logId");
|
when(requestStatusChecker.getLogId()).thenReturn("logId");
|
||||||
Truth8.assertThat(
|
Truth8.assertThat(
|
||||||
Lock.acquire(
|
Lock.acquireSql(
|
||||||
ReplicateToDatastoreAction.class.getSimpleName(),
|
ReplicateToDatastoreAction.class.getSimpleName(),
|
||||||
null,
|
null,
|
||||||
Duration.standardHours(1),
|
Duration.standardHours(1),
|
||||||
|
|
|
@ -20,6 +20,7 @@ import static google.registry.model.server.Lock.LockState.FREE;
|
||||||
import static google.registry.model.server.Lock.LockState.IN_USE;
|
import static google.registry.model.server.Lock.LockState.IN_USE;
|
||||||
import static google.registry.model.server.Lock.LockState.OWNER_DIED;
|
import static google.registry.model.server.Lock.LockState.OWNER_DIED;
|
||||||
import static google.registry.model.server.Lock.LockState.TIMED_OUT;
|
import static google.registry.model.server.Lock.LockState.TIMED_OUT;
|
||||||
|
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -28,8 +29,10 @@ import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import google.registry.model.EntityTestCase;
|
import google.registry.model.EntityTestCase;
|
||||||
import google.registry.model.server.Lock.LockState;
|
import google.registry.model.server.Lock.LockState;
|
||||||
|
import google.registry.testing.DatabaseHelper;
|
||||||
import google.registry.testing.DualDatabaseTest;
|
import google.registry.testing.DualDatabaseTest;
|
||||||
import google.registry.testing.TestOfyAndSql;
|
import google.registry.testing.TestOfyAndSql;
|
||||||
|
import google.registry.testing.TestOfyOnly;
|
||||||
import google.registry.util.RequestStatusChecker;
|
import google.registry.util.RequestStatusChecker;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.joda.time.Duration;
|
import org.joda.time.Duration;
|
||||||
|
@ -132,6 +135,19 @@ public class LockTest extends EntityTestCase {
|
||||||
assertThat(acquire("b", ONE_DAY, IN_USE)).isEmpty();
|
assertThat(acquire("b", ONE_DAY, IN_USE)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestOfyOnly
|
||||||
|
void testSqlLock_inOfyMode() {
|
||||||
|
Lock.lockMetrics = origLockMetrics;
|
||||||
|
Optional<Lock> lock = Lock.acquireSql(RESOURCE_NAME, null, ONE_DAY, requestStatusChecker, true);
|
||||||
|
assertThat(lock).isPresent();
|
||||||
|
assertThat(DatabaseHelper.loadAllOf(Lock.class)).isEmpty();
|
||||||
|
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(Lock.class))).containsExactly(lock.get());
|
||||||
|
|
||||||
|
lock.get().releaseSql();
|
||||||
|
assertThat(DatabaseHelper.loadAllOf(Lock.class)).isEmpty();
|
||||||
|
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(Lock.class))).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@TestOfyAndSql
|
@TestOfyAndSql
|
||||||
void testFailure_emptyResourceName() {
|
void testFailure_emptyResourceName() {
|
||||||
IllegalArgumentException thrown =
|
IllegalArgumentException thrown =
|
||||||
|
|
|
@ -40,7 +40,9 @@ final class LockHandlerImplTest {
|
||||||
|
|
||||||
private final FakeClock clock = new FakeClock(DateTime.parse("2001-08-29T12:20:00Z"));
|
private final FakeClock clock = new FakeClock(DateTime.parse("2001-08-29T12:20:00Z"));
|
||||||
|
|
||||||
@RegisterExtension final AppEngineExtension appEngine = AppEngineExtension.builder().build();
|
@RegisterExtension
|
||||||
|
final AppEngineExtension appEngine =
|
||||||
|
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
|
||||||
|
|
||||||
private static class CountingCallable implements Callable<Void> {
|
private static class CountingCallable implements Callable<Void> {
|
||||||
int numCalled = 0;
|
int numCalled = 0;
|
||||||
|
@ -69,18 +71,13 @@ final class LockHandlerImplTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean executeWithLocks(Callable<Void> callable, final @Nullable Lock acquiredLock) {
|
private boolean executeWithLocks(Callable<Void> callable, final @Nullable Lock acquiredLock) {
|
||||||
LockHandlerImpl lockHandler = new LockHandlerImpl(new RequestStatusCheckerImpl(), clock) {
|
return createTestLockHandler(acquiredLock)
|
||||||
private static final long serialVersionUID = 0L;
|
.executeWithLocks(callable, "tld", ONE_DAY, "resourceName");
|
||||||
@Override
|
}
|
||||||
Optional<Lock> acquire(String resourceName, String tld, Duration leaseLength) {
|
|
||||||
assertThat(resourceName).isEqualTo("resourceName");
|
|
||||||
assertThat(tld).isEqualTo("tld");
|
|
||||||
assertThat(leaseLength).isEqualTo(ONE_DAY);
|
|
||||||
return Optional.ofNullable(acquiredLock);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return lockHandler.executeWithLocks(callable, "tld", ONE_DAY, "resourceName");
|
private boolean executeWithSqlLocks(Callable<Void> callable, final @Nullable Lock acquiredLock) {
|
||||||
|
return createTestLockHandler(acquiredLock)
|
||||||
|
.executeWithSqlLocks(callable, "tld", ONE_DAY, "resourceName");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -92,6 +89,15 @@ final class LockHandlerImplTest {
|
||||||
verify(lock, times(1)).release();
|
verify(lock, times(1)).release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSqlLockSucceeds() {
|
||||||
|
Lock lock = mock(Lock.class);
|
||||||
|
CountingCallable countingCallable = new CountingCallable();
|
||||||
|
assertThat(executeWithSqlLocks(countingCallable, lock)).isTrue();
|
||||||
|
assertThat(countingCallable.numCalled).isEqualTo(1);
|
||||||
|
verify(lock, times(1)).release();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLockSucceeds_uncheckedException() {
|
void testLockSucceeds_uncheckedException() {
|
||||||
Lock lock = mock(Lock.class);
|
Lock lock = mock(Lock.class);
|
||||||
|
@ -140,4 +146,23 @@ final class LockHandlerImplTest {
|
||||||
assertThat(executeWithLocks(countingCallable, lock)).isFalse();
|
assertThat(executeWithLocks(countingCallable, lock)).isFalse();
|
||||||
assertThat(countingCallable.numCalled).isEqualTo(0);
|
assertThat(countingCallable.numCalled).isEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LockHandler createTestLockHandler(@Nullable Lock acquiredLock) {
|
||||||
|
return new LockHandlerImpl(new RequestStatusCheckerImpl(), clock) {
|
||||||
|
private static final long serialVersionUID = 0L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Lock> acquire(String resourceName, String tld, Duration leaseLength) {
|
||||||
|
assertThat(resourceName).isEqualTo("resourceName");
|
||||||
|
assertThat(tld).isEqualTo("tld");
|
||||||
|
assertThat(leaseLength).isEqualTo(ONE_DAY);
|
||||||
|
return Optional.ofNullable(acquiredLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Lock> acquireSql(String resourceName, String tld, Duration leaseLength) {
|
||||||
|
return acquire(resourceName, tld, leaseLength);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,11 @@ public class FakeLockHandler implements LockHandler {
|
||||||
|
|
||||||
private static final long serialVersionUID = 6437880915118738492L;
|
private static final long serialVersionUID = 6437880915118738492L;
|
||||||
|
|
||||||
boolean lockSucceeds;
|
private final boolean lockSucceeds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param lockSucceeds if true - the lock acquisition will succeed and the callable will be
|
* @param lockSucceeds if true - the lock acquisition will succeed and the callable will be
|
||||||
* called. If false, lock acquisition will fail and the caller isn't called.
|
* called. If false, lock acquisition will fail and the caller isn't called.
|
||||||
*/
|
*/
|
||||||
public FakeLockHandler(boolean lockSucceeds) {
|
public FakeLockHandler(boolean lockSucceeds) {
|
||||||
this.lockSucceeds = lockSucceeds;
|
this.lockSucceeds = lockSucceeds;
|
||||||
|
@ -38,10 +38,17 @@ public class FakeLockHandler implements LockHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean executeWithLocks(
|
public boolean executeWithLocks(
|
||||||
final Callable<Void> callable,
|
Callable<Void> callable, @Nullable String tld, Duration leaseLength, String... lockNames) {
|
||||||
@Nullable String tld,
|
return execute(callable);
|
||||||
Duration leaseLength,
|
}
|
||||||
String... lockNames) {
|
|
||||||
|
@Override
|
||||||
|
public boolean executeWithSqlLocks(
|
||||||
|
Callable<Void> callable, @Nullable String tld, Duration leaseLength, String... lockNames) {
|
||||||
|
return execute(callable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean execute(Callable<Void> callable) {
|
||||||
if (!lockSucceeds) {
|
if (!lockSucceeds) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue