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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 4dd3f8ca6bb branch-4.1: [fix](group commit) support forward group 
commit stream load (#63594) (#65350)
4dd3f8ca6bb is described below

commit 4dd3f8ca6bb344462b8f4744d5f9e78ed01536fa
Author: meiyi <[email protected]>
AuthorDate: Mon Jul 13 14:00:06 2026 +0800

    branch-4.1: [fix](group commit) support forward group commit stream load 
(#63594) (#65350)
    
    pick https://github.com/apache/doris/pull/63594
---
 .../main/java/org/apache/doris/common/Config.java  |   5 +
 .../apache/doris/service/FrontendServiceImpl.java  |  78 +++++++++++++
 ..._group_commit_stream_load_multi_follower.groovy | 123 +++++++++++++++++++++
 3 files changed, 206 insertions(+)

diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index 3a9acfde59a..dbb18eece62 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -712,6 +712,11 @@ public class Config extends ConfigBase {
             "Whether to enable memtable on sink node by default in stream 
load"})
     public static boolean stream_load_default_memtable_on_sink_node = false;
 
+    @ConfField(mutable = true, masterOnly = true, description = {
+            "Whether to enable forwarding group commit stream load to follower 
nodes."
+                    + " If true, stream load with group commit mode will be 
forwarded to a follower FE round robin."})
+    public static boolean enable_forward_group_commit_stream_load_to_follower 
= false;
+
     @ConfField(mutable = true, masterOnly = true, description = {"Load 
的最大超时时间,单位是秒。",
             "Maximal timeout for load job, in seconds."})
     public static int max_load_timeout_second = 259200; // 3days
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java 
b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
index f0d70434183..5082a62ce4c 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
@@ -58,6 +58,7 @@ import org.apache.doris.cluster.ClusterNamespace;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.AuthenticationException;
 import org.apache.doris.common.CaseSensibility;
+import org.apache.doris.common.ClientPool;
 import org.apache.doris.common.Config;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.DuplicatedRequestException;
@@ -87,6 +88,7 @@ import org.apache.doris.datasource.InternalCatalog;
 import org.apache.doris.datasource.SplitSource;
 import org.apache.doris.datasource.maxcompute.MCTransaction;
 import org.apache.doris.encryption.EncryptionKey;
+import org.apache.doris.ha.FrontendNodeType;
 import org.apache.doris.info.PartitionNamesInfo;
 import org.apache.doris.info.TableNameInfo;
 import org.apache.doris.info.TableRefInfo;
@@ -106,6 +108,7 @@ import org.apache.doris.nereids.trees.plans.PlanNodeAndHash;
 import org.apache.doris.nereids.trees.plans.commands.RestoreCommand;
 import org.apache.doris.nereids.trees.plans.commands.info.LabelNameInfo;
 import org.apache.doris.persist.gson.GsonUtils;
+import org.apache.doris.planner.GroupCommitPlanner;
 import org.apache.doris.planner.OlapTableSink;
 import org.apache.doris.plsql.metastore.PlsqlPackage;
 import org.apache.doris.plsql.metastore.PlsqlProcedureKey;
@@ -138,6 +141,7 @@ import org.apache.doris.statistics.query.QueryStats;
 import org.apache.doris.system.Backend;
 import org.apache.doris.system.Frontend;
 import org.apache.doris.system.SystemInfoService;
+import org.apache.doris.system.SystemInfoService.HostInfo;
 import org.apache.doris.tablefunction.MetadataGenerator;
 import org.apache.doris.thrift.FrontendService;
 import org.apache.doris.thrift.TAddPlsqlPackageRequest;
@@ -351,6 +355,7 @@ public class FrontendServiceImpl implements 
FrontendService.Iface {
     private static final Logger LOG = 
LogManager.getLogger(FrontendServiceImpl.class);
 
     private static final String NOT_MASTER_ERR_MSG = "FE is not master";
+    private static final AtomicInteger GROUP_COMMIT_FOLLOWER_INDEX = new 
AtomicInteger(0);
 
     private MasterImpl masterImpl;
     private ExecuteEnv exeEnv;
@@ -2108,6 +2113,70 @@ public class FrontendServiceImpl implements 
FrontendService.Iface {
         }
     }
 
+    private static int nextGroupCommitFollowerIndex(int followerCount) {
+        return Math.floorMod(GROUP_COMMIT_FOLLOWER_INDEX.getAndIncrement(), 
followerCount);
+    }
+
+    private TStreamLoadPutResult 
forwardGroupCommitStreamLoad(TStreamLoadPutRequest request) {
+        HostInfo selfNode = Env.getCurrentEnv().getSelfNode();
+        List<Frontend> followers = 
Env.getCurrentEnv().getFrontends(FrontendNodeType.FOLLOWER).stream()
+                .filter(fe -> fe.isAlive() && 
!(fe.getHost().equals(selfNode.getHost())
+                        && fe.getEditLogPort() == selfNode.getPort())).collect(
+                        Collectors.toList());
+        if (CollectionUtils.isEmpty(followers)) {
+            return null;
+        }
+
+        // check table enable light_schema_change and group commit does not 
block for schema change
+        TStreamLoadPutResult result = new TStreamLoadPutResult();
+        TStatus status = new TStatus(TStatusCode.OK);
+        result.setStatus(status);
+        try {
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrDdlException(request.getDb());
+            OlapTable table = (OlapTable) 
db.getTableOrDdlException(request.getTbl());
+            if (!table.getTableProperty().getUseSchemaLightChange()) {
+                status.setStatusCode(TStatusCode.ANALYSIS_ERROR);
+                status.addToErrorMsgs(
+                        "table light_schema_change is false, can't do stream 
load with group commit mode");
+                return result;
+            }
+            if 
(Env.getCurrentEnv().getGroupCommitManager().isBlock(table.getId())) {
+                String msg = "insert table " + table.getId() + 
GroupCommitPlanner.SCHEMA_CHANGE;
+                LOG.info(msg);
+                status.setStatusCode(TStatusCode.ANALYSIS_ERROR);
+                status.addToErrorMsgs(msg);
+                return result;
+            }
+        } catch (Exception e) {
+            LOG.warn("failed to pre-check group commit stream load, fallback 
to local. db={}, tbl={}",
+                    request.getDb(), request.getTbl(), e);
+            return null;
+        }
+
+        int idx = nextGroupCommitFollowerIndex(followers.size());
+        Frontend follower = followers.get(idx);
+        TNetworkAddress address = new TNetworkAddress(follower.getHost(), 
follower.getRpcPort());
+        LOG.info("forward group commit stream load put to follower {}, db={}, 
tbl={}, groupCommitMode={}",
+                address, request.getDb(), request.getTbl(), 
request.getGroupCommitMode());
+        FrontendService.Client client = null;
+        boolean ok = false;
+        try {
+            client = ClientPool.frontendPool.borrowObject(address);
+            TStreamLoadPutResult streamLoadPutResult = 
client.streamLoadPut(request);
+            ok = true;
+            return streamLoadPutResult;
+        } catch (Exception e) {
+            LOG.warn("failed to forward stream load put to follower: {}, 
fallback to local", address, e);
+        } finally {
+            if (ok) {
+                ClientPool.frontendPool.returnObject(address, client);
+            } else {
+                ClientPool.frontendPool.invalidateObject(address, client);
+            }
+        }
+        return null;
+    }
+
     @Override
     public TStreamLoadPutResult streamLoadPut(TStreamLoadPutRequest request) {
         String clientAddr = getClientAddrAsString();
@@ -2115,6 +2184,15 @@ public class FrontendServiceImpl implements 
FrontendService.Iface {
             LOG.debug("receive stream load put request: {}, backend: {}", 
request, clientAddr);
         }
 
+        String groupCommitMode = request.getGroupCommitMode();
+        if (groupCommitMode != null && !groupCommitMode.equals("off_mode") && 
Env.getCurrentEnv().isMaster()
+                && Config.enable_forward_group_commit_stream_load_to_follower) 
{
+            TStreamLoadPutResult result = 
forwardGroupCommitStreamLoad(request);
+            if (result != null) {
+                return result;
+            }
+        }
+
         TStreamLoadPutResult result = new TStreamLoadPutResult();
         TStatus status = new TStatus(TStatusCode.OK);
         result.setStatus(status);
diff --git 
a/regression-test/suites/load_p0/stream_load/test_group_commit_stream_load_multi_follower.groovy
 
b/regression-test/suites/load_p0/stream_load/test_group_commit_stream_load_multi_follower.groovy
new file mode 100644
index 00000000000..bf7df4d18ef
--- /dev/null
+++ 
b/regression-test/suites/load_p0/stream_load/test_group_commit_stream_load_multi_follower.groovy
@@ -0,0 +1,123 @@
+// 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.
+
+import org.apache.doris.regression.suite.ClusterOptions
+import org.codehaus.groovy.runtime.IOGroovyMethods
+
+suite('test_group_commit_stream_load_multi_follower', 'docker') {
+    def databaseName = context.config.getDbNameByFile(context.file)
+    def tableName = "tbl"
+
+    def groupCommitStreamLoad = { fe ->
+        def feIp = fe.getHttpAddress()[0]
+        def fePort = fe.getHttpAddress()[1]
+        def command = """ curl -sS --location-trusted -u root:
+                -H group_commit:async_mode
+                -H column_separator:,
+                -H columns:id,name
+                -T 
${context.config.dataPath}/load_p0/stream_load/test_stream_load1.csv
+                
http://${feIp}:${fePort}/api/${databaseName}/${tableName}/_stream_load """
+        log.info("group commit command: ${command}")
+
+        def process = command.execute()
+        def code = process.waitFor()
+        def err = IOGroovyMethods.getText(new BufferedReader(new 
InputStreamReader(process.getErrorStream())))
+        def out = process.getText()
+        logger.info("load through fe {}:{} master={} code={}, out={}, err={}",
+                feIp, fePort, fe.isMaster, code, out, err)
+        assertEquals(code, 0)
+
+        def json = parseJson(out)
+        assertEquals("success", json.Status.toLowerCase())
+        assertTrue(json.GroupCommit)
+        assertTrue(json.Label.startsWith("group_commit_"))
+        assertEquals(2, json.NumberTotalRows)
+        assertEquals(2, json.NumberLoadedRows)
+        assertEquals(0, json.NumberFilteredRows)
+        assertEquals(0, json.NumberUnselectedRows)
+        assertTrue(json.ErrorURL == null || json.ErrorURL.isEmpty())
+    }
+
+    def getRowCount = { expectedRowCount ->
+        def retry = 0
+        while (retry < 30) {
+            sleep(1000)
+            def rowCount = sql "select count(*) from 
${databaseName}.${tableName}"
+            logger.info("rowCount: " + rowCount + ", retry: " + retry)
+            if (rowCount[0][0] >= expectedRowCount) {
+                break
+            }
+            retry++
+        }
+    }
+
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.beNum = 1
+    options.cloudMode = true
+    options.useFollowersMode = true
+    options.beConfigs.add('enable_java_support=false')
+    
options.feConfigs.add('enable_forward_group_commit_stream_load_to_follower=true')
+    options.feConfigs.add('cloud_cluster_check_interval_second=1')
+    options.feConfigs.add('heartbeat_interval_second=1')
+    docker(options) {
+        awaitUntil(60) {
+            def ret = sql_return_maparray """SHOW FRONTENDS"""
+            ret.size() == 3
+                    && ret.count { it.Role.contains("FOLLOWER") } == 3
+                    && ret.count { it.IsMaster == "true" } == 1
+                    && ret.count { it.Alive == "true" } == 3
+        }
+
+        def frontendRows = sql_return_maparray """SHOW FRONTENDS"""
+        logger.info("show frontends result {}", frontendRows)
+        frontendRows.each { row ->
+            assertTrue(row.Role.contains("FOLLOWER"))
+            assertEquals("true", row.Alive)
+        }
+
+        sql """ CREATE DATABASE IF NOT EXISTS ${databaseName} """
+        sql """ DROP TABLE IF EXISTS ${databaseName}.${tableName} """
+        sql """
+             CREATE TABLE IF NOT EXISTS ${databaseName}.${tableName} (
+                 `id` int(11) NOT NULL,
+                 `name` varchar(1100) NULL,
+                 `score` int(11) NULL default "-1"
+             ) ENGINE=OLAP
+             DUPLICATE KEY(`id`, `name`)
+             DISTRIBUTED BY HASH(`id`) BUCKETS 1
+             PROPERTIES (
+                 "replication_num" = "1",
+                 "group_commit_interval_ms" = "200"
+             );
+        """
+
+        def expectedRowCount = 0
+        def frontends = cluster.getAllFrontends(true).sort { it.index }
+        for (int i = 0; i < 2; i++) {
+            frontends.each { fe ->
+                groupCommitStreamLoad(fe)
+                expectedRowCount += 2
+            }
+        }
+        getRowCount(expectedRowCount)
+
+        def rowCount = sql "select count(*) from ${databaseName}.${tableName}"
+        logger.info("rowCount: " + rowCount)
+        assertEquals(expectedRowCount, rowCount[0][0])    
+    }
+}


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

Reply via email to