github-actions[bot] commented on code in PR #63534:
URL: https://github.com/apache/doris/pull/63534#discussion_r3315808718


##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy:
##########
@@ -384,6 +384,156 @@ class Suite implements GroovyInterceptable {
         }
     }
 
+    /**
+     * Create and manage multiple Docker clusters for multi-cluster test 
scenarios.
+     *
+     * Usage example:
+     *   dockers([
+     *       "cluster_1": new ClusterOptions(cloudMode: true, feNum: 1, beNum: 
1, msNum: 1),
+     *       "cluster_2": new ClusterOptions(cloudMode: true, feNum: 1, beNum: 
1, msNum: 0, externalMsCluster: "cluster_1")
+     *   ]) { clusters ->
+     *       connectWithDockerCluster(clusters.cluster_1) { sql "..." }
+     *       connectWithDockerCluster(clusters.cluster_2) { sql "..." }
+     *   }
+     *
+     * Important:
+     *   - Must use LinkedHashMap to preserve insertion order
+     *   - Clusters are created in map insertion order
+     *   - Clusters are destroyed in reverse order (dependent clusters first)
+     *   - If using externalMsCluster, the referenced cluster must appear 
earlier in the map
+     *
+     * @param clusterConfigs LinkedHashMap of cluster name to ClusterOptions
+     * @param manual_init_clusters Set of cluster names to skip automatic 
initialization
+     * @param actionSupplier Closure receiving Map<String, SuiteCluster> for 
test execution
+     */
+    void dockers(LinkedHashMap<String, ClusterOptions> clusterConfigs,
+        Set<String> manual_init_clusters = new HashSet<>(), Closure 
actionSupplier) throws Exception {
+        if (context.config.excludeDockerTest) {
+            logger.info("do not run the docker suite {}, because regression 
config excludeDockerTest=true", name)
+            return
+        }
+
+        if (RegressionTest.getGroupExecType(group) != 
RegressionTest.GroupExecType.DOCKER) {
+            throw new Exception("Need to add 'docker' to docker suite's belong 
groups, "
+                    + "see example demo_p0/docker_action.groovy")
+        }
+
+        if (context.isMultiDockerClusterRunning) {
+            throw new Exception("Nested dockers() calls are not supported")
+        }
+
+        // Validate cluster configs
+        Set<String> clusterNames = new HashSet<>()
+        for (def entry : clusterConfigs.entrySet()) {
+            String clusterName = entry.key
+            ClusterOptions options = entry.value
+
+            if (clusterNames.contains(clusterName)) {
+                throw new Exception("Duplicate cluster name: ${clusterName}")
+            }
+            clusterNames.add(clusterName)
+
+            // Validate externalMsCluster reference
+            if (options.externalMsCluster != null && 
!options.externalMsCluster.isEmpty()) {
+                if (!clusterNames.contains(options.externalMsCluster)) {
+                    throw new Exception("Cluster ${clusterName} references 
non-existent external MS cluster: ${options.externalMsCluster}")
+                }
+                if (options.msNum > 0) {
+                    throw new Exception("Cluster ${clusterName} cannot have 
its own MS when using external MS cluster")
+                }
+            }
+        }
+
+        List<String> clusterNamesReversed = new 
ArrayList<>(clusterConfigs.keySet())
+        Collections.reverse(clusterNamesReversed)
+
+         // Use LinkedHashMap to preserve order
+        Map<String, SuiteCluster> clusters = new LinkedHashMap<>()
+
+        try {
+            // Create and initialize clusters in order
+            for (def entry : clusterConfigs.entrySet()) {
+                String clusterName = entry.key
+                ClusterOptions options = entry.value
+
+                logger.info("Creating cluster: ${clusterName}")
+                SuiteCluster cluster = new SuiteCluster(clusterName, 
context.config)
+
+                clusters.put(clusterName, cluster)
+            }
+
+            for (String clusterName : clusterNamesReversed) {
+                clusters.get(clusterName).destroy(true)
+            }
+
+            for (def entry : clusterConfigs.entrySet()) {
+                String clusterName = entry.key
+                ClusterOptions options = entry.value
+                SuiteCluster cluster = clusters.get(clusterName)
+
+                if (manual_init_clusters.contains(clusterName)) {
+                    logger.info("Skipping initialization of cluster: 
${clusterName}")
+                    continue
+                }
+
+                // Determine cloud mode
+                boolean isCloud = false
+                if (options.cloudMode == null) {
+                    // If not specified, use config default or run both modes
+                    if (context.config.runMode == RunMode.CLOUD) {
+                        isCloud = true
+                    } else if (context.config.runMode == RunMode.NOT_CLOUD) {
+                        isCloud = false
+                    } else {
+                        throw new Exception("cloudMode must be specified when 
runMode is UNKNOWN for multi-cluster setup")
+                    }
+                } else {
+                    if (options.cloudMode == true && context.config.runMode == 
RunMode.NOT_CLOUD) {
+                        logger.info("Skip cluster ${clusterName} because 
cloudMode=true but regression test is in local mode")
+                        continue
+                    }
+                    if (options.cloudMode == false && context.config.runMode 
== RunMode.CLOUD) {
+                        logger.info("Skip cluster ${clusterName} because 
cloudMode=false but regression test is in cloud mode")
+                        continue
+                    }
+                    isCloud = options.cloudMode
+                }
+                logger.info("Initializing cluster ${cluster.name} in ${isCloud 
? 'cloud' : 'not_cloud'} mode")
+                cluster.init(options, isCloud)
+                logger.info("Cluster ${clusterName} initialized successfully")
+            }
+
+            // Wait for BE to report
+            Thread.sleep(5000)
+
+            Connection originConnection = context.threadLocalConn.get()
+            context.threadLocalConn.remove()

Review Comment:
   `threadLocalConn` stores `ConnectionInfo`, not `Connection` (see 
`SuiteContext.threadLocalConn`). If there is an existing connection this cast 
will throw at runtime, and even the restore path would put the wrong type back 
into the thread local. This should mirror `dockerImpl` and keep the saved value 
as `def`/`ConnectionInfo`.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy:
##########
@@ -384,6 +384,156 @@ class Suite implements GroovyInterceptable {
         }
     }
 
+    /**
+     * Create and manage multiple Docker clusters for multi-cluster test 
scenarios.
+     *
+     * Usage example:
+     *   dockers([
+     *       "cluster_1": new ClusterOptions(cloudMode: true, feNum: 1, beNum: 
1, msNum: 1),
+     *       "cluster_2": new ClusterOptions(cloudMode: true, feNum: 1, beNum: 
1, msNum: 0, externalMsCluster: "cluster_1")
+     *   ]) { clusters ->
+     *       connectWithDockerCluster(clusters.cluster_1) { sql "..." }
+     *       connectWithDockerCluster(clusters.cluster_2) { sql "..." }
+     *   }
+     *
+     * Important:
+     *   - Must use LinkedHashMap to preserve insertion order
+     *   - Clusters are created in map insertion order
+     *   - Clusters are destroyed in reverse order (dependent clusters first)
+     *   - If using externalMsCluster, the referenced cluster must appear 
earlier in the map
+     *
+     * @param clusterConfigs LinkedHashMap of cluster name to ClusterOptions
+     * @param manual_init_clusters Set of cluster names to skip automatic 
initialization
+     * @param actionSupplier Closure receiving Map<String, SuiteCluster> for 
test execution
+     */
+    void dockers(LinkedHashMap<String, ClusterOptions> clusterConfigs,
+        Set<String> manual_init_clusters = new HashSet<>(), Closure 
actionSupplier) throws Exception {
+        if (context.config.excludeDockerTest) {
+            logger.info("do not run the docker suite {}, because regression 
config excludeDockerTest=true", name)
+            return
+        }
+
+        if (RegressionTest.getGroupExecType(group) != 
RegressionTest.GroupExecType.DOCKER) {
+            throw new Exception("Need to add 'docker' to docker suite's belong 
groups, "
+                    + "see example demo_p0/docker_action.groovy")
+        }
+
+        if (context.isMultiDockerClusterRunning) {
+            throw new Exception("Nested dockers() calls are not supported")

Review Comment:
   `SuiteContext` does not define `isMultiDockerClusterRunning`, so the first 
property read in `dockers()` throws `MissingPropertyException` before any 
cluster is created. Please add a real field to `SuiteContext` (or keep this 
guard local) before using it here.



##########
docker/runtime/doris-compose/cluster.py:
##########
@@ -851,6 +888,14 @@ def __init__(self, name, subnet, image, is_cloud, 
is_root_user, fe_config,
         self.extra_hosts = extra_hosts
         self.coverage_dir = coverage_dir
         self.cloud_store_config = cloud_store_config
+        self.external_ms_cluster = external_ms_cluster
+        self.instance_id = instance_id
+        if not self.instance_id:

Review Comment:
   The configured `instance_id` never reaches the container: `docker_env()` 
does not export `INSTANCE_ID`, while `common.sh`'s create/get instance paths 
read `${INSTANCE_ID}` and `register_sql_server_cluster` still posts 
`default_instance_id`. As a result `--instance-id`/external-MS multi-instance 
startup creates or waits for an empty/default instance instead of the requested 
one. Please export the chosen instance id and use it consistently in the 
registration payloads.



##########
docker/runtime/doris-compose/cluster.py:
##########
@@ -851,6 +888,14 @@ def __init__(self, name, subnet, image, is_cloud, 
is_root_user, fe_config,
         self.extra_hosts = extra_hosts
         self.coverage_dir = coverage_dir
         self.cloud_store_config = cloud_store_config
+        self.external_ms_cluster = external_ms_cluster
+        self.instance_id = instance_id

Review Comment:
   This stores `external_ms_cluster`, but no code actually uses the referenced 
cluster for FDB/MS/recycler. `get_fdb_cluster()` and `get_meta_server_addr()` 
still read this cluster's local node groups, and `command.py` still creates 
local FDB/MS/recycler nodes. After the already-noted `external_ms_cluster` 
NameError is fixed, `--external-ms` will still not use the external service and 
can fail when `msNum=0`/`recyclerNum=0` as documented. Please load the 
referenced cluster's endpoints and skip local service creation for this mode.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteCluster.groovy:
##########
@@ -356,6 +379,14 @@ class SuiteCluster {
             cmd += ['--extra-hosts']
             cmd += options.extraHosts
         }
+        def envs = new ArrayList<String>(options.environments)
+        if (options.enableStorageVault) {
+            envs.add('ENABLE_STORAGE_VAULT=1')
+        }
+        if (!envs.isEmpty()) {
+            cmd += ['--env']
+            cmd += envs

Review Comment:
   `doris-compose up` does not define a `--env` option, so any suite that sets 
`options.environments` or `enableStorageVault` will fail in argparse with 
`unrecognized arguments: --env ...` before `command.py` can read these values. 
Please add the parser option and wire it into the cluster environment, or pass 
storage-vault settings through an existing supported option.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to