diff --git a/core/src/main/java/google/registry/flows/FlowUtils.java b/core/src/main/java/google/registry/flows/FlowUtils.java index ca8fbcd2a..40a723df2 100644 --- a/core/src/main/java/google/registry/flows/FlowUtils.java +++ b/core/src/main/java/google/registry/flows/FlowUtils.java @@ -15,7 +15,6 @@ package google.registry.flows; import static com.google.common.base.Preconditions.checkState; -import static google.registry.model.IdService.allocateId; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.xml.ValidationMode.LENIENT; import static google.registry.xml.ValidationMode.STRICT; @@ -103,7 +102,7 @@ public final class FlowUtils { } public static HistoryEntryId createHistoryEntryId(EppResource parent) { - return new HistoryEntryId(parent.getRepoId(), allocateId()); + return new HistoryEntryId(parent.getRepoId(), tm().allocateId()); } /** Registrar is not logged in. */ diff --git a/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java b/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java index 6ebea8cd8..db1251302 100644 --- a/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java +++ b/core/src/main/java/google/registry/flows/contact/ContactCreateFlow.java @@ -19,7 +19,6 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo; import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy; import static google.registry.model.EppResourceUtils.createRepoId; -import static google.registry.model.IdService.allocateId; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import com.google.common.collect.ImmutableSet; @@ -79,7 +78,7 @@ public final class ContactCreateFlow implements MutatingFlow { .setAuthInfo(command.getAuthInfo()) .setCreationRegistrarId(registrarId) .setPersistedCurrentSponsorRegistrarId(registrarId) - .setRepoId(createRepoId(allocateId(), roidSuffix)) + .setRepoId(createRepoId(tm().allocateId(), roidSuffix)) .setFaxNumber(command.getFax()) .setVoiceNumber(command.getVoice()) .setDisclose(command.getDisclose()) diff --git a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java index a8301c8bd..7c604cfeb 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java @@ -46,7 +46,6 @@ import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNo import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; import static google.registry.model.EppResourceUtils.createDomainRepoId; -import static google.registry.model.IdService.allocateId; import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE; import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY; @@ -345,8 +344,8 @@ public final class DomainCreateFlow implements MutatingFlow { Optional secDnsCreate = validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class)); DateTime registrationExpirationTime = leapSafeAddYears(now, years); - String repoId = createDomainRepoId(allocateId(), tld.getTldStr()); - long historyRevisionId = allocateId(); + String repoId = createDomainRepoId(tm().allocateId(), tld.getTldStr()); + long historyRevisionId = tm().allocateId(); HistoryEntryId domainHistoryId = new HistoryEntryId(repoId, historyRevisionId); historyBuilder.setRevisionId(historyRevisionId); // Bill for the create. diff --git a/core/src/main/java/google/registry/flows/host/HostCreateFlow.java b/core/src/main/java/google/registry/flows/host/HostCreateFlow.java index 86694f138..ed513e89d 100644 --- a/core/src/main/java/google/registry/flows/host/HostCreateFlow.java +++ b/core/src/main/java/google/registry/flows/host/HostCreateFlow.java @@ -22,7 +22,6 @@ import static google.registry.flows.host.HostFlowUtils.validateHostName; import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete; import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership; import static google.registry.model.EppResourceUtils.createRepoId; -import static google.registry.model.IdService.allocateId; import static google.registry.model.reporting.HistoryEntry.Type.HOST_CREATE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.CollectionUtils.isNullOrEmpty; @@ -123,7 +122,7 @@ public final class HostCreateFlow implements MutatingFlow { .setPersistedCurrentSponsorRegistrarId(registrarId) .setHostName(targetId) .setInetAddresses(command.getInetAddresses()) - .setRepoId(createRepoId(allocateId(), roidSuffix)) + .setRepoId(createRepoId(tm().allocateId(), roidSuffix)) .setSuperordinateDomain(superordinateDomain.map(Domain::createVKey).orElse(null)) .build(); historyBuilder.setType(HOST_CREATE).setHost(newHost); diff --git a/core/src/main/java/google/registry/model/Buildable.java b/core/src/main/java/google/registry/model/Buildable.java index e67e5aa76..1abae2f3e 100644 --- a/core/src/main/java/google/registry/model/Buildable.java +++ b/core/src/main/java/google/registry/model/Buildable.java @@ -16,8 +16,8 @@ package google.registry.model; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import static google.registry.model.IdService.allocateId; import static google.registry.model.ModelUtils.getAllFields; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import google.registry.model.annotations.IdAllocation; import google.registry.util.TypeUtils.TypeInstantiator; @@ -65,7 +65,7 @@ public interface Buildable { && !idField.getType().equals(String.class) && Optional.ofNullable((Long) ModelUtils.getFieldValue(instance, idField)) .orElse(0L) == 0) { - ModelUtils.setFieldValue(instance, idField, allocateId()); + ModelUtils.setFieldValue(instance, idField, tm().reTransact(tm()::allocateId)); } return instance; } finally { diff --git a/core/src/main/java/google/registry/model/annotations/IdAllocation.java b/core/src/main/java/google/registry/model/annotations/IdAllocation.java index df8956dfa..29ac518ba 100644 --- a/core/src/main/java/google/registry/model/annotations/IdAllocation.java +++ b/core/src/main/java/google/registry/model/annotations/IdAllocation.java @@ -14,15 +14,14 @@ package google.registry.model.annotations; -import google.registry.model.IdService; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * This annotation is needed for any ID field that needs to be allocated with {@link IdService} - * class + * This annotation is needed for any ID field that needs to be allocated with {@link + * google.registry.persistence.transaction.TransactionManager#allocateId} class */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) diff --git a/core/src/main/java/google/registry/model/domain/GracePeriod.java b/core/src/main/java/google/registry/model/domain/GracePeriod.java index f461459c4..6b98c506f 100644 --- a/core/src/main/java/google/registry/model/domain/GracePeriod.java +++ b/core/src/main/java/google/registry/model/domain/GracePeriod.java @@ -15,7 +15,7 @@ package google.registry.model.domain; import static com.google.common.base.Preconditions.checkArgument; -import static google.registry.model.IdService.allocateId; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import com.google.common.annotations.VisibleForTesting; @@ -70,7 +70,8 @@ public class GracePeriod extends GracePeriodBase { (billingRecurrence != null) == GracePeriodStatus.AUTO_RENEW.equals(type), "BillingRecurrences must be present on (and only on) autorenew grace periods"); GracePeriod instance = new GracePeriod(); - instance.gracePeriodId = gracePeriodId == null ? allocateId() : gracePeriodId; + instance.gracePeriodId = + gracePeriodId == null ? tm().reTransact(tm()::allocateId) : gracePeriodId; instance.type = checkArgumentNotNull(type); instance.domainRepoId = checkArgumentNotNull(domainRepoId); instance.expirationTime = checkArgumentNotNull(expirationTime); @@ -198,7 +199,7 @@ public class GracePeriod extends GracePeriodBase { static GracePeriodHistory createFrom(long historyRevisionId, GracePeriod gracePeriod) { GracePeriodHistory instance = new GracePeriodHistory(); - instance.gracePeriodHistoryRevisionId = allocateId(); + instance.gracePeriodHistoryRevisionId = tm().reTransact(tm()::allocateId); instance.domainHistoryRevisionId = historyRevisionId; instance.gracePeriodId = gracePeriod.gracePeriodId; instance.type = gracePeriod.type; diff --git a/core/src/main/java/google/registry/model/domain/secdns/DomainDsDataHistory.java b/core/src/main/java/google/registry/model/domain/secdns/DomainDsDataHistory.java index 212ddc35e..89591e27a 100644 --- a/core/src/main/java/google/registry/model/domain/secdns/DomainDsDataHistory.java +++ b/core/src/main/java/google/registry/model/domain/secdns/DomainDsDataHistory.java @@ -14,7 +14,7 @@ package google.registry.model.domain.secdns; -import static google.registry.model.IdService.allocateId; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import google.registry.model.domain.DomainHistory; import google.registry.model.reporting.HistoryEntry.HistoryEntryId; @@ -48,7 +48,7 @@ public class DomainDsDataHistory extends DomainDsDataBase { instance.algorithm = dsData.getAlgorithm(); instance.digestType = dsData.getDigestType(); instance.digest = dsData.getDigest(); - instance.dsDataHistoryRevisionId = allocateId(); + instance.dsDataHistoryRevisionId = tm().reTransact(tm()::allocateId); return instance; } diff --git a/core/src/main/java/google/registry/model/IdService.java b/core/src/main/java/google/registry/persistence/transaction/IdService.java similarity index 93% rename from core/src/main/java/google/registry/model/IdService.java rename to core/src/main/java/google/registry/persistence/transaction/IdService.java index eb002ba76..8741a9fe8 100644 --- a/core/src/main/java/google/registry/model/IdService.java +++ b/core/src/main/java/google/registry/persistence/transaction/IdService.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package google.registry.model; +package google.registry.persistence.transaction; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicLong; /** * Allocates a {@code long} to use as a {@code @Id}, (part) of the primary SQL key for an entity. */ -public final class IdService { +final class IdService { private IdService() {} @@ -32,7 +32,7 @@ public final class IdService { * *

