mirror of
https://github.com/google/nomulus
synced 2026-01-11 00:10:36 +00:00
Fix failures in retries when inserting new objects (#2788)
Given an entity with auto-filled id fields (annotated with @GeneratedValue, with null as initial value), when inserting it using Hibernate, the id fields will be filled with non-nulls even if the transaction fails. If the same entity instance is used again in a retry, Hibernate mistakes it as a detached entity and raises an error. The work around is to make a new copy of the entity in each transaction. This PR applies this pattern to affected entity types. We considered applying this pattern to JpaTransactionManagerImpl's insert method so that individual call sites do not have to change. However, we decided against it because: - It is unnecessary for entity types that do not have auto-filled id - The JpaTransactionManager cannot tell if copying is cheap or expensive. It is better exposing this to the user. - The JpaTransactionManager needs to know how to clone entities. A new interface may need to be introduced just for a handful of use cases.
This commit is contained in:
@@ -179,6 +179,7 @@ import google.registry.model.reporting.DomainTransactionRecord;
|
||||
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.smd.SignedMarkRevocationListDao;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.model.tld.Tld.TldType;
|
||||
@@ -2727,8 +2728,9 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
|
||||
@Test
|
||||
void testFail_startDateSunriseRegistration_revokedSignedMark() throws Exception {
|
||||
SmdrlCsvParser.parse(TmchTestData.loadFile("smd/smdrl.csv").lines().collect(toImmutableList()))
|
||||
.save();
|
||||
SignedMarkRevocationListDao.save(
|
||||
SmdrlCsvParser.parse(
|
||||
TmchTestData.loadFile("smd/smdrl.csv").lines().collect(toImmutableList())));
|
||||
createTld("tld", START_DATE_SUNRISE);
|
||||
clock.setTo(SMD_VALID_TIME);
|
||||
String revokedSmd =
|
||||
@@ -2753,9 +2755,9 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
if (labels.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SmdrlCsvParser.parse(
|
||||
TmchTestData.loadFile("idn/idn_smdrl.csv").lines().collect(toImmutableList()))
|
||||
.save();
|
||||
SignedMarkRevocationListDao.save(
|
||||
SmdrlCsvParser.parse(
|
||||
TmchTestData.loadFile("idn/idn_smdrl.csv").lines().collect(toImmutableList())));
|
||||
createTld("tld", START_DATE_SUNRISE);
|
||||
clock.setTo(SMD_VALID_TIME);
|
||||
String revokedSmd =
|
||||
|
||||
@@ -16,9 +16,12 @@ package google.registry.model.smd;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.EntityTestCase;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SignedMarkRevocationListDaoTest extends EntityTestCase {
|
||||
@@ -32,11 +35,29 @@ public class SignedMarkRevocationListDaoTest extends EntityTestCase {
|
||||
SignedMarkRevocationList list =
|
||||
SignedMarkRevocationList.create(
|
||||
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
|
||||
SignedMarkRevocationListDao.save(list);
|
||||
list = SignedMarkRevocationListDao.save(list);
|
||||
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
|
||||
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSave_retrySuccess() {
|
||||
SignedMarkRevocationList list =
|
||||
SignedMarkRevocationList.create(
|
||||
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
|
||||
AtomicBoolean isFirstAttempt = new AtomicBoolean(true);
|
||||
tm().transact(
|
||||
() -> {
|
||||
SignedMarkRevocationListDao.save(list);
|
||||
if (isFirstAttempt.get()) {
|
||||
isFirstAttempt.set(false);
|
||||
throw new OptimisticLockException();
|
||||
}
|
||||
});
|
||||
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
|
||||
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list, "revisionId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveAndLoad_emptyList() {
|
||||
SignedMarkRevocationList list =
|
||||
|
||||
@@ -48,7 +48,8 @@ public class SignedMarkRevocationListTest {
|
||||
for (int i = 0; i < rows; i++) {
|
||||
revokes.put(Integer.toString(i), clock.nowUtc());
|
||||
}
|
||||
SignedMarkRevocationList.create(clock.nowUtc(), revokes.build()).save();
|
||||
SignedMarkRevocationListDao.save(
|
||||
SignedMarkRevocationList.create(clock.nowUtc(), revokes.build()));
|
||||
SignedMarkRevocationList res = SignedMarkRevocationList.get();
|
||||
assertThat(res.size()).isEqualTo(rows);
|
||||
return res;
|
||||
|
||||
@@ -31,10 +31,12 @@ import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TestCacheExtension;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.IntStream;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
@@ -93,6 +95,27 @@ public class PremiumListDaoTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveNew_retry_success() {
|
||||
AtomicBoolean isFirstAttempt = new AtomicBoolean(true);
|
||||
tm().transact(
|
||||
() -> {
|
||||
PremiumListDao.save(testList);
|
||||
if (isFirstAttempt.get()) {
|
||||
isFirstAttempt.set(false);
|
||||
throw new OptimisticLockException();
|
||||
}
|
||||
});
|
||||
tm().transact(
|
||||
() -> {
|
||||
Optional<PremiumList> persistedListOpt = PremiumListDao.getLatestRevision("testname");
|
||||
assertThat(persistedListOpt).isPresent();
|
||||
PremiumList persistedList = persistedListOpt.get();
|
||||
assertThat(persistedList.getLabelsToPrices()).containsExactlyEntriesIn(TEST_PRICES);
|
||||
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void update_worksSuccessfully() {
|
||||
PremiumListDao.save(testList);
|
||||
|
||||
@@ -22,6 +22,8 @@ import google.registry.model.tld.label.ReservedList.ReservedListEntry;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -71,11 +73,34 @@ public class ReservedListDaoTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_withRetry_worksSuccessfully() {
|
||||
AtomicBoolean isFirstAttempt = new AtomicBoolean(true);
|
||||
tm().transact(
|
||||
() -> {
|
||||
ReservedListDao.save(testReservedList);
|
||||
if (isFirstAttempt.get()) {
|
||||
isFirstAttempt.set(false);
|
||||
throw new OptimisticLockException();
|
||||
}
|
||||
});
|
||||
tm().transact(
|
||||
() -> {
|
||||
ReservedList persistedList =
|
||||
tm().query("FROM ReservedList WHERE name = :name", ReservedList.class)
|
||||
.setParameter("name", "testlist")
|
||||
.getSingleResult();
|
||||
assertThat(persistedList.getReservedListEntries())
|
||||
.containsExactlyEntriesIn(testReservations);
|
||||
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete_worksSuccessfully() {
|
||||
ReservedListDao.save(testReservedList);
|
||||
var persisted = ReservedListDao.save(testReservedList);
|
||||
assertThat(ReservedListDao.checkExists("testlist")).isTrue();
|
||||
ReservedListDao.delete(testReservedList);
|
||||
ReservedListDao.delete(persisted);
|
||||
assertThat(ReservedListDao.checkExists("testlist")).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ package google.registry.model.tmch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TestCacheExtension;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
@@ -49,27 +49,36 @@ public class ClaimsListDaoTest {
|
||||
void save_insertsClaimsListSuccessfully() {
|
||||
ClaimsList claimsList =
|
||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||
ClaimsListDao.save(claimsList);
|
||||
claimsList = ClaimsListDao.save(claimsList);
|
||||
ClaimsList insertedClaimsList = ClaimsListDao.get();
|
||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||
assertThat(insertedClaimsList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_fail_duplicateId() {
|
||||
void save_insertsClaimsListSuccessfully_withRetries() {
|
||||
ClaimsList claimsList =
|
||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||
ClaimsListDao.save(claimsList);
|
||||
AtomicBoolean isFirstAttempt = new AtomicBoolean(true);
|
||||
tm().transact(
|
||||
() -> {
|
||||
ClaimsListDao.save(claimsList);
|
||||
if (isFirstAttempt.get()) {
|
||||
isFirstAttempt.set(false);
|
||||
throw new OptimisticLockException();
|
||||
}
|
||||
});
|
||||
ClaimsList insertedClaimsList = ClaimsListDao.get();
|
||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||
// Save ClaimsList with existing revisionId should fail because revisionId is the primary key.
|
||||
assertThrows(PersistenceException.class, () -> ClaimsListDao.save(insertedClaimsList));
|
||||
assertThat(insertedClaimsList.getTmdbGenerationTime())
|
||||
.isEqualTo(claimsList.getTmdbGenerationTime());
|
||||
assertThat(insertedClaimsList.getLabelsToKeys()).isEqualTo(claimsList.getLabelsToKeys());
|
||||
assertThat(insertedClaimsList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_claimsListWithNoEntries() {
|
||||
ClaimsList claimsList = ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of());
|
||||
ClaimsListDao.save(claimsList);
|
||||
claimsList = ClaimsListDao.save(claimsList);
|
||||
ClaimsList insertedClaimsList = ClaimsListDao.get();
|
||||
assertClaimsListEquals(claimsList, insertedClaimsList);
|
||||
assertThat(insertedClaimsList.getLabelsToKeys()).isEmpty();
|
||||
@@ -86,8 +95,8 @@ public class ClaimsListDaoTest {
|
||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||
ClaimsList newClaimsList =
|
||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
|
||||
ClaimsListDao.save(oldClaimsList);
|
||||
ClaimsListDao.save(newClaimsList);
|
||||
oldClaimsList = ClaimsListDao.save(oldClaimsList);
|
||||
newClaimsList = ClaimsListDao.save(newClaimsList);
|
||||
assertClaimsListEquals(newClaimsList, ClaimsListDao.get());
|
||||
}
|
||||
|
||||
@@ -96,11 +105,11 @@ public class ClaimsListDaoTest {
|
||||
assertThat(ClaimsListDao.CACHE.getIfPresent(ClaimsListDao.class)).isNull();
|
||||
ClaimsList oldList =
|
||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
|
||||
ClaimsListDao.save(oldList);
|
||||
oldList = ClaimsListDao.save(oldList);
|
||||
assertThat(ClaimsListDao.CACHE.getIfPresent(ClaimsListDao.class)).isEqualTo(oldList);
|
||||
ClaimsList newList =
|
||||
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
|
||||
ClaimsListDao.save(newList);
|
||||
newList = ClaimsListDao.save(newList);
|
||||
assertThat(ClaimsListDao.CACHE.getIfPresent(ClaimsListDao.class)).isEqualTo(newList);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user