dd column seqTxId BIGINT to table base.tx_context
This commit is contained in:
		| @@ -34,6 +34,33 @@ create table base.tx_context | |||||||
| create index on base.tx_context using brin (txTimestamp); | create index on base.tx_context using brin (txTimestamp); | ||||||
| --// | --// | ||||||
|  |  | ||||||
|  |  | ||||||
|  | -- ============================================================================ | ||||||
|  | --changeset michael.hoennig:audit-TX-CONTEXT-TABLE-COLUMN-SEQUENTIAL-TX-ID endDelimiter:--// | ||||||
|  | -- ---------------------------------------------------------------------------- | ||||||
|  | /* | ||||||
|  |     Adds a column to base.tx_context which keeps a strictly sequentially ordered tx-id. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | alter table base.tx_context | ||||||
|  |     add column seqTxId BIGINT; | ||||||
|  |  | ||||||
|  | CREATE OR REPLACE FUNCTION set_next_sequential_txid() | ||||||
|  |     RETURNS TRIGGER AS $$ | ||||||
|  | BEGIN | ||||||
|  |     LOCK TABLE base.tx_context IN EXCLUSIVE MODE; | ||||||
|  |     SELECT COALESCE(MAX(seqTxId)+1, 0) INTO NEW.seqTxId FROM base.tx_context; | ||||||
|  |     RETURN NEW; | ||||||
|  | END; | ||||||
|  | $$ LANGUAGE plpgsql; | ||||||
|  |  | ||||||
|  | CREATE TRIGGER set_commit_order_trigger | ||||||
|  |     BEFORE INSERT ON base.tx_context | ||||||
|  |     FOR EACH ROW | ||||||
|  | EXECUTE FUNCTION set_next_sequential_txid(); | ||||||
|  | --// | ||||||
|  |  | ||||||
|  |  | ||||||
| -- ============================================================================ | -- ============================================================================ | ||||||
| --changeset michael.hoennig:audit-TX-JOURNAL-TABLE endDelimiter:--// | --changeset michael.hoennig:audit-TX-JOURNAL-TABLE endDelimiter:--// | ||||||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||||||
| @@ -53,13 +80,24 @@ create index on base.tx_journal (targetTable, targetUuid); | |||||||
| --// | --// | ||||||
|  |  | ||||||
| -- ============================================================================ | -- ============================================================================ | ||||||
| --changeset michael.hoennig:audit-TX-JOURNAL-VIEW endDelimiter:--// | --changeset michael.hoennig:audit-TX-JOURNAL-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// | ||||||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||||||
| /* | /* | ||||||
|     A view combining base.tx_journal with base.tx_context. |     A view combining base.tx_journal with base.tx_context. | ||||||
|  */ |  */ | ||||||
|  | drop view if exists base.tx_journal_v; | ||||||
| create view base.tx_journal_v as | create view base.tx_journal_v as | ||||||
| select txc.*, txj.targettable, txj.targetop, txj.targetuuid, txj.targetdelta | select txc.seqTxId, | ||||||
|  |        txc.txId, | ||||||
|  |        txc.txTimeStamp, | ||||||
|  |        txc.currentSubject, | ||||||
|  |        txc.assumedRoles, | ||||||
|  |        txc.currentTask, | ||||||
|  |        txc.currentRequest, | ||||||
|  |        txj.targetTable, | ||||||
|  |        txj.targeTop, | ||||||
|  |        txj.targetUuid, | ||||||
|  |        txj.targetDelta | ||||||
|     from base.tx_journal txj |     from base.tx_journal txj | ||||||
|     left join base.tx_context txc using (txId) |     left join base.tx_context txc using (txId) | ||||||
|     order by txc.txtimestamp; |     order by txc.txtimestamp; | ||||||
|   | |||||||
| @@ -0,0 +1,122 @@ | |||||||
|  | package net.hostsharing.hsadminng.journal; | ||||||
|  |  | ||||||
|  | import lombok.SneakyThrows; | ||||||
|  | import net.hostsharing.hsadminng.context.Context; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.JpaAttempt; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerRepository; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | import org.junit.jupiter.api.Tag; | ||||||
|  | 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.Import; | ||||||
|  | import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||||||
|  | import org.springframework.transaction.PlatformTransactionManager; | ||||||
|  | import org.springframework.transaction.annotation.Transactional; | ||||||
|  |  | ||||||
|  | import jakarta.servlet.http.HttpServletRequest; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | import static org.springframework.transaction.annotation.Propagation.NEVER; | ||||||
|  |  | ||||||
|  | @DataJpaTest | ||||||
|  | @Import({ Context.class, JpaAttempt.class }) | ||||||
|  | @Tag("generalIntegrationTest") | ||||||
|  | class TransactionContextIntegrationTest extends ContextBasedTestWithCleanup { | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private PlatformTransactionManager transactionManager; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     JpaAttempt jpaAttempt; | ||||||
|  |  | ||||||
|  |     @MockitoBean | ||||||
|  |     HttpServletRequest request; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private TestCustomerRepository repository; | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     @Transactional(propagation = NEVER) | ||||||
|  |     void testConcurrentCommitOrder() { | ||||||
|  |  | ||||||
|  |         // determine initial row count | ||||||
|  |         final var rowCount = jpaAttempt.transacted(() -> { | ||||||
|  |             context("superuser-alex@hostsharing.net"); | ||||||
|  |             return em.createQuery("SELECT e FROM TestCustomerEntity e", TestCustomerEntity.class).getResultList(); | ||||||
|  |         }).assertSuccessful().returnedValue().size(); | ||||||
|  |  | ||||||
|  |         // when 3 transactions with different runtime run concurrently | ||||||
|  |         runThreads( | ||||||
|  |                 // starts first, ends last (because it's slow) | ||||||
|  |                 createTransactionThread("t01", 91001, 500), | ||||||
|  |  | ||||||
|  |                 // starts second, ends first (because it's faster than the one that got started first) | ||||||
|  |                 createTransactionThread("t02", 91002, 0), | ||||||
|  |  | ||||||
|  |                 // starts third, ends second | ||||||
|  |                 createTransactionThread("t03", 91003, 100) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         // then all 3 threads did insert one row each | ||||||
|  |         jpaAttempt.transacted(() -> { | ||||||
|  |             context("superuser-alex@hostsharing.net"); | ||||||
|  |             var all = em.createQuery("SELECT e FROM TestCustomerEntity e", TestCustomerEntity.class).getResultList(); | ||||||
|  |             assertThat(all).hasSize(rowCount + 3); | ||||||
|  |         }).assertSuccessful(); | ||||||
|  |  | ||||||
|  |         // and seqTxId order is in correct order | ||||||
|  |         final var txContextsX = em.createNativeQuery( | ||||||
|  |                 "select concat(c.txId, ':', c.currentTask) from base.tx_context c order by c.seqTxId" | ||||||
|  |             ).getResultList(); | ||||||
|  |         final var txContextTasks = last(3, txContextsX).stream().map(Object::toString).toList(); | ||||||
|  |         assertThat(txContextTasks.get(0)).endsWith( | ||||||
|  |                 ":TestCustomerEntity(uuid=null, version=0, prefix=t02, reference=91002, adminUserName=null)"); | ||||||
|  |         assertThat(txContextTasks.get(1)).endsWith( | ||||||
|  |                 "TestCustomerEntity(uuid=null, version=0, prefix=t03, reference=91003, adminUserName=null)"); | ||||||
|  |         assertThat(txContextTasks.get(2)).endsWith( | ||||||
|  |                 "TestCustomerEntity(uuid=null, version=0, prefix=t01, reference=91001, adminUserName=null)"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private @NotNull Thread createTransactionThread(final String t01, final int reference, final int millis) { | ||||||
|  |         return new Thread(() -> { | ||||||
|  |             jpaAttempt.transacted(() -> { | ||||||
|  |                 final var entity1 = new TestCustomerEntity(); | ||||||
|  |                 entity1.setPrefix(t01); | ||||||
|  |                 entity1.setReference(reference); | ||||||
|  |  | ||||||
|  |                 context.define(entity1.toString(), null, "superuser-alex@hostsharing.net", null); | ||||||
|  |                 entity1.setReference(80000 + toInt(em.createNativeQuery("SELECT txid_current()").getSingleResult())); | ||||||
|  |                 repository.save(entity1); | ||||||
|  |                 sleep(millis); // simulate a delay | ||||||
|  |             }).assertSuccessful(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int toInt(final Object singleResult) { | ||||||
|  |         return ((Long)singleResult).intValue(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SneakyThrows | ||||||
|  |     private void sleep(final int millis) { | ||||||
|  |         Thread.sleep(millis); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SneakyThrows | ||||||
|  |     private void runThreads(final Thread... threads) { | ||||||
|  |         for (final Thread thread : threads) { | ||||||
|  |             thread.start(); | ||||||
|  |             sleep(100); | ||||||
|  |         } | ||||||
|  |         for (final Thread thread : threads) { | ||||||
|  |             thread.join(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |     private List<?> last(final int n, final List<?> list) { | ||||||
|  |         return list.subList(Math.max(list.size() - n, 0), list.size()); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user