mirror of
https://github.com/google/nomulus.git
synced 2025-07-23 19:20:44 +02:00
Refactor transact() related methods. (#2195)
This PR makes a few changes to make it possible to turn on per-transaction isolation level with minimal disruption: 1) Changed the signatures of transact() and reTransact() methods to allow passing in lambdas that throw checked exceptions. Previously one has always to wrap such lambdas in try-and-retrow blocks, which wasn't a big issue when one can liberally open nested transactions around small lambdas and keeps the "throwing" part outside the lambda. This becomes a much bigger hassle when the goal is to eliminate nested transactions and put as much code as possible within the top-level lambda. As a result, the transactNoRetry() method now handles checked exceptions by re-throwing them as runtime exceptions. 2) Changed the name and meaning of the config file field that used to indicate if per-transaction isolation level is enabled or not. Now it decides if transact() is called within a transaction, whether to throw or to log, regardless whether the transaction could have succeeded based on the isolation override level (if provided). The flag will initially be set to false and would help us identify all instances of nested calls and either refactor them or use reTransact() instead. Once we are fairly certain that no nested calls to transact() exists, we flip the flag to true and start enforcing this logic. Eventually the flag will go away and nested calls to transact() will always throw. 3) Per-transaction isolation level will now always be applied, if an override is provided. Because currently there should be no actual use of such feature (except for places where we explicitly use an override and have ensured no nested transactions exist, like in RefreshDnsForAllDomainsAction), we do not expect any issues with conflicting isolation levels, which would resulted in failure. 3) transactNoRetry() is made package private and removed from the exposed API of JpaTransactionManager. This saves a lot of redundant methods that do not have a practical use. The only instances where this method was called outside the package was in the reader of RegistryJpaIO, which should have no problem with retrying.
This commit is contained in:
parent
cd23fea698
commit
08471242df
13 changed files with 270 additions and 329 deletions
|
@ -209,7 +209,7 @@ public final class RegistryJpaIO {
|
||||||
|
|
||||||
@ProcessElement
|
@ProcessElement
|
||||||
public void processElement(OutputReceiver<T> outputReceiver) {
|
public void processElement(OutputReceiver<T> outputReceiver) {
|
||||||
tm().transactNoRetry(
|
tm().transact(
|
||||||
() -> {
|
() -> {
|
||||||
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
|
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1548,9 +1548,9 @@ public final class RegistryConfig {
|
||||||
return CONFIG_SETTINGS.get().hibernate.connectionIsolation;
|
return CONFIG_SETTINGS.get().hibernate.connectionIsolation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if per-transaction isolation level is enabled. */
|
/** Returns true if nested calls to {@code tm().transact()} are allowed. */
|
||||||
public static boolean getHibernatePerTransactionIsolationEnabled() {
|
public static boolean getHibernateAllowNestedTransactions() {
|
||||||
return CONFIG_SETTINGS.get().hibernate.perTransactionIsolation;
|
return CONFIG_SETTINGS.get().hibernate.allowNestedTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if hibernate.show_sql is enabled. */
|
/** Returns true if hibernate.show_sql is enabled. */
|
||||||
|
|
|
@ -113,7 +113,7 @@ public class RegistryConfigSettings {
|
||||||
|
|
||||||
/** Configuration for Hibernate. */
|
/** Configuration for Hibernate. */
|
||||||
public static class Hibernate {
|
public static class Hibernate {
|
||||||
public boolean perTransactionIsolation;
|
public boolean allowNestedTransactions;
|
||||||
public String connectionIsolation;
|
public String connectionIsolation;
|
||||||
public String logSqlQueries;
|
public String logSqlQueries;
|
||||||
public String hikariConnectionTimeout;
|
public String hikariConnectionTimeout;
|
||||||
|
|
|
@ -189,11 +189,13 @@ registryPolicy:
|
||||||
sunriseDomainCreateDiscount: 0.15
|
sunriseDomainCreateDiscount: 0.15
|
||||||
|
|
||||||
hibernate:
|
hibernate:
|
||||||
# Make it possible to specify the isolation level for each transaction. If set
|
# If set to false, calls to tm().transact() cannot be nested. If set to true,
|
||||||
# to true, nested transactions will throw an exception. If set to false, a
|
# nested calls to tm().transact() are allowed, as long as they do not specify
|
||||||
# transaction with the isolation override specified will still execute at the
|
# a transaction isolation level override. These nested transactions should
|
||||||
# default level (specified below).
|
# either be refactored to non-nested transactions, or changed to
|
||||||
perTransactionIsolation: true
|
# tm().reTransact(), which explicitly allows nested transactions, but does not
|
||||||
|
# allow setting an isolation level override.
|
||||||
|
allowNestedTransactions: true
|
||||||
|
|
||||||
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
|
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
|
||||||
#
|
#
|
||||||
|
|
|
@ -25,9 +25,10 @@ import com.google.common.base.Strings;
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
|
import google.registry.persistence.transaction.TransactionManager.ThrowingRunnable;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.concurrent.Callable;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
|
@ -184,7 +185,7 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||||
public static Optional<Lock> acquire(
|
public static Optional<Lock> acquire(
|
||||||
String resourceName, @Nullable String tld, Duration leaseLength) {
|
String resourceName, @Nullable String tld, Duration leaseLength) {
|
||||||
String scope = tld != null ? tld : GLOBAL;
|
String scope = tld != null ? tld : GLOBAL;
|
||||||
Supplier<AcquireResult> lockAcquirer =
|
Callable<AcquireResult> lockAcquirer =
|
||||||
() -> {
|
() -> {
|
||||||
DateTime now = tm().getTransactionTime();
|
DateTime now = tm().getTransactionTime();
|
||||||
|
|
||||||
|
@ -221,7 +222,7 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||||
/** Release the lock. */
|
/** Release the lock. */
|
||||||
public void release() {
|
public void release() {
|
||||||
// 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 =
|
ThrowingRunnable lockReleaser =
|
||||||
() -> {
|
() -> {
|
||||||
// To release a lock, check that no one else has already obtained it and if not
|
// To release a lock, check that no one else has already obtained it and if not
|
||||||
// delete it. If the lock in the database was different, then this lock is gone already;
|
// delete it. If the lock in the database was different, then this lock is gone already;
|
||||||
|
@ -246,7 +247,6 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||||
logger.atInfo().log(
|
logger.atInfo().log(
|
||||||
"Not deleting lock: %s - someone else has it: %s", lockId, loadedLock);
|
"Not deleting lock: %s - someone else has it: %s", lockId, loadedLock);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
tm().transact(lockReleaser);
|
tm().transact(lockReleaser);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class DatabaseException extends PersistenceException {
|
||||||
* <p>If the {@code original Throwable} has at least one {@link SQLException} in its chain of
|
* <p>If the {@code original Throwable} has at least one {@link SQLException} in its chain of
|
||||||
* causes, a {@link DatabaseException} is thrown; otherwise this does nothing.
|
* causes, a {@link DatabaseException} is thrown; otherwise this does nothing.
|
||||||
*/
|
*/
|
||||||
static void tryWrapAndThrow(Throwable original) {
|
static void throwIfSqlException(Throwable original) {
|
||||||
Throwable t = original;
|
Throwable t = original;
|
||||||
do {
|
do {
|
||||||
if (t instanceof SQLException) {
|
if (t instanceof SQLException) {
|
||||||
|
|
|
@ -16,7 +16,6 @@ package google.registry.persistence.transaction;
|
||||||
|
|
||||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.Query;
|
import javax.persistence.Query;
|
||||||
import javax.persistence.TypedQuery;
|
import javax.persistence.TypedQuery;
|
||||||
|
@ -62,24 +61,6 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||||
*/
|
*/
|
||||||
Query query(String sqlString);
|
Query query(String sqlString);
|
||||||
|
|
||||||
/** Executes the work in a transaction with no retries and returns the result. */
|
|
||||||
<T> T transactNoRetry(Supplier<T> work);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
|
|
||||||
* retries and returns the result.
|
|
||||||
*/
|
|
||||||
<T> T transactNoRetry(Supplier<T> work, TransactionIsolationLevel isolationLevel);
|
|
||||||
|
|
||||||
/** Executes the work in a transaction with no retries. */
|
|
||||||
void transactNoRetry(Runnable work);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
|
|
||||||
* retries.
|
|
||||||
*/
|
|
||||||
void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel);
|
|
||||||
|
|
||||||
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
|
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
|
||||||
<T> void assertDelete(VKey<T> key);
|
<T> void assertDelete(VKey<T> key);
|
||||||
|
|
||||||
|
@ -103,7 +84,4 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||||
|
|
||||||
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
|
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
|
||||||
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
|
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
|
||||||
|
|
||||||
/** Asserts that the current transaction runs at the given level. */
|
|
||||||
void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,12 @@
|
||||||
package google.registry.persistence.transaction;
|
package google.registry.persistence.transaction;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||||
import static google.registry.config.RegistryConfig.getHibernatePerTransactionIsolationEnabled;
|
import static google.registry.config.RegistryConfig.getHibernateAllowNestedTransactions;
|
||||||
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
|
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||||
import static java.util.AbstractMap.SimpleEntry;
|
import static java.util.AbstractMap.SimpleEntry;
|
||||||
import static java.util.stream.Collectors.joining;
|
import static java.util.stream.Collectors.joining;
|
||||||
|
@ -31,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Streams;
|
import com.google.common.collect.Streams;
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import com.google.common.flogger.StackSize;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.persistence.JpaRetries;
|
import google.registry.persistence.JpaRetries;
|
||||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||||
|
@ -52,7 +54,7 @@ import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -76,6 +78,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
|
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
|
||||||
|
private static final String NESTED_TRANSACTION_MESSAGE =
|
||||||
|
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
|
||||||
|
+ " use reTransact() in nested transactions";
|
||||||
|
|
||||||
// EntityManagerFactory is thread safe.
|
// EntityManagerFactory is thread safe.
|
||||||
private final EntityManagerFactory emf;
|
private final EntityManagerFactory emf;
|
||||||
|
@ -138,21 +143,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
public <T> T reTransact(Callable<T> work) {
|
||||||
assertInTransaction();
|
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||||
TransactionIsolationLevel currentLevel = getCurrentTransactionIsolationLevel();
|
if (inTransaction()) {
|
||||||
if (currentLevel != expectedLevel) {
|
return transactNoRetry(work, null);
|
||||||
throw new IllegalStateException(
|
|
||||||
String.format(
|
|
||||||
"Current transaction isolation level (%s) is not as expected (%s)",
|
|
||||||
currentLevel, expectedLevel));
|
|
||||||
}
|
}
|
||||||
|
return retrier.callWithRetry(
|
||||||
|
() -> transactNoRetry(work, null), JpaRetries::isFailedTxnRetriable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
|
||||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
|
||||||
if (inTransaction()) {
|
if (inTransaction()) {
|
||||||
|
if (!getHibernateAllowNestedTransactions()) {
|
||||||
|
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
|
||||||
|
}
|
||||||
|
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||||
|
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||||
return transactNoRetry(work, isolationLevel);
|
return transactNoRetry(work, isolationLevel);
|
||||||
}
|
}
|
||||||
return retrier.callWithRetry(
|
return retrier.callWithRetry(
|
||||||
|
@ -160,30 +167,32 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T reTransact(Supplier<T> work) {
|
public <T> T transact(Callable<T> work) {
|
||||||
return transact(work);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T transact(Supplier<T> work) {
|
|
||||||
return transact(work, null);
|
return transact(work, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T transactNoRetry(
|
public <T> T transactNoRetry(
|
||||||
Supplier<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
|
Callable<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
|
||||||
if (inTransaction()) {
|
if (inTransaction()) {
|
||||||
if (isolationLevel != null && getHibernatePerTransactionIsolationEnabled()) {
|
// This check will no longer be necessary when the transact() method always throws
|
||||||
TransactionIsolationLevel enclosingLevel = getCurrentTransactionIsolationLevel();
|
// inside a nested transaction, as the only way to pass a non-null isolation level
|
||||||
if (isolationLevel != enclosingLevel) {
|
// is by calling the transact() method (and its variants), which would have already
|
||||||
throw new IllegalStateException(
|
// thrown before calling transactNoRetry() when inside a nested transaction.
|
||||||
String.format(
|
//
|
||||||
"Isolation level conflict detected in nested transactions.\n"
|
// For now, we still need it, so we don't accidentally call a nested transact() with an
|
||||||
+ "Enclosing transaction: %s\nCurrent transaction: %s",
|
// isolation level override. This buys us time to detect nested transact() calls and either
|
||||||
enclosingLevel, isolationLevel));
|
// remove them or change the call site to reTransact().
|
||||||
}
|
if (isolationLevel != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Transaction isolation level cannot be specified for nested transactions");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return work.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throwIfSqlException(e);
|
||||||
|
throwIfUnchecked(e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
return work.get();
|
|
||||||
}
|
}
|
||||||
TransactionInfo txnInfo = transactionInfo.get();
|
TransactionInfo txnInfo = transactionInfo.get();
|
||||||
txnInfo.entityManager = emf.createEntityManager();
|
txnInfo.entityManager = emf.createEntityManager();
|
||||||
|
@ -191,43 +200,36 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
try {
|
try {
|
||||||
txn.begin();
|
txn.begin();
|
||||||
txnInfo.start(clock);
|
txnInfo.start(clock);
|
||||||
if (isolationLevel != null) {
|
if (isolationLevel != null && isolationLevel != getDefaultTransactionIsolationLevel()) {
|
||||||
if (getHibernatePerTransactionIsolationEnabled()) {
|
getEntityManager()
|
||||||
getEntityManager()
|
.createNativeQuery(
|
||||||
.createNativeQuery(
|
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
|
||||||
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
|
.executeUpdate();
|
||||||
.executeUpdate();
|
logger.atInfo().log(
|
||||||
logger.atInfo().log("Running transaction at %s", isolationLevel);
|
"Overriding transaction isolation level from %s to %s",
|
||||||
} else {
|
getDefaultTransactionIsolationLevel(), isolationLevel);
|
||||||
logger.atWarning().log(
|
|
||||||
"Per-transaction isolation level disabled, but %s was requested", isolationLevel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
T result = work.get();
|
T result = work.call();
|
||||||
txn.commit();
|
txn.commit();
|
||||||
return result;
|
return result;
|
||||||
} catch (RuntimeException | Error e) {
|
} catch (Throwable e) {
|
||||||
// Error is unchecked!
|
// Catch a Throwable here so even Errors would lead to a rollback.
|
||||||
try {
|
try {
|
||||||
txn.rollback();
|
txn.rollback();
|
||||||
logger.atWarning().log("Error during transaction; transaction rolled back.");
|
logger.atWarning().log("Error during transaction; transaction rolled back.");
|
||||||
} catch (Throwable rollbackException) {
|
} catch (Exception rollbackException) {
|
||||||
logger.atSevere().withCause(rollbackException).log("Rollback failed; suppressing error.");
|
logger.atSevere().withCause(rollbackException).log("Rollback failed; suppressing error.");
|
||||||
}
|
}
|
||||||
tryWrapAndThrow(e);
|
throwIfSqlException(e);
|
||||||
throw e;
|
throwIfUnchecked(e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
} finally {
|
} finally {
|
||||||
txnInfo.clear();
|
txnInfo.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T transactNoRetry(Supplier<T> work) {
|
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
|
||||||
return transactNoRetry(work, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void transact(Runnable work, TransactionIsolationLevel isolationLevel) {
|
|
||||||
transact(
|
transact(
|
||||||
() -> {
|
() -> {
|
||||||
work.run();
|
work.run();
|
||||||
|
@ -237,28 +239,17 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reTransact(Runnable work) {
|
public void transact(ThrowingRunnable work) {
|
||||||
transact(work);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void transact(Runnable work) {
|
|
||||||
transact(work, null);
|
transact(work, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel) {
|
public void reTransact(ThrowingRunnable work) {
|
||||||
transactNoRetry(
|
reTransact(
|
||||||
() -> {
|
() -> {
|
||||||
work.run();
|
work.run();
|
||||||
return null;
|
return null;
|
||||||
},
|
});
|
||||||
isolationLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void transactNoRetry(Runnable work) {
|
|
||||||
transactNoRetry(work, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -22,7 +22,7 @@ import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
|
@ -48,51 +48,51 @@ public interface TransactionManager {
|
||||||
void assertInTransaction();
|
void assertInTransaction();
|
||||||
|
|
||||||
/** Executes the work in a transaction and returns the result. */
|
/** Executes the work in a transaction and returns the result. */
|
||||||
<T> T transact(Supplier<T> work);
|
<T> T transact(Callable<T> work);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} and returns
|
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} and returns
|
||||||
* the result.
|
* the result.
|
||||||
*/
|
*/
|
||||||
<T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel);
|
<T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
||||||
*
|
*
|
||||||
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
||||||
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
|
* top-level transactions themselves or are nested inside larger transactions (e.g. a
|
||||||
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
||||||
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
||||||
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
||||||
*
|
*
|
||||||
* <p>In the future we will be enforcing that {@link #transact(Supplier)} calls be top-level only,
|
* <p>In the future we will be enforcing that {@link #transact(Callable)} calls be top-level only,
|
||||||
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
||||||
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload
|
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload
|
||||||
* provided to specify a (potentially conflicting) transaction isolation level.
|
* provided to specify a (potentially conflicting) transaction isolation level.
|
||||||
*/
|
*/
|
||||||
<T> T reTransact(Supplier<T> work);
|
<T> T reTransact(Callable<T> work);
|
||||||
|
|
||||||
/** Executes the work in a transaction. */
|
/** Executes the work in a transaction. */
|
||||||
void transact(Runnable work);
|
void transact(ThrowingRunnable work);
|
||||||
|
|
||||||
/** Executes the work in a transaction at the given {@link TransactionIsolationLevel}. */
|
/** Executes the work in a transaction at the given {@link TransactionIsolationLevel}. */
|
||||||
void transact(Runnable work, TransactionIsolationLevel isolationLevel);
|
void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
||||||
*
|
*
|
||||||
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
||||||
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
|
* top-level transactions themselves or are nested inside larger transactions (e.g. a
|
||||||
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
||||||
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
||||||
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
||||||
*
|
*
|
||||||
* <p>In the future we will be enforcing that {@link #transact(Runnable)} calls be top-level only,
|
* <p>In the future we will be enforcing that {@link #transact(ThrowingRunnable)} calls be
|
||||||
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
* top-level only, with reTransact calls being the only ones that can potentially be an inner
|
||||||
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload *
|
* nested transaction (which is a noop). Note that, as this can be a nested inner exception, there
|
||||||
* provided to specify a (potentially conflicting) transaction isolation level.
|
* is no overload provided to specify a (potentially conflicting) transaction isolation level.
|
||||||
*/
|
*/
|
||||||
void reTransact(Runnable work);
|
void reTransact(ThrowingRunnable work);
|
||||||
|
|
||||||
/** Returns the time associated with the start of this particular transaction attempt. */
|
/** Returns the time associated with the start of this particular transaction attempt. */
|
||||||
DateTime getTransactionTime();
|
DateTime getTransactionTime();
|
||||||
|
@ -216,4 +216,15 @@ public interface TransactionManager {
|
||||||
|
|
||||||
/** Returns a QueryComposer which can be used to perform queries against the current database. */
|
/** Returns a QueryComposer which can be used to perform queries against the current database. */
|
||||||
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
|
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A runnable that allows for checked exceptions to be thrown.
|
||||||
|
*
|
||||||
|
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
|
||||||
|
* checked excpetions as unchecked ones.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ThrowingRunnable {
|
||||||
|
void run() throws Exception;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,6 @@ import com.google.common.base.Splitter;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
import com.google.common.collect.ImmutableSortedMap;
|
||||||
import com.google.common.testing.TestLogHandler;
|
import com.google.common.testing.TestLogHandler;
|
||||||
import google.registry.config.RegistryConfig;
|
|
||||||
import google.registry.flows.certs.CertificateChecker;
|
import google.registry.flows.certs.CertificateChecker;
|
||||||
import google.registry.model.eppcommon.Trid;
|
import google.registry.model.eppcommon.Trid;
|
||||||
import google.registry.model.eppoutput.EppOutput.ResponseOrGreeting;
|
import google.registry.model.eppoutput.EppOutput.ResponseOrGreeting;
|
||||||
|
@ -89,8 +88,8 @@ class FlowRunnerTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseOrGreeting run() {
|
public ResponseOrGreeting run() {
|
||||||
tm().assertTransactionIsolationLevel(
|
assertThat(tm().getCurrentTransactionIsolationLevel())
|
||||||
isolationLevel.orElse(tm().getDefaultTransactionIsolationLevel()));
|
.isEqualTo(isolationLevel.orElse(tm().getDefaultTransactionIsolationLevel()));
|
||||||
return mock(EppResponse.class);
|
return mock(EppResponse.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,10 +135,8 @@ class FlowRunnerTest {
|
||||||
Optional.of(TransactionIsolationLevel.TRANSACTION_READ_UNCOMMITTED);
|
Optional.of(TransactionIsolationLevel.TRANSACTION_READ_UNCOMMITTED);
|
||||||
flowRunner.flowClass = TestTransactionalFlow.class;
|
flowRunner.flowClass = TestTransactionalFlow.class;
|
||||||
flowRunner.flowProvider = () -> new TestTransactionalFlow(flowRunner.isolationLevelOverride);
|
flowRunner.flowProvider = () -> new TestTransactionalFlow(flowRunner.isolationLevelOverride);
|
||||||
if (RegistryConfig.getHibernatePerTransactionIsolationEnabled()) {
|
flowRunner.run(eppMetricBuilder);
|
||||||
flowRunner.run(eppMetricBuilder);
|
assertThat(eppMetricBuilder.build().getCommandName()).hasValue("TestTransactional");
|
||||||
assertThat(eppMetricBuilder.build().getCommandName()).hasValue("TestTransactional");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -18,7 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth8.assertThat;
|
import static com.google.common.truth.Truth8.assertThat;
|
||||||
import static google.registry.persistence.transaction.DatabaseException.getSqlError;
|
import static google.registry.persistence.transaction.DatabaseException.getSqlError;
|
||||||
import static google.registry.persistence.transaction.DatabaseException.getSqlExceptionDetails;
|
import static google.registry.persistence.transaction.DatabaseException.getSqlExceptionDetails;
|
||||||
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
|
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||||
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.times;
|
import static org.mockito.Mockito.times;
|
||||||
|
@ -99,13 +99,13 @@ public class DatabaseExceptionTest {
|
||||||
@Test
|
@Test
|
||||||
void tryWrapAndThrow_notSQLException() {
|
void tryWrapAndThrow_notSQLException() {
|
||||||
RuntimeException orig = new RuntimeException(new Exception());
|
RuntimeException orig = new RuntimeException(new Exception());
|
||||||
tryWrapAndThrow(orig);
|
throwIfSqlException(orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void tryWrapAndThrow_hasSQLException() {
|
void tryWrapAndThrow_hasSQLException() {
|
||||||
Throwable orig = new Throwable(new SQLException());
|
Throwable orig = new Throwable(new SQLException());
|
||||||
assertThrows(DatabaseException.class, () -> tryWrapAndThrow(orig));
|
assertThrows(DatabaseException.class, () -> throwIfSqlException(orig));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
package google.registry.persistence.transaction;
|
package google.registry.persistence.transaction;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_READ_COMMITTED;
|
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_READ_COMMITTED;
|
||||||
|
@ -28,6 +29,7 @@ import static google.registry.testing.TestDataHelper.fileClassPath;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
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;
|
||||||
|
@ -36,6 +38,7 @@ import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import google.registry.config.RegistryConfig;
|
import google.registry.config.RegistryConfig;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
|
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
|
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
|
||||||
import google.registry.testing.DatabaseHelper;
|
import google.registry.testing.DatabaseHelper;
|
||||||
|
@ -43,7 +46,6 @@ import google.registry.testing.FakeClock;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.Id;
|
import javax.persistence.Id;
|
||||||
|
@ -53,6 +55,8 @@ import javax.persistence.PersistenceException;
|
||||||
import javax.persistence.RollbackException;
|
import javax.persistence.RollbackException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import org.junit.jupiter.api.function.Executable;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for SQL only APIs defined in {@link JpaTransactionManagerImpl}. Note that the tests
|
* Unit tests for SQL only APIs defined in {@link JpaTransactionManagerImpl}. Note that the tests
|
||||||
|
@ -94,7 +98,7 @@ class JpaTransactionManagerImplTest {
|
||||||
insertPerson(10);
|
insertPerson(10);
|
||||||
insertCompany("Foo");
|
insertCompany("Foo");
|
||||||
insertCompany("Bar");
|
insertCompany("Bar");
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||||
});
|
});
|
||||||
assertPersonCount(1);
|
assertPersonCount(1);
|
||||||
assertPersonExist(10);
|
assertPersonExist(10);
|
||||||
|
@ -105,145 +109,98 @@ class JpaTransactionManagerImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transact_setIsolationLevel() {
|
void transact_setIsolationLevel() {
|
||||||
|
// If not specified, run at the default isolation level.
|
||||||
tm().transact(
|
tm().transact(
|
||||||
() -> {
|
() -> assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel()),
|
||||||
tm().assertTransactionIsolationLevel(
|
null);
|
||||||
RegistryConfig.getHibernatePerTransactionIsolationEnabled()
|
tm().transact(
|
||||||
? TRANSACTION_READ_UNCOMMITTED
|
() -> assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED),
|
||||||
: tm().getDefaultTransactionIsolationLevel());
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
TRANSACTION_READ_UNCOMMITTED);
|
||||||
// Make sure that we can start a new transaction on the same thread with a different isolation
|
// Make sure that we can start a new transaction on the same thread at a different level.
|
||||||
// level.
|
|
||||||
tm().transact(
|
tm().transact(
|
||||||
() -> {
|
() -> assertTransactionIsolationLevel(TRANSACTION_REPEATABLE_READ),
|
||||||
tm().assertTransactionIsolationLevel(
|
|
||||||
RegistryConfig.getHibernatePerTransactionIsolationEnabled()
|
|
||||||
? TRANSACTION_REPEATABLE_READ
|
|
||||||
: tm().getDefaultTransactionIsolationLevel());
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
TRANSACTION_REPEATABLE_READ);
|
TRANSACTION_REPEATABLE_READ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transact_nestedTransactions_perTransactionIsolationLevelEnabled() {
|
void transact_nestedTransactions_disabled() {
|
||||||
if (!RegistryConfig.getHibernatePerTransactionIsolationEnabled()) {
|
try (MockedStatic<RegistryConfig> config = mockStatic(RegistryConfig.class)) {
|
||||||
return;
|
config.when(RegistryConfig::getHibernateAllowNestedTransactions).thenReturn(false);
|
||||||
}
|
// transact() not allowed in nested transactions.
|
||||||
// Nested transactions allowed (both at the default isolation level).
|
IllegalStateException thrown =
|
||||||
tm().transact(
|
assertThrows(
|
||||||
() -> {
|
IllegalStateException.class,
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
() ->
|
||||||
tm().transact(
|
tm().transact(
|
||||||
() -> {
|
() -> {
|
||||||
tm().assertTransactionIsolationLevel(
|
assertTransactionIsolationLevel(
|
||||||
tm().getDefaultTransactionIsolationLevel());
|
tm().getDefaultTransactionIsolationLevel());
|
||||||
});
|
tm().transact(() -> null);
|
||||||
});
|
}));
|
||||||
// Nested transactions allowed (enclosed transaction does not have an override, using the
|
assertThat(thrown).hasMessageThat().contains("Nested transaction detected");
|
||||||
// enclosing transaction's level).
|
// reTransact() allowed in nested transactions.
|
||||||
tm().transact(
|
tm().transact(
|
||||||
() -> {
|
() -> {
|
||||||
tm().assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||||
tm().transact(
|
tm().reTransact(
|
||||||
() -> {
|
() ->
|
||||||
tm().assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
assertTransactionIsolationLevel(
|
||||||
});
|
tm().getDefaultTransactionIsolationLevel()));
|
||||||
},
|
});
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
// reTransact() respects enclosing transaction's isolation level.
|
||||||
// Nested transactions allowed (Both have the same override).
|
tm().transact(
|
||||||
tm().transact(
|
() -> {
|
||||||
() -> {
|
assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
||||||
tm().assertTransactionIsolationLevel(TRANSACTION_REPEATABLE_READ);
|
tm().reTransact(
|
||||||
tm().transact(
|
() -> assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED));
|
||||||
() -> {
|
},
|
||||||
tm().assertTransactionIsolationLevel(TRANSACTION_REPEATABLE_READ);
|
TRANSACTION_READ_UNCOMMITTED);
|
||||||
},
|
}
|
||||||
TRANSACTION_REPEATABLE_READ);
|
|
||||||
},
|
|
||||||
TRANSACTION_REPEATABLE_READ);
|
|
||||||
// Nested transactions disallowed (enclosed transaction has an override that conflicts from the
|
|
||||||
// default).
|
|
||||||
IllegalStateException e =
|
|
||||||
assertThrows(
|
|
||||||
IllegalStateException.class,
|
|
||||||
() ->
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().transact(() -> {}, TRANSACTION_READ_COMMITTED);
|
|
||||||
}));
|
|
||||||
assertThat(e).hasMessageThat().contains("conflict detected");
|
|
||||||
// Nested transactions disallowed (conflicting overrides).
|
|
||||||
e =
|
|
||||||
assertThrows(
|
|
||||||
IllegalStateException.class,
|
|
||||||
() ->
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().transact(() -> {}, TRANSACTION_READ_COMMITTED);
|
|
||||||
},
|
|
||||||
TRANSACTION_REPEATABLE_READ));
|
|
||||||
assertThat(e).hasMessageThat().contains("conflict detected");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transact_nestedTransactions_perTransactionIsolationLevelDisabled() {
|
void transact_nestedTransactions_enabled() {
|
||||||
if (RegistryConfig.getHibernatePerTransactionIsolationEnabled()) {
|
try (MockedStatic<RegistryConfig> config = mockStatic(RegistryConfig.class)) {
|
||||||
return;
|
config.when(RegistryConfig::getHibernateAllowNestedTransactions).thenReturn(true);
|
||||||
|
// transact() allowed in nested transactions.
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||||
|
tm().reTransact(
|
||||||
|
() ->
|
||||||
|
assertTransactionIsolationLevel(
|
||||||
|
tm().getDefaultTransactionIsolationLevel()));
|
||||||
|
});
|
||||||
|
// transact() not allowed in nested transactions if isolation level is specified.
|
||||||
|
IllegalStateException thrown =
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() ->
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertTransactionIsolationLevel(
|
||||||
|
tm().getDefaultTransactionIsolationLevel());
|
||||||
|
tm().transact(() -> null, TRANSACTION_READ_COMMITTED);
|
||||||
|
}));
|
||||||
|
assertThat(thrown).hasMessageThat().contains("cannot be specified");
|
||||||
|
// reTransact() allowed in nested transactions.
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||||
|
tm().reTransact(
|
||||||
|
() ->
|
||||||
|
assertTransactionIsolationLevel(
|
||||||
|
tm().getDefaultTransactionIsolationLevel()));
|
||||||
|
});
|
||||||
|
// reTransact() respects enclosing transaction's isolation level.
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
||||||
|
tm().reTransact(
|
||||||
|
() -> assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED));
|
||||||
|
},
|
||||||
|
TRANSACTION_READ_UNCOMMITTED);
|
||||||
}
|
}
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(
|
|
||||||
tm().getDefaultTransactionIsolationLevel());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(
|
|
||||||
tm().getDefaultTransactionIsolationLevel());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(
|
|
||||||
tm().getDefaultTransactionIsolationLevel());
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
|
||||||
});
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(
|
|
||||||
tm().getDefaultTransactionIsolationLevel());
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
|
||||||
tm().transact(
|
|
||||||
() -> {
|
|
||||||
tm().assertTransactionIsolationLevel(
|
|
||||||
tm().getDefaultTransactionIsolationLevel());
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_COMMITTED);
|
|
||||||
},
|
|
||||||
TRANSACTION_READ_UNCOMMITTED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -299,32 +256,55 @@ class JpaTransactionManagerImplTest {
|
||||||
OptimisticLockException.class,
|
OptimisticLockException.class,
|
||||||
() -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
() -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||||
verify(spyJpaTm, times(3)).delete(theEntityKey);
|
verify(spyJpaTm, times(3)).delete(theEntityKey);
|
||||||
Supplier<Runnable> supplier =
|
assertThrows(
|
||||||
() -> {
|
OptimisticLockException.class,
|
||||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
() -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||||
work.run();
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
assertThrows(OptimisticLockException.class, () -> spyJpaTm.transact(supplier));
|
|
||||||
verify(spyJpaTm, times(6)).delete(theEntityKey);
|
verify(spyJpaTm, times(6)).delete(theEntityKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transactNoRetry_doesNotRetryOptimisticLockException() {
|
void transactNoRetry_nested() {
|
||||||
JpaTransactionManager spyJpaTm = spy(tm());
|
JpaTransactionManagerImpl tm = (JpaTransactionManagerImpl) tm();
|
||||||
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
// Calling transactNoRetry() without an isolation level override inside a transaction is fine.
|
||||||
spyJpaTm.transactNoRetry(() -> spyJpaTm.insert(theEntity));
|
tm.transact(
|
||||||
assertThrows(
|
|
||||||
OptimisticLockException.class,
|
|
||||||
() -> spyJpaTm.transactNoRetry(() -> spyJpaTm.delete(theEntityKey)));
|
|
||||||
verify(spyJpaTm, times(1)).delete(theEntityKey);
|
|
||||||
Supplier<Runnable> supplier =
|
|
||||||
() -> {
|
() -> {
|
||||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
tm.transactNoRetry(
|
||||||
work.run();
|
() -> {
|
||||||
|
assertTransactionIsolationLevel(tm.getDefaultTransactionIsolationLevel());
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
null);
|
||||||
|
});
|
||||||
|
// Calling transactNoRetry() with an isolation level override inside a transaction is not
|
||||||
|
// allowed.
|
||||||
|
IllegalStateException thrown =
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() -> tm.transact(() -> tm.transactNoRetry(() -> null, TRANSACTION_READ_UNCOMMITTED)));
|
||||||
|
assertThat(thrown).hasMessageThat().contains("cannot be specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void transactNoRetry_doesNotRetryOptimisticLockException() {
|
||||||
|
JpaTransactionManagerImpl spyJpaTm = spy((JpaTransactionManagerImpl) tm());
|
||||||
|
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
||||||
|
spyJpaTm.transactNoRetry(
|
||||||
|
() -> {
|
||||||
|
spyJpaTm.insert(theEntity);
|
||||||
return null;
|
return null;
|
||||||
};
|
},
|
||||||
assertThrows(OptimisticLockException.class, () -> spyJpaTm.transactNoRetry(supplier));
|
null);
|
||||||
|
Executable transaction =
|
||||||
|
() ->
|
||||||
|
spyJpaTm.transactNoRetry(
|
||||||
|
() -> {
|
||||||
|
spyJpaTm.delete(theEntityKey);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
null);
|
||||||
|
assertThrows(OptimisticLockException.class, transaction);
|
||||||
|
verify(spyJpaTm, times(1)).delete(theEntityKey);
|
||||||
|
assertThrows(OptimisticLockException.class, transaction);
|
||||||
verify(spyJpaTm, times(2)).delete(theEntityKey);
|
verify(spyJpaTm, times(2)).delete(theEntityKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,13 +318,8 @@ class JpaTransactionManagerImplTest {
|
||||||
assertThrows(
|
assertThrows(
|
||||||
RuntimeException.class, () -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
RuntimeException.class, () -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||||
verify(spyJpaTm, times(3)).delete(theEntityKey);
|
verify(spyJpaTm, times(3)).delete(theEntityKey);
|
||||||
Supplier<Runnable> supplier =
|
assertThrows(
|
||||||
() -> {
|
RuntimeException.class, () -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
|
||||||
work.run();
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
assertThrows(RuntimeException.class, () -> spyJpaTm.transact(supplier));
|
|
||||||
verify(spyJpaTm, times(6)).delete(theEntityKey);
|
verify(spyJpaTm, times(6)).delete(theEntityKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -740,20 +715,13 @@ class JpaTransactionManagerImplTest {
|
||||||
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
||||||
spyJpaTm.transact(() -> spyJpaTm.insert(theEntity));
|
spyJpaTm.transact(() -> spyJpaTm.insert(theEntity));
|
||||||
|
|
||||||
Supplier<Runnable> supplier =
|
|
||||||
() -> {
|
|
||||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
|
||||||
work.run();
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
assertThrows(
|
assertThrows(
|
||||||
OptimisticLockException.class,
|
OptimisticLockException.class,
|
||||||
() ->
|
() ->
|
||||||
spyJpaTm.transact(
|
spyJpaTm.transact(
|
||||||
() -> {
|
() -> {
|
||||||
spyJpaTm.exists(theEntity);
|
spyJpaTm.exists(theEntity);
|
||||||
spyJpaTm.transact(supplier);
|
spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
verify(spyJpaTm, times(3)).exists(theEntity);
|
verify(spyJpaTm, times(3)).exists(theEntity);
|
||||||
|
@ -814,6 +782,16 @@ class JpaTransactionManagerImplTest {
|
||||||
assertCompanyCount(0);
|
assertCompanyCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
||||||
|
tm().assertInTransaction();
|
||||||
|
TransactionIsolationLevel currentLevel = tm().getCurrentTransactionIsolationLevel();
|
||||||
|
checkState(
|
||||||
|
currentLevel == expectedLevel,
|
||||||
|
"Current transaction isolation level (%s) is not as expected (%s)",
|
||||||
|
currentLevel,
|
||||||
|
expectedLevel);
|
||||||
|
}
|
||||||
|
|
||||||
private static int countTable(String tableName) {
|
private static int countTable(String tableName) {
|
||||||
return tm().transact(
|
return tm().transact(
|
||||||
() -> {
|
() -> {
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
|
|
||||||
package google.registry.persistence.transaction;
|
package google.registry.persistence.transaction;
|
||||||
|
|
||||||
|
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||||
|
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableCollection;
|
import com.google.common.collect.ImmutableCollection;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
@ -21,7 +24,7 @@ import google.registry.model.ImmutableObject;
|
||||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.Query;
|
import javax.persistence.Query;
|
||||||
|
@ -60,11 +63,6 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||||
return delegate.getCurrentTransactionIsolationLevel();
|
return delegate.getCurrentTransactionIsolationLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
|
||||||
delegate.assertTransactionIsolationLevel(expectedLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EntityManager getStandaloneEntityManager() {
|
public EntityManager getStandaloneEntityManager() {
|
||||||
return delegate.getStandaloneEntityManager();
|
return delegate.getStandaloneEntityManager();
|
||||||
|
@ -101,9 +99,15 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
|
||||||
if (delegate.inTransaction()) {
|
if (inTransaction()) {
|
||||||
return work.get();
|
try {
|
||||||
|
return work.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throwIfSqlException(e);
|
||||||
|
throwIfUnchecked(e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return delegate.transact(
|
return delegate.transact(
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -111,33 +115,23 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||||
.getEntityManager()
|
.getEntityManager()
|
||||||
.createNativeQuery("SET TRANSACTION READ ONLY")
|
.createNativeQuery("SET TRANSACTION READ ONLY")
|
||||||
.executeUpdate();
|
.executeUpdate();
|
||||||
return work.get();
|
return work.call();
|
||||||
},
|
},
|
||||||
isolationLevel);
|
isolationLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T reTransact(Supplier<T> work) {
|
public <T> T reTransact(Callable<T> work) {
|
||||||
return transact(work);
|
return transact(work);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T transact(Supplier<T> work) {
|
public <T> T transact(Callable<T> work) {
|
||||||
return transact(work, null);
|
return transact(work, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T transactNoRetry(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
|
||||||
return transact(work, isolationLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T transactNoRetry(Supplier<T> work) {
|
|
||||||
return transactNoRetry(work, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void transact(Runnable work, TransactionIsolationLevel isolationLevel) {
|
|
||||||
transact(
|
transact(
|
||||||
() -> {
|
() -> {
|
||||||
work.run();
|
work.run();
|
||||||
|
@ -147,25 +141,15 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reTransact(Runnable work) {
|
public void reTransact(ThrowingRunnable work) {
|
||||||
transact(work);
|
transact(work);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void transact(Runnable work) {
|
public void transact(ThrowingRunnable work) {
|
||||||
transact(work, null);
|
transact(work, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel) {
|
|
||||||
transact(work, isolationLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void transactNoRetry(Runnable work) {
|
|
||||||
transactNoRetry(work, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DateTime getTransactionTime() {
|
public DateTime getTransactionTime() {
|
||||||
return delegate.getTransactionTime();
|
return delegate.getTransactionTime();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue