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

Reply via email to