mirror of
https://github.com/google/nomulus
synced 2026-09-20 23:14:44 +00:00
Add a DelegatingReplicaJpaTransactionManager to handle multiple replicas (#3005)
This will allow us to spread the load across multiple Postgres replica instances which should help with latency and stability.
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package google.registry.keyring;
|
package google.registry.keyring;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
@@ -21,7 +22,6 @@ import google.registry.config.RegistryConfig.Config;
|
|||||||
import google.registry.keyring.api.Keyring;
|
import google.registry.keyring.api.Keyring;
|
||||||
import google.registry.keyring.secretmanager.SecretManagerKeyring;
|
import google.registry.keyring.secretmanager.SecretManagerKeyring;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/** Dagger module for {@link Keyring} */
|
/** Dagger module for {@link Keyring} */
|
||||||
@Module
|
@Module
|
||||||
@@ -38,9 +38,10 @@ public abstract class KeyringModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Config("cloudSqlReplicaInstanceConnectionName")
|
@Config("cloudSqlReplicaInstanceConnectionNames")
|
||||||
public static Optional<String> provideCloudSqlReplicaInstanceConnectionName(Keyring keyring) {
|
public static ImmutableList<String> provideCloudSqlReplicaInstanceConnectionNames(
|
||||||
return Optional.ofNullable(keyring.getSqlReplicaConnectionName());
|
Keyring keyring) {
|
||||||
|
return ImmutableList.copyOf(keyring.getSqlReplicaConnectionNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package google.registry.keyring.api;
|
package google.registry.keyring.api;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import javax.annotation.concurrent.ThreadSafe;
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
import org.bouncycastle.openpgp.PGPKeyPair;
|
||||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||||
@@ -151,9 +152,17 @@ public interface Keyring extends AutoCloseable {
|
|||||||
/** Returns the Cloud SQL connection name of the primary database instance. */
|
/** Returns the Cloud SQL connection name of the primary database instance. */
|
||||||
String getSqlPrimaryConnectionName();
|
String getSqlPrimaryConnectionName();
|
||||||
|
|
||||||
/** Returns the Cloud SQL connection name of the replica database instance. */
|
/**
|
||||||
|
* Returns the Cloud SQL connection name of the replica database instance.
|
||||||
|
*
|
||||||
|
* <p>Note: It is likely a better idea to use multiple replicas and {@link
|
||||||
|
* #getSqlReplicaConnectionNames()} instead.
|
||||||
|
*/
|
||||||
String getSqlReplicaConnectionName();
|
String getSqlReplicaConnectionName();
|
||||||
|
|
||||||
|
/** Returns the Cloud SQL connection names of the replica database instances. */
|
||||||
|
ImmutableList<String> getSqlReplicaConnectionNames();
|
||||||
|
|
||||||
// Don't throw so try-with-resources works better.
|
// Don't throw so try-with-resources works better.
|
||||||
@Override
|
@Override
|
||||||
void close();
|
void close();
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ package google.registry.keyring.secretmanager;
|
|||||||
import static com.google.common.base.CaseFormat.LOWER_HYPHEN;
|
import static com.google.common.base.CaseFormat.LOWER_HYPHEN;
|
||||||
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
|
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
|
||||||
|
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import google.registry.keyring.api.KeySerializer;
|
import google.registry.keyring.api.KeySerializer;
|
||||||
import google.registry.keyring.api.Keyring;
|
import google.registry.keyring.api.Keyring;
|
||||||
import google.registry.keyring.api.KeyringException;
|
import google.registry.keyring.api.KeyringException;
|
||||||
@@ -66,7 +68,8 @@ public class SecretManagerKeyring implements Keyring {
|
|||||||
RDE_SSH_CLIENT_PUBLIC_STRING,
|
RDE_SSH_CLIENT_PUBLIC_STRING,
|
||||||
SAFE_BROWSING_API_KEY,
|
SAFE_BROWSING_API_KEY,
|
||||||
SQL_PRIMARY_CONN_NAME,
|
SQL_PRIMARY_CONN_NAME,
|
||||||
SQL_REPLICA_CONN_NAME;
|
SQL_REPLICA_CONN_NAME,
|
||||||
|
SQL_REPLICA_CONN_NAMES;
|
||||||
|
|
||||||
String getLabel() {
|
String getLabel() {
|
||||||
return UPPER_UNDERSCORE.to(LOWER_HYPHEN, name());
|
return UPPER_UNDERSCORE.to(LOWER_HYPHEN, name());
|
||||||
@@ -157,7 +160,25 @@ public class SecretManagerKeyring implements Keyring {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSqlReplicaConnectionName() {
|
public String getSqlReplicaConnectionName() {
|
||||||
return getString(StringKeyLabel.SQL_REPLICA_CONN_NAME);
|
try {
|
||||||
|
return getString(StringKeyLabel.SQL_REPLICA_CONN_NAME);
|
||||||
|
} catch (KeyringException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImmutableList<String> getSqlReplicaConnectionNames() {
|
||||||
|
try {
|
||||||
|
String names = getString(StringKeyLabel.SQL_REPLICA_CONN_NAMES);
|
||||||
|
return ImmutableList.copyOf(
|
||||||
|
Splitter.on('\n').trimResults().omitEmptyStrings().splitToList(names));
|
||||||
|
} catch (KeyringException e) {
|
||||||
|
String replicaConnectionName = getSqlReplicaConnectionName();
|
||||||
|
return replicaConnectionName == null
|
||||||
|
? ImmutableList.of()
|
||||||
|
: ImmutableList.of(replicaConnectionName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** No persistent resources are maintained for this Keyring implementation. */
|
/** No persistent resources are maintained for this Keyring implementation. */
|
||||||
|
|||||||
+5
@@ -34,6 +34,7 @@ import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringK
|
|||||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SAFE_BROWSING_API_KEY;
|
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SAFE_BROWSING_API_KEY;
|
||||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SQL_PRIMARY_CONN_NAME;
|
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SQL_PRIMARY_CONN_NAME;
|
||||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SQL_REPLICA_CONN_NAME;
|
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SQL_REPLICA_CONN_NAME;
|
||||||
|
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.SQL_REPLICA_CONN_NAMES;
|
||||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||||
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
@@ -134,6 +135,10 @@ public final class SecretManagerKeyringUpdater {
|
|||||||
return setString(name, SQL_REPLICA_CONN_NAME);
|
return setString(name, SQL_REPLICA_CONN_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SecretManagerKeyringUpdater setSqlReplicaConnectionNames(String names) {
|
||||||
|
return setString(names, SQL_REPLICA_CONN_NAMES);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists the secrets in the Secret Manager.
|
* Persists the secrets in the Secret Manager.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package google.registry.persistence;
|
package google.registry.persistence;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
import static google.registry.config.RegistryConfig.getHibernateConnectionIsolation;
|
import static google.registry.config.RegistryConfig.getHibernateConnectionIsolation;
|
||||||
import static google.registry.config.RegistryConfig.getHibernateHikariConnectionTimeout;
|
import static google.registry.config.RegistryConfig.getHibernateHikariConnectionTimeout;
|
||||||
import static google.registry.config.RegistryConfig.getHibernateHikariIdleTimeout;
|
import static google.registry.config.RegistryConfig.getHibernateHikariIdleTimeout;
|
||||||
@@ -28,6 +29,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
|||||||
import com.google.auth.oauth2.GoogleCredentials;
|
import com.google.auth.oauth2.GoogleCredentials;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import dagger.BindsOptionalOf;
|
import dagger.BindsOptionalOf;
|
||||||
@@ -36,6 +38,7 @@ import dagger.Module;
|
|||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
|
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
|
||||||
|
import google.registry.persistence.transaction.DelegatingReplicaJpaTransactionManager;
|
||||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||||
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
|
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
|
||||||
import google.registry.persistence.transaction.TransactionManager;
|
import google.registry.persistence.transaction.TransactionManager;
|
||||||
@@ -59,6 +62,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.hibernate.cfg.Environment;
|
import org.hibernate.cfg.Environment;
|
||||||
@@ -264,16 +268,13 @@ public abstract class PersistenceModule {
|
|||||||
static JpaTransactionManager provideReadOnlyReplicaJpaTm(
|
static JpaTransactionManager provideReadOnlyReplicaJpaTm(
|
||||||
SqlCredentialStore credentialStore,
|
SqlCredentialStore credentialStore,
|
||||||
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
||||||
@Config("cloudSqlReplicaInstanceConnectionName")
|
@Config("cloudSqlReplicaInstanceConnectionNames")
|
||||||
Optional<String> replicaInstanceConnectionName,
|
ImmutableList<String> replicaInstanceConnectionNames,
|
||||||
Clock clock) {
|
Clock clock,
|
||||||
|
Random random) {
|
||||||
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
|
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
|
||||||
setSqlCredential(credentialStore, new RobotUser(RobotId.NOMULUS), overrides);
|
setSqlCredential(credentialStore, new RobotUser(RobotId.NOMULUS), overrides);
|
||||||
replicaInstanceConnectionName.ifPresent(
|
return createReplicaJpaTm(overrides, replicaInstanceConnectionNames, clock, random);
|
||||||
name -> overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, name));
|
|
||||||
overrides.put(
|
|
||||||
Environment.ISOLATION, TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ.name());
|
|
||||||
return new JpaTransactionManagerImpl(create(overrides), clock, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -281,15 +282,34 @@ public abstract class PersistenceModule {
|
|||||||
@BeamReadOnlyReplicaJpaTm
|
@BeamReadOnlyReplicaJpaTm
|
||||||
static JpaTransactionManager provideBeamReadOnlyReplicaJpaTm(
|
static JpaTransactionManager provideBeamReadOnlyReplicaJpaTm(
|
||||||
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs,
|
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs,
|
||||||
@Config("cloudSqlReplicaInstanceConnectionName")
|
@Config("cloudSqlReplicaInstanceConnectionNames")
|
||||||
Optional<String> replicaInstanceConnectionName,
|
ImmutableList<String> replicaInstanceConnectionNames,
|
||||||
Clock clock) {
|
Clock clock,
|
||||||
|
Random random) {
|
||||||
HashMap<String, String> overrides = Maps.newHashMap(beamCloudSqlConfigs);
|
HashMap<String, String> overrides = Maps.newHashMap(beamCloudSqlConfigs);
|
||||||
replicaInstanceConnectionName.ifPresent(
|
return createReplicaJpaTm(overrides, replicaInstanceConnectionNames, clock, random);
|
||||||
name -> overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, name));
|
}
|
||||||
overrides.put(
|
|
||||||
|
private static JpaTransactionManager createReplicaJpaTm(
|
||||||
|
Map<String, String> baseOverrides,
|
||||||
|
ImmutableList<String> replicaInstanceConnectionNames,
|
||||||
|
Clock clock,
|
||||||
|
Random random) {
|
||||||
|
baseOverrides.put(
|
||||||
Environment.ISOLATION, TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ.name());
|
Environment.ISOLATION, TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ.name());
|
||||||
return new JpaTransactionManagerImpl(create(overrides), clock, true);
|
if (replicaInstanceConnectionNames.isEmpty()) {
|
||||||
|
return new JpaTransactionManagerImpl(create(baseOverrides), clock, true);
|
||||||
|
}
|
||||||
|
ImmutableList<JpaTransactionManager> replicas =
|
||||||
|
replicaInstanceConnectionNames.stream()
|
||||||
|
.map(
|
||||||
|
name -> {
|
||||||
|
HashMap<String, String> overrides = Maps.newHashMap(baseOverrides);
|
||||||
|
overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, name);
|
||||||
|
return new JpaTransactionManagerImpl(create(overrides), clock, true);
|
||||||
|
})
|
||||||
|
.collect(toImmutableList());
|
||||||
|
return new DelegatingReplicaJpaTransactionManager(replicas, random);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs the {@link EntityManagerFactory} instance. */
|
/** Constructs the {@link EntityManagerFactory} instance. */
|
||||||
|
|||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
// Copyright 2026 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.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
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 jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.Query;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.metamodel.Metamodel;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link JpaTransactionManager} that load-balances across multiple read-only replicas.
|
||||||
|
*
|
||||||
|
* <p>For each top-level transaction, one replica is chosen and used for the duration of the
|
||||||
|
* transaction. For non-transactional methods, a replica is chosen for each call.
|
||||||
|
*/
|
||||||
|
public class DelegatingReplicaJpaTransactionManager implements JpaTransactionManager {
|
||||||
|
|
||||||
|
private final ImmutableList<JpaTransactionManager> replicas;
|
||||||
|
private final Random random;
|
||||||
|
private static final AtomicLong nextId = new AtomicLong(1);
|
||||||
|
|
||||||
|
private static final ThreadLocal<JpaTransactionManager> activeReplica = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public DelegatingReplicaJpaTransactionManager(
|
||||||
|
ImmutableList<JpaTransactionManager> replicas, Random random) {
|
||||||
|
checkArgument(!replicas.isEmpty(), "At least one replica must be provided");
|
||||||
|
this.replicas = replicas;
|
||||||
|
this.random = random;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JpaTransactionManager getReplica() {
|
||||||
|
JpaTransactionManager replica = activeReplica.get();
|
||||||
|
if (replica != null) {
|
||||||
|
return replica;
|
||||||
|
}
|
||||||
|
return getRandomReplica();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T runMaybeAssigningReplica(Function<JpaTransactionManager, T> work) {
|
||||||
|
JpaTransactionManager existing = activeReplica.get();
|
||||||
|
if (existing != null) {
|
||||||
|
return work.apply(existing);
|
||||||
|
}
|
||||||
|
JpaTransactionManager replica = getRandomReplica();
|
||||||
|
activeReplica.set(replica);
|
||||||
|
try {
|
||||||
|
return work.apply(replica);
|
||||||
|
} finally {
|
||||||
|
activeReplica.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JpaTransactionManager getRandomReplica() {
|
||||||
|
return replicas.get(random.nextInt(replicas.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inTransaction() {
|
||||||
|
var replica = activeReplica.get();
|
||||||
|
return replica != null && replica.inTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void assertInTransaction() {
|
||||||
|
JpaTransactionManager replica = activeReplica.get();
|
||||||
|
if (replica == null) {
|
||||||
|
throw new IllegalStateException("Not in a transaction");
|
||||||
|
}
|
||||||
|
replica.assertInTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long allocateId() {
|
||||||
|
return nextId.getAndIncrement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transact(Callable<T> work) {
|
||||||
|
return transact(null, work, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transact(TransactionIsolationLevel isolationLevel, Callable<T> work) {
|
||||||
|
return transact(isolationLevel, work, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transactNoRetry(Callable<T> work) {
|
||||||
|
return transactNoRetry(null, work, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transactNoRetry(TransactionIsolationLevel isolationLevel, Callable<T> work) {
|
||||||
|
return transactNoRetry(isolationLevel, work, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T reTransact(Callable<T> work) {
|
||||||
|
return runMaybeAssigningReplica(replica -> replica.reTransact(work));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transact(ThrowingRunnable work) {
|
||||||
|
transact(
|
||||||
|
() -> {
|
||||||
|
work.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transact(TransactionIsolationLevel isolationLevel, ThrowingRunnable work) {
|
||||||
|
transact(
|
||||||
|
isolationLevel,
|
||||||
|
() -> {
|
||||||
|
work.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reTransact(ThrowingRunnable work) {
|
||||||
|
reTransact(
|
||||||
|
() -> {
|
||||||
|
work.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DateTime getTransactionTime() {
|
||||||
|
return getReplica().getTransactionTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getTxTime() {
|
||||||
|
return getReplica().getTxTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insert(Object entity) {
|
||||||
|
getReplica().insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertAll(ImmutableCollection<?> entities) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertAll(ImmutableObject... entities) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(Object entity) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putAll(ImmutableObject... entities) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putAll(ImmutableCollection<?> entities) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(Object entity) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAll(ImmutableCollection<?> entities) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAll(ImmutableObject... entities) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists(Object entity) {
|
||||||
|
return getReplica().exists(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> boolean exists(VKey<T> key) {
|
||||||
|
return getReplica().exists(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Optional<T> loadByKeyIfPresent(VKey<T> key) {
|
||||||
|
return getReplica().loadByKeyIfPresent(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ImmutableMap<VKey<? extends T>, T> loadByKeysIfPresent(
|
||||||
|
Iterable<? extends VKey<? extends T>> keys) {
|
||||||
|
return getReplica().loadByKeysIfPresent(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ImmutableList<T> loadByEntitiesIfPresent(Iterable<T> entities) {
|
||||||
|
return getReplica().loadByEntitiesIfPresent(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T loadByKey(VKey<T> key) {
|
||||||
|
return getReplica().loadByKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ImmutableMap<VKey<? extends T>, T> loadByKeys(
|
||||||
|
Iterable<? extends VKey<? extends T>> keys) {
|
||||||
|
return getReplica().loadByKeys(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T loadByEntity(T entity) {
|
||||||
|
return getReplica().loadByEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ImmutableList<T> loadByEntities(Iterable<T> entities) {
|
||||||
|
return getReplica().loadByEntities(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ImmutableList<T> loadAllOf(Class<T> clazz) {
|
||||||
|
return getReplica().loadAllOf(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Stream<T> loadAllOfStream(Class<T> clazz) {
|
||||||
|
return getReplica().loadAllOfStream(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Optional<T> loadSingleton(Class<T> clazz) {
|
||||||
|
return getReplica().loadSingleton(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(VKey<?> key) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Iterable<? extends VKey<?>> keys) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T delete(T entity) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
|
||||||
|
return getReplica().createQueryComposer(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityManager getStandaloneEntityManager() {
|
||||||
|
return getReplica().getStandaloneEntityManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Metamodel getMetaModel() {
|
||||||
|
return getReplica().getMetaModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityManager getEntityManager() {
|
||||||
|
return getReplica().getEntityManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
|
||||||
|
return getReplica().query(sqlString, resultClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> TypedQuery<T> criteriaQuery(CriteriaQuery<T> criteriaQuery) {
|
||||||
|
return getReplica().criteriaQuery(criteriaQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query query(String sqlString) {
|
||||||
|
return getReplica().query(sqlString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void assertDelete(VKey<T> key) {
|
||||||
|
throw new UnsupportedOperationException("This is a replica database");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void teardown() {
|
||||||
|
for (JpaTransactionManager replica : replicas) {
|
||||||
|
replica.teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionIsolationLevel getDefaultTransactionIsolationLevel() {
|
||||||
|
return replicas.get(0).getDefaultTransactionIsolationLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionIsolationLevel getCurrentTransactionIsolationLevel() {
|
||||||
|
return getReplica().getCurrentTransactionIsolationLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transact(
|
||||||
|
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements) {
|
||||||
|
return runMaybeAssigningReplica(
|
||||||
|
replica -> replica.transact(isolationLevel, work, logSqlStatements));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transactNoRetry(
|
||||||
|
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements) {
|
||||||
|
return runMaybeAssigningReplica(
|
||||||
|
replica -> replica.transactNoRetry(isolationLevel, work, logSqlStatements));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package google.registry.tools;
|
package google.registry.tools;
|
||||||
|
|
||||||
|
|
||||||
import com.beust.jcommander.Parameter;
|
import com.beust.jcommander.Parameter;
|
||||||
import com.beust.jcommander.Parameters;
|
import com.beust.jcommander.Parameters;
|
||||||
import google.registry.keyring.api.KeySerializer;
|
import google.registry.keyring.api.KeySerializer;
|
||||||
@@ -95,6 +96,10 @@ final class GetKeyringSecretCommand implements Command {
|
|||||||
out.write(KeySerializer.serializeString(keyring.getSqlPrimaryConnectionName()));
|
out.write(KeySerializer.serializeString(keyring.getSqlPrimaryConnectionName()));
|
||||||
case SQL_REPLICA_CONN_NAME ->
|
case SQL_REPLICA_CONN_NAME ->
|
||||||
out.write(KeySerializer.serializeString(keyring.getSqlReplicaConnectionName()));
|
out.write(KeySerializer.serializeString(keyring.getSqlReplicaConnectionName()));
|
||||||
|
case SQL_REPLICA_CONN_NAMES ->
|
||||||
|
out.write(
|
||||||
|
KeySerializer.serializeString(
|
||||||
|
String.join("\n", keyring.getSqlReplicaConnectionNames())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ final class UpdateKeyringSecretCommand implements Command {
|
|||||||
secretManagerKeyringUpdater.setSqlPrimaryConnectionName(deserializeString(input));
|
secretManagerKeyringUpdater.setSqlPrimaryConnectionName(deserializeString(input));
|
||||||
case SQL_REPLICA_CONN_NAME ->
|
case SQL_REPLICA_CONN_NAME ->
|
||||||
secretManagerKeyringUpdater.setSqlReplicaConnectionName(deserializeString(input));
|
secretManagerKeyringUpdater.setSqlReplicaConnectionName(deserializeString(input));
|
||||||
|
case SQL_REPLICA_CONN_NAMES ->
|
||||||
|
secretManagerKeyringUpdater.setSqlReplicaConnectionNames(deserializeString(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
secretManagerKeyringUpdater.update();
|
secretManagerKeyringUpdater.update();
|
||||||
|
|||||||
@@ -38,5 +38,6 @@ public enum KeyringKeyName {
|
|||||||
RDE_STAGING_PUBLIC_KEY,
|
RDE_STAGING_PUBLIC_KEY,
|
||||||
SAFE_BROWSING_API_KEY,
|
SAFE_BROWSING_API_KEY,
|
||||||
SQL_PRIMARY_CONN_NAME,
|
SQL_PRIMARY_CONN_NAME,
|
||||||
SQL_REPLICA_CONN_NAME
|
SQL_REPLICA_CONN_NAME,
|
||||||
|
SQL_REPLICA_CONN_NAMES
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -120,6 +120,23 @@ public class SecretManagerKeyringUpdaterTest {
|
|||||||
verifyPersistedSecret("sql-replica-conn-name", name);
|
verifyPersistedSecret("sql-replica-conn-name", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sqlReplicaConnectionNames() {
|
||||||
|
String names = "name1\nname2";
|
||||||
|
updater.setSqlReplicaConnectionNames(names).update();
|
||||||
|
|
||||||
|
assertThat(keyring.getSqlReplicaConnectionNames()).containsExactly("name1", "name2").inOrder();
|
||||||
|
verifyPersistedSecret("sql-replica-conn-names", names);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sqlReplicaConnectionNames_fallback() {
|
||||||
|
String name = "name";
|
||||||
|
updater.setSqlReplicaConnectionName(name).update();
|
||||||
|
|
||||||
|
assertThat(keyring.getSqlReplicaConnectionNames()).containsExactly(name);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void marksdbDnlLoginAndPassword() {
|
void marksdbDnlLoginAndPassword() {
|
||||||
String secret = "marksdbDnlLoginAndPassword";
|
String secret = "marksdbDnlLoginAndPassword";
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright 2026 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.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Tests for {@link DelegatingReplicaJpaTransactionManager}. */
|
||||||
|
public class DelegatingReplicaJpaTransactionManagerTest {
|
||||||
|
|
||||||
|
private JpaTransactionManager replica1 = mock(JpaTransactionManager.class);
|
||||||
|
private JpaTransactionManager replica2 = mock(JpaTransactionManager.class);
|
||||||
|
private Random random = mock(Random.class);
|
||||||
|
private DelegatingReplicaJpaTransactionManager transactionManager =
|
||||||
|
new DelegatingReplicaJpaTransactionManager(ImmutableList.of(replica1, replica2), random);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetReplica_rotates() {
|
||||||
|
when(random.nextInt(2)).thenReturn(0).thenReturn(1);
|
||||||
|
|
||||||
|
transactionManager.loadByKey(null);
|
||||||
|
verify(replica1).loadByKey(null);
|
||||||
|
|
||||||
|
transactionManager.loadByKey(null);
|
||||||
|
verify(replica2).loadByKey(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTransact_usesSameReplica() throws Exception {
|
||||||
|
when(random.nextInt(2)).thenReturn(1);
|
||||||
|
when(replica2.transact(any(), any(), anyBoolean()))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Callable<Object> work = invocation.getArgument(1);
|
||||||
|
return work.call();
|
||||||
|
});
|
||||||
|
|
||||||
|
transactionManager.transact(
|
||||||
|
() -> {
|
||||||
|
transactionManager.loadByKey(null);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(replica2).transact(any(), any(), anyBoolean());
|
||||||
|
// The loadByKey inside the transact should also use replica2.
|
||||||
|
verify(replica2).loadByKey(null);
|
||||||
|
// And it should NOT have called random again for the nested call.
|
||||||
|
verify(random).nextInt(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTransactNoRetry_usesSameReplica() throws Exception {
|
||||||
|
when(random.nextInt(2)).thenReturn(0);
|
||||||
|
when(replica1.transactNoRetry(any(), any(), anyBoolean()))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Callable<Object> work = invocation.getArgument(1);
|
||||||
|
return work.call();
|
||||||
|
});
|
||||||
|
|
||||||
|
transactionManager.transactNoRetry(
|
||||||
|
() -> {
|
||||||
|
transactionManager.loadByKey(null);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(replica1).transactNoRetry(any(), any(), anyBoolean());
|
||||||
|
verify(replica1).loadByKey(null);
|
||||||
|
verify(random).nextInt(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReTransactNoRetry_usesSameReplica() throws Exception {
|
||||||
|
when(random.nextInt(2)).thenReturn(0);
|
||||||
|
when(replica1.reTransact(any(Callable.class)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Callable<Object> work = invocation.getArgument(0);
|
||||||
|
return work.call();
|
||||||
|
});
|
||||||
|
|
||||||
|
transactionManager.reTransact(
|
||||||
|
() -> {
|
||||||
|
transactionManager.loadByKey(null);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(replica1).reTransact(any(Callable.class));
|
||||||
|
verify(replica1).loadByKey(null);
|
||||||
|
verify(random).nextInt(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInTransaction() {
|
||||||
|
when(random.nextInt(2)).thenReturn(0);
|
||||||
|
when(replica1.inTransaction()).thenReturn(true);
|
||||||
|
|
||||||
|
// Not in transaction yet
|
||||||
|
assertThat(transactionManager.inTransaction()).isFalse();
|
||||||
|
|
||||||
|
transactionManager.transact(
|
||||||
|
() -> {
|
||||||
|
assertThat(transactionManager.inTransaction()).isTrue();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTeardown_tearsDownAllReplicas() {
|
||||||
|
transactionManager.teardown();
|
||||||
|
verify(replica1).teardown();
|
||||||
|
verify(replica2).teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import static google.registry.keyring.api.PgpHelper.KeyRequirement.SIGN;
|
|||||||
import static google.registry.testing.TestDataHelper.loadBytes;
|
import static google.registry.testing.TestDataHelper.loadBytes;
|
||||||
import static google.registry.testing.TestDataHelper.loadFile;
|
import static google.registry.testing.TestDataHelper.loadFile;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.io.ByteSource;
|
import com.google.common.io.ByteSource;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
@@ -57,7 +58,8 @@ public final class FakeKeyringModule {
|
|||||||
private static final String MARKSDB_SMDRL_LOGIN_AND_PASSWORD = "smdrl:yolo";
|
private static final String MARKSDB_SMDRL_LOGIN_AND_PASSWORD = "smdrl:yolo";
|
||||||
private static final String BSA_API_KEY = "bsaapikey";
|
private static final String BSA_API_KEY = "bsaapikey";
|
||||||
private static final String SQL_PRIMARY_CONNECTION = "project:primary-region:primary-name";
|
private static final String SQL_PRIMARY_CONNECTION = "project:primary-region:primary-name";
|
||||||
private static final String SQL_REPLICA_CONNECTION = "project:replica-region:replica-name";
|
private static final String SQL_REPLICA_CONNECTION_1 = "project:replica-region:replica-name";
|
||||||
|
private static final String SQL_REPLICA_CONNECTION_2 = "project:replica-region:replica-name-2";
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public Keyring get() {
|
public Keyring get() {
|
||||||
@@ -160,7 +162,12 @@ public final class FakeKeyringModule {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSqlReplicaConnectionName() {
|
public String getSqlReplicaConnectionName() {
|
||||||
return SQL_REPLICA_CONNECTION;
|
return SQL_REPLICA_CONNECTION_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImmutableList<String> getSqlReplicaConnectionNames() {
|
||||||
|
return ImmutableList.of(SQL_REPLICA_CONNECTION_1, SQL_REPLICA_CONNECTION_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user