Skip to content

Commit 83acd2e

Browse files
authored
Pooled Test Containers (#329)
the recent test flakiness is happening because we are spinning up a unique PG container for each test. This PR changes that to a pool of 4 containers, but we create a new DB for each test and drop the DB on test completion. Updated the junit default to run tests concurrently unless otherwise annotated and to run 4 tests in parallel maximum. DRYed out test reporing in the build files + added logic to pring the failing tests at the end of the test run for easier diagnosis
1 parent 0b0366e commit 83acd2e

47 files changed

Lines changed: 184 additions & 110 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle.kts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,44 @@ subprojects {
157157
// use the environment's JDK instead of the toolchain's JDK for tests
158158
tasks.withType<Test> { javaLauncher.set(null as JavaLauncher?) }
159159

160+
tasks.withType<Test> {
161+
useJUnitPlatform()
162+
testLogging {
163+
events("passed", "skipped", "failed")
164+
showStandardStreams = true
165+
}
166+
addTestListener(
167+
object : TestListener {
168+
private val failedTests = mutableListOf<String>()
169+
170+
override fun beforeSuite(suite: TestDescriptor) {}
171+
172+
override fun beforeTest(testDescriptor: TestDescriptor) {}
173+
174+
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {
175+
if (result.resultType == TestResult.ResultType.FAILURE) {
176+
failedTests.add("${testDescriptor.className}.${testDescriptor.name}")
177+
}
178+
}
179+
180+
override fun afterSuite(suite: TestDescriptor, result: TestResult) {
181+
if (suite.parent == null) {
182+
println("\nTest Results:")
183+
println(" Tests run: ${result.testCount}")
184+
println(" Passed: ${result.successfulTestCount}")
185+
println(" Failed: ${result.failedTestCount}")
186+
println(" Skipped: ${result.skippedTestCount}")
187+
188+
if (failedTests.isNotEmpty()) {
189+
println("\nFailed Tests:")
190+
failedTests.forEach { println(" - $it") }
191+
}
192+
}
193+
}
194+
}
195+
)
196+
}
197+
160198
tasks.named<Jar>("jar") {
161199
manifest {
162200
attributes["Implementation-Version"] = project.version

transact-cli/build.gradle.kts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,6 @@ dependencies {
1818
testImplementation("org.testcontainers:testcontainers-postgresql:2.0.3")
1919
}
2020

21-
tasks.test {
22-
useJUnitPlatform()
23-
testLogging {
24-
events("passed", "skipped", "failed")
25-
showStandardStreams = true
26-
}
27-
28-
addTestListener(
29-
object : TestListener {
30-
override fun beforeSuite(suite: TestDescriptor) {}
31-
32-
override fun beforeTest(testDescriptor: TestDescriptor) {}
33-
34-
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {}
35-
36-
override fun afterSuite(suite: TestDescriptor, result: TestResult) {
37-
if (suite.parent == null) {
38-
println("\nTest Results:")
39-
println(" Tests run: ${result.testCount}")
40-
println(" Passed: ${result.successfulTestCount}")
41-
println(" Failed: ${result.failedTestCount}")
42-
println(" Skipped: ${result.skippedTestCount}")
43-
}
44-
}
45-
}
46-
)
47-
}
48-
4921
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
5022
archiveBaseName.set("dbos")
5123
archiveVersion.set("")

transact-cli/src/test/java/dev/dbos/transact/cli/MigrateCommandTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import picocli.CommandLine;
2121

2222
@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES)
23-
@org.junit.jupiter.api.parallel.Execution(org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT)
2423
public class MigrateCommandTest {
2524

2625
@AutoClose final PgContainer pgContainer = new PgContainer();

transact-cli/src/test/java/dev/dbos/transact/cli/PgContainer.java

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,86 @@
33
import java.sql.Connection;
44
import java.sql.DriverManager;
55
import java.sql.SQLException;
6+
import java.util.ArrayList;
67
import java.util.List;
8+
import java.util.UUID;
9+
import java.util.concurrent.ArrayBlockingQueue;
10+
import java.util.concurrent.BlockingQueue;
11+
import java.util.concurrent.Semaphore;
712

813
import org.testcontainers.postgresql.PostgreSQLContainer;
914

1015
public class PgContainer implements AutoCloseable {
11-
private final PostgreSQLContainer pgContainer = new PostgreSQLContainer("postgres:18");
16+
17+
// SIZE should match junit.jupiter.execution.parallel.config.fixed.parallelism value
18+
private static final int SIZE = 4;
19+
private static final BlockingQueue<PostgreSQLContainer> POOL = new ArrayBlockingQueue<>(SIZE);
20+
private static final Semaphore PERMITS = new Semaphore(SIZE);
21+
22+
static {
23+
Runtime.getRuntime()
24+
.addShutdownHook(
25+
new Thread(
26+
() -> {
27+
var containers = new ArrayList<PostgreSQLContainer>();
28+
POOL.drainTo(containers);
29+
containers.forEach(PostgreSQLContainer::stop);
30+
}));
31+
}
32+
33+
static PostgreSQLContainer acquire() {
34+
try {
35+
PERMITS.acquire();
36+
var container = POOL.poll();
37+
if (container == null) {
38+
container = new PostgreSQLContainer("postgres:18");
39+
container.start();
40+
}
41+
return container;
42+
} catch (InterruptedException e) {
43+
throw new RuntimeException(e);
44+
}
45+
}
46+
47+
static void release(PostgreSQLContainer c) {
48+
POOL.offer(c);
49+
PERMITS.release();
50+
}
51+
52+
private final PostgreSQLContainer pgContainer;
53+
private final String jdbcUrl;
54+
private final String dbName;
1255

1356
public PgContainer() {
14-
pgContainer.start();
57+
// take a container from the pool and create a new database for it
58+
pgContainer = acquire();
59+
dbName = "test_" + UUID.randomUUID().toString().replace("-", "");
60+
jdbcUrl = pgContainer.getJdbcUrl().replaceFirst("/[^/]+$", "/" + dbName);
61+
62+
try (var conn =
63+
DriverManager.getConnection(
64+
pgContainer.getJdbcUrl(), pgContainer.getUsername(), pgContainer.getPassword());
65+
var stmt = conn.createStatement()) {
66+
stmt.execute("CREATE DATABASE " + dbName);
67+
} catch (SQLException e) {
68+
throw new RuntimeException(e);
69+
}
1570
}
1671

1772
@Override
1873
public void close() throws Exception {
19-
pgContainer.stop();
74+
// drop a database we created and return the container too the pool
75+
var _jdbcUrl = pgContainer.getJdbcUrl();
76+
try (var conn = DriverManager.getConnection(_jdbcUrl, username(), password());
77+
var stmt = conn.createStatement()) {
78+
var sql = "DROP DATABASE IF EXISTS %s WITH (FORCE)".formatted(dbName);
79+
stmt.execute(sql);
80+
}
81+
release(pgContainer);
2082
}
2183

2284
public String jdbcUrl() {
23-
return pgContainer.getJdbcUrl();
85+
return jdbcUrl;
2486
}
2587

2688
public String username() {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
junit.jupiter.execution.parallel.enabled = true
2+
junit.jupiter.execution.parallel.mode.default = concurrent
3+
junit.jupiter.execution.parallel.mode.classes.default = concurrent
4+
junit.jupiter.execution.parallel.config.strategy = fixed
5+
junit.jupiter.execution.parallel.config.fixed.parallelism = 4

transact/build.gradle.kts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,6 @@ tasks.processResources {
5757
filesMatching("**/app.properties") { expand(mapOf("projectVersion" to projectVersion)) }
5858
}
5959

60-
tasks.test {
61-
useJUnitPlatform()
62-
testLogging {
63-
events("passed", "skipped", "failed")
64-
showStandardStreams = true
65-
}
66-
67-
addTestListener(
68-
object : TestListener {
69-
override fun beforeSuite(suite: TestDescriptor) {}
70-
71-
override fun beforeTest(testDescriptor: TestDescriptor) {}
72-
73-
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {}
74-
75-
override fun afterSuite(suite: TestDescriptor, result: TestResult) {
76-
if (suite.parent == null) {
77-
println("\nTest Results:")
78-
println(" Tests run: ${result.testCount}")
79-
println(" Passed: ${result.successfulTestCount}")
80-
println(" Failed: ${result.failedTestCount}")
81-
println(" Skipped: ${result.skippedTestCount}")
82-
}
83-
}
84-
}
85-
)
86-
}
87-
8860
tasks.withType<KotlinCompile>().configureEach {
8961
compilerOptions {
9062
// jvmTarget now uses the JvmTarget enum instead of a String

transact/src/test/java/dev/dbos/transact/admin/AdminServerTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.mockito.ArgumentCaptor;
3939

4040
@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES)
41-
@org.junit.jupiter.api.parallel.Execution(org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT)
4241
class AdminServerTest {
4342

4443
int port;

transact/src/test/java/dev/dbos/transact/client/ClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
import com.zaxxer.hikari.HikariDataSource;
2323
import org.junit.jupiter.api.*;
24+
import org.junitpioneer.jupiter.RetryingTest;
2425

2526
@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES)
26-
@org.junit.jupiter.api.parallel.Execution(org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT)
2727
public class ClientTest {
2828

2929
@AutoClose final PgContainer pgContainer = new PgContainer();
@@ -104,7 +104,7 @@ public void clientSend() throws Exception {
104104
assertEquals("42-test.message", handle.getResult());
105105
}
106106

107-
@Test
107+
@RetryingTest(3)
108108
public void clientEnqueueTimeouts() throws Exception {
109109
try (var client = pgContainer.dbosClient()) {
110110
var options = new DBOSClient.EnqueueOptions("ClientServiceImpl", "sleep", "testQueue");

transact/src/test/java/dev/dbos/transact/client/EnqueueOptionsTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import org.junit.jupiter.api.Test;
1111

1212
@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES)
13-
@org.junit.jupiter.api.parallel.Execution(org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT)
1413
public class EnqueueOptionsTest {
1514
@Test
1615
public void enqueueOptionsValidation() throws Exception {

transact/src/test/java/dev/dbos/transact/client/PgSqlClientTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.postgresql.util.PSQLException;
3333

3434
@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES)
35-
@org.junit.jupiter.api.parallel.Execution(org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT)
3635
public class PgSqlClientTest {
3736

3837
private static final ObjectMapper MAPPER = new ObjectMapper();

0 commit comments

Comments
 (0)