mirror of
https://github.com/google/nomulus
synced 2026-09-06 08:07:03 +00:00
Add JpaTransactionManagerRule (#277)
* Add RegistryRuntime and JpaTransactionManagerRule * Revert RegistryJavaRuntime change * Add JpaTransactionManager interface
This commit is contained in:
@@ -14,7 +14,6 @@
|
||||
|
||||
package google.registry.model.ofy;
|
||||
|
||||
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
|
||||
import google.registry.model.transaction.TransactionManager;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.transaction;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* A dummy implementation for {@link JpaTransactionManager} which throws exception when any of its
|
||||
* method is invoked.
|
||||
*
|
||||
* <p>This is used to initialize the {@link TransactionManagerFactory#jpaTm} when running unit
|
||||
* tests, because obviously we cannot connect to the actual Cloud SQL backend in a unit test.
|
||||
*
|
||||
* <p>If a unit test needs to access the Cloud SQL database, it must add JpaTransactionManagerRule
|
||||
* as a JUnit rule in the test class.
|
||||
*/
|
||||
public class DummyJpaTransactionManager {
|
||||
|
||||
/** Constructs a dummy {@link JpaTransactionManager} instance. */
|
||||
public static JpaTransactionManager create() {
|
||||
return (JpaTransactionManager)
|
||||
Proxy.newProxyInstance(
|
||||
JpaTransactionManager.class.getClassLoader(),
|
||||
new Class[] {JpaTransactionManager.class},
|
||||
(proxy, method, args) -> {
|
||||
throw new UnsupportedOperationException(
|
||||
"JpaTransactionManager was not initialized as the runtime is detected as"
|
||||
+ " Unittest. Add JpaTransactionManagerRule in the unit test for"
|
||||
+ " initialization.");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,154 +14,10 @@
|
||||
|
||||
package google.registry.model.transaction;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.persistence.PersistenceModule.AppEngineEmf;
|
||||
import google.registry.util.Clock;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.EntityTransaction;
|
||||
import javax.persistence.PersistenceException;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Implementation of {@link TransactionManager} for JPA compatible database. */
|
||||
@Singleton
|
||||
public class JpaTransactionManager implements TransactionManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// EntityManagerFactory is thread safe.
|
||||
private final EntityManagerFactory emf;
|
||||
private final Clock clock;
|
||||
// TODO(shicong): Investigate alternatives for managing transaction information. ThreadLocal adds
|
||||
// an unnecessary restriction that each request has to be processed by one thread synchronously.
|
||||
private final ThreadLocal<TransactionInfo> transactionInfo =
|
||||
ThreadLocal.withInitial(TransactionInfo::new);
|
||||
|
||||
@Inject
|
||||
JpaTransactionManager(@AppEngineEmf EntityManagerFactory emf, Clock clock) {
|
||||
this.emf = emf;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public EntityManager getEntityManager() {
|
||||
if (transactionInfo.get().entityManager == null) {
|
||||
throw new PersistenceException(
|
||||
"No EntityManager has been initialized. getEntityManager() must be invoked in the scope"
|
||||
+ " of a transaction");
|
||||
}
|
||||
return transactionInfo.get().entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inTransaction() {
|
||||
return transactionInfo.get().inTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertInTransaction() {
|
||||
if (!inTransaction()) {
|
||||
throw new PersistenceException("Not in a transaction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Work<T> work) {
|
||||
// TODO(shicong): Investigate removing transactNew functionality after migration as it may
|
||||
// be same as this one.
|
||||
if (inTransaction()) {
|
||||
return work.run();
|
||||
}
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
txnInfo.entityManager = emf.createEntityManager();
|
||||
EntityTransaction txn = txnInfo.entityManager.getTransaction();
|
||||
try {
|
||||
txn.begin();
|
||||
txnInfo.inTransaction = true;
|
||||
txnInfo.transactionTime = clock.nowUtc();
|
||||
T result = work.run();
|
||||
txn.commit();
|
||||
return result;
|
||||
} catch (Throwable transactionException) {
|
||||
String rollbackMessage;
|
||||
try {
|
||||
txn.rollback();
|
||||
rollbackMessage = "transaction rolled back";
|
||||
} catch (Throwable rollbackException) {
|
||||
logger.atSevere().withCause(rollbackException).log("Rollback failed, suppressing error");
|
||||
rollbackMessage = "transaction rollback failed";
|
||||
}
|
||||
throw new PersistenceException(
|
||||
"Error during transaction, " + rollbackMessage, transactionException);
|
||||
} finally {
|
||||
txnInfo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work) {
|
||||
transact(
|
||||
() -> {
|
||||
work.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNew(Work<T> work) {
|
||||
// TODO(shicong): Implements the functionality to start a new transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNew(Runnable work) {
|
||||
// TODO(shicong): Implements the functionality to start a new transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNewReadOnly(Work<T> work) {
|
||||
// TODO(shicong): Implements read only transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNewReadOnly(Runnable work) {
|
||||
// TODO(shicong): Implements read only transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T doTransactionless(Work<T> work) {
|
||||
// TODO(shicong): Implements doTransactionless.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime getTransactionTime() {
|
||||
assertInTransaction();
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
if (txnInfo.transactionTime == null) {
|
||||
throw new PersistenceException("In a transaction but transactionTime is null");
|
||||
}
|
||||
return txnInfo.transactionTime;
|
||||
}
|
||||
|
||||
private static class TransactionInfo {
|
||||
EntityManager entityManager;
|
||||
boolean inTransaction = false;
|
||||
DateTime transactionTime;
|
||||
|
||||
private void clear() {
|
||||
inTransaction = false;
|
||||
transactionTime = null;
|
||||
if (entityManager != null) {
|
||||
// Close this EntityManager just let the connection pool be able to reuse it, it doesn't
|
||||
// close the underlying database connection.
|
||||
entityManager.close();
|
||||
entityManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
|
||||
public interface JpaTransactionManager extends TransactionManager {
|
||||
/** Returns the {@link EntityManager} for the current request. */
|
||||
EntityManager getEntityManager();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.transaction;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.persistence.PersistenceModule.AppEngineEmf;
|
||||
import google.registry.util.Clock;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.EntityTransaction;
|
||||
import javax.persistence.PersistenceException;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Implementation of {@link JpaTransactionManager} for JPA compatible database. */
|
||||
@Singleton
|
||||
public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// EntityManagerFactory is thread safe.
|
||||
private final EntityManagerFactory emf;
|
||||
private final Clock clock;
|
||||
// TODO(shicong): Investigate alternatives for managing transaction information. ThreadLocal adds
|
||||
// an unnecessary restriction that each request has to be processed by one thread synchronously.
|
||||
private final ThreadLocal<TransactionInfo> transactionInfo =
|
||||
ThreadLocal.withInitial(TransactionInfo::new);
|
||||
|
||||
@Inject
|
||||
JpaTransactionManagerImpl(@AppEngineEmf EntityManagerFactory emf, Clock clock) {
|
||||
this.emf = emf;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityManager getEntityManager() {
|
||||
if (transactionInfo.get().entityManager == null) {
|
||||
throw new PersistenceException(
|
||||
"No EntityManager has been initialized. getEntityManager() must be invoked in the scope"
|
||||
+ " of a transaction");
|
||||
}
|
||||
return transactionInfo.get().entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inTransaction() {
|
||||
return transactionInfo.get().inTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertInTransaction() {
|
||||
if (!inTransaction()) {
|
||||
throw new PersistenceException("Not in a transaction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Work<T> work) {
|
||||
// TODO(shicong): Investigate removing transactNew functionality after migration as it may
|
||||
// be same as this one.
|
||||
if (inTransaction()) {
|
||||
return work.run();
|
||||
}
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
txnInfo.entityManager = emf.createEntityManager();
|
||||
EntityTransaction txn = txnInfo.entityManager.getTransaction();
|
||||
try {
|
||||
txn.begin();
|
||||
txnInfo.inTransaction = true;
|
||||
txnInfo.transactionTime = clock.nowUtc();
|
||||
T result = work.run();
|
||||
txn.commit();
|
||||
return result;
|
||||
} catch (Throwable transactionException) {
|
||||
String rollbackMessage;
|
||||
try {
|
||||
txn.rollback();
|
||||
rollbackMessage = "transaction rolled back";
|
||||
} catch (Throwable rollbackException) {
|
||||
logger.atSevere().withCause(rollbackException).log("Rollback failed, suppressing error");
|
||||
rollbackMessage = "transaction rollback failed";
|
||||
}
|
||||
throw new PersistenceException(
|
||||
"Error during transaction, " + rollbackMessage, transactionException);
|
||||
} finally {
|
||||
txnInfo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work) {
|
||||
transact(
|
||||
() -> {
|
||||
work.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNew(Work<T> work) {
|
||||
// TODO(shicong): Implements the functionality to start a new transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNew(Runnable work) {
|
||||
// TODO(shicong): Implements the functionality to start a new transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNewReadOnly(Work<T> work) {
|
||||
// TODO(shicong): Implements read only transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNewReadOnly(Runnable work) {
|
||||
// TODO(shicong): Implements read only transaction.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T doTransactionless(Work<T> work) {
|
||||
// TODO(shicong): Implements doTransactionless.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime getTransactionTime() {
|
||||
assertInTransaction();
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
if (txnInfo.transactionTime == null) {
|
||||
throw new PersistenceException("In a transaction but transactionTime is null");
|
||||
}
|
||||
return txnInfo.transactionTime;
|
||||
}
|
||||
|
||||
private static class TransactionInfo {
|
||||
EntityManager entityManager;
|
||||
boolean inTransaction = false;
|
||||
DateTime transactionTime;
|
||||
|
||||
private void clear() {
|
||||
inTransaction = false;
|
||||
transactionTime = null;
|
||||
if (entityManager != null) {
|
||||
// Close this EntityManager just let the connection pool be able to reuse it, it doesn't
|
||||
// close the underlying database connection.
|
||||
entityManager.close();
|
||||
entityManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -14,21 +14,29 @@
|
||||
|
||||
package google.registry.model.transaction;
|
||||
|
||||
import com.google.appengine.api.utils.SystemProperty;
|
||||
import com.google.appengine.api.utils.SystemProperty.Environment.Value;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.model.ofy.DatastoreTransactionManager;
|
||||
import google.registry.persistence.DaggerPersistenceComponent;
|
||||
import google.registry.persistence.PersistenceComponent;
|
||||
|
||||
/** Factory class to create {@link TransactionManager} instance. */
|
||||
// TODO: Rename this to PersistenceFactory and move to persistence package.
|
||||
public class TransactionManagerFactory {
|
||||
|
||||
private static final TransactionManager TM = createTransactionManager();
|
||||
|
||||
@VisibleForTesting static PersistenceComponent component = DaggerPersistenceComponent.create();
|
||||
@VisibleForTesting static JpaTransactionManager jpaTm = createJpaTransactionManager();
|
||||
|
||||
private TransactionManagerFactory() {}
|
||||
|
||||
private static JpaTransactionManager createJpaTransactionManager() {
|
||||
if (SystemProperty.environment.value() == Value.Production) {
|
||||
return DaggerPersistenceComponent.create().jpaTransactionManager();
|
||||
} else {
|
||||
return DummyJpaTransactionManager.create();
|
||||
}
|
||||
}
|
||||
|
||||
private static TransactionManager createTransactionManager() {
|
||||
// TODO: Determine how to provision TransactionManager after the dual-write. During the
|
||||
// dual-write transitional phase, we need the TransactionManager for both Datastore and Cloud
|
||||
@@ -48,6 +56,6 @@ public class TransactionManagerFactory {
|
||||
// 1. App Engine
|
||||
// 2. Local JVM used by nomulus tool
|
||||
// 3. Unit test
|
||||
return component.jpaTransactionManager();
|
||||
return jpaTm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import dagger.Component;
|
||||
import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.keyring.kms.KmsModule;
|
||||
import google.registry.model.transaction.JpaTransactionManager;
|
||||
import google.registry.model.transaction.JpaTransactionManagerImpl;
|
||||
import google.registry.util.UtilsModule;
|
||||
import javax.inject.Singleton;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
@@ -34,5 +34,5 @@ import javax.persistence.EntityManagerFactory;
|
||||
UtilsModule.class
|
||||
})
|
||||
public interface PersistenceComponent {
|
||||
JpaTransactionManager jpaTransactionManager();
|
||||
JpaTransactionManagerImpl jpaTransactionManager();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user