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;
@Version
private int version;
@ManyToOne(optional = false)
@JoinColumn(name = "customeruuid")
private CustomerEntity customer;

View File

@ -7,6 +7,7 @@
create table if not exists package
(
uuid uuid unique references RbacObject (uuid),
version int not null default 0,
customerUuid uuid references customer (uuid),
name varchar(5),
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.hs.hscustomer.CustomerRepository;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
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;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class, JpaAttempt.class })
@DirtiesContext
class PackageRepositoryIntegrationTest {
@ -28,7 +30,11 @@ class PackageRepositoryIntegrationTest {
@Autowired
PackageRepository packageRepository;
@Autowired EntityManager em;
@Autowired
EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@Nested
class FindAllByOptionalNameLike {
@ -88,13 +94,13 @@ class PackageRepositoryIntegrationTest {
// when
final var result = attempt(
em,
() -> packageRepository.findAllByOptionalNameLike(null));
em,
() -> packageRepository.findAllByOptionalNameLike(null));
// then
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
JpaSystemException.class,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
}
@Test
@ -102,12 +108,12 @@ class PackageRepositoryIntegrationTest {
currentUser("unknown@example.org");
final var result = attempt(
em,
() -> packageRepository.findAllByOptionalNameLike(null));
em,
() -> packageRepository.findAllByOptionalNameLike(null));
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
JpaSystemException.class,
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
}
@Test
@ -117,16 +123,59 @@ class PackageRepositoryIntegrationTest {
assumedRoles("customer#aaa.admin");
final var result = attempt(
em,
() -> packageRepository.findAllByOptionalNameLike(null));
em,
() -> packageRepository.findAllByOptionalNameLike(null));
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
JpaSystemException.class,
"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) {
context.setCurrentUser(currentUser);
assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser);
@ -139,14 +188,14 @@ class PackageRepositoryIntegrationTest {
void noPackagesAreReturned(final List<PackageEntity> actualResult) {
assertThat(actualResult)
.extracting(PackageEntity::getName)
.isEmpty();
.extracting(PackageEntity::getName)
.isEmpty();
}
void exactlyThesePackagesAreReturned(final List<PackageEntity> actualResult, final String... packageNames) {
assertThat(actualResult)
.extracting(PackageEntity::getName)
.containsExactlyInAnyOrder(packageNames);
.extracting(PackageEntity::getName)
.containsExactlyInAnyOrder(packageNames);
}
}

View File

@ -12,6 +12,6 @@ public class TestPackage {
public static final PackageEntity xxx02 = hsPackage(TestCustomer.xxx, "xxx02");
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);
}
}