This is an automated email from the ASF dual-hosted git repository.

yuqi4733 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 8cfb108729 [#10032] feat(IT): Introduce docker-compose support for 
DorisContainer (#10046)
8cfb108729 is described below

commit 8cfb10872941a80f595c8d434f8ad3c86d24e795
Author: Kang <[email protected]>
AuthorDate: Wed Mar 4 15:36:21 2026 +0800

    [#10032] feat(IT): Introduce docker-compose support for DorisContainer 
(#10046)
    
    ### What changes were proposed in this pull request?
    
    Introduce docker-compose based Doris container environment using
    separate FE and BE containers with official apache/doris:2.1.7 images,
    enabling proper multi-node cluster testing and Doris 2.1.x
    compatibility.
    
    ### Why are the changes needed?
    
    Fix: #10032
    
    ### Does this PR introduce _any_ user-facing change?
    
    N/A
    
    ### How was this patch tested?
    
    IT
---
 .../doris-docker-script/docker-compose.yaml        |  62 ++++++++
 .../doris-docker-script/start-be.sh                |  89 ++++++++++++
 .../doris-docker-script/start-fe.sh                |  60 ++++++++
 .../integration/test/container/ContainerSuite.java |   2 +-
 .../integration/test/container/DorisContainer.java | 158 ++++++++++++++++-----
 5 files changed, 336 insertions(+), 35 deletions(-)

diff --git a/integration-test-common/doris-docker-script/docker-compose.yaml 
b/integration-test-common/doris-docker-script/docker-compose.yaml
new file mode 100644
index 0000000000..1f62ffbd78
--- /dev/null
+++ b/integration-test-common/doris-docker-script/docker-compose.yaml
@@ -0,0 +1,62 @@
+#
+# 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.
+
+services:
+
+  doris-fe:
+    image: ${GRAVITINO_CI_DORIS_DOCKER_IMAGE:-apache/gravitino-ci:doris-0.1.5}
+    hostname: doris-fe
+    networks:
+      - doris-net
+    volumes:
+      - ./start-fe.sh:/opt/apache-doris/start-fe.sh
+    command: ["bash", "start-fe.sh"]
+    ports:
+      - "8030:8030"
+      - "9030:9030"
+    healthcheck:
+      test: ["CMD", "bash", "-c", "curl -s http://localhost:8030/api/bootstrap 
| grep -q '\"msg\":\"success\"'"]
+      interval: 10s
+      timeout: 60s
+      retries: 15
+      start_period: 30s
+
+  doris-be:
+    image: ${GRAVITINO_CI_DORIS_DOCKER_IMAGE:-apache/gravitino-ci:doris-0.1.5}
+    networks:
+      - doris-net
+    volumes:
+      - ./start-be.sh:/opt/apache-doris/start-be.sh
+    command: ["bash", "start-be.sh"]
+    depends_on:
+      doris-fe:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD", "bash", "-c", "curl -s http://localhost:8040/api/health | 
grep -q '\"status\": \"OK\"'"]
+      interval: 10s
+      timeout: 60s
+      retries: 15
+      start_period: 30s
+
+networks:
+  doris-net:
+    driver: bridge
+    name: doris-net
+    ipam:
+      config:
+        - subnet: 10.20.31.32/28
diff --git a/integration-test-common/doris-docker-script/start-be.sh 
b/integration-test-common/doris-docker-script/start-be.sh
new file mode 100755
index 0000000000..1178e4fa0f
--- /dev/null
+++ b/integration-test-common/doris-docker-script/start-be.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# BE-only startup script for docker-compose multi-container deployment.
+# This overrides the image's default start.sh which starts both FE and BE.
+
+DORIS_HOME="$(dirname "${BASH_SOURCE-$0}")"
+DORIS_HOME="$(cd "${DORIS_HOME}" >/dev/null; pwd)"
+DORIS_BE_HOME="${DORIS_HOME}/be/"
+
+# Patch BE startup script: skip vm.max_map_count and ulimit checks, remove 
slow chmod
+DORIS_BE_SCRIPT="${DORIS_BE_HOME}/bin/start_be.sh"
+sed -i '/Please set vm.max_map_count/,/exit 1/{s/exit 1/#exit 1\n        echo 
"skip this"/}' ${DORIS_BE_SCRIPT}
+sed -i '/Please set the maximum number of open file descriptors/,/exit 
1/{s/exit 1/#exit 1\n        echo "skip this"/}' ${DORIS_BE_SCRIPT}
+sed -i 's/chmod 755 "${DORIS_HOME}\/lib\/doris_be"/#&/' ${DORIS_BE_SCRIPT}
+
+# Configure priority_networks and report interval in be.conf
+CONTAINER_IP=$(hostname -i)
+PRIORITY_NETWORKS=$(echo "${CONTAINER_IP}" | awk -F '.' 
'{print$1"."$2"."$3".0/24"}')
+echo "add priority_networks = ${PRIORITY_NETWORKS} to be.conf"
+echo "priority_networks = ${PRIORITY_NETWORKS}" >> 
${DORIS_BE_HOME}/conf/be.conf
+echo "report_disk_state_interval_seconds = 10" >> ${DORIS_BE_HOME}/conf/be.conf
+
+# Start only BE in daemon mode
+${DORIS_BE_HOME}/bin/start_be.sh --daemon
+
+# Wait for BE to be ready
+be_started=false
+for i in {1..20}; do
+  sleep 5
+  url="localhost:8040/api/health"
+  echo "check be for the $i times"
+  response=$(curl --silent --request GET $url)
+
+  if echo "$response" | grep -q '"status": "OK"'; then
+    be_started=true
+    echo "Doris BE started successfully, response: $response"
+    break
+  else
+    echo "check failed, response: $response"
+  fi
+done
+
+if [ "$be_started" = false ]; then
+  echo "ERROR: Doris BE failed to start"
+  exit 1
+fi
+
+# Register this BE to the central FE
+be_added=false
+for i in {1..10}; do
+  echo "add Doris BE to FE for the $i times"
+
+  mysql -h doris-fe -P9030 -uroot -e "ALTER SYSTEM ADD BACKEND 
'${CONTAINER_IP}:9050'"
+  if [ $? -ne 0 ]; then
+    echo "Failed to add the BE to the FE"
+  else
+    be_added=true
+    echo "Doris BE added to FE successfully"
+    break
+  fi
+
+  sleep 1
+done
+
+if [ "$be_added" = false ]; then
+  echo "ERROR: Doris BE failed to add to FE"
+  exit 1
+fi
+
+# Keep container alive
+tail -f /dev/null
diff --git a/integration-test-common/doris-docker-script/start-fe.sh 
b/integration-test-common/doris-docker-script/start-fe.sh
new file mode 100755
index 0000000000..65dde74e4f
--- /dev/null
+++ b/integration-test-common/doris-docker-script/start-fe.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# FE-only startup script for docker-compose multi-container deployment.
+# This overrides the image's default start.sh which starts both FE and BE.
+
+DORIS_HOME="$(dirname "${BASH_SOURCE-$0}")"
+DORIS_HOME="$(cd "${DORIS_HOME}" >/dev/null; pwd)"
+DORIS_FE_HOME="${DORIS_HOME}/fe/"
+
+# Configure priority_networks based on container IP
+CONTAINER_IP=$(hostname -i)
+PRIORITY_NETWORKS=$(echo "${CONTAINER_IP}" | awk -F '.' 
'{print$1"."$2"."$3".0/24"}')
+echo "add priority_networks = ${PRIORITY_NETWORKS} to fe.conf"
+echo "priority_networks = ${PRIORITY_NETWORKS}" >> 
${DORIS_FE_HOME}/conf/fe.conf
+
+# Start only FE in daemon mode
+${DORIS_FE_HOME}/bin/start_fe.sh --daemon
+
+# Wait for FE to be ready
+fe_started=false
+for i in {1..20}; do
+  sleep 5
+  url="http://localhost:8030/api/bootstrap";
+  echo "check fe for the $i times"
+  response=$(curl --silent --request GET $url)
+
+  if echo "$response" | grep -q '"msg":"success"'; then
+    fe_started=true
+    echo "Doris FE started successfully, response: $response"
+    break
+  else
+    echo "check failed, response: $response"
+  fi
+done
+
+if [ "$fe_started" = false ]; then
+  echo "ERROR: Doris FE failed to start"
+  exit 1
+fi
+
+# Keep container alive
+tail -f /dev/null
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 ede2d33766..8c3dc2ab80 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
@@ -334,7 +334,7 @@ public class ContainerSuite implements Closeable {
       synchronized (ContainerSuite.class) {
         if (dorisContainer == null) {
           initIfNecessary();
-          // Start Doris container
+          // Start Doris docker-compose containers
           DorisContainer.Builder dorisBuilder =
               
DorisContainer.builder().withHostName("gravitino-ci-doris").withNetwork(network);
           DorisContainer container = closer.register(dorisBuilder.build());
diff --git 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DorisContainer.java
 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DorisContainer.java
index 2f879041d7..f9a898c88d 100644
--- 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DorisContainer.java
+++ 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/DorisContainer.java
@@ -32,11 +32,24 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import org.apache.gravitino.integration.test.util.ITUtils;
 import org.rnorth.ducttape.Preconditions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.ComposeContainer;
+import org.testcontainers.containers.ContainerState;
 import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
 
+/**
+ * Docker-compose based Doris container for integration tests. This uses 
Testcontainers' {@link
+ * ComposeContainer} to orchestrate separate FE and BE containers, enabling 
proper multi-node
+ * cluster testing and compatibility with Doris 2.1.x features.
+ *
+ * <p>Although this class extends {@link BaseContainer} to preserve the 
existing contract, it
+ * overrides all lifecycle methods and delegates to a {@link ComposeContainer} 
internally. The
+ * parent class's {@link org.testcontainers.containers.GenericContainer} is 
not used.
+ */
 public class DorisContainer extends BaseContainer {
   public static final Logger LOG = 
LoggerFactory.getLogger(DorisContainer.class);
 
@@ -46,10 +59,21 @@ public class DorisContainer extends BaseContainer {
   public static final String PASSWORD = "root";
   public static final int FE_HTTP_PORT = 8030;
   public static final int FE_MYSQL_PORT = 9030;
+  public static final long STARTUP_TIMEOUT_SECONDS =
+      Long.parseLong(
+          
System.getenv().getOrDefault("GRAVITINO_CI_DORIS_STARTUP_TIMEOUT_SECONDS", 
"120"));
 
+  private static final String FE_SERVICE = "doris-fe";
+  private static final String BE_SERVICE = "doris-be";
+  private static final int BE_HEARTBEAT_PORT = 9050;
+  private static final int BE_WEBSERVER_PORT = 8040;
+  private static final int BE_SCALE = 3;
   private static final String DORIS_FE_PATH = "/opt/apache-doris/fe/log";
   private static final String DORIS_BE_PATH = "/opt/apache-doris/be/log";
 
+  private final ComposeContainer composeContainer;
+  private String feIpAddress;
+
   public static Builder builder() {
     return new Builder();
   }
@@ -63,18 +87,53 @@ public class DorisContainer extends BaseContainer {
       Map<String, String> envVars,
       Optional<Network> network) {
     super(image, hostName, ports, extraHosts, filesToMount, envVars, network);
-  }
 
-  @Override
-  protected void setupContainer() {
-    super.setupContainer();
-    withLogConsumer(new PrintingContainerLog(format("%-14s| ", 
"DorisContainer")));
-    withStartupTimeout(Duration.ofMinutes(5));
+    String dir = System.getenv("GRAVITINO_ROOT_DIR");
+    if (dir == null || dir.isEmpty()) {
+      throw new RuntimeException("GRAVITINO_ROOT_DIR is not set");
+    }
+    String composeFile =
+        ITUtils.joinPath(
+            dir, "integration-test-common", "doris-docker-script", 
"docker-compose.yaml");
+
+    String effectiveImage =
+        (DEFAULT_IMAGE != null && !DEFAULT_IMAGE.isEmpty()) ? DEFAULT_IMAGE : 
image;
+    Preconditions.check(
+        "Doris Docker image must be configured via 
GRAVITINO_CI_DORIS_DOCKER_IMAGE or "
+            + "DorisContainer builder image",
+        effectiveImage != null && !effectiveImage.isEmpty());
+
+    composeContainer =
+        new ComposeContainer(new File(composeFile))
+            .withEnv("GRAVITINO_CI_DORIS_DOCKER_IMAGE", effectiveImage)
+            .withExposedService(
+                FE_SERVICE,
+                FE_MYSQL_PORT,
+                Wait.forListeningPort()
+                    
.withStartupTimeout(Duration.ofSeconds(STARTUP_TIMEOUT_SECONDS)))
+            .withExposedService(FE_SERVICE, FE_HTTP_PORT)
+            .withScaledService(BE_SERVICE, BE_SCALE)
+            .withStartupTimeout(Duration.ofSeconds(STARTUP_TIMEOUT_SECONDS))
+            .withTailChildContainers(true)
+            .withLocalCompose(true);
+
+    // Expose ports for each BE instance (1-indexed)
+    for (int i = 1; i <= BE_SCALE; i++) {
+      composeContainer
+          .withExposedService(
+              BE_SERVICE,
+              i,
+              BE_HEARTBEAT_PORT,
+              Wait.forListeningPort()
+                  
.withStartupTimeout(Duration.ofSeconds(STARTUP_TIMEOUT_SECONDS)))
+          .withExposedService(BE_SERVICE, i, BE_WEBSERVER_PORT);
+    }
   }
 
   @Override
   public void start() {
-    super.start();
+    composeContainer.start();
+    resolveFeIpAddress();
     Preconditions.check("Doris container startup failed!", 
checkContainerStatus(5));
     Preconditions.check("Doris container password change failed!", 
changePassword());
   }
@@ -82,59 +141,84 @@ public class DorisContainer extends BaseContainer {
   @Override
   public void close() {
     copyDorisLog();
-    super.close();
+    if (composeContainer != null) {
+      composeContainer.stop();
+    }
   }
 
   private void copyDorisLog() {
     try {
-      // stop Doris container
       String destPath = System.getenv("IT_PROJECT_DIR");
-      LOG.info("Copy doris log file to {}", destPath);
-
-      String feTarPath = "/doris-be.tar";
-      String beTarPath = "/doris-fe.tar";
-
-      // Pack the jar files
-      container.execInContainer("tar", "cf", feTarPath, DORIS_BE_PATH);
-      container.execInContainer("tar", "cf", beTarPath, DORIS_FE_PATH);
-
-      container.copyFileFromContainer(feTarPath, destPath + File.separator + 
"doris-be.tar");
-      container.copyFileFromContainer(beTarPath, destPath + File.separator + 
"doris-fe.tar");
+      if (destPath == null || destPath.isEmpty()) {
+        LOG.warn("IT_PROJECT_DIR is not set, skipping Doris log copy");
+        return;
+      }
+      LOG.info("Copy doris log files to {}", destPath);
+      copyContainerLog(FE_SERVICE, DORIS_FE_PATH, destPath, "doris-fe");
+      for (int i = 1; i <= BE_SCALE; i++) {
+        copyContainerLog(BE_SERVICE + "-" + i, DORIS_BE_PATH, destPath, 
"doris-be-" + i);
+      }
     } catch (Exception e) {
-      LOG.error("Failed to copy container log to local", e);
+      LOG.error("Failed to copy Doris container logs to local", e);
+    }
+  }
+
+  private void copyContainerLog(String serviceName, String logPath, String 
destPath, String tarName)
+      throws Exception {
+    Optional<ContainerState> container = 
composeContainer.getContainerByServiceName(serviceName);
+    if (container.isPresent()) {
+      String tarPath = "/" + tarName + ".tar";
+      container.get().execInContainer("tar", "cf", tarPath, logPath);
+      container.get().copyFileFromContainer(tarPath, destPath + File.separator 
+ tarName + ".tar");
+    } else {
+      LOG.warn("Doris {} container not found, skipping log copy", serviceName);
     }
   }
 
+  @Override
+  public String getContainerIpAddress() {
+    return feIpAddress;
+  }
+
   @Override
   protected boolean checkContainerStatus(int retryLimit) {
-    String dorisJdbcUrl = format("jdbc:mysql://%s:%d/", 
getContainerIpAddress(), FE_MYSQL_PORT);
-    LOG.info("Doris url is " + dorisJdbcUrl);
+    String dorisJdbcUrl = format("jdbc:mysql://%s:%d/", feIpAddress, 
FE_MYSQL_PORT);
+    LOG.info("Doris JDBC url is {}", dorisJdbcUrl);
 
     await()
-        .atMost(30, TimeUnit.SECONDS)
-        .pollInterval(30 / retryLimit, TimeUnit.SECONDS)
+        .atMost(STARTUP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+        .pollInterval(STARTUP_TIMEOUT_SECONDS / retryLimit, TimeUnit.SECONDS)
         .until(
             () -> {
               try (Connection connection =
                       DriverManager.getConnection(dorisJdbcUrl, USER_NAME, "");
                   Statement statement = connection.createStatement()) {
 
-                // execute `SHOW PROC '/backends';` to check if backends is 
ready
+                // execute `SHOW PROC '/backends';` to check if all backends 
are ready
                 String query = "SHOW PROC '/backends';";
+                int aliveCount = 0;
                 try (ResultSet resultSet = statement.executeQuery(query)) {
                   while (resultSet.next()) {
                     String alive = resultSet.getString("Alive");
                     String totalCapacity = 
resultSet.getString("TotalCapacity");
                     float totalCapacityFloat = 
Float.parseFloat(totalCapacity.split(" ")[0]);
 
-                    // alive should be true and totalCapacity should not be 
0.000
                     if (alive.equalsIgnoreCase("true") && totalCapacityFloat > 
0.0f) {
-                      LOG.info("Doris container startup success!");
-                      return true;
+                      aliveCount++;
                     }
                   }
                 }
-                LOG.info("Doris container is not ready yet!");
+
+                if (aliveCount >= BE_SCALE) {
+                  LOG.info(
+                      "Doris docker-compose cluster startup success! All {} 
backends are alive.",
+                      aliveCount);
+                  return true;
+                }
+                LOG.info(
+                    "Doris docker-compose cluster is not ready yet! {}/{} 
backends alive.",
+                    aliveCount,
+                    BE_SCALE);
               } catch (Exception e) {
                 LOG.error(e.getMessage(), e);
               }
@@ -144,8 +228,15 @@ public class DorisContainer extends BaseContainer {
     return true;
   }
 
+  private void resolveFeIpAddress() {
+    // Use the host-accessible address from Testcontainers (works on 
macOS/Linux).
+    // The internal Docker network IP is not reachable from the host on macOS.
+    feIpAddress = composeContainer.getServiceHost(FE_SERVICE, FE_MYSQL_PORT);
+    LOG.info("Doris FE host address: {}", feIpAddress);
+  }
+
   private boolean changePassword() {
-    String dorisJdbcUrl = format("jdbc:mysql://%s:%d/", 
getContainerIpAddress(), FE_MYSQL_PORT);
+    String dorisJdbcUrl = format("jdbc:mysql://%s:%d/", feIpAddress, 
FE_MYSQL_PORT);
 
     // change password for root user, Gravitino API must set password in 
catalog properties
     try (Connection connection = DriverManager.getConnection(dorisJdbcUrl, 
USER_NAME, "");
@@ -153,12 +244,11 @@ public class DorisContainer extends BaseContainer {
 
       String query = String.format("SET PASSWORD FOR '%s' = PASSWORD('%s');", 
USER_NAME, PASSWORD);
       statement.execute(query);
-      LOG.info("Doris container password has been changed");
+      LOG.info("Doris docker-compose cluster password has been changed");
       return true;
     } catch (Exception e) {
-      LOG.error(e.getMessage(), e);
+      LOG.error("Failed to change Doris password: {}", e.getMessage(), e);
     }
-
     return false;
   }
 

Reply via email to