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;
}