This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 5fb4a0fd4e [#13515] improvement(test): Speed up the core MySQL and
PostgreSQL test lanes with disposable database data (#13899)
5fb4a0fd4e is described below
commit 5fb4a0fd4e87e456fb3f63c35dac4435c5f1f811
Author: Nevin Zheng <[email protected]>
AuthorDate: Tue Oct 20 22:51:48 2026 -0700
[#13515] improvement(test): Speed up the core MySQL and PostgreSQL test
lanes with disposable database data (#13899)
### What changes were proposed in this pull request?
The core MySQL and PostgreSQL test lanes run on disposable database
data: the data directory is on `tmpfs` and commits are not synced to
disk. MySQL's lane is 2.0x faster and PostgreSQL's 1.4x; with #13518,
the three core database lanes take 10.6 minutes instead of 55. Nothing a
test sees changes.
Wall time per lane on one machine (Apple Silicon, OrbStack),
`--rerun-tasks`, build cache off, 0 failures. Speed-up against `main`
before this stack:
| Stage | `coreH2Test` | `coreMySQLTest` | `corePostgreSQLTest` | All
three |
| --- | ---: | ---: | ---: | ---: |
| `main` before this stack | 270s | 2,038s | 982s | 3,290s (55 min) |
| + class reuse (#13518) | 97s | 967s | 555s | 1,619s (2.0x) |
| + `DELETE_ROWS` reset (#13518) | 99s | 406s | 372s | 877s (3.8x) |
| + Testcontainers modules, default durability | 99s | 441s | 317s |
857s (3.8x) |
| **+ disposable data (this PR)** | **100s (2.7x)** | **223s (9.1x)** |
**313s (3.1x)** | **636s, 10.6 min (5.2x)** |
The first three rows are from an earlier run of the stack (590 tests per
lane). The last two are this PR at `1716a4a` (594 tests per lane): the
median of three rounds for MySQL and PostgreSQL, the mean of two for H2.
This PR's own step, measured side by side in the same rounds:
| Lane | `main` (`026a660`) | Modules, default durability | This PR |
Speed-up |
| --- | ---: | ---: | ---: | ---: |
| `coreH2Test` | 102s | 99s | 100s | 1.0x |
| `coreMySQLTest` | 448s | 441s | 223s | 2.0x |
| `corePostgreSQLTest` | 432s | 317s | 313s | 1.4x |
"Modules, default durability" is this PR with the disposable setting
off: PostgreSQL runs with the Testcontainers module's default
`fsync=off`, MySQL with the image's defaults. H2 is in-process and
unaffected.
- **Durability:** `DatabaseContainer` has a builder setting, `DEFAULT`
or `DISPOSABLE`. `ContainerSuite` reads it from the
`gravitino.test.db.disposableData` system property, which only
`coreMySQLTest` and `corePostgreSQLTest` set. `DISPOSABLE` mounts the
data directory on `tmpfs` (2 GB cap) and turns off commit syncing: MySQL
`innodb_flush_log_at_trx_commit = 0`, `skip-log-bin` and
`innodb_doublewrite = OFF`; PostgreSQL `fsync`, `synchronous_commit` and
`full_page_writes` off. The data directory is pinned to the mount
(`datadir`, `PGDATA`), so a newer image cannot put data elsewhere.
- **Testcontainers modules, the means:** `MySQLContainer` and
`PostgreSQLContainer` now extend `DatabaseContainer`, which wraps
`org.testcontainers.containers.MySQLContainer` / `PostgreSQLContainer`.
The module owns credentials and readiness, which replaces our
`mysqladmin ping` / `pg_isready` polling and the MySQL driver workaround
(#6392). Network alias, in-network JDBC URLs for the Trino, Spark and
Flink containers, `createDatabase` and the log prefix stay. Public
method names are unchanged, so no test changes.
- **MySQL settings we own:** MySQL reads `gravitino.cnf` from
`integration-test-common`, one file per durability mode, instead of the
small `my.cnf` Testcontainers 1.x copies in (16 MB buffer pool). Both
pin `performance_schema = ON`, which `TestRoleMembershipWrites` and
`TestOwnerAssignmentWrites` poll.
- **Startup log:** each container logs the settings the server reports
and whether lower durability is on, with a warning if disposable data is
not on `tmpfs`.
- **Other:** `BaseContainer` implements `Startable` and can wrap an
existing container. `integration-test-common` exports the two modules
through `testArtifacts`. `docs/how-to-test.md` describes the lane
setting.
### Why are the changes needed?
After #13518, most of the remaining MySQL and PostgreSQL lane time is
the tests' own commits, each waiting for an fsync. The lane data is
thrown away after every run, so that durability buys nothing.
Testcontainers and PostgreSQL both document `tmpfs` and relaxed syncing
for tests.
Building on the modules also removes a second way of starting the same
databases. `ContainerSuite`'s own containers reimplemented readiness,
and the MySQL check reported ready after 10 seconds whether or not the
server answered.
Related to: #13515, #13900
### Does this PR introduce _any_ user-facing change?
No. The changes are test-only. Integration tests outside the core lanes
keep durable MySQL with the image's defaults; their PostgreSQL now runs
with the Testcontainers module's default, `fsync=off`.
### How was this patch tested?
- Test sources of every module compile.
- `coreH2Test`, `coreMySQLTest` and `corePostgreSQLTest` pass, 594 tests
each, 0 failures (the timing runs above). The core lanes' startup log
reports lower durability enabled with data on `tmpfs`.
- Default durability: `CatalogMysqlIT` (MySQL 8.0, 35 tests),
`CatalogMysqlVersion5IT` (MySQL 5.7, 35 tests, 1 skipped),
`TestMultipleJDBCLoad` (PostgreSQL and MySQL) and `TestJdbcMetricsStore`
(PostgreSQL) pass.
**Stack:** **this PR** → #13837
---
core/build.gradle.kts | 3 +
docs/how-to-test.md | 2 +
integration-test-common/build.gradle.kts | 7 +
.../integration/test/container/BaseContainer.java | 46 ++++--
.../integration/test/container/ContainerSuite.java | 17 +--
.../test/container/DatabaseContainer.java | 164 +++++++++++++++++++++
.../integration/test/container/MySQLContainer.java | 130 +++++++---------
.../test/container/PostgreSQLContainer.java | 105 ++++++-------
.../resources/mysql-conf/default/gravitino.cnf | 25 ++++
.../resources/mysql-conf/disposable/gravitino.cnf | 31 ++++
10 files changed, 371 insertions(+), 159 deletions(-)
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index f281d658c9..e53c185bae 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -162,6 +162,9 @@ fun registerCoreTestTask(
systemProperty("junit.jupiter.execution.parallel.enabled", "false")
if (backend != "h2") {
+ // The test databases are thrown away, so their containers can skip
durability work.
+ // Read by DatabaseContainer.Durability.fromSystemProperty(); keep the
name in sync.
+ systemProperty("gravitino.test.db.disposableData", "true")
doFirst {
if (rootProject.extra["dockerTest"] != true) {
throw GradleException(
diff --git a/docs/how-to-test.md b/docs/how-to-test.md
index b80a329a94..8eb064ed02 100644
--- a/docs/how-to-test.md
+++ b/docs/how-to-test.md
@@ -57,6 +57,8 @@ To run one class or method, add `--tests` to its lane:
If the class is not in that lane, Gradle fails with "No tests found for given
includes".
+`coreMySQLTest` and `corePostgreSQLTest` run their test JVM with the system
property `gravitino.test.db.disposableData=true`, so the MySQL and PostgreSQL
containers keep their data in memory (`tmpfs`, 2 GB) and skip syncing commits
to disk. No other task sets it. Other tasks run MySQL with the image's default
settings and PostgreSQL with the Testcontainers module's default, `fsync=off`.
+
#### Database reuse and reset
In a database lane, each test class shares one database per backend.
`@DatabaseFixture(resetStrategy = ...)` picks how much is rebuilt before each
test, from cheapest to most isolated:
diff --git a/integration-test-common/build.gradle.kts
b/integration-test-common/build.gradle.kts
index 7adeffc08b..a14fed40cb 100644
--- a/integration-test-common/build.gradle.kts
+++ b/integration-test-common/build.gradle.kts
@@ -82,6 +82,13 @@ configurations {
create("testArtifacts")
}
+dependencies {
+ // ContainerSuite builds its MySQL and PostgreSQL containers on these
Testcontainers modules, so
+ // every module that uses the test artifacts needs them at test runtime.
+ "testArtifacts"(libs.testcontainers.mysql)
+ "testArtifacts"(libs.testcontainers.postgresql)
+}
+
artifacts {
add("testArtifacts", testJar)
}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/BaseContainer.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/BaseContainer.java
index 517d6e877e..11c7ec50e2 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/BaseContainer.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/BaseContainer.java
@@ -40,16 +40,18 @@ import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.OutputFrame;
import
org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.lifecycle.Startable;
/**
* The BaseContainer is the base for all containers. It's contains the common
methods and settings
* for all containers. You can extend this class to create your own container
to integration test.
*/
-public abstract class BaseContainer implements AutoCloseable {
+public abstract class BaseContainer implements Startable {
public static final Logger LOG =
LoggerFactory.getLogger(BaseContainer.class);
// Host name of the container
protected final String hostName;
@@ -74,8 +76,27 @@ public abstract class BaseContainer implements AutoCloseable
{
Map<String, String> filesToMount,
Map<String, String> envVars,
Optional<Network> network) {
+ this(
+ new GenericContainer<>(requireNonNull(image, "image is null")),
+ hostName,
+ ports,
+ extraHosts,
+ filesToMount,
+ envVars,
+ network);
+ }
+
+ /** Wraps a container a subclass has already created, such as a
Testcontainers module. */
+ protected BaseContainer(
+ GenericContainer<?> container,
+ String hostName,
+ Set<Integer> ports,
+ Map<String, String> extraHosts,
+ Map<String, String> filesToMount,
+ Map<String, String> envVars,
+ Optional<Network> network) {
this.container =
- new GenericContainer<>(requireNonNull(image, "image is null"))
+ requireNonNull(container, "container is null")
.withCreateContainerCmdModifier(
cmd ->
cmd.getHostConfig()
@@ -109,9 +130,13 @@ public abstract class BaseContainer implements
AutoCloseable {
extraHosts.forEach((hostName, ipAddress) ->
container.withExtraHost(hostName, ipAddress));
container
.withCreateContainerCmdModifier(c -> c.withHostName(hostName))
- .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
- .waitingFor(Wait.forListeningPort())
- .withStartupTimeout(Duration.ofMinutes(5));
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy());
+ // A Testcontainers JDBC module has its own readiness check: PostgreSQL's
waits for a log line
+ // through the wait strategy, which a port wait would replace; MySQL's
queries the database.
+ if (!(container instanceof JdbcDatabaseContainer)) {
+ container.waitingFor(Wait.forListeningPort());
+ }
+ container.withStartupTimeout(Duration.ofMinutes(5));
network.ifPresent(net ->
container.withNetwork(net).withNetworkAliases(hostName));
}
@@ -151,10 +176,16 @@ public abstract class BaseContainer implements
AutoCloseable {
throw new RuntimeException("Impossible to reach here");
}
+ @Override
public void start() {
container.start();
}
+ @Override
+ public void stop() {
+ container.stop();
+ }
+
protected abstract boolean checkContainerStatus(int retryLimit);
// Execute the command in the container.
@@ -167,11 +198,6 @@ public abstract class BaseContainer implements
AutoCloseable {
}
}
- @Override
- public void close() {
- container.stop();
- }
-
protected abstract static class Builder<
SELF extends Builder<SELF, CONTAINER>, CONTAINER extends BaseContainer> {
protected String image;
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
index 7be314f041..f182acfb29 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
@@ -46,6 +46,7 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
+import
org.apache.gravitino.integration.test.container.DatabaseContainer.Durability;
import org.apache.gravitino.integration.test.util.CloseableGroup;
import org.apache.gravitino.integration.test.util.CommandExecutor;
import org.apache.gravitino.integration.test.util.ITUtils;
@@ -391,10 +392,7 @@ public class ContainerSuite implements Closeable {
MySQLContainer.Builder mysqlBuilder =
MySQLContainer.builder()
.withHostName("gravitino-ci-mysql")
- .withEnvVars(
- ImmutableMap.<String, String>builder()
- .put("MYSQL_ROOT_PASSWORD", "root")
- .build())
+ .withDurability(Durability.fromSystemProperty())
.withExposePorts(ImmutableSet.of(MySQLContainer.MYSQL_PORT))
.withNetwork(network);
@@ -420,10 +418,7 @@ public class ContainerSuite implements Closeable {
MySQLContainer.builder()
.withImage("mysql:5.7")
.withHostName("gravitino-ci-mysql-v5")
- .withEnvVars(
- ImmutableMap.<String, String>builder()
- .put("MYSQL_ROOT_PASSWORD", "root")
- .build())
+ .withDurability(Durability.fromSystemProperty())
.withExposePorts(ImmutableSet.of(MySQLContainer.MYSQL_PORT))
.withNetwork(network);
@@ -449,11 +444,7 @@ public class ContainerSuite implements Closeable {
PostgreSQLContainer.builder()
.withImage(pgImageName.toString())
.withHostName(PostgreSQLContainer.HOST_NAME)
- .withEnvVars(
- ImmutableMap.<String, String>builder()
- .put("POSTGRES_USER", PostgreSQLContainer.USER_NAME)
- .put("POSTGRES_PASSWORD",
PostgreSQLContainer.PASSWORD)
- .build())
+ .withDurability(Durability.fromSystemProperty())
.withExposePorts(ImmutableSet.of(PostgreSQLContainer.PG_PORT))
.withNetwork(network);
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DatabaseContainer.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DatabaseContainer.java
new file mode 100644
index 0000000000..d668b9384f
--- /dev/null
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DatabaseContainer.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.integration.test.container;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.Network;
+
+/**
+ * A database test container backed by a Testcontainers JDBC module, which
owns the credentials and
+ * the readiness check. This class adds Gravitino's network, logging and
{@link Durability} settings
+ * on top.
+ */
+public abstract class DatabaseContainer extends BaseContainer {
+ private static final Logger LOG =
LoggerFactory.getLogger(DatabaseContainer.class);
+
+ /**
+ * System property a test task sets to {@code true} when its database data
may be lost; {@link
+ * Durability#fromSystemProperty()} then returns {@link
Durability#DISPOSABLE}.
+ */
+ public static final String DISPOSABLE_DATA_PROPERTY =
"gravitino.test.db.disposableData";
+
+ /**
+ * {@code tmpfs} mount for a disposable data directory. 2 GB is a ceiling;
the mount only takes
+ * what the database writes.
+ */
+ private static final String DISPOSABLE_DATA_TMPFS = "rw,size=2g";
+
+ /** How much of its durability work a database container keeps. */
+ public enum Durability {
+ /** Durable settings: the server's own defaults and our config for it. */
+ DEFAULT,
+ /** Data in memory ({@code tmpfs}) and commits not synced to disk. */
+ DISPOSABLE;
+
+ /** Returns {@link #DISPOSABLE} when {@link #DISPOSABLE_DATA_PROPERTY} is
set in this JVM. */
+ public static Durability fromSystemProperty() {
+ return Boolean.getBoolean(DISPOSABLE_DATA_PROPERTY) ? DISPOSABLE :
DEFAULT;
+ }
+ }
+
+ /** The Testcontainers module; the same object as {@link #container}. */
+ protected final JdbcDatabaseContainer<?> db;
+
+ private final Durability durability;
+
+ /** Wraps {@code db}, which must not be started yet. */
+ protected DatabaseContainer(
+ JdbcDatabaseContainer<?> db,
+ Durability durability,
+ String hostName,
+ Set<Integer> ports,
+ Map<String, String> extraHosts,
+ Map<String, String> filesToMount,
+ Map<String, String> envVars,
+ Optional<Network> network) {
+ super(db, hostName, ports, extraHosts, filesToMount, envVars, network);
+ this.db = db;
+ this.durability = durability;
+ }
+
+ /**
+ * Keeps the data directory on {@code tmpfs}, and starts the server with
{@code command} if one is
+ * given, when this container is {@link Durability#DISPOSABLE}. Callers also
pin the server's data
+ * directory to {@code dataDir}, so a newer image cannot place the data
outside the mount.
+ */
+ protected void applyDurability(String dataDir, String... command) {
+ if (durability == Durability.DISPOSABLE) {
+ db.withTmpFs(Map.of(dataDir, DISPOSABLE_DATA_TMPFS));
+ if (command.length > 0) {
+ db.withCommand(command);
+ }
+ }
+ }
+
+ /** The user tests connect as. */
+ public String getUsername() {
+ return db.getUsername();
+ }
+
+ /** The password of {@link #getUsername()}. */
+ public String getPassword() {
+ return db.getPassword();
+ }
+
+ /** The Testcontainers module waits for the database itself, so there is
nothing left to check. */
+ @Override
+ protected boolean checkContainerStatus(int retryLimit) {
+ return db.isRunning();
+ }
+
+ /**
+ * Logs whether lower durability is enabled and the settings the server
reports, so each run's
+ * output shows which mode it ran in. Warns when lower durability is enabled
but the data
+ * directory is not on {@code tmpfs}. The query must print tab-separated
values, the data
+ * directory first, in the order of {@code names}.
+ */
+ protected void logDurability(List<String> names, String... query) {
+ String name = getClass().getSimpleName();
+ Container.ExecResult settings = executeInContainer(query);
+ String[] values = settings.getStdout().trim().split("\t");
+ if (settings.getExitCode() != 0 || values.length != names.size()) {
+ LOG.warn("{}: could not read the durability settings: {}", name,
settings.getStderr());
+ return;
+ }
+ String report =
+ IntStream.range(0, names.size())
+ .mapToObj(i -> names.get(i) + "=" + values[i])
+ .collect(Collectors.joining(", "));
+ if (durability == Durability.DEFAULT) {
+ LOG.info("{}: lower durability disabled, so tests run slower: {}", name,
report);
+ return;
+ }
+ Container.ExecResult stat = executeInContainer("stat", "-f", "-c", "%T",
values[0]);
+ String fileSystem =
+ stat.getExitCode() == 0 ? stat.getStdout().trim() : "an unknown file
system";
+ if ("tmpfs".equals(fileSystem)) {
+ LOG.info("{}: lower durability enabled, data on tmpfs: {}", name,
report);
+ } else {
+ LOG.warn(
+ "{}: lower durability enabled, but data is on {}, not tmpfs, so
tests run slower: {}",
+ name,
+ fileSystem,
+ report);
+ }
+ }
+
+ /** Builder for a {@link DatabaseContainer}. */
+ protected abstract static class Builder<
+ SELF extends Builder<SELF, CONTAINER>, CONTAINER extends
DatabaseContainer>
+ extends BaseContainer.Builder<SELF, CONTAINER> {
+ protected Durability durability = Durability.DEFAULT;
+
+ /** Sets the durability; {@link Durability#DEFAULT} when not set. */
+ public SELF withDurability(Durability durability) {
+ this.durability = durability;
+ return self;
+ }
+ }
+}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/MySQLContainer.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/MySQLContainer.java
index 3cfb6ecc56..b71a092f97 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/MySQLContainer.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/MySQLContainer.java
@@ -19,26 +19,23 @@
package org.apache.gravitino.integration.test.container;
import static java.lang.String.format;
-import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
import com.google.common.collect.ImmutableSet;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.integration.test.util.TestDatabaseName;
-import org.rnorth.ducttape.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.testcontainers.containers.Container;
import org.testcontainers.containers.Network;
+import org.testcontainers.utility.DockerImageName;
-public class MySQLContainer extends BaseContainer {
+public class MySQLContainer extends DatabaseContainer {
public static final Logger LOG =
LoggerFactory.getLogger(MySQLContainer.class);
public static final String DEFAULT_IMAGE = "mysql:8.0";
@@ -46,6 +43,9 @@ public class MySQLContainer extends BaseContainer {
public static final int MYSQL_PORT = 3306;
public static final String USER_NAME = "root";
public static final String PASSWORD = "root";
+ private static final String DATA_DIR = "/var/lib/mysql";
+ private static final String DEFAULT_CONFIG = "mysql-conf/default";
+ private static final String DISPOSABLE_CONFIG = "mysql-conf/disposable";
public static Builder builder() {
return new Builder();
@@ -53,13 +53,23 @@ public class MySQLContainer extends BaseContainer {
protected MySQLContainer(
String image,
+ Durability durability,
String hostName,
Set<Integer> ports,
Map<String, String> extraHosts,
Map<String, String> filesToMount,
Map<String, String> envVars,
Optional<Network> network) {
- super(image, hostName, ports, extraHosts, filesToMount, envVars, network);
+ super(
+ module(image, durability),
+ durability,
+ hostName,
+ ports,
+ extraHosts,
+ filesToMount,
+ envVars,
+ network);
+ applyDurability(DATA_DIR);
}
@Override
@@ -71,73 +81,26 @@ public class MySQLContainer extends BaseContainer {
@Override
public void start() {
super.start();
- Preconditions.check("MySQL container startup failed!",
checkContainerStatus(5));
- }
-
- @Override
- protected boolean checkContainerStatus(int retryLimit) {
- await()
- .atMost(10, TimeUnit.SECONDS)
- .pollInterval(10 / retryLimit, TimeUnit.SECONDS)
- .until(
- () -> {
- try {
- String[] commandAndArgs =
- new String[] {
- "mysqladmin",
- "ping",
- "-h",
- "localhost",
- "-u",
- getUsername(),
- String.format("-p%s", getPassword())
- };
- Container.ExecResult execResult =
executeInContainer(commandAndArgs);
- if (execResult.getExitCode() != 0) {
- String message =
- format(
- "Command [%s] exited with %s",
- String.join(" ", commandAndArgs),
execResult.getExitCode());
- LOG.error("{}", message);
- LOG.error("stderr: {}", execResult.getStderr());
- LOG.error("stdout: {}", execResult.getStdout());
- } else {
- LOG.info("MySQL container startup success!");
- return true;
- }
- } catch (RuntimeException e) {
- LOG.error(e.getMessage(), e);
- }
- return false;
- });
-
- return true;
+ logDurability(
+ List.of("datadir", "innodb_flush_log_at_trx_commit", "log_bin",
"innodb_doublewrite"),
+ "mysql",
+ "-u" + getUsername(),
+ "-p" + getPassword(),
+ "--batch",
+ "--skip-column-names",
+ "-e",
+ "SELECT @@datadir, @@innodb_flush_log_at_trx_commit, @@log_bin,
@@innodb_doublewrite");
}
public void createDatabase(TestDatabaseName testDatabaseName) {
- String mySQLJdbcUrl =
- StringUtils.substring(
- getJdbcUrl(testDatabaseName), 0,
getJdbcUrl(testDatabaseName).lastIndexOf("/"));
-
- // Fix https://github.com/apache/gravitino/issues/6392, MYSQL JDBC driver
may not load
- // automatically.
- try {
- Class.forName("com.mysql.jdbc.Driver");
- } catch (Exception e) {
- throw new RuntimeException("Failed to load MySQL JDBC driver", e);
+ // validate database name to ensure it only contains safe characters
+ String databaseName = testDatabaseName.toString();
+ if (!isValidDatabaseName(databaseName)) {
+ throw new IllegalArgumentException("Invalid database name: " +
databaseName);
}
- // change password for root user, Gravitino API must set password in
catalog properties
- try (Connection connection =
- DriverManager.getConnection(mySQLJdbcUrl, USER_NAME,
getPassword());
+ try (Connection connection = db.createConnection("");
Statement statement = connection.createStatement()) {
-
- // validate database name to ensure it only contains safe characters
- String databaseName = testDatabaseName.toString();
- if (!isValidDatabaseName(databaseName)) {
- throw new IllegalArgumentException("Invalid database name: " +
databaseName);
- }
-
String query = String.format("CREATE DATABASE IF NOT EXISTS `%s`;",
databaseName);
// FIXME: String, which is used in SQL, can be unsafe
statement.execute(query);
@@ -159,14 +122,6 @@ public class MySQLContainer extends BaseContainer {
return databaseName.matches("^[a-zA-Z0-9_$]+$");
}
- public String getUsername() {
- return USER_NAME;
- }
-
- public String getPassword() {
- return PASSWORD;
- }
-
public String getJdbcUrl() {
return format("jdbc:mysql://%s:%d", getContainerIpAddress(), MYSQL_PORT);
}
@@ -179,8 +134,27 @@ public class MySQLContainer extends BaseContainer {
return
DriverManager.getDriver(getJdbcUrl(testDatabaseName)).getClass().getName();
}
+ private static org.testcontainers.containers.MySQLContainer<?> module(
+ String image, Durability durability) {
+ // The image may be a mirror or a custom tag; the module only needs a
MySQL server.
+ return new org.testcontainers.containers.MySQLContainer<>(
+ DockerImageName.parse(image).asCompatibleSubstituteFor("mysql"))
+ .withUsername(USER_NAME)
+ .withPassword(PASSWORD)
+ // The module always creates a database. Its default, "test", is also
a schema name catalog
+ // tests create, so name MySQL's own system database instead.
+ .withDatabaseName("mysql")
+ // Our own config replaces the one Testcontainers 1.x bundles, so the
server settings do not
+ // depend on the Testcontainers version; it carries the disposable
settings too.
+ .withConfigurationOverride(
+ durability == Durability.DISPOSABLE ? DISPOSABLE_CONFIG :
DEFAULT_CONFIG)
+ // The module's readiness check queries the database and ignores
BaseContainer's startup
+ // timeout, so it gets the same budget here.
+ .withStartupTimeoutSeconds(300);
+ }
+
public static class Builder
- extends BaseContainer.Builder<MySQLContainer.Builder, MySQLContainer> {
+ extends DatabaseContainer.Builder<MySQLContainer.Builder,
MySQLContainer> {
private Builder() {
this.image = DEFAULT_IMAGE;
@@ -191,7 +165,7 @@ public class MySQLContainer extends BaseContainer {
@Override
public MySQLContainer build() {
return new MySQLContainer(
- image, hostName, exposePorts, extraHosts, filesToMount, envVars,
network);
+ image, durability, hostName, exposePorts, extraHosts, filesToMount,
envVars, network);
}
}
}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/PostgreSQLContainer.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/PostgreSQLContainer.java
index 6b2b147b1c..087561fe9c 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/PostgreSQLContainer.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/PostgreSQLContainer.java
@@ -19,31 +19,30 @@
package org.apache.gravitino.integration.test.container;
import static java.lang.String.format;
-import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
import com.google.common.collect.ImmutableSet;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import org.apache.gravitino.integration.test.util.TestDatabaseName;
-import org.rnorth.ducttape.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.testcontainers.containers.Container;
import org.testcontainers.containers.Network;
+import org.testcontainers.utility.DockerImageName;
-public class PostgreSQLContainer extends BaseContainer {
+public class PostgreSQLContainer extends DatabaseContainer {
public static final Logger LOG =
LoggerFactory.getLogger(PostgreSQLContainer.class);
public static final PGImageName DEFAULT_IMAGE = PGImageName.VERSION_13;
public static final String HOST_NAME = "gravitino-ci-pg";
public static final int PG_PORT = 5432;
public static final String USER_NAME = "root";
public static final String PASSWORD = "root";
+ private static final String DATA_DIR = "/var/lib/postgresql/data";
public static Builder builder() {
return new Builder();
@@ -51,13 +50,39 @@ public class PostgreSQLContainer extends BaseContainer {
protected PostgreSQLContainer(
String image,
+ Durability durability,
String hostName,
Set<Integer> ports,
Map<String, String> extraHosts,
Map<String, String> filesToMount,
Map<String, String> envVars,
Optional<Network> network) {
- super(image, hostName, ports, extraHosts, filesToMount, envVars, network);
+ super(
+ // The image may be a mirror or a custom tag; the module only needs a
PostgreSQL server.
+ new org.testcontainers.containers.PostgreSQLContainer<>(
+
DockerImageName.parse(image).asCompatibleSubstituteFor("postgres"))
+ .withUsername(USER_NAME)
+ .withPassword(PASSWORD)
+ // getJdbcUrl() names no database, so connections use the one
named after the user.
+ .withDatabaseName(USER_NAME),
+ durability,
+ hostName,
+ ports,
+ extraHosts,
+ filesToMount,
+ envVars,
+ network);
+ // Same path as the tmpfs mount, so a newer image cannot place the data
elsewhere.
+ db.withEnv("PGDATA", DATA_DIR);
+ applyDurability(
+ DATA_DIR,
+ "postgres",
+ "-c",
+ "fsync=off",
+ "-c",
+ "synchronous_commit=off",
+ "-c",
+ "full_page_writes=off");
}
@Override
@@ -69,51 +94,23 @@ public class PostgreSQLContainer extends BaseContainer {
@Override
public void start() {
super.start();
- Preconditions.check("PostgreSQL container startup failed!",
checkContainerStatus(5));
- }
-
- @Override
- protected boolean checkContainerStatus(int retryLimit) {
- int nRetry = 0;
- boolean isPostgreSQLContainerReady = false;
- int sleepTimeMillis = 20_00;
- while (nRetry++ < retryLimit) {
- try {
- String[] commandAndArgs =
- new String[] {
- "pg_isready", "-h", "localhost", "-U", getUsername(),
- };
- Container.ExecResult execResult = executeInContainer(commandAndArgs);
- if (execResult.getExitCode() != 0) {
- String message =
- format(
- "Command [%s] exited with %s",
- String.join(" ", commandAndArgs), execResult.getExitCode());
- LOG.error("{}", message);
- LOG.error("stderr: {}", execResult.getStderr());
- LOG.error("stdout: {}", execResult.getStdout());
- } else {
- LOG.info("PostgreSQL container startup success!");
- isPostgreSQLContainerReady = true;
- break;
- }
- LOG.info(
- "PostgreSQL container is not ready, recheck({}/{}) after {}ms",
- nRetry,
- retryLimit,
- sleepTimeMillis);
- await().atLeast(sleepTimeMillis, TimeUnit.MILLISECONDS);
- } catch (RuntimeException e) {
- LOG.error(e.getMessage(), e);
- }
- }
-
- return isPostgreSQLContainerReady;
+ logDurability(
+ List.of("data_directory", "fsync", "synchronous_commit",
"full_page_writes"),
+ "psql",
+ "-U",
+ getUsername(),
+ "-d",
+ "postgres",
+ "--no-align",
+ "--tuples-only",
+ "--field-separator=\t",
+ "-c",
+ "SELECT current_setting('data_directory'), current_setting('fsync'),"
+ + " current_setting('synchronous_commit'),
current_setting('full_page_writes')");
}
public void createDatabase(TestDatabaseName testDatabaseName) {
- try (Connection connection =
- DriverManager.getConnection(getJdbcUrl(), getUsername(),
getPassword());
+ try (Connection connection = db.createConnection("");
Statement statement = connection.createStatement()) {
String query = format("CREATE DATABASE \"%s\"", testDatabaseName);
@@ -127,14 +124,6 @@ public class PostgreSQLContainer extends BaseContainer {
}
}
- public String getUsername() {
- return USER_NAME;
- }
-
- public String getPassword() {
- return PASSWORD;
- }
-
/** getJdbcUrl without database name. */
public String getJdbcUrl() {
return format("jdbc:postgresql://%s:%d/", getContainerIpAddress(),
PG_PORT);
@@ -150,7 +139,7 @@ public class PostgreSQLContainer extends BaseContainer {
}
public static class Builder
- extends BaseContainer.Builder<PostgreSQLContainer.Builder,
PostgreSQLContainer> {
+ extends DatabaseContainer.Builder<PostgreSQLContainer.Builder,
PostgreSQLContainer> {
private Builder() {
this.image = DEFAULT_IMAGE.toString();
@@ -161,7 +150,7 @@ public class PostgreSQLContainer extends BaseContainer {
@Override
public PostgreSQLContainer build() {
return new PostgreSQLContainer(
- image, hostName, exposePorts, extraHosts, filesToMount, envVars,
network);
+ image, durability, hostName, exposePorts, extraHosts, filesToMount,
envVars, network);
}
}
}
diff --git
a/integration-test-common/src/test/resources/mysql-conf/default/gravitino.cnf
b/integration-test-common/src/test/resources/mysql-conf/default/gravitino.cnf
new file mode 100644
index 0000000000..bb84ed0822
--- /dev/null
+++
b/integration-test-common/src/test/resources/mysql-conf/default/gravitino.cnf
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Gravitino's MySQL test containers use this file instead of the small one
Testcontainers 1.x
+# bundles, so the server runs with the image's defaults apart from what is set
here.
+[mysqld]
+# The image default, pinned: TestRoleMembershipWrites and
TestOwnerAssignmentWrites poll
+# performance_schema.data_lock_waits to see a blocked writer.
+performance_schema = ON
diff --git
a/integration-test-common/src/test/resources/mysql-conf/disposable/gravitino.cnf
b/integration-test-common/src/test/resources/mysql-conf/disposable/gravitino.cnf
new file mode 100644
index 0000000000..d815b29ecc
--- /dev/null
+++
b/integration-test-common/src/test/resources/mysql-conf/disposable/gravitino.cnf
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Gravitino's MySQL test containers use this file when their data is
disposable: the data
+# directory is on tmpfs and is thrown away, so the server skips crash-safety
work.
+[mysqld]
+# Same path as the tmpfs mount, so a newer image cannot place the data
elsewhere.
+datadir = /var/lib/mysql
+# Write and flush the redo log once a second instead of at each commit.
+innodb_flush_log_at_trx_commit = 0
+skip-log-bin
+innodb_doublewrite = OFF
+# The image default, pinned: TestRoleMembershipWrites and
TestOwnerAssignmentWrites poll
+# performance_schema.data_lock_waits to see a blocked writer.
+performance_schema = ON