diff --git a/pom.xml b/pom.xml
index 1cb9792e..a6bb4427 100644
--- a/pom.xml
+++ b/pom.xml
@@ -158,9 +158,9 @@
test
- mysql
- mysql-connector-java
- 8.0.33
+ com.mysql
+ mysql-connector-j
+ 8.2.0
test
@@ -286,6 +286,15 @@
transactionoutbox-spring
+
+ java-21-modules
+
+ [21,)
+
+
+ transactionoutbox-virtthreads
+
+
release
diff --git a/transactionoutbox-acceptance/pom.xml b/transactionoutbox-acceptance/pom.xml
index 64570f7c..99704440 100644
--- a/transactionoutbox-acceptance/pom.xml
+++ b/transactionoutbox-acceptance/pom.xml
@@ -61,8 +61,8 @@
ojdbc11
- mysql
- mysql-connector-java
+ com.mysql
+ mysql-connector-j
com.h2database
diff --git a/transactionoutbox-jooq/pom.xml b/transactionoutbox-jooq/pom.xml
index e449c602..c01e5916 100644
--- a/transactionoutbox-jooq/pom.xml
+++ b/transactionoutbox-jooq/pom.xml
@@ -71,8 +71,8 @@
ojdbc11
- mysql
- mysql-connector-java
+ com.mysql
+ mysql-connector-j
diff --git a/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java b/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java
index d4f16c5f..2e48f70f 100644
--- a/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java
+++ b/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java
@@ -1,11 +1,12 @@
package com.gruelbox.transactionoutbox.testing;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.*;
import com.gruelbox.transactionoutbox.*;
-import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.DriverManager;
@@ -22,40 +23,22 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
-import lombok.Builder;
import lombok.SneakyThrows;
-import lombok.Value;
-import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
-import org.hamcrest.MatcherAssert;
-import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
-public abstract class AbstractAcceptanceTest {
+public abstract class AbstractAcceptanceTest extends BaseTest {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAcceptanceTest.class);
private final ExecutorService unreliablePool =
new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(16));
private static final Random random = new Random();
- protected HikariDataSource dataSource;
-
- @BeforeEach
- final void baseBeforeEach() {
- HikariConfig config = new HikariConfig();
- config.setJdbcUrl(connectionDetails().url());
- config.setUsername(connectionDetails().user());
- config.setPassword(connectionDetails().password());
- config.addDataSourceProperty("cachePrepStmts", "true");
- dataSource = new HikariDataSource(config);
- }
-
- @AfterEach
- final void baseAfterEach() {
- dataSource.close();
- }
/**
* Uses a simple direct transaction manager and connection manager and attempts to fire an
@@ -107,15 +90,15 @@ public void success(TransactionOutboxEntry entry) {
outbox.schedule(InterfaceProcessor.class).process(3, "Whee");
try {
// Should not be fired until after commit
- assertFalse(latch.await(2, TimeUnit.SECONDS));
+ assertFalse(latch.await(2, SECONDS));
} catch (InterruptedException e) {
fail("Interrupted");
}
});
// Should be fired after commit
- assertTrue(chainedLatch.await(2, TimeUnit.SECONDS));
- assertTrue(latch.await(1, TimeUnit.SECONDS));
+ assertTrue(chainedLatch.await(2, SECONDS));
+ assertTrue(latch.await(1, SECONDS));
assertTrue(gotScheduled.get());
}
@@ -244,7 +227,7 @@ public void success(TransactionOutboxEntry entry) {
.schedule(ClassProcessor.class)
.process("6"));
- MatcherAssert.assertThat(ids, containsInAnyOrder("1", "2", "4", "6"));
+ assertThat(ids, containsInAnyOrder("1", "2", "4", "6"));
}
/**
@@ -272,7 +255,7 @@ final void dataSourceConnectionProviderReflectionInstantiatorConcreteClass()
transactionManager.inTransaction(() -> outbox.schedule(ClassProcessor.class).process(myId));
- assertTrue(latch.await(2, TimeUnit.SECONDS));
+ assertTrue(latch.await(2, SECONDS));
assertEquals(List.of(myId), ClassProcessor.PROCESSED);
}
}
@@ -364,7 +347,7 @@ public T requireTransactionReturns(
}
postCommitHooks.forEach(Runnable::run);
- assertTrue(latch.await(2, TimeUnit.SECONDS));
+ assertTrue(latch.await(2, SECONDS));
assertEquals(List.of(myId), ClassProcessor.PROCESSED);
}
}
@@ -395,7 +378,7 @@ final void retryBehaviour() throws Exception {
() -> {
transactionManager.inTransaction(
() -> outbox.schedule(InterfaceProcessor.class).process(3, "Whee"));
- assertTrue(latch.await(15, TimeUnit.SECONDS));
+ assertTrue(latch.await(15, SECONDS));
});
}
@@ -467,12 +450,12 @@ final void lastAttemptTime_updatesEveryTime() throws Exception {
() -> {
transactionManager.inTransaction(
() -> outbox.schedule(InterfaceProcessor.class).process(3, "Whee"));
- assertTrue(blockLatch.await(10, TimeUnit.SECONDS));
+ assertTrue(blockLatch.await(10, SECONDS));
assertTrue(
(Boolean)
transactionManager.inTransactionReturns(
tx -> outbox.unblock(orderedEntryListener.getBlocked().getId())));
- assertTrue(successLatch.await(10, TimeUnit.SECONDS));
+ assertTrue(successLatch.await(10, SECONDS));
var orderedEntryEvents = orderedEntryListener.getOrderedEntries();
log.info("The entry life cycle is: {}", orderedEntryEvents);
@@ -521,12 +504,12 @@ final void blockAndThenUnblockForRetry() throws Exception {
() -> {
transactionManager.inTransaction(
() -> outbox.schedule(InterfaceProcessor.class).process(3, "Whee"));
- assertTrue(blockLatch.await(3, TimeUnit.SECONDS));
+ assertTrue(blockLatch.await(3, SECONDS));
assertTrue(
(Boolean)
transactionManager.inTransactionReturns(
tx -> outbox.unblock(latchListener.getBlocked().getId())));
- assertTrue(successLatch.await(3, TimeUnit.SECONDS));
+ assertTrue(successLatch.await(3, SECONDS));
});
}
@@ -574,79 +557,17 @@ public void success(TransactionOutboxEntry entry) {
outbox.schedule(InterfaceProcessor.class).process(i * 10 + j, "Whee");
}
}));
- assertTrue(latch.await(30, TimeUnit.SECONDS), "Latch not opened in time");
+ assertTrue(latch.await(30, SECONDS), "Latch not opened in time");
});
- MatcherAssert.assertThat(
+ assertThat(
"Should never get duplicates running to full completion", duplicates.keySet(), empty());
- MatcherAssert.assertThat(
+ assertThat(
"Only got: " + results.keySet(),
results.keySet(),
containsInAnyOrder(IntStream.range(0, count * 10).boxed().toArray()));
}
- protected ConnectionDetails connectionDetails() {
- return ConnectionDetails.builder()
- .dialect(Dialect.H2)
- .driverClassName("org.h2.Driver")
- .url(
- "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE;DATABASE_TO_UPPER=FALSE")
- .user("test")
- .password("test")
- .build();
- }
-
- protected TransactionManager txManager() {
- return TransactionManager.fromDataSource(dataSource);
- }
-
- protected Persistor persistor() {
- return Persistor.forDialect(connectionDetails().dialect());
- }
-
- protected void clearOutbox() {
- DefaultPersistor persistor = Persistor.forDialect(connectionDetails().dialect());
- TransactionManager transactionManager = txManager();
- transactionManager.inTransaction(
- tx -> {
- try {
- persistor.clear(tx);
- } catch (SQLException e) {
- throw new RuntimeException(e);
- }
- });
- }
-
- protected void withRunningFlusher(TransactionOutbox outbox, ThrowingRunnable runnable)
- throws Exception {
- Thread backgroundThread =
- new Thread(
- () -> {
- while (!Thread.interrupted()) {
- try {
- // Keep flushing work until there's nothing left to flush
- //noinspection StatementWithEmptyBody
- while (outbox.flush()) {}
- } catch (Exception e) {
- log.error("Error flushing transaction outbox. Pausing", e);
- }
- try {
- //noinspection BusyWait
- Thread.sleep(250);
- } catch (InterruptedException e) {
- break;
- }
- }
- });
- backgroundThread.start();
- try {
- runnable.run();
- } finally {
- backgroundThread.interrupt();
- backgroundThread.join();
- }
- }
-
private static class FailingInstantiator implements Instantiator {
private final AtomicInteger attempts;
@@ -699,15 +620,4 @@ public Object getInstance(String name) {
}
}
}
-
- @Value
- @Accessors(fluent = true)
- @Builder
- public static class ConnectionDetails {
- String driverClassName;
- String url;
- String user;
- String password;
- Dialect dialect;
- }
}
diff --git a/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/BaseTest.java b/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/BaseTest.java
new file mode 100644
index 00000000..7cf9ed2f
--- /dev/null
+++ b/transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/BaseTest.java
@@ -0,0 +1,106 @@
+package com.gruelbox.transactionoutbox.testing;
+
+import com.gruelbox.transactionoutbox.*;
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import java.sql.SQLException;
+import lombok.Builder;
+import lombok.Value;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+@Slf4j
+public abstract class BaseTest {
+
+ protected HikariDataSource dataSource;
+
+ @BeforeEach
+ final void baseBeforeEach() {
+ HikariConfig config = new HikariConfig();
+ config.setJdbcUrl(connectionDetails().url());
+ config.setUsername(connectionDetails().user());
+ config.setPassword(connectionDetails().password());
+ config.addDataSourceProperty("cachePrepStmts", "true");
+ dataSource = new HikariDataSource(config);
+ }
+
+ @AfterEach
+ final void baseAfterEach() {
+ dataSource.close();
+ }
+
+ protected ConnectionDetails connectionDetails() {
+ return ConnectionDetails.builder()
+ .dialect(Dialect.H2)
+ .driverClassName("org.h2.Driver")
+ .url(
+ "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE;DATABASE_TO_UPPER=FALSE")
+ .user("test")
+ .password("test")
+ .build();
+ }
+
+ protected TransactionManager txManager() {
+ return TransactionManager.fromDataSource(dataSource);
+ }
+
+ protected Persistor persistor() {
+ return Persistor.forDialect(connectionDetails().dialect());
+ }
+
+ protected void clearOutbox() {
+ DefaultPersistor persistor = Persistor.forDialect(connectionDetails().dialect());
+ TransactionManager transactionManager = txManager();
+ transactionManager.inTransaction(
+ tx -> {
+ try {
+ persistor.clear(tx);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ protected void withRunningFlusher(TransactionOutbox outbox, ThrowingRunnable runnable)
+ throws Exception {
+ Thread backgroundThread =
+ new Thread(
+ () -> {
+ while (!Thread.interrupted()) {
+ try {
+ // Keep flushing work until there's nothing left to flush
+ //noinspection StatementWithEmptyBody
+ while (outbox.flush()) {}
+ } catch (Exception e) {
+ log.error("Error flushing transaction outbox. Pausing", e);
+ }
+ try {
+ //noinspection BusyWait
+ Thread.sleep(250);
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ });
+ backgroundThread.start();
+ try {
+ runnable.run();
+ } finally {
+ backgroundThread.interrupt();
+ backgroundThread.join();
+ }
+ }
+
+ @Value
+ @Accessors(fluent = true)
+ @Builder
+ public static class ConnectionDetails {
+ String driverClassName;
+ String url;
+ String user;
+ String password;
+ Dialect dialect;
+ }
+}
diff --git a/transactionoutbox-testing/src/main/resources/logback-test.xml b/transactionoutbox-testing/src/main/resources/logback-test.xml
new file mode 100644
index 00000000..96692642
--- /dev/null
+++ b/transactionoutbox-testing/src/main/resources/logback-test.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n
+
+
+
+
+
+
diff --git a/transactionoutbox-virtthreads/pom.xml b/transactionoutbox-virtthreads/pom.xml
new file mode 100644
index 00000000..4db8afdb
--- /dev/null
+++ b/transactionoutbox-virtthreads/pom.xml
@@ -0,0 +1,88 @@
+
+
+
+ transactionoutbox-parent
+ com.gruelbox
+ ${revision}
+
+ 4.0.0
+ Transaction Outbox Virtual Threads support
+ jar
+ transactionoutbox-virtthreads
+ A safe implementation of the transactional outbox pattern for Java (core library)
+
+ 21
+ 21
+
+
+
+ com.gruelbox
+ transactionoutbox-core
+ ${project.version}
+ test
+
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+ com.gruelbox
+ transactionoutbox-testing
+ ${project.version}
+ test
+
+
+ com.gruelbox
+ transactionoutbox-jooq
+ ${project.version}
+ test
+
+
+ me.escoffier.loom
+ loom-unit
+ 0.3.0
+ test
+
+
+ org.testcontainers
+ testcontainers
+
+
+ org.testcontainers
+ junit-jupiter
+
+
+ org.testcontainers
+ postgresql
+
+
+ org.testcontainers
+ oracle-xe
+
+
+ org.testcontainers
+ mysql
+
+
+ org.postgresql
+ postgresql
+
+
+ com.oracle.database.jdbc
+ ojdbc11
+
+
+ com.mysql
+ mysql-connector-j
+
+
+ com.h2database
+ h2
+
+
+
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/AbstractVirtualThreadsTest.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/AbstractVirtualThreadsTest.java
new file mode 100644
index 00000000..59e4baa6
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/AbstractVirtualThreadsTest.java
@@ -0,0 +1,105 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.gruelbox.transactionoutbox.*;
+import com.gruelbox.transactionoutbox.testing.BaseTest;
+import com.gruelbox.transactionoutbox.testing.InterfaceProcessor;
+import java.time.Duration;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+import lombok.extern.slf4j.Slf4j;
+import me.escoffier.loom.loomunit.LoomUnitExtension;
+import me.escoffier.loom.loomunit.ShouldNotPin;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@Slf4j
+@ExtendWith(LoomUnitExtension.class)
+abstract class AbstractVirtualThreadsTest extends BaseTest {
+
+ private static final String VIRTUAL_THREAD_SCHEDULER_PARALLELISM =
+ "jdk.virtualThreadScheduler.parallelism";
+
+ @Test
+ @ShouldNotPin
+ final void highVolumeVirtualThreads() throws Exception {
+ var count = 10;
+ var latch = new CountDownLatch(count * 10);
+ var transactionManager = txManager();
+ var results = new ConcurrentHashMap();
+ var duplicates = new ConcurrentHashMap();
+ var outbox =
+ TransactionOutbox.builder()
+ .transactionManager(transactionManager)
+ .persistor(Persistor.forDialect(connectionDetails().dialect()))
+ .instantiator(Instantiator.using(clazz -> (InterfaceProcessor) (foo, bar) -> {}))
+ .submitter(
+ Submitter.withExecutor(
+ r -> Thread.ofVirtual().name(UUID.randomUUID().toString()).start(r)))
+ .attemptFrequency(Duration.ofMillis(500))
+ .flushBatchSize(1000)
+ .listener(
+ new TransactionOutboxListener() {
+ @Override
+ public void success(TransactionOutboxEntry entry) {
+ Integer i = (Integer) entry.getInvocation().getArgs()[0];
+ if (results.putIfAbsent(i, i) != null) {
+ duplicates.put(i, i);
+ }
+ latch.countDown();
+ }
+ })
+ .build();
+
+ var parallelism = System.getProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM);
+ System.setProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM, "1");
+ try {
+ withRunningFlusher(
+ outbox,
+ () -> {
+ var futures =
+ IntStream.range(0, count)
+ .mapToObj(
+ i ->
+ new FutureTask(
+ () ->
+ transactionManager.inTransaction(
+ () -> {
+ for (int j = 0; j < 10; j++) {
+ outbox
+ .schedule(InterfaceProcessor.class)
+ .process(i * 10 + j, "Whee");
+ }
+ }),
+ null))
+ .toList();
+ futures.forEach(Thread::startVirtualThread);
+ for (var future : futures) {
+ future.get(20, TimeUnit.SECONDS);
+ }
+ assertTrue(latch.await(30, TimeUnit.SECONDS), "Latch not opened in time");
+ });
+ } finally {
+ if (parallelism == null) {
+ System.clearProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM);
+ } else {
+ System.setProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM, parallelism);
+ }
+ }
+
+ assertThat(
+ "Should never get duplicates running to full completion", duplicates.keySet(), empty());
+ assertThat(
+ "Only got: " + results.keySet(),
+ results.keySet(),
+ containsInAnyOrder(IntStream.range(0, count * 10).boxed().toArray()));
+ }
+}
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2.java
new file mode 100644
index 00000000..9495febf
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2.java
@@ -0,0 +1,3 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+public class TestVirtualThreadsH2 extends AbstractVirtualThreadsTest {}
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2Jooq.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2Jooq.java
new file mode 100644
index 00000000..d03a93d3
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2Jooq.java
@@ -0,0 +1,34 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+import com.gruelbox.transactionoutbox.ThreadLocalContextTransactionManager;
+import com.gruelbox.transactionoutbox.jooq.JooqTransactionListener;
+import com.gruelbox.transactionoutbox.jooq.JooqTransactionManager;
+import org.jooq.SQLDialect;
+import org.jooq.impl.DSL;
+import org.jooq.impl.DataSourceConnectionProvider;
+import org.jooq.impl.DefaultConfiguration;
+import org.jooq.impl.ThreadLocalTransactionProvider;
+import org.junit.jupiter.api.BeforeEach;
+
+public class TestVirtualThreadsH2Jooq extends AbstractVirtualThreadsTest {
+
+ private ThreadLocalContextTransactionManager txm;
+
+ @Override
+ protected final ThreadLocalContextTransactionManager txManager() {
+ return txm;
+ }
+
+ @BeforeEach
+ final void beforeEach() {
+ DataSourceConnectionProvider connectionProvider = new DataSourceConnectionProvider(dataSource);
+ DefaultConfiguration configuration = new DefaultConfiguration();
+ configuration.setConnectionProvider(connectionProvider);
+ configuration.setSQLDialect(SQLDialect.H2);
+ configuration.setTransactionProvider(
+ new ThreadLocalTransactionProvider(connectionProvider, true));
+ JooqTransactionListener listener = JooqTransactionManager.createListener();
+ configuration.set(listener);
+ txm = JooqTransactionManager.create(DSL.using(configuration), listener);
+ }
+}
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql5.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql5.java
new file mode 100644
index 00000000..308f579e
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql5.java
@@ -0,0 +1,32 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+import static com.gruelbox.transactionoutbox.Dialect.MY_SQL_5;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Disabled;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SuppressWarnings("WeakerAccess")
+@Testcontainers
+@Disabled
+class TestVirtualThreadsMySql5 extends AbstractVirtualThreadsTest {
+
+ @Container
+ @SuppressWarnings({"rawtypes", "resource"})
+ private static final JdbcDatabaseContainer container =
+ new MySQLContainer<>("mysql:5").withStartupTimeout(Duration.ofHours(1));
+
+ @Override
+ protected ConnectionDetails connectionDetails() {
+ return ConnectionDetails.builder()
+ .dialect(MY_SQL_5)
+ .driverClassName("com.mysql.cj.jdbc.Driver")
+ .url(container.getJdbcUrl())
+ .user(container.getUsername())
+ .password(container.getPassword())
+ .build();
+ }
+}
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql8.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql8.java
new file mode 100644
index 00000000..b2233c7c
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql8.java
@@ -0,0 +1,32 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+import static com.gruelbox.transactionoutbox.Dialect.MY_SQL_8;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Disabled;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SuppressWarnings("WeakerAccess")
+@Testcontainers
+@Disabled
+class TestVirtualThreadsMySql8 extends AbstractVirtualThreadsTest {
+
+ @Container
+ @SuppressWarnings({"rawtypes", "resource"})
+ private static final JdbcDatabaseContainer container =
+ new MySQLContainer<>("mysql:8").withStartupTimeout(Duration.ofMinutes(5));
+
+ @Override
+ protected ConnectionDetails connectionDetails() {
+ return ConnectionDetails.builder()
+ .dialect(MY_SQL_8)
+ .driverClassName("com.mysql.cj.jdbc.Driver")
+ .url(container.getJdbcUrl())
+ .user(container.getUsername())
+ .password(container.getPassword())
+ .build();
+ }
+}
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsOracle21.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsOracle21.java
new file mode 100644
index 00000000..6ccd450e
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsOracle21.java
@@ -0,0 +1,30 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+import static com.gruelbox.transactionoutbox.Dialect.ORACLE;
+
+import java.time.Duration;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.OracleContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SuppressWarnings("WeakerAccess")
+@Testcontainers
+class TestVirtualThreadsOracle21 extends AbstractVirtualThreadsTest {
+
+ @Container
+ @SuppressWarnings("rawtypes")
+ private static final JdbcDatabaseContainer container =
+ new OracleContainer("gvenzl/oracle-xe:21-slim").withStartupTimeout(Duration.ofHours(1));
+
+ @Override
+ protected ConnectionDetails connectionDetails() {
+ return ConnectionDetails.builder()
+ .dialect(ORACLE)
+ .driverClassName("oracle.jdbc.OracleDriver")
+ .url(container.getJdbcUrl())
+ .user(container.getUsername())
+ .password(container.getPassword())
+ .build();
+ }
+}
diff --git a/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsPostgres16.java b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsPostgres16.java
new file mode 100644
index 00000000..4d8b8e34
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsPostgres16.java
@@ -0,0 +1,31 @@
+package com.gruelbox.transactionoutbox.virtthreads;
+
+import static com.gruelbox.transactionoutbox.Dialect.POSTGRESQL_9;
+
+import java.time.Duration;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SuppressWarnings("WeakerAccess")
+@Testcontainers
+class TestVirtualThreadsPostgres16 extends AbstractVirtualThreadsTest {
+
+ @Container
+ @SuppressWarnings({"rawtypes", "resource"})
+ private static final JdbcDatabaseContainer container =
+ (JdbcDatabaseContainer)
+ new PostgreSQLContainer("postgres:16").withStartupTimeout(Duration.ofHours(1));
+
+ @Override
+ protected ConnectionDetails connectionDetails() {
+ return ConnectionDetails.builder()
+ .dialect(POSTGRESQL_9)
+ .driverClassName("org.postgresql.Driver")
+ .url(container.getJdbcUrl())
+ .user(container.getUsername())
+ .password(container.getPassword())
+ .build();
+ }
+}
diff --git a/transactionoutbox-virtthreads/src/test/resources/logback-test.xml b/transactionoutbox-virtthreads/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..96692642
--- /dev/null
+++ b/transactionoutbox-virtthreads/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n
+
+
+
+
+
+