From 59784025756175637e676fafc0880e1c01bbc0d7 Mon Sep 17 00:00:00 2001 From: Nat Pryce Date: Mon, 6 Jul 2026 11:16:17 +0100 Subject: [PATCH 1/2] Separate tx4k core and tx4k-jdbc into separate modules --- settings.gradle.kts | 7 ++++++- tx4k/{ => core}/README.md | 0 tx4k/core/build.gradle.kts | 6 ++++++ .../src/main/kotlin/dev/forkhandles/tx/Transactor.kt | 0 .../kotlin/dev/forkhandles/tx/mem/InMemoryTransactor.kt | 0 .../{ => core}/src/main/kotlin/dev/forkhandles/tx/retry.kt | 0 .../test/kotlin/dev/forkhandles/tx/BackoffJitterTest.kt | 0 .../src/test/kotlin/dev/forkhandles/tx/Counter.kt | 0 .../kotlin/dev/forkhandles/tx/IncreasingBackoffTest.kt | 0 .../test/kotlin/dev/forkhandles/tx/TransactorContract.kt | 0 .../dev/forkhandles/tx/mem/InMemoryTransactorTest.kt | 0 tx4k/{ => jdbc}/build.gradle.kts | 4 ++-- .../main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt | 0 .../src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt | 0 .../dev/forkhandles/tx/jdbc/JdbcTransactorContract.kt | 0 .../dev/forkhandles/tx/jdbc/MariaDBTransactorTest.kt | 0 .../dev/forkhandles/tx/jdbc/PostgreSQLTransactorTest.kt | 0 17 files changed, 14 insertions(+), 3 deletions(-) rename tx4k/{ => core}/README.md (100%) create mode 100644 tx4k/core/build.gradle.kts rename tx4k/{ => core}/src/main/kotlin/dev/forkhandles/tx/Transactor.kt (100%) rename tx4k/{ => core}/src/main/kotlin/dev/forkhandles/tx/mem/InMemoryTransactor.kt (100%) rename tx4k/{ => core}/src/main/kotlin/dev/forkhandles/tx/retry.kt (100%) rename tx4k/{ => core}/src/test/kotlin/dev/forkhandles/tx/BackoffJitterTest.kt (100%) rename tx4k/{ => core}/src/test/kotlin/dev/forkhandles/tx/Counter.kt (100%) rename tx4k/{ => core}/src/test/kotlin/dev/forkhandles/tx/IncreasingBackoffTest.kt (100%) rename tx4k/{ => core}/src/test/kotlin/dev/forkhandles/tx/TransactorContract.kt (100%) rename tx4k/{ => core}/src/test/kotlin/dev/forkhandles/tx/mem/InMemoryTransactorTest.kt (100%) rename tx4k/{ => jdbc}/build.gradle.kts (75%) rename tx4k/{ => jdbc}/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt (100%) rename tx4k/{ => jdbc}/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt (100%) rename tx4k/{ => jdbc}/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactorContract.kt (100%) rename tx4k/{ => jdbc}/src/test/kotlin/dev/forkhandles/tx/jdbc/MariaDBTransactorTest.kt (100%) rename tx4k/{ => jdbc}/src/test/kotlin/dev/forkhandles/tx/jdbc/PostgreSQLTransactorTest.kt (100%) diff --git a/settings.gradle.kts b/settings.gradle.kts index 2a9902c..74239be 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,5 +36,10 @@ include("ropes4k") include("state4k") include("time4k") include("tuples4k") -include("tx4k") + +"tx4k".apply { + includeModule("core") + includeSubModule("jdbc") +} + include("values4k") diff --git a/tx4k/README.md b/tx4k/core/README.md similarity index 100% rename from tx4k/README.md rename to tx4k/core/README.md diff --git a/tx4k/core/build.gradle.kts b/tx4k/core/build.gradle.kts new file mode 100644 index 0000000..7ce30bd --- /dev/null +++ b/tx4k/core/build.gradle.kts @@ -0,0 +1,6 @@ +description = "ForkHandles Transactor library" + +dependencies { + testImplementation(kotlin("test-junit5")) + testImplementation(libs.bundles.junit) +} diff --git a/tx4k/src/main/kotlin/dev/forkhandles/tx/Transactor.kt b/tx4k/core/src/main/kotlin/dev/forkhandles/tx/Transactor.kt similarity index 100% rename from tx4k/src/main/kotlin/dev/forkhandles/tx/Transactor.kt rename to tx4k/core/src/main/kotlin/dev/forkhandles/tx/Transactor.kt diff --git a/tx4k/src/main/kotlin/dev/forkhandles/tx/mem/InMemoryTransactor.kt b/tx4k/core/src/main/kotlin/dev/forkhandles/tx/mem/InMemoryTransactor.kt similarity index 100% rename from tx4k/src/main/kotlin/dev/forkhandles/tx/mem/InMemoryTransactor.kt rename to tx4k/core/src/main/kotlin/dev/forkhandles/tx/mem/InMemoryTransactor.kt diff --git a/tx4k/src/main/kotlin/dev/forkhandles/tx/retry.kt b/tx4k/core/src/main/kotlin/dev/forkhandles/tx/retry.kt similarity index 100% rename from tx4k/src/main/kotlin/dev/forkhandles/tx/retry.kt rename to tx4k/core/src/main/kotlin/dev/forkhandles/tx/retry.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/BackoffJitterTest.kt b/tx4k/core/src/test/kotlin/dev/forkhandles/tx/BackoffJitterTest.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/BackoffJitterTest.kt rename to tx4k/core/src/test/kotlin/dev/forkhandles/tx/BackoffJitterTest.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/Counter.kt b/tx4k/core/src/test/kotlin/dev/forkhandles/tx/Counter.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/Counter.kt rename to tx4k/core/src/test/kotlin/dev/forkhandles/tx/Counter.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/IncreasingBackoffTest.kt b/tx4k/core/src/test/kotlin/dev/forkhandles/tx/IncreasingBackoffTest.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/IncreasingBackoffTest.kt rename to tx4k/core/src/test/kotlin/dev/forkhandles/tx/IncreasingBackoffTest.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/TransactorContract.kt b/tx4k/core/src/test/kotlin/dev/forkhandles/tx/TransactorContract.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/TransactorContract.kt rename to tx4k/core/src/test/kotlin/dev/forkhandles/tx/TransactorContract.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/mem/InMemoryTransactorTest.kt b/tx4k/core/src/test/kotlin/dev/forkhandles/tx/mem/InMemoryTransactorTest.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/mem/InMemoryTransactorTest.kt rename to tx4k/core/src/test/kotlin/dev/forkhandles/tx/mem/InMemoryTransactorTest.kt diff --git a/tx4k/build.gradle.kts b/tx4k/jdbc/build.gradle.kts similarity index 75% rename from tx4k/build.gradle.kts rename to tx4k/jdbc/build.gradle.kts index 9d9f0ea..b3c91de 100644 --- a/tx4k/build.gradle.kts +++ b/tx4k/jdbc/build.gradle.kts @@ -1,9 +1,9 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile - description = "ForkHandles Transactor library" dependencies { + api(project(":tx4k")) testImplementation(kotlin("test-junit5")) + testImplementation(project(path = ":tx4k", configuration = "testArtifacts")) testImplementation(libs.bundles.junit) testImplementation(libs.bundles.testcontainers) testImplementation(libs.bundles.testcontainers.postgres) diff --git a/tx4k/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt b/tx4k/jdbc/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt similarity index 100% rename from tx4k/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt rename to tx4k/jdbc/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt rename to tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactorContract.kt b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactorContract.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactorContract.kt rename to tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactorContract.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/MariaDBTransactorTest.kt b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/MariaDBTransactorTest.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/MariaDBTransactorTest.kt rename to tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/MariaDBTransactorTest.kt diff --git a/tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/PostgreSQLTransactorTest.kt b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/PostgreSQLTransactorTest.kt similarity index 100% rename from tx4k/src/test/kotlin/dev/forkhandles/tx/jdbc/PostgreSQLTransactorTest.kt rename to tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/PostgreSQLTransactorTest.kt From 5e23b6633b62e4800d0d49525091898fb1068431 Mon Sep 17 00:00:00 2001 From: Nat Pryce Date: Mon, 6 Jul 2026 11:24:48 +0100 Subject: [PATCH 2/2] Tx4k JPA Transactor tested against Hibernate and HSQLDB --- build.gradle.kts | 2 + gradle/libs.versions.toml | 6 ++ settings.gradle.kts | 1 + tx4k/jdbc/build.gradle.kts | 4 +- .../dev/forkhandles/tx/jdbc/JdbcTransactor.kt | 2 +- .../tx/jdbc/HsqldbInMemoryTransactorTest.kt | 53 ++++++++++++++++ .../dev/forkhandles/tx/jdbc/JdbcCounter.kt | 2 +- tx4k/jpa/build.gradle.kts | 21 +++++++ .../dev/forkhandles/tx/jpa/JpaTransactor.kt | 61 ++++++++++++++++++ .../jpa/HibernateHsqldbJpaTransactorTest.kt | 62 +++++++++++++++++++ .../dev/forkhandles/tx/jpa/JpaCounter.kt | 43 +++++++++++++ .../tx/jpa/JpaTransactorContract.kt | 20 ++++++ 12 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/HsqldbInMemoryTransactorTest.kt create mode 100644 tx4k/jpa/build.gradle.kts create mode 100644 tx4k/jpa/src/main/kotlin/dev/forkhandles/tx/jpa/JpaTransactor.kt create mode 100644 tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/HibernateHsqldbJpaTransactorTest.kt create mode 100644 tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaCounter.kt create mode 100644 tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaTransactorContract.kt diff --git a/build.gradle.kts b/build.gradle.kts index 83a9288..2c317b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ plugins { alias(libs.plugins.maven.publish) alias(libs.plugins.versions) alias(libs.plugins.version.catalog.update) + kotlin("plugin.jpa") version "2.3.21" } buildscript { @@ -204,6 +205,7 @@ dependencies { .forEach { api(project(it.name)) } + implementation(kotlin("stdlib")) } sourceSets { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a482dc5..e886a98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,12 @@ [versions] champeau-jmh = "0.7.3" hamkrest = "1.8.0.1" +hibernate = "7.4.4.Final" +hsqldb = "2.7.2" jackson = "3.1.3" jmh = "1.37" jmhreport = "0.9.6" +jpa = "3.2.0" junit = "5.11.4" junit-platform-launcher = "1.9.3" kotest = "6.1.11" @@ -22,9 +25,12 @@ versions = "0.54.0" [libraries] hamkrest = { module = "com.natpryce:hamkrest", version.ref = "hamkrest" } +hibernate = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate" } +hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" } jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-bytecode = { module = "org.openjdk.jmh:jmh-generator-bytecode", version.ref = "jmh" } +jpa = { module = "jakarta.persistence:jakarta.persistence-api", version.ref = "jpa" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 74239be..99a399b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -40,6 +40,7 @@ include("tuples4k") "tx4k".apply { includeModule("core") includeSubModule("jdbc") + includeSubModule("jpa") } include("values4k") diff --git a/tx4k/jdbc/build.gradle.kts b/tx4k/jdbc/build.gradle.kts index b3c91de..b78737f 100644 --- a/tx4k/jdbc/build.gradle.kts +++ b/tx4k/jdbc/build.gradle.kts @@ -1,4 +1,5 @@ -description = "ForkHandles Transactor library" +description = "ForkHandles Transactor library implementation for JDBC" + dependencies { api(project(":tx4k")) @@ -8,4 +9,5 @@ dependencies { testImplementation(libs.bundles.testcontainers) testImplementation(libs.bundles.testcontainers.postgres) testImplementation(libs.bundles.testcontainers.mariadb) + testImplementation("org.hsqldb:hsqldb:2.7.2") } diff --git a/tx4k/jdbc/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt b/tx4k/jdbc/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt index 004953d..4e163fc 100644 --- a/tx4k/jdbc/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt +++ b/tx4k/jdbc/src/main/kotlin/dev/forkhandles/tx/jdbc/JdbcTransactor.kt @@ -45,5 +45,5 @@ class JdbcTransactor( } -fun jdbcStandardRetryability(e: Exception): Boolean = +fun jdbcStandardRetryability(e: Throwable): Boolean = e is SQLException && (e.sqlState == "40001" || e.sqlState == "40P01") diff --git a/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/HsqldbInMemoryTransactorTest.kt b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/HsqldbInMemoryTransactorTest.kt new file mode 100644 index 0000000..acf34f8 --- /dev/null +++ b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/HsqldbInMemoryTransactorTest.kt @@ -0,0 +1,53 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package dev.forkhandles.tx.jdbc + +import dev.forkhandles.tx.Transactional +import org.hsqldb.jdbc.JDBCDataSource +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestInfo +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.postgresql.PostgreSQLContainer +import kotlin.uuid.ExperimentalUuidApi + +class HsqldbInMemoryTransactorTest : JdbcTransactorContract() { + override lateinit var transactor: Transactional + + @BeforeEach + fun createCounter(testInfo: TestInfo) { + val testName = testInfo.testMethod.map { it.name }.orElseThrow() + transactor = JdbcTransactor( + createConnection = { dataSource.connection }, + createWrapper = { JdbcCounter(it, testName) } + ) + + transactor.perform { it.init() } + } + + companion object { + val dataSource = JDBCDataSource().apply { + database = "jdbc:hsqldb:mem:${HsqldbInMemoryTransactorTest::class.simpleName}" + } + + @BeforeAll + @JvmStatic + fun createSchema() { + dataSource.connection.use(::createSchema) + } + + @AfterAll + @JvmStatic + fun closeDataSource() { + dataSource.connection.use { c -> + c.createStatement().use { s -> + s.execute("SHUTDOWN") + } + } + } + } +} + diff --git a/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt index 3dbb140..507d23d 100644 --- a/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt +++ b/tx4k/jdbc/src/test/kotlin/dev/forkhandles/tx/jdbc/JdbcCounter.kt @@ -56,7 +56,7 @@ fun createSchema(c: Connection): Boolean = c.createStatement().use { s -> """ create table COUNTER ( id VARCHAR(64) PRIMARY KEY, - count NUMERIC(8) NOT NULL DEFAULT 0 + count NUMERIC(8) DEFAULT 0 NOT NULL ) """ ) diff --git a/tx4k/jpa/build.gradle.kts b/tx4k/jpa/build.gradle.kts new file mode 100644 index 0000000..6de056b --- /dev/null +++ b/tx4k/jpa/build.gradle.kts @@ -0,0 +1,21 @@ +description = "ForkHandles Transactor library implementation for JPA" + +plugins { + kotlin("plugin.jpa") +} + +dependencies { + api(project(":tx4k")) + implementation(project(":tx4k-jdbc")) + compileOnly(libs.jpa) + testImplementation(kotlin("test-junit5")) + testImplementation(libs.hibernate) + testImplementation(project(path = ":tx4k", configuration = "testArtifacts")) + testImplementation(libs.hsqldb) + testImplementation(libs.bundles.junit) + implementation(kotlin("stdlib")) +} + +repositories { + mavenCentral() +} diff --git a/tx4k/jpa/src/main/kotlin/dev/forkhandles/tx/jpa/JpaTransactor.kt b/tx4k/jpa/src/main/kotlin/dev/forkhandles/tx/jpa/JpaTransactor.kt new file mode 100644 index 0000000..bb52c5a --- /dev/null +++ b/tx4k/jpa/src/main/kotlin/dev/forkhandles/tx/jpa/JpaTransactor.kt @@ -0,0 +1,61 @@ +package dev.forkhandles.tx.jpa + +import dev.forkhandles.tx.RetryPolicy +import dev.forkhandles.tx.Transactor +import dev.forkhandles.tx.increasingBackoff +import dev.forkhandles.tx.jdbc.jdbcStandardRetryability +import dev.forkhandles.tx.withAdditiveJitter +import dev.forkhandles.tx.maxAttempts +import jakarta.persistence.EntityManager +import jakarta.persistence.EntityManagerFactory +import jakarta.persistence.RollbackException +import java.time.Duration + + +class JpaTransactor( + private val emf: EntityManagerFactory, + private val retryPolicy: RetryPolicy = + increasingBackoff(Duration.ofMillis(50)) + .withAdditiveJitter() + .maxAttempts(5), + private val retryableFailurePolicy: (RollbackException) -> Boolean = + ::jpaDefaultRetryability, + private val createWrapper: (EntityManager) -> API + +) : Transactor() { + override fun createResource(): EntityManager = + emf.createEntityManager() + + override fun configureResource(resource: EntityManager) { + } + + override fun destroyResource(resource: EntityManager) = + resource.close() + + override fun createApi(resource: EntityManager): API = + createWrapper(resource) + + override fun startTransaction(resource: EntityManager) { + resource.transaction.begin() + } + + override fun rollbackTransaction(resource: EntityManager) { + resource.transaction.rollback() + } + + override fun commitTransaction(resource: EntityManager) { + resource.transaction.commit() + } + + override fun canRetry(e: Exception): Boolean = + e is RollbackException && retryableFailurePolicy(e) + + override fun retryBackoff(attempt: Int): Duration? = + retryPolicy(attempt) +} + +fun jpaDefaultRetryability(e: RollbackException): Boolean = + e.causes().any(::jdbcStandardRetryability) + +private fun Throwable.causes(): Sequence = + generateSequence(this) { it.cause } diff --git a/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/HibernateHsqldbJpaTransactorTest.kt b/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/HibernateHsqldbJpaTransactorTest.kt new file mode 100644 index 0000000..26aa219 --- /dev/null +++ b/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/HibernateHsqldbJpaTransactorTest.kt @@ -0,0 +1,62 @@ +package dev.forkhandles.tx.jpa + +import dev.forkhandles.tx.Transactional +import jakarta.persistence.EntityManagerFactory +import jakarta.persistence.PersistenceConfiguration +import jakarta.persistence.PersistenceConfiguration.JDBC_URL +import jakarta.persistence.PersistenceConfiguration.SCHEMAGEN_DATABASE_ACTION +import org.hibernate.cfg.JdbcSettings.AUTOCOMMIT +import org.hibernate.cfg.JdbcSettings.CONNECTION_HANDLING +import org.hibernate.cfg.JdbcSettings.DIALECT +import org.hibernate.cfg.JdbcSettings.ISOLATION +import org.hibernate.dialect.HSQLDialect +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode.IMMEDIATE_ACQUISITION_AND_HOLD +import org.hibernate.tool.schema.Action.SPEC_ACTION_DROP_AND_CREATE +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestInfo +import java.sql.Connection.TRANSACTION_SERIALIZABLE + + +class HibernateHsqldbJpaTransactorTest : JpaTransactorContract() { + override lateinit var transactor: Transactional + + @BeforeEach + fun createCounter(testInfo: TestInfo) { + transactor = JpaTransactor(emf) { em -> + JpaCounter(em, testInfo.testMethod.map { it.name }.orElseThrow()) + } + + transactor.perform { it.init() } + } + + companion object { + val persistenceUnitName = HibernateHsqldbJpaTransactorTest::class.simpleName + + val persistenceConfiguration: PersistenceConfiguration = + PersistenceConfiguration(persistenceUnitName) + .managedClass(NamedCount::class.java) + .property(JDBC_URL, "jdbc:hsqldb:mem:${persistenceUnitName}") + .property(SCHEMAGEN_DATABASE_ACTION, SPEC_ACTION_DROP_AND_CREATE) + .property(DIALECT, HSQLDialect()) + .property(CONNECTION_HANDLING, IMMEDIATE_ACQUISITION_AND_HOLD) + .property(AUTOCOMMIT, false) + .property(ISOLATION, TRANSACTION_SERIALIZABLE) + + lateinit var emf: EntityManagerFactory + + @BeforeAll + @JvmStatic + fun createSchema() { + emf = persistenceConfiguration.createEntityManagerFactory() + } + + @AfterAll + @JvmStatic + fun cleanUp() { + if (::emf.isInitialized) emf.close() + } + } +} + diff --git a/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaCounter.kt b/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaCounter.kt new file mode 100644 index 0000000..e8ad529 --- /dev/null +++ b/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaCounter.kt @@ -0,0 +1,43 @@ +package dev.forkhandles.tx.jpa + +import dev.forkhandles.tx.Counter +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityManager +import jakarta.persistence.Id + +@Entity + +class NamedCount( + @Id + var id: String? = null, + + @Column + var value: Int = 0 +) + +class JpaCounter( + val entityManager: EntityManager, + val name: String +) : Counter { + fun init() { + entityManager.persist(NamedCount(name)) + } + + override fun incrementBy(n: Int) { + val count = loadCount() + count.value += n + } + + override fun count(): Int { + return loadCount().value + } + + private fun loadCount(): NamedCount = + entityManager.find(NamedCount::class.java, name) + ?: error("no count found for name: $name") + + fun causeUnrecoverableFailure() { + entityManager.persist(NamedCount(null, 0)) + } +} diff --git a/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaTransactorContract.kt b/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaTransactorContract.kt new file mode 100644 index 0000000..65d54b3 --- /dev/null +++ b/tx4k/jpa/src/test/kotlin/dev/forkhandles/tx/jpa/JpaTransactorContract.kt @@ -0,0 +1,20 @@ +package dev.forkhandles.tx.jpa + +import dev.forkhandles.tx.Transactional +import dev.forkhandles.tx.TransactorContract +import org.junit.jupiter.api.assertThrows +import java.sql.SQLException +import kotlin.test.Test + +abstract class JpaTransactorContract : TransactorContract() { + abstract override val transactor: Transactional + + @Test + fun `passes through unrecoverable database errors without retry`() { + assertThrows { + transactor.perform { counter -> + counter.causeUnrecoverableFailure() + } + } + } +}