Skip to content

Commit 7ae3e7e

Browse files
authored
DBOS Instance (#309)
This PR is a **_major breaking change_** to the DBOS public API surface area. We are removing the global singleton and most of the static methods on `DBOS` in favor of using DBOS as an instance. We had already done the work to ensure that everything worked with `DBOS.Instance` directly. However, the combination of static and instance APIs made for a poor developer experience, so we've decided to remove the static API entirely. Mostly, this PR is test work since nearly all the tests used the static API. Updating all the tests allowed us to make a couple other changes - in particular, using [`@AutoClose`](https://docs.junit.org/6.0.3/writing-tests/built-in-extensions.html#AutoClose) to simplify test cleanup and using [TestContainers](https://testcontainers.com/) for database isolation. The combination of DBOS Instance and TestContainers enables us to run tests in parallel, bringing a test run down to 6 minutes from 15. > Major thanks to @hannosgit who did the original TestContainers work! In addition to the Java work, this PR updates the Kotlin DBOSExtension class. The previous DBOSExtension primarily provided top level Kotlin functions for DBOS static methods. Now, the DBOSExtension simply wraps `DBOS.startWorkflow` and `DBOS.runStep` to support [Kotlin trailing lambdas syntax](https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas). We also added a Kotlin test! fixes #306
1 parent f797601 commit 7ae3e7e

77 files changed

Lines changed: 4637 additions & 5423 deletions

File tree

Some content is hidden

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

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ jobs:
1919
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/v')
2020
steps:
2121
- name: Checkout code
22-
uses: actions/checkout@v4
22+
uses: actions/checkout@v6
2323
with:
2424
fetch-depth: 0 # fetch-depth 0 needed for version calculation
2525

2626
- name: Set up OpenJDK 21
27-
uses: actions/setup-java@v4
27+
uses: actions/setup-java@v5
2828
with:
2929
java-version: '21'
3030
distribution: 'temurin'

.github/workflows/test.yml

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,28 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
jdk-version: ['17', '21', '25']
15-
jdk-distro: ['corretto', 'graalvm', 'microsoft', 'oracle', 'semeru', 'temurin', 'zulu' ]
15+
jdk-distro: ['graalvm', 'microsoft', 'oracle', 'temurin' ]
1616
exclude:
17-
- jdk-version: 17
18-
jdk-distro: graalvm
17+
- jdk-distro: graalvm
18+
jdk-version: 17
1919
include:
2020
- jdk-distro: graalvm
2121
jdk-version: 17.0.12
2222

23-
services:
24-
postgres:
25-
image: sibedge/postgres-plv8:16.3-3.2.2
26-
env:
27-
POSTGRES_PASSWORD: dbos
28-
options: >-
29-
--health-cmd pg_isready
30-
--health-interval 10s
31-
--health-timeout 5s
32-
--health-retries 5
33-
ports:
34-
- 5432:5432
35-
3623
steps:
3724
- name: Checkout code
38-
uses: actions/checkout@v4
25+
uses: actions/checkout@v6
3926
with:
4027
fetch-depth: 0 # fetch-depth 0 needed for version calculation
4128

4229
- name: Set up JDK ${{ matrix.jdk-distro }} ${{ matrix.jdk-version }}
43-
uses: actions/setup-java@v4
30+
uses: actions/setup-java@v5
4431
with:
4532
java-version: ${{ matrix.jdk-version }}
4633
distribution: ${{ matrix.jdk-distro }}
4734

4835
- name: Setup Gradle
49-
uses: gradle/actions/setup-gradle@v4
50-
51-
- name: Wait for PostgreSQL
52-
run: |
53-
until pg_isready -h localhost -p 5432 -U postgres; do
54-
echo "Waiting for PostgreSQL..."
55-
sleep 2
56-
done
57-
env:
58-
PGPASSWORD: dbos
36+
uses: gradle/actions/setup-gradle@v5
5937

6038
- name: Run tests
6139
run: ./gradlew clean build
@@ -72,7 +50,7 @@ jobs:
7250
if: always()
7351

7452
- name: Upload JDK ${{ matrix.jdk-distro }} ${{ matrix.jdk-version }} test results
75-
uses: actions/upload-artifact@v4
53+
uses: actions/upload-artifact@v7
7654
if: always()
7755
with:
7856
name: test-results-${{ matrix.jdk-version }}-${{ matrix.jdk-distro }}

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

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,45 @@
11
package dev.dbos.transact.cli;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4-
import static org.junit.jupiter.api.Assertions.assertFalse;
54
import static org.junit.jupiter.api.Assertions.assertTrue;
65

76
import dev.dbos.transact.Constants;
87
import dev.dbos.transact.database.SystemDatabase;
9-
import dev.dbos.transact.migrations.MigrationManager;
108

119
import java.io.PrintWriter;
1210
import java.io.StringWriter;
13-
import java.sql.DriverManager;
1411
import java.sql.SQLException;
15-
import java.util.Objects;
16-
import java.util.concurrent.TimeUnit;
12+
import java.util.Collection;
13+
import java.util.List;
14+
import java.util.stream.Stream;
1715

18-
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.AutoClose;
1917
import org.junit.jupiter.api.Test;
20-
import org.junit.jupiter.api.Timeout;
2118
import org.junit.jupiter.params.ParameterizedTest;
2219
import org.junit.jupiter.params.provider.ValueSource;
2320
import picocli.CommandLine;
2421

25-
@Timeout(value = 2, unit = TimeUnit.MINUTES)
22+
@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)
2624
public class MigrateCommandTest {
2725

28-
static String db_url = "jdbc:postgresql://localhost:5432/migrate_cmd_test";
29-
static String db_user = Objects.requireNonNullElse(System.getenv("PGUSER"), "postgres");
30-
static String db_password = Objects.requireNonNullElse(System.getenv("PGPASSWORD"), "dbos");
31-
32-
@BeforeEach
33-
public void setup() throws Exception {
34-
var pair = MigrationManager.extractDbAndPostgresUrl(db_url);
35-
var dropDbSql = String.format("DROP DATABASE IF EXISTS %s WITH (FORCE)", pair.database());
36-
try (var conn = DriverManager.getConnection(pair.url(), db_user, db_password);
37-
var stmt = conn.createStatement()) {
38-
stmt.execute(dropDbSql);
39-
}
40-
}
26+
@AutoClose final PgContainer pgContainer = new PgContainer();
4127

4228
@Test
4329
public void migrate() throws Exception {
4430

45-
assertFalse(checkConnection());
46-
4731
var cmd = new CommandLine(new DBOSCommand());
4832
var sw = new StringWriter();
4933
cmd.setOut(new PrintWriter(sw));
5034

51-
var exitCode = cmd.execute("migrate", "-D=" + db_url, "-U=" + db_user);
35+
var args =
36+
Stream.of(List.of("migrate"), pgContainer.options())
37+
.flatMap(Collection::stream)
38+
.toArray(String[]::new);
39+
40+
var exitCode = cmd.execute(args);
5241
assertEquals(0, exitCode);
5342

54-
assertTrue(checkConnection());
5543
assertTrue(checkTable(Constants.DB_SCHEMA, "workflow_status"));
5644
}
5745

@@ -60,67 +48,69 @@ public void migrate_twice() throws Exception {
6048

6149
migrate();
6250

63-
assertTrue(checkConnection());
64-
6551
var app = new DBOSCommand();
6652
var cmd = new CommandLine(app);
6753

6854
var sw = new StringWriter();
6955
cmd.setOut(new PrintWriter(sw));
7056

71-
var exitCode = cmd.execute("migrate", "-D=" + db_url, "-U=" + db_user);
57+
var args =
58+
Stream.of(List.of("migrate"), pgContainer.options())
59+
.flatMap(Collection::stream)
60+
.toArray(String[]::new);
61+
62+
var exitCode = cmd.execute(args);
7263
assertEquals(0, exitCode);
7364

74-
assertTrue(checkConnection());
7565
assertTrue(checkTable(Constants.DB_SCHEMA, "workflow_status"));
7666
}
7767

7868
@ParameterizedTest
7969
@ValueSource(strings = {"invalid\"schema", "invalid'schema"})
8070
void testRunMigrations_fails_invalid_schema(String schema) throws Exception {
81-
assertFalse(checkConnection());
8271

8372
var cmd = new CommandLine(new DBOSCommand());
8473
var sw = new StringWriter();
8574
cmd.setOut(new PrintWriter(sw));
8675

87-
var exitCode =
88-
cmd.execute("migrate", "-D=" + db_url, "-U=" + db_user, "--schema", "%s".formatted(schema));
76+
var args =
77+
Stream.of(
78+
List.of("migrate"),
79+
pgContainer.options(),
80+
List.of("--schema", "%s".formatted(schema)))
81+
.flatMap(Collection::stream)
82+
.toArray(String[]::new);
83+
84+
var exitCode = cmd.execute(args);
8985
assertEquals(1, exitCode);
9086
}
9187

9288
@ParameterizedTest
9389
@ValueSource(strings = {"F8nny_sCHem@-n@m3", "embedded\0null"})
9490
public void migrate_custom_schema(String schema) throws Exception {
9591

96-
assertFalse(checkConnection());
97-
9892
var cmd = new CommandLine(new DBOSCommand());
9993
var sw = new StringWriter();
10094
cmd.setOut(new PrintWriter(sw));
10195

102-
var exitCode =
103-
cmd.execute("migrate", "-D=" + db_url, "-U=" + db_user, "--schema", "%s".formatted(schema));
96+
var args =
97+
Stream.of(
98+
List.of("migrate"),
99+
pgContainer.options(),
100+
List.of("--schema", "%s".formatted(schema)))
101+
.flatMap(Collection::stream)
102+
.toArray(String[]::new);
103+
104+
var exitCode = cmd.execute(args);
104105
assertEquals(0, exitCode);
105106

106-
assertTrue(checkConnection());
107107
assertTrue(checkTable(schema, "workflow_status"));
108108
}
109109

110-
static boolean checkConnection() {
111-
try (var conn = DriverManager.getConnection(db_url, db_user, db_password);
112-
var stmt = conn.createStatement()) {
113-
stmt.execute("SELECT 1");
114-
return true;
115-
} catch (SQLException e) {
116-
return false;
117-
}
118-
}
119-
120-
static boolean checkTable(String schema, String table) throws SQLException {
110+
boolean checkTable(String schema, String table) throws SQLException {
121111
var sql =
122112
"SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema = ? AND table_name = ?)";
123-
try (var conn = DriverManager.getConnection(db_url, db_user, db_password);
113+
try (var conn = pgContainer.connection();
124114
var stmt = conn.prepareStatement(sql)) {
125115
stmt.setString(1, SystemDatabase.sanitizeSchema(schema));
126116
stmt.setString(2, table);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package dev.dbos.transact.cli;
2+
3+
import java.sql.Connection;
4+
import java.sql.DriverManager;
5+
import java.sql.SQLException;
6+
import java.util.List;
7+
8+
import org.testcontainers.postgresql.PostgreSQLContainer;
9+
10+
public class PgContainer implements AutoCloseable {
11+
private final PostgreSQLContainer pgContainer = new PostgreSQLContainer("postgres:18");
12+
13+
public PgContainer() {
14+
pgContainer.start();
15+
}
16+
17+
@Override
18+
public void close() throws Exception {
19+
pgContainer.stop();
20+
}
21+
22+
public String jdbcUrl() {
23+
return pgContainer.getJdbcUrl();
24+
}
25+
26+
public String username() {
27+
return pgContainer.getUsername();
28+
}
29+
30+
public String password() {
31+
return pgContainer.getPassword();
32+
}
33+
34+
public Connection connection() throws SQLException {
35+
return DriverManager.getConnection(jdbcUrl(), username(), password());
36+
}
37+
38+
public List<String> options() {
39+
return List.of(urlOption(), userOption(), passwordOption());
40+
}
41+
42+
public String urlOption() {
43+
return "-D=" + jdbcUrl();
44+
}
45+
46+
public String userOption() {
47+
return "-U=" + username();
48+
}
49+
50+
public String passwordOption() {
51+
return "-P=" + password();
52+
}
53+
}

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

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
import static org.junit.jupiter.api.Assertions.assertFalse;
55
import static org.junit.jupiter.api.Assertions.assertTrue;
66

7-
import dev.dbos.transact.migrations.MigrationManager;
8-
97
import java.io.PrintWriter;
108
import java.io.StringWriter;
119
import java.sql.Connection;
1210
import java.sql.DatabaseMetaData;
13-
import java.sql.DriverManager;
1411
import java.sql.ResultSet;
1512
import java.sql.SQLException;
16-
import java.util.Objects;
13+
import java.util.Collection;
14+
import java.util.List;
1715
import java.util.concurrent.TimeUnit;
16+
import java.util.stream.Stream;
1817

19-
import org.junit.jupiter.api.BeforeAll;
18+
import org.junit.jupiter.api.AutoClose;
19+
import org.junit.jupiter.api.BeforeEach;
2020
import org.junit.jupiter.api.MethodOrderer;
2121
import org.junit.jupiter.api.Test;
2222
import org.junit.jupiter.api.TestMethodOrder;
@@ -27,32 +27,19 @@
2727
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
2828
public class ResetCommandTest {
2929

30-
static String db_url = "jdbc:postgresql://localhost:5432/reset_cmd_test";
31-
static String db_user = Objects.requireNonNullElse(System.getenv("PGUSER"), "postgres");
32-
static String db_password = Objects.requireNonNullElse(System.getenv("PGPASSWORD"), "dbos");
33-
34-
@BeforeAll
35-
static void setup() throws Exception {
36-
37-
var pair = MigrationManager.extractDbAndPostgresUrl(db_url);
38-
var dropDbSql = String.format("DROP DATABASE IF EXISTS %s WITH (FORCE)", pair.database());
39-
var createDbSql = String.format("CREATE DATABASE %s", pair.database());
40-
try (var conn = DriverManager.getConnection(pair.url(), db_user, db_password);
41-
var stmt = conn.createStatement()) {
42-
stmt.execute(dropDbSql);
43-
stmt.execute(createDbSql);
44-
}
30+
@AutoClose final PgContainer pgContainer = new PgContainer();
4531

46-
var createDummyTableSql =
47-
"CREATE TABLE dummy_table (id SERIAL PRIMARY KEY, name VARCHAR(100));";
48-
try (var conn = DriverManager.getConnection(db_url, db_user, db_password);
32+
@BeforeEach
33+
public void setup() throws SQLException {
34+
var sql = "CREATE TABLE dummy_table (id SERIAL PRIMARY KEY, name VARCHAR(100));";
35+
try (var conn = pgContainer.connection();
4936
var stmt = conn.createStatement()) {
50-
stmt.execute(createDummyTableSql);
37+
stmt.execute(sql);
5138
}
5239
}
5340

54-
static boolean dummyTableExists(String url, String user, String password) throws SQLException {
55-
try (Connection conn = DriverManager.getConnection(url, user, password)) {
41+
boolean dummyTableExists() throws SQLException {
42+
try (Connection conn = pgContainer.connection()) {
5643
DatabaseMetaData metaData = conn.getMetaData();
5744
try (ResultSet rs = metaData.getTables(null, null, "dummy_table", new String[] {"TABLE"})) {
5845
return rs.next();
@@ -63,15 +50,19 @@ static boolean dummyTableExists(String url, String user, String password) throws
6350
@Test
6451
public void reset() throws Exception {
6552

66-
assertTrue(dummyTableExists(db_url, db_user, db_password));
53+
assertTrue(dummyTableExists());
6754

6855
var cmd = new CommandLine(new DBOSCommand());
6956
var sw = new StringWriter();
7057
cmd.setOut(new PrintWriter(sw));
7158

72-
var exitCode = cmd.execute("reset", "-D=" + db_url, "-U=" + db_user, "-y");
59+
var args =
60+
Stream.of(List.of("reset"), pgContainer.options(), List.of("-y"))
61+
.flatMap(Collection::stream)
62+
.toArray(String[]::new);
63+
var exitCode = cmd.execute(args);
7364
assertEquals(0, exitCode);
7465

75-
assertFalse(dummyTableExists(db_url, db_user, db_password));
66+
assertFalse(dummyTableExists());
7667
}
7768
}

0 commit comments

Comments
 (0)