1
0

dd column seqTxId BIGINT to table base.tx_context

This commit is contained in:
Michael Hoennig
2025-05-20 17:38:20 +02:00
parent 2c8ddc4250
commit f4bf614d77
2 changed files with 162 additions and 2 deletions

View File

@@ -34,6 +34,33 @@ create table base.tx_context
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:--//
-- ----------------------------------------------------------------------------
@@ -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.
*/
drop view if exists base.tx_journal_v;
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
left join base.tx_context txc using (txId)
order by txc.txtimestamp;

View File

@@ -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());
}
}