1
0
mirror of https://github.com/google/nomulus synced 2026-04-21 16:50:44 +00:00

Make read-only transactions more performant (#2233)

Since the replica SQL instance is read-only, any transaction performed
on it should be explicitly read-only, which would allow PostgreSQL to
optimize away (some) use of predicate locks.

Also changed the EPP cache to read from the replica. The foreign key
cache already behaves this way.

See: https://www.postgresql.org/docs/current/transaction-iso.html
This commit is contained in:
Lai Jiang
2023-11-29 15:55:50 -05:00
committed by GitHub
parent 853e571d01
commit 028e5cc958
7 changed files with 62 additions and 300 deletions

View File

@@ -219,10 +219,10 @@ public abstract class JpaTransactionManagerExtension
recreateSchema();
}
JpaTransactionManagerImpl txnManager = new JpaTransactionManagerImpl(emf, clock);
JpaTransactionManagerImpl readOnlyTxnManager = new JpaTransactionManagerImpl(emf, clock, true);
cachedTm = TransactionManagerFactory.tm();
TransactionManagerFactory.setJpaTm(Suppliers.ofInstance(txnManager));
TransactionManagerFactory.setReplicaJpaTm(
Suppliers.ofInstance(new ReplicaSimulatingJpaTransactionManager(txnManager)));
TransactionManagerFactory.setReplicaJpaTm(Suppliers.ofInstance(readOnlyTxnManager));
// Reset SQL Sequence based id allocation so that ids are deterministic in tests.
TransactionManagerFactory.tm()
.transact(

View File

@@ -20,6 +20,7 @@ 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_UNCOMMITTED;
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertDetachedFromEntityManager;
import static google.registry.testing.DatabaseHelper.existsInDb;
@@ -107,6 +108,44 @@ class JpaTransactionManagerImplTest {
assertCompanyExist("Bar");
}
@Test
void transact_replica_failureOnWrite() {
assertPersonEmpty();
assertCompanyEmpty();
DatabaseException thrown =
assertThrows(
DatabaseException.class,
() ->
replicaTm()
.transact(
() -> {
insertPerson(10);
}));
assertThat(thrown)
.hasMessageThat()
.contains("cannot execute INSERT in a read-only transaction");
}
@Test
void transact_replica_successOnRead() {
assertPersonEmpty();
assertCompanyEmpty();
tm().transact(
() -> {
insertPerson(10);
});
replicaTm()
.transact(
() -> {
EntityManager em = replicaTm().getEntityManager();
Integer maybeAge =
(Integer)
em.createNativeQuery("SELECT age FROM Person WHERE age = 10")
.getSingleResult();
assertThat(maybeAge).isEqualTo(10);
});
}
@Test
void transact_setIsolationLevel() {
// If not specified, run at the default isolation level.

View File

@@ -1,289 +0,0 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.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.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.model.ImmutableObject;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import org.joda.time.DateTime;
/**
* A {@link JpaTransactionManager} that simulates a read-only replica SQL instance.
*
* <p>We accomplish this by delegating all calls to the standard transaction manager except for
* calls that start transactions. For these, we create a transaction like normal but set it to READ
* ONLY mode before doing any work. This is similar to how the read-only Postgres replica works; it
* treats all transactions as read-only transactions.
*/
public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionManager {
private final JpaTransactionManager delegate;
public ReplicaSimulatingJpaTransactionManager(JpaTransactionManager delegate) {
this.delegate = delegate;
}
@Override
public void teardown() {
delegate.teardown();
}
@Override
public TransactionIsolationLevel getDefaultTransactionIsolationLevel() {
return delegate.getDefaultTransactionIsolationLevel();
}
@Override
public TransactionIsolationLevel getCurrentTransactionIsolationLevel() {
return delegate.getCurrentTransactionIsolationLevel();
}
@Override
public EntityManager getStandaloneEntityManager() {
return delegate.getStandaloneEntityManager();
}
@Override
public EntityManager getEntityManager() {
return delegate.getEntityManager();
}
@Override
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
return delegate.query(sqlString, resultClass);
}
@Override
public <T> TypedQuery<T> criteriaQuery(CriteriaQuery<T> criteriaQuery) {
return delegate.criteriaQuery(criteriaQuery);
}
@Override
public Query query(String sqlString) {
return delegate.query(sqlString);
}
@Override
public boolean inTransaction() {
return delegate.inTransaction();
}
@Override
public void assertInTransaction() {
delegate.assertInTransaction();
}
@Override
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
if (inTransaction()) {
try {
return work.call();
} catch (Exception e) {
throwIfSqlException(e);
throwIfUnchecked(e);
throw new RuntimeException(e);
}
}
return delegate.transact(
() -> {
delegate
.getEntityManager()
.createNativeQuery("SET TRANSACTION READ ONLY")
.executeUpdate();
return work.call();
},
isolationLevel);
}
@Override
public <T> T reTransact(Callable<T> work) {
return transact(work);
}
@Override
public <T> T transact(Callable<T> work) {
return transact(work, null);
}
@Override
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
transact(
() -> {
work.run();
return null;
},
isolationLevel);
}
@Override
public void reTransact(ThrowingRunnable work) {
transact(work);
}
@Override
public void transact(ThrowingRunnable work) {
transact(work, null);
}
@Override
public DateTime getTransactionTime() {
return delegate.getTransactionTime();
}
@Override
public void insert(Object entity) {
delegate.insert(entity);
}
@Override
public void insertAll(ImmutableCollection<?> entities) {
delegate.insertAll(entities);
}
@Override
public void insertAll(ImmutableObject... entities) {
delegate.insertAll(entities);
}
@Override
public void put(Object entity) {
delegate.put(entity);
}
@Override
public void putAll(ImmutableObject... entities) {
delegate.putAll(entities);
}
@Override
public void putAll(ImmutableCollection<?> entities) {
delegate.putAll(entities);
}
@Override
public void update(Object entity) {
delegate.update(entity);
}
@Override
public void updateAll(ImmutableCollection<?> entities) {
delegate.updateAll(entities);
}
@Override
public void updateAll(ImmutableObject... entities) {
delegate.updateAll(entities);
}
@Override
public <T> boolean exists(VKey<T> key) {
return delegate.exists(key);
}
@Override
public boolean exists(Object entity) {
return delegate.exists(entity);
}
@Override
public <T> Optional<T> loadByKeyIfPresent(VKey<T> key) {
return delegate.loadByKeyIfPresent(key);
}
@Override
public <T> ImmutableMap<VKey<? extends T>, T> loadByKeysIfPresent(
Iterable<? extends VKey<? extends T>> vKeys) {
return delegate.loadByKeysIfPresent(vKeys);
}
@Override
public <T> ImmutableList<T> loadByEntitiesIfPresent(Iterable<T> entities) {
return delegate.loadByEntitiesIfPresent(entities);
}
@Override
public <T> T loadByKey(VKey<T> key) {
return delegate.loadByKey(key);
}
@Override
public <T> ImmutableMap<VKey<? extends T>, T> loadByKeys(
Iterable<? extends VKey<? extends T>> vKeys) {
return delegate.loadByKeys(vKeys);
}
@Override
public <T> T loadByEntity(T entity) {
return delegate.loadByEntity(entity);
}
@Override
public <T> ImmutableList<T> loadByEntities(Iterable<T> entities) {
return delegate.loadByEntities(entities);
}
@Override
public <T> ImmutableList<T> loadAllOf(Class<T> clazz) {
return delegate.loadAllOf(clazz);
}
@Override
public <T> Stream<T> loadAllOfStream(Class<T> clazz) {
return delegate.loadAllOfStream(clazz);
}
@Override
public <T> Optional<T> loadSingleton(Class<T> clazz) {
return delegate.loadSingleton(clazz);
}
@Override
public void delete(VKey<?> key) {
delegate.delete(key);
}
@Override
public void delete(Iterable<? extends VKey<?>> vKeys) {
delegate.delete(vKeys);
}
@Override
public <T> T delete(T entity) {
return delegate.delete(entity);
}
@Override
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
return delegate.createQueryComposer(entity);
}
@Override
public <T> void assertDelete(VKey<T> key) {
delegate.assertDelete(key);
}
}