implements optimistic locking for PackageEntity

This commit is contained in:
Michael Hoennig 2022-08-20 12:29:14 +02:00
parent 5ea8069608
commit a04929453c
4 changed files with 72 additions and 19 deletions

View File

@ -19,6 +19,9 @@ public class PackageEntity {
private @Id UUID uuid; private @Id UUID uuid;
@Version
private int version;
@ManyToOne(optional = false) @ManyToOne(optional = false)
@JoinColumn(name = "customeruuid") @JoinColumn(name = "customeruuid")
private CustomerEntity customer; private CustomerEntity customer;

View File

@ -7,6 +7,7 @@
create table if not exists package create table if not exists package
( (
uuid uuid unique references RbacObject (uuid), uuid uuid unique references RbacObject (uuid),
version int not null default 0,
customerUuid uuid references customer (uuid), customerUuid uuid references customer (uuid),
name varchar(5), name varchar(5),
description varchar(96) description varchar(96)

View File

@ -2,11 +2,13 @@ package net.hostsharing.hsadminng.hs.hspackage;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.hscustomer.CustomerRepository; import net.hostsharing.hsadminng.hs.hscustomer.CustomerRepository;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -18,7 +20,7 @@ import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class }) @ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class, JpaAttempt.class })
@DirtiesContext @DirtiesContext
class PackageRepositoryIntegrationTest { class PackageRepositoryIntegrationTest {
@ -28,7 +30,11 @@ class PackageRepositoryIntegrationTest {
@Autowired @Autowired
PackageRepository packageRepository; PackageRepository packageRepository;
@Autowired EntityManager em; @Autowired
EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@Nested @Nested
class FindAllByOptionalNameLike { class FindAllByOptionalNameLike {
@ -88,13 +94,13 @@ class PackageRepositoryIntegrationTest {
// when // when
final var result = attempt( final var result = attempt(
em, em,
() -> packageRepository.findAllByOptionalNameLike(null)); () -> packageRepository.findAllByOptionalNameLike(null));
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
JpaSystemException.class, JpaSystemException.class,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); "[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
} }
@Test @Test
@ -102,12 +108,12 @@ class PackageRepositoryIntegrationTest {
currentUser("unknown@example.org"); currentUser("unknown@example.org");
final var result = attempt( final var result = attempt(
em, em,
() -> packageRepository.findAllByOptionalNameLike(null)); () -> packageRepository.findAllByOptionalNameLike(null));
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
JpaSystemException.class, JpaSystemException.class,
"hsadminng.currentUser defined as unknown@example.org, but does not exists"); "hsadminng.currentUser defined as unknown@example.org, but does not exists");
} }
@Test @Test
@ -117,16 +123,59 @@ class PackageRepositoryIntegrationTest {
assumedRoles("customer#aaa.admin"); assumedRoles("customer#aaa.admin");
final var result = attempt( final var result = attempt(
em, em,
() -> packageRepository.findAllByOptionalNameLike(null)); () -> packageRepository.findAllByOptionalNameLike(null));
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
JpaSystemException.class, JpaSystemException.class,
"hsadminng.currentUser defined as unknown@example.org, but does not exists"); "hsadminng.currentUser defined as unknown@example.org, but does not exists");
} }
} }
@Nested
class OptimisticLocking {
@Test
public void supportsOptimisticLocking() throws InterruptedException {
// given
hostsharingAdminWithAssumedRole("package#aaa00.admin");
final var pac = packageRepository.findAllByOptionalNameLike("%").get(0);
// when
final var result1 = jpaAttempt.transacted(() -> {
hostsharingAdminWithAssumedRole("package#aaa00.admin");
pac.setDescription("description set by thread 1");
packageRepository.save(pac);
});
final var result2 = jpaAttempt.transacted(() -> {
hostsharingAdminWithAssumedRole("package#aaa00.admin");
pac.setDescription("description set by thread 2");
packageRepository.save(pac);
sleep(1500);
});
// then
em.refresh(pac);
assertThat(pac.getDescription()).isEqualTo("description set by thread 1");
assertThat(result1.caughtException()).isNull();
assertThat(result2.caughtException()).isInstanceOf(ObjectOptimisticLockingFailureException.class);
}
private void sleep(final int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private void hostsharingAdminWithAssumedRole(final String assumedRoles) {
currentUser("mike@hostsharing.net");
assumedRoles(assumedRoles);
}
void currentUser(final String currentUser) { void currentUser(final String currentUser) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser); assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser);
@ -139,14 +188,14 @@ class PackageRepositoryIntegrationTest {
void noPackagesAreReturned(final List<PackageEntity> actualResult) { void noPackagesAreReturned(final List<PackageEntity> actualResult) {
assertThat(actualResult) assertThat(actualResult)
.extracting(PackageEntity::getName) .extracting(PackageEntity::getName)
.isEmpty(); .isEmpty();
} }
void exactlyThesePackagesAreReturned(final List<PackageEntity> actualResult, final String... packageNames) { void exactlyThesePackagesAreReturned(final List<PackageEntity> actualResult, final String... packageNames) {
assertThat(actualResult) assertThat(actualResult)
.extracting(PackageEntity::getName) .extracting(PackageEntity::getName)
.containsExactlyInAnyOrder(packageNames); .containsExactlyInAnyOrder(packageNames);
} }
} }

View File

@ -12,6 +12,6 @@ public class TestPackage {
public static final PackageEntity xxx02 = hsPackage(TestCustomer.xxx, "xxx02"); public static final PackageEntity xxx02 = hsPackage(TestCustomer.xxx, "xxx02");
public static PackageEntity hsPackage(final CustomerEntity customer, final String name) { public static PackageEntity hsPackage(final CustomerEntity customer, final String name) {
return new PackageEntity(randomUUID(), customer, name, "initial description of package " + name); return new PackageEntity(randomUUID(), 0, customer, name, "initial description of package " + name);
} }
} }