Add deleteAll method to TransactionManager (#604)

* Add deleteAll method to TransactionManager

* Rename deleteAll to delete

* Add bucket.getLastWrittenTime() before second mutation
This commit is contained in:
Shicong Huang 2020-06-03 10:02:48 -04:00 committed by GitHub
parent 456a4ee7f4
commit abd207330b
4 changed files with 116 additions and 82 deletions

View file

@ -14,6 +14,7 @@
package google.registry.model.ofy;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@ -157,7 +158,7 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public <T> ImmutableList<T> load(Iterable<VKey<T>> keys) {
Iterator<Key<T>> iter =
StreamSupport.stream(keys.spliterator(), false).map(key -> key.getOfyKey()).iterator();
StreamSupport.stream(keys.spliterator(), false).map(VKey::getOfyKey).iterator();
// The lambda argument to keys() effectively converts Iterator -> Iterable.
return ImmutableList.copyOf(getOfy().load().keys(() -> iter).values());
@ -170,7 +171,18 @@ public class DatastoreTransactionManager implements TransactionManager {
}
@Override
public <T> void delete(VKey<T> key) {
public void delete(VKey<?> key) {
getOfy().delete().key(key.getOfyKey()).now();
}
@Override
public void delete(Iterable<? extends VKey<?>> vKeys) {
// We have to create a list to work around the wildcard capture issue here.
// See https://docs.oracle.com/javase/tutorial/java/generics/capture.html
ImmutableList<Key<?>> list =
StreamSupport.stream(vKeys.spliterator(), false)
.map(VKey::getOfyKey)
.collect(toImmutableList());
getOfy().delete().keys(list).now();
}
}

View file

@ -278,7 +278,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
.getResultList());
}
private <T> int internalDelete(VKey<T> key) {
private int internalDelete(VKey<?> key) {
checkArgumentNotNull(key, "key must be specified");
assertInTransaction();
EntityType<?> entityType = getEntityType(key.getKind());
@ -291,10 +291,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
@Override
public <T> void delete(VKey<T> key) {
public void delete(VKey<?> key) {
internalDelete(key);
}
@Override
public void delete(Iterable<? extends VKey<?>> vKeys) {
checkArgumentNotNull(vKeys, "vKeys must be specified");
vKeys.forEach(this::internalDelete);
}
@Override
public <T> void assertDelete(VKey<T> key) {
if (internalDelete(key) != 1) {

View file

@ -115,7 +115,7 @@ public interface TransactionManager {
<T> T load(VKey<T> key);
/**
* Leads the set of entities by their key id.
* Loads the set of entities by their key id.
*
* @throws NoSuchElementException if any of the keys are not found.
*/
@ -125,5 +125,8 @@ public interface TransactionManager {
<T> ImmutableList<T> loadAll(Class<T> clazz);
/** Deletes the entity by its id. */
<T> void delete(VKey<T> key);
void delete(VKey<?> key);
/** Deletes the set of entities by their key id. */
void delete(Iterable<? extends VKey<?>> keys);
}

View file

@ -14,6 +14,8 @@
package google.registry.persistence.transaction;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.junit.Assert.assertThrows;
@ -30,7 +32,9 @@ import google.registry.testing.AppEngineRule;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -62,39 +66,38 @@ public class TransactionManagerTest {
.withJpaUnitTestEntities(TestEntity.class)
.build();
public TransactionManagerTest() {}
TransactionManagerTest() {}
@BeforeEach
public void setUp() {
void setUp() {
inject.setStaticField(Ofy.class, "clock", fakeClock);
fakeClock.advanceOneMilli();
}
@TestTemplate
public void inTransaction_returnsCorrespondingResult() {
void inTransaction_returnsCorrespondingResult() {
assertThat(tm().inTransaction()).isFalse();
tm().transact(() -> assertThat(tm().inTransaction()).isTrue());
assertThat(tm().inTransaction()).isFalse();
}
@TestTemplate
public void assertInTransaction_throwsExceptionWhenNotInTransaction() {
void assertInTransaction_throwsExceptionWhenNotInTransaction() {
assertThrows(IllegalStateException.class, () -> tm().assertInTransaction());
tm().transact(() -> tm().assertInTransaction());
assertThrows(IllegalStateException.class, () -> tm().assertInTransaction());
}
@TestTemplate
public void getTransactionTime_throwsExceptionWhenNotInTransaction() {
FakeClock txnClock = fakeClock;
txnClock.advanceOneMilli();
void getTransactionTime_throwsExceptionWhenNotInTransaction() {
assertThrows(IllegalStateException.class, () -> tm().getTransactionTime());
tm().transact(() -> assertThat(tm().getTransactionTime()).isEqualTo(txnClock.nowUtc()));
tm().transact(() -> assertThat(tm().getTransactionTime()).isEqualTo(fakeClock.nowUtc()));
assertThrows(IllegalStateException.class, () -> tm().getTransactionTime());
}
@TestTemplate
public void transact_hasNoEffectWithPartialSuccess() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
void transact_hasNoEffectWithPartialSuccess() {
assertEntityNotExist(theEntity);
assertThrows(
RuntimeException.class,
() ->
@ -104,152 +107,162 @@ public class TransactionManagerTest {
tm().saveNew(theEntity);
throw new RuntimeException();
}));
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
assertEntityNotExist(theEntity);
}
@TestTemplate
public void transact_reusesExistingTransaction() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void transact_reusesExistingTransaction() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().transact(() -> tm().saveNew(theEntity)));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
assertEntityExists(theEntity);
}
@TestTemplate
public void saveNew_succeeds() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void saveNew_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
fakeClock.advanceOneMilli();
assertEntityExists(theEntity);
assertThat(tm().transact(() -> tm().load(theEntity.key()))).isEqualTo(theEntity);
}
@TestTemplate
public void saveAllNew_succeeds() {
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isFalse());
fakeClock.advanceOneMilli();
void saveAllNew_succeeds() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().saveAllNew(moreEntities));
fakeClock.advanceOneMilli();
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isTrue());
assertAllEntitiesExist(moreEntities);
}
@TestTemplate
public void saveNewOrUpdate_persistsNewEntity() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void saveNewOrUpdate_persistsNewEntity() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().saveNewOrUpdate(theEntity));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
fakeClock.advanceOneMilli();
assertEntityExists(theEntity);
assertThat(tm().transact(() -> tm().load(theEntity.key()))).isEqualTo(theEntity);
}
@TestTemplate
public void saveNewOrUpdate_updatesExistingEntity() {
fakeClock.advanceOneMilli();
void saveNewOrUpdate_updatesExistingEntity() {
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted = tm().transact(() -> tm().load(theEntity.key()));
assertThat(persisted.data).isEqualTo("foo");
theEntity.data = "bar";
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNewOrUpdate(theEntity));
fakeClock.advanceOneMilli();
persisted = tm().transact(() -> tm().load(theEntity.key()));
fakeClock.advanceOneMilli();
assertThat(persisted.data).isEqualTo("bar");
}
@TestTemplate
public void saveNewOrUpdateAll_succeeds() {
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isFalse());
fakeClock.advanceOneMilli();
void saveNewOrUpdateAll_succeeds() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().saveNewOrUpdateAll(moreEntities));
fakeClock.advanceOneMilli();
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isTrue());
assertAllEntitiesExist(moreEntities);
}
@TestTemplate
public void update_succeeds() {
fakeClock.advanceOneMilli();
void update_succeeds() {
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted =
tm().transact(
() ->
tm().load(
VKey.create(TestEntity.class, theEntity.name, Key.create(theEntity))));
fakeClock.advanceOneMilli();
assertThat(persisted.data).isEqualTo("foo");
theEntity.data = "bar";
tm().transact(() -> tm().update(theEntity));
fakeClock.advanceOneMilli();
tm().transact(() -> tm().update(theEntity));
persisted = tm().transact(() -> tm().load(theEntity.key()));
assertThat(persisted.data).isEqualTo("bar");
}
@TestTemplate
public void load_succeeds() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void load_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted = tm().transact(() -> tm().load(theEntity.key()));
assertThat(persisted.name).isEqualTo("theEntity");
assertThat(persisted.data).isEqualTo("foo");
}
@TestTemplate
public void load_throwsOnMissingElement() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void load_throwsOnMissingElement() {
assertEntityNotExist(theEntity);
assertThrows(
NoSuchElementException.class, () -> tm().transact(() -> tm().load(theEntity.key())));
}
@TestTemplate
public void maybeLoad_succeeds() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void maybeLoad_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted = tm().transact(() -> tm().maybeLoad(theEntity.key()).get());
assertThat(persisted.name).isEqualTo("theEntity");
assertThat(persisted.data).isEqualTo("foo");
}
@TestTemplate
public void maybeLoad_nonExistentObject() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void maybeLoad_nonExistentObject() {
assertEntityNotExist(theEntity);
assertThat(tm().transact(() -> tm().maybeLoad(theEntity.key())).isPresent()).isFalse();
}
@TestTemplate
public void delete_succeeds() {
fakeClock.advanceOneMilli();
void delete_succeeds() {
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
assertEntityExists(theEntity);
fakeClock.advanceOneMilli();
tm().transact(() -> tm().delete(theEntity.key()));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
assertEntityNotExist(theEntity);
}
@TestTemplate
public void delete_returnsZeroWhenNoEntity() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
void delete_doNothingWhenEntityNotExist() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().delete(theEntity.key()));
assertEntityNotExist(theEntity);
}
@TestTemplate
void delete_succeedsForEntitySet() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().saveAllNew(moreEntities));
Set<VKey<TestEntity>> keys =
moreEntities.stream().map(TestEntity::key).collect(toImmutableSet());
assertAllEntitiesExist(moreEntities);
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
tm().transact(() -> tm().delete(keys));
assertAllEntitiesNotExist(moreEntities);
}
@TestTemplate
void delete_ignoreNonExistentEntity() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().saveAllNew(moreEntities));
List<VKey<TestEntity>> keys =
moreEntities.stream().map(TestEntity::key).collect(toImmutableList());
assertAllEntitiesExist(moreEntities);
fakeClock.advanceOneMilli();
tm().transact(() -> tm().delete(keys.get(0)));
assertEntityNotExist(moreEntities.get(0));
fakeClock.advanceOneMilli();
tm().transact(() -> tm().delete(keys));
assertAllEntitiesNotExist(moreEntities);
}
private static void assertEntityExists(TestEntity entity) {
assertThat(tm().transact(() -> tm().checkExists(entity))).isTrue();
}
private static void assertEntityNotExist(TestEntity entity) {
assertThat(tm().transact(() -> tm().checkExists(entity))).isFalse();
}
private static void assertAllEntitiesExist(ImmutableList<TestEntity> entities) {
entities.forEach(TransactionManagerTest::assertEntityExists);
}
private static void assertAllEntitiesNotExist(ImmutableList<TestEntity> entities) {
entities.forEach(TransactionManagerTest::assertEntityNotExist);
}
@Entity(name = "TestEntity")