The generated IDs are project-wide unique. */ - public static long allocateId() { + static long allocateId() { return tm().transact( () -> (BigInteger) diff --git a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java index 436d7a5c7..19fedd0e3 100644 --- a/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java +++ b/core/src/main/java/google/registry/persistence/transaction/JpaTransactionManagerImpl.java @@ -56,6 +56,7 @@ import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; +import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.annotation.Nullable; @@ -137,6 +138,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { return transactionInfo.get().inTransaction; } + @Override + public long allocateId() { + assertInTransaction(); + return transactionInfo.get().idProvider.get(); + } + @Override public void assertInTransaction() { if (!inTransaction()) { @@ -210,7 +217,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { EntityTransaction txn = txnInfo.entityManager.getTransaction(); try { txn.begin(); - txnInfo.start(clock); + txnInfo.start(clock, readOnly ? ReplicaDbIdService::allocatedId : IdService::allocateId); if (readOnly) { getEntityManager().createNativeQuery("SET TRANSACTION READ ONLY").executeUpdate(); logger.atInfo().log("Using read-only SQL replica"); @@ -668,6 +675,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { EntityManager entityManager; boolean inTransaction = false; DateTime transactionTime; + Supplier idProvider; // The set of entity objects that have been either persisted (via insert()) or merged (via // put()/update()). If the entity manager returns these as a result of a find() or query @@ -676,13 +684,15 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager { Set objectsToSave = Collections.newSetFromMap(new IdentityHashMap<>()); /** Start a new transaction. */ - private void start(Clock clock) { + private void start(Clock clock, Supplier idProvider) { checkArgumentNotNull(clock); inTransaction = true; transactionTime = clock.nowUtc(); + this.idProvider = idProvider; } private void clear() { + idProvider = null; inTransaction = false; transactionTime = null; objectsToSave = Collections.newSetFromMap(new IdentityHashMap<>()); diff --git a/core/src/main/java/google/registry/persistence/transaction/ReplicaDbIdService.java b/core/src/main/java/google/registry/persistence/transaction/ReplicaDbIdService.java new file mode 100644 index 000000000..30a99d674 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/transaction/ReplicaDbIdService.java @@ -0,0 +1,38 @@ +// Copyright 2024 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 java.util.concurrent.atomic.AtomicLong; + +/** + * Provides {@code long} values for use as {@code id} by JPA model entities in (read-only) + * transactions in the replica database. Each id is only unique in the JVM instance. + * + *

The {@link IdService database sequence-based id service} cannot be used with the replica + * because id generation is a write operation. + */ +final class ReplicaDbIdService { + + private ReplicaDbIdService() {} + + private static final AtomicLong nextId = new AtomicLong(1); + + /** + * Returns the next long value from a {@link AtomicLong}. Each id is unique in the JVM instance. + */ + static final long allocatedId() { + return nextId.getAndIncrement(); + } +} diff --git a/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java b/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java index d8cccaa53..8cd63befe 100644 --- a/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java +++ b/core/src/main/java/google/registry/persistence/transaction/TransactionManager.java @@ -48,6 +48,15 @@ public interface TransactionManager { */ void assertInTransaction(); + /** + * Returns a {@link long} value that can be used as {@code id} by a JPA model entity. + * + *

The returned value must be project-wide unique when transacting on the primary database + * instance, but only needs to be unique within a JVM instance when transacting on the replica + * instance. + */ + long allocateId(); + /** Executes the work in a transaction and returns the result. */ T transact(Callable work); diff --git a/core/src/main/java/google/registry/tools/UpdateRecurrenceCommand.java b/core/src/main/java/google/registry/tools/UpdateRecurrenceCommand.java index 0130ea896..857e7c35f 100644 --- a/core/src/main/java/google/registry/tools/UpdateRecurrenceCommand.java +++ b/core/src/main/java/google/registry/tools/UpdateRecurrenceCommand.java @@ -15,7 +15,6 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; -import static google.registry.model.IdService.allocateId; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.DateTimeUtils.END_OF_TIME; @@ -119,7 +118,7 @@ public class UpdateRecurrenceCommand extends ConfirmingCommand { domainsAndRecurrences.forEach( (domain, existingRecurrence) -> { // Make a new history ID to break the (recurrence, history, domain) circular dep chain - long newHistoryId = allocateId(); + long newHistoryId = tm().allocateId(); HistoryEntryId newDomainHistoryId = new HistoryEntryId(domain.getRepoId(), newHistoryId); BillingRecurrence endingNow = existingRecurrence.asBuilder().setRecurrenceEndTime(now).build(); diff --git a/core/src/main/java/google/registry/whois/DomainLookupCommand.java b/core/src/main/java/google/registry/whois/DomainLookupCommand.java index 98844f833..7b1d40fb5 100644 --- a/core/src/main/java/google/registry/whois/DomainLookupCommand.java +++ b/core/src/main/java/google/registry/whois/DomainLookupCommand.java @@ -20,7 +20,7 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.EppResourceUtils.loadByForeignKeyCached; import static google.registry.model.tld.Tlds.findTldForName; import static google.registry.model.tld.Tlds.getTlds; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm; import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; import com.google.common.annotations.VisibleForTesting; @@ -67,7 +67,8 @@ public class DomainLookupCommand implements WhoisCommand { // Include `getResponse` and `isBlockedByBsa` in one transaction to reduce latency. // Must pass the exceptions outside to throw. ResponseOrException result = - tm().transact( + replicaTm() + .transact( () -> { final Optional response = getResponse(domainName, now); if (response.isPresent()) { diff --git a/core/src/main/java/google/registry/whois/NameserverLookupByIpCommand.java b/core/src/main/java/google/registry/whois/NameserverLookupByIpCommand.java index e784fa4b3..b88d48638 100644 --- a/core/src/main/java/google/registry/whois/NameserverLookupByIpCommand.java +++ b/core/src/main/java/google/registry/whois/NameserverLookupByIpCommand.java @@ -16,7 +16,7 @@ package google.registry.whois; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm; import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; import com.google.common.annotations.VisibleForTesting; @@ -51,10 +51,12 @@ final class NameserverLookupByIpCommand implements WhoisCommand { public WhoisResponse executeQuery(DateTime now) throws WhoisException { Iterable hostsFromDb; hostsFromDb = - tm().transact( + replicaTm() + .transact( () -> // We cannot query @Convert-ed fields in HQL, so we must use native Postgres. - tm().getEntityManager() + replicaTm() + .getEntityManager() /* * Using array_operator <@ (contained-by) with gin index on inet_address. * Without gin index, this is slightly slower than the alternative form of diff --git a/core/src/test/java/google/registry/testing/DatabaseHelper.java b/core/src/test/java/google/registry/testing/DatabaseHelper.java index 4adc09405..c5666a552 100644 --- a/core/src/test/java/google/registry/testing/DatabaseHelper.java +++ b/core/src/test/java/google/registry/testing/DatabaseHelper.java @@ -28,7 +28,6 @@ import static google.registry.config.RegistryConfig.getContactAndHostRoidSuffix; import static google.registry.config.RegistryConfig.getContactAutomaticTransferLength; import static google.registry.model.EppResourceUtils.createDomainRepoId; import static google.registry.model.EppResourceUtils.createRepoId; -import static google.registry.model.IdService.allocateId; import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence; import static google.registry.model.ResourceTransferUtils.createTransferResponse; @@ -393,7 +392,7 @@ public final class DatabaseHelper { // prevent breaking some hard-coded flow tests. IDs in tests are allocated in a strictly // increasing sequence, if we don't pad out the ID here, we would have to renumber hundreds of // unit tests. - allocateId(); + tm().reTransact(tm()::allocateId); PremiumListDao.save(premiumList); maybeAdvanceClock(); return premiumList; @@ -963,12 +962,12 @@ public final class DatabaseHelper { /** Returns a newly allocated, globally unique domain repoId of the format HEX-TLD. */ public static String generateNewDomainRoid(String tld) { - return createDomainRepoId(allocateId(), tld); + return createDomainRepoId(tm().reTransact(tm()::allocateId), tld); } /** Returns a newly allocated, globally unique contact/host repoId of the format HEX_TLD-ROID. */ public static String generateNewContactHostRoid() { - return createRepoId(allocateId(), getContactAndHostRoidSuffix()); + return createRepoId(tm().reTransact(tm()::allocateId), getContactAndHostRoidSuffix()); } /** Persists an object in the DB for tests. */