1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
gbrodman b78d12e73f 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.
2026-04-10 20:46:06 +00:00
Weimin Yu 4a1d0609f3 Support Fee-1.0 XML parser in all environments (#3007)
Add the Fee-1.0 schema in production, allowing the requests with this
extension to be parsed. This allows us to test this extension before hand.

The announcement of this extension in greeting is controlled by a
feature flag in ProtocolDefinition.java. As long as it is not announced,
we do not expect real customers to use this extension.
2026-04-09 20:45:36 +00:00
gbrodman 61b121f464 Fix Gradle issues when running devTool (#3006)
I think this may have been introduced as part of Gradle 9? Not sure why
it's not showing up in the remote builds but without this, I can't run
any "devTool" commands.

Note: the fixes were suggested by gemini-cli
2026-04-09 20:11:44 +00:00
Weimin Yu 074f78cfb3 Fix docker client api version on CloudBuild (#3004)
The docker engine provided by CloudBuild only supports up to 1.41.

Explicitly set API version for downloaded client for now.
2026-04-08 18:13:26 +00:00
17 changed files with 634 additions and 49 deletions
+3 -5
View File
@@ -74,11 +74,7 @@ sourceSets {
nonprod {
java {
compileClasspath += main.output
// Add the DB runtime classpath to nonprod so we can load the flyway
// scripts.
runtimeClasspath += main.output +
rootProject.project(":db").sourceSets.main.runtimeClasspath
runtimeClasspath += main.output
}
}
test {
@@ -102,6 +98,7 @@ configurations {
devtool
nonprodImplementation.extendsFrom implementation
nonprodRuntimeOnly.extendsFrom runtimeOnly
testImplementation.extendsFrom nonprodImplementation
@@ -259,6 +256,7 @@ dependencies {
implementation project(':util')
// Import NomulusPostreSql from ':db' for implementation but exclude dependencies.
implementation project(path: ':db', configuration: 'implementationApi')
nonprodRuntimeOnly project(':db')
testRuntimeOnly project(':db')
annotationProcessor deps['com.google.auto.service:auto-service']
+15 -8
View File
@@ -100,8 +100,10 @@ com.google.apis:google-api-services-admin-directory:directory_v1-rev20260227-2.0
com.google.apis:google-api-services-bigquery:v2-rev20240815-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudresourcemanager:v1-rev20240310-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dataflow:v1b3-rev20260213-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20260219-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260322-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20260219-2.0.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20260402-2.0.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260322-2.0.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260405-2.0.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
com.google.apis:google-api-services-gmail:v1-rev20260112-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-groupssettings:v1-rev20220614-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-healthcare:v1-rev20240130-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -132,7 +134,7 @@ com.google.cloud.opentelemetry:detector-resources-support:0.33.0=deploy_jar,nonp
com.google.cloud.opentelemetry:exporter-metrics:0.33.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud.opentelemetry:shared-resourcemapping:0.33.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.google.cloud.sql:jdbc-socket-factory-core:1.28.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud.sql:postgres-socket-factory:1.28.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.cloud.sql:postgres-socket-factory:1.28.2=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-bigquerystorage:3.9.0=compileClasspath,nonprodCompileClasspath,testCompileClasspath
com.google.cloud:google-cloud-bigquerystorage:3.9.2=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-bigtable:2.43.0=compileClasspath,nonprodCompileClasspath,testCompileClasspath
@@ -601,12 +603,17 @@ tools.jackson.core:jackson-core:3.1.0=compileClasspath,deploy_jar,nonprodCompile
tools.jackson.core:jackson-databind:3.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:17.1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.9.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-operations:17.9.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.9.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:17.9.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.10.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.9.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-operations:17.10.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
us.fatehi:schemacrawler-operations:17.9.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.10.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.9.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:17.10.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
us.fatehi:schemacrawler-text:17.9.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:17.1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:17.1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:17.9.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:17.10.0=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath
us.fatehi:schemacrawler:17.9.0=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
xerces:xmlParserAPIs:2.6.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=devtool,shadow
@@ -14,6 +14,7 @@
package google.registry.keyring;
import com.google.common.collect.ImmutableList;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -21,7 +22,6 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.Keyring;
import google.registry.keyring.secretmanager.SecretManagerKeyring;
import jakarta.inject.Singleton;
import java.util.Optional;
/** Dagger module for {@link Keyring} */
@Module
@@ -38,9 +38,10 @@ public abstract class KeyringModule {
}
@Provides
@Config("cloudSqlReplicaInstanceConnectionName")
public static Optional<String> provideCloudSqlReplicaInstanceConnectionName(Keyring keyring) {
return Optional.ofNullable(keyring.getSqlReplicaConnectionName());
@Config("cloudSqlReplicaInstanceConnectionNames")
public static ImmutableList<String> provideCloudSqlReplicaInstanceConnectionNames(
Keyring keyring) {
return ImmutableList.copyOf(keyring.getSqlReplicaConnectionNames());
}
@Provides
@@ -14,6 +14,7 @@
package google.registry.keyring.api;
import com.google.common.collect.ImmutableList;
import javax.annotation.concurrent.ThreadSafe;
import org.bouncycastle.openpgp.PGPKeyPair;
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. */
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();
/** Returns the Cloud SQL connection names of the replica database instances. */
ImmutableList<String> getSqlReplicaConnectionNames();
// Don't throw so try-with-resources works better.
@Override
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.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.Keyring;
import google.registry.keyring.api.KeyringException;
@@ -66,7 +68,8 @@ public class SecretManagerKeyring implements Keyring {
RDE_SSH_CLIENT_PUBLIC_STRING,
SAFE_BROWSING_API_KEY,
SQL_PRIMARY_CONN_NAME,
SQL_REPLICA_CONN_NAME;
SQL_REPLICA_CONN_NAME,
SQL_REPLICA_CONN_NAMES;
String getLabel() {
return UPPER_UNDERSCORE.to(LOWER_HYPHEN, name());
@@ -157,7 +160,25 @@ public class SecretManagerKeyring implements Keyring {
@Override
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. */
@@ -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.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_NAMES;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.flogger.FluentLogger;
@@ -134,6 +135,10 @@ public final class SecretManagerKeyringUpdater {
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.
*
@@ -14,7 +14,6 @@
package google.registry.model.eppcommon;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
@@ -28,7 +27,6 @@ import google.registry.model.domain.fee06.FeeInfoResponseExtensionV06;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.eppoutput.EppResponse;
import google.registry.util.RegistryEnvironment;
import google.registry.xml.ValidationMode;
import google.registry.xml.XmlException;
import google.registry.xml.XmlTransformer;
@@ -71,13 +69,9 @@ public class EppXmlTransformer {
private static final XmlTransformer OUTPUT_TRANSFORMER =
new XmlTransformer(getSchemas(), EppOutput.class);
// TODO(b/159033801): remove method and inline ALL_SCHEMA.
@VisibleForTesting
public static ImmutableList<String> getSchemas() {
if (RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)) {
return ALL_SCHEMAS.stream()
.filter(s -> !NON_PROD_SCHEMAS.contains(s))
.collect(toImmutableList());
}
return ALL_SCHEMAS;
}
@@ -15,6 +15,7 @@
package google.registry.persistence;
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.getHibernateHikariConnectionTimeout;
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.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import dagger.BindsOptionalOf;
@@ -36,6 +38,7 @@ import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
import google.registry.persistence.transaction.DelegatingReplicaJpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.persistence.transaction.TransactionManager;
@@ -59,6 +62,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Random;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.hibernate.cfg.Environment;
@@ -264,16 +268,13 @@ public abstract class PersistenceModule {
static JpaTransactionManager provideReadOnlyReplicaJpaTm(
SqlCredentialStore credentialStore,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
@Config("cloudSqlReplicaInstanceConnectionName")
Optional<String> replicaInstanceConnectionName,
Clock clock) {
@Config("cloudSqlReplicaInstanceConnectionNames")
ImmutableList<String> replicaInstanceConnectionNames,
Clock clock,
Random random) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
setSqlCredential(credentialStore, new RobotUser(RobotId.NOMULUS), overrides);
replicaInstanceConnectionName.ifPresent(
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);
return createReplicaJpaTm(overrides, replicaInstanceConnectionNames, clock, random);
}
@Provides
@@ -281,15 +282,34 @@ public abstract class PersistenceModule {
@BeamReadOnlyReplicaJpaTm
static JpaTransactionManager provideBeamReadOnlyReplicaJpaTm(
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs,
@Config("cloudSqlReplicaInstanceConnectionName")
Optional<String> replicaInstanceConnectionName,
Clock clock) {
@Config("cloudSqlReplicaInstanceConnectionNames")
ImmutableList<String> replicaInstanceConnectionNames,
Clock clock,
Random random) {
HashMap<String, String> overrides = Maps.newHashMap(beamCloudSqlConfigs);
replicaInstanceConnectionName.ifPresent(
name -> overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, name));
overrides.put(
return createReplicaJpaTm(overrides, replicaInstanceConnectionNames, clock, random);
}
private static JpaTransactionManager createReplicaJpaTm(
Map<String, String> baseOverrides,
ImmutableList<String> replicaInstanceConnectionNames,
Clock clock,
Random random) {
baseOverrides.put(
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. */
@@ -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;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.keyring.api.KeySerializer;
@@ -95,6 +96,10 @@ final class GetKeyringSecretCommand implements Command {
out.write(KeySerializer.serializeString(keyring.getSqlPrimaryConnectionName()));
case SQL_REPLICA_CONN_NAME ->
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));
case SQL_REPLICA_CONN_NAME ->
secretManagerKeyringUpdater.setSqlReplicaConnectionName(deserializeString(input));
case SQL_REPLICA_CONN_NAMES ->
secretManagerKeyringUpdater.setSqlReplicaConnectionNames(deserializeString(input));
}
secretManagerKeyringUpdater.update();
@@ -38,5 +38,6 @@ public enum KeyringKeyName {
RDE_STAGING_PUBLIC_KEY,
SAFE_BROWSING_API_KEY,
SQL_PRIMARY_CONN_NAME,
SQL_REPLICA_CONN_NAME
SQL_REPLICA_CONN_NAME,
SQL_REPLICA_CONN_NAMES
}
@@ -120,6 +120,23 @@ public class SecretManagerKeyringUpdaterTest {
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
void marksdbDnlLoginAndPassword() {
String secret = "marksdbDnlLoginAndPassword";
@@ -82,11 +82,11 @@ class EppXmlTransformerTest {
}
@Test
void testSchemas_inProduction_skipsFee1Point0() {
void testSchemas_inProduction_includesFee1Point0() {
var currentEnv = RegistryEnvironment.get();
try {
RegistryEnvironment.PRODUCTION.setup();
assertThat(EppXmlTransformer.getSchemas()).doesNotContain("fee-std-v1.xsd");
assertThat(EppXmlTransformer.getSchemas()).contains("fee-std-v1.xsd");
} finally {
currentEnv.setup();
}
@@ -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.loadFile;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteSource;
import dagger.Module;
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 BSA_API_KEY = "bsaapikey";
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
public Keyring get() {
@@ -160,7 +162,12 @@ public final class FakeKeyringModule {
@Override
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
+4 -2
View File
@@ -27,7 +27,7 @@ steps:
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
# Set home for Gradle caches. Must be consistent with last step below
# and ./build_nomulus_for_env.sh
env: [ 'GRADLE_USER_HOME=/workspace/cloudbuild-caches' ]
env: [ 'GRADLE_USER_HOME=/workspace/cloudbuild-caches', 'DOCKER_API_VERSION=1.41' ]
entrypoint: /bin/bash
args:
- -c
@@ -45,7 +45,7 @@ steps:
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
# Set home for Gradle caches. Must be consistent with last step below
# and ./build_nomulus_for_env.sh
env: [ 'GRADLE_USER_HOME=/workspace/cloudbuild-caches' ]
env: [ 'GRADLE_USER_HOME=/workspace/cloudbuild-caches', 'DOCKER_API_VERSION=1.41' ]
entrypoint: /bin/bash
args:
- -c
@@ -102,6 +102,7 @@ steps:
# nomulus.jar built earlier.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
env: [ 'DOCKER_API_VERSION=1.41' ]
args:
- -c
- |
@@ -119,6 +120,7 @@ steps:
# nomulus.jar built earlier.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
env: [ 'DOCKER_API_VERSION=1.41' ]
args:
- -c
- |