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

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


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 11902c1ca31 branch-4.2: [fix](load) Fix adaptive random bucket routing 
for stream and routine load (#68006) (#69146)
11902c1ca31 is described below

commit 11902c1ca31c7a6251b77af00eab5421aea83523
Author: Xin Liao <[email protected]>
AuthorDate: Sun Oct 18 11:44:09 2026 +0800

    branch-4.2: [fix](load) Fix adaptive random bucket routing for stream and 
routine load (#68006) (#69146)
    
    Pick apache/doris#68006 to branch-4.2.
    
    Branch adaptation: KafkaTaskInfo is in the routineload package on 4.2;
    Kinesis routine load is absent, so its source change is omitted. The FE
    test uses JUnit 4, matching this branch.
    
    Validation: whitespace check passed with the regression framework's
    generated EOF blank line exempted. `DORIS_GCC_HOME=/usr ./run-fe-ut.sh
    --run org.apache.doris.load.StreamLoadHandlerTest` was attempted but
    generated-code build stopped because `thirdparty/installed/bin/protoc`
    is missing from this worktree. Regression tests were not run.
    
    Co-authored-by: Refrain <[email protected]>
---
 .../org/apache/doris/load/StreamLoadHandler.java   |  74 ++++++++++
 .../doris/load/routineload/KafkaTaskInfo.java      |   3 +
 .../apache/doris/load/StreamLoadHandlerTest.java   | 156 +++++++++++++++++++++
 .../test_adaptive_random_bucket_routine_load.out   |  23 +++
 ...test_adaptive_random_bucket_routine_load.groovy | 139 ++++++++++++++++++
 .../test_adaptive_random_bucket_stream_load.groovy |  84 +++++++++++
 6 files changed, 479 insertions(+)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java 
b/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
index 99f516f022e..40f72f309f7 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
@@ -42,10 +42,14 @@ import 
org.apache.doris.nereids.load.NereidsCloudStreamLoadPlanner;
 import org.apache.doris.nereids.load.NereidsStreamLoadPlanner;
 import org.apache.doris.nereids.load.NereidsStreamLoadTask;
 import org.apache.doris.planner.GroupCommitPlanner;
+import org.apache.doris.planner.OlapTableSink;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.service.ExecuteEnv;
 import org.apache.doris.system.Backend;
 import org.apache.doris.system.SystemInfoService;
+import org.apache.doris.thrift.TDataSinkType;
+import org.apache.doris.thrift.TOlapTablePartitionParam;
+import org.apache.doris.thrift.TOlapTableSink;
 import org.apache.doris.thrift.TPipelineFragmentParams;
 import org.apache.doris.thrift.TStreamLoadPutRequest;
 import org.apache.doris.thrift.TStreamLoadPutResult;
@@ -61,6 +65,7 @@ import org.apache.logging.log4j.Logger;
 
 import java.security.SecureRandom;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
@@ -286,6 +291,7 @@ public class StreamLoadHandler {
                     ? multiTableFragmentInstanceIdIndex.getAndIncrement() : 0;
             TPipelineFragmentParams result = null;
             result = planner.plan(streamLoadTask.getId(), index);
+            assignAdaptiveRandomBucket(result);
             result.setTableName(table.getName());
             
result.query_options.setFeProcessUuid(ExecuteEnv.getInstance().getProcessUUID());
             result.setIsMowTable(table.getEnableUniqueKeyMergeOnWrite());
@@ -321,4 +327,72 @@ public class StreamLoadHandler {
     public List getFragmentParams() {
         return fragmentParams;
     }
+
+    /**
+     * Stream load sends the whole plan to the BE that issued this request, so 
the sink routing
+     * has to be resolved for exactly that BE. Without it the BE only knows 
the sink is in
+     * adaptive random bucket mode, assumes it owns every bucket and fails with
+     * "unknown partition channel" as soon as a partition has no tablet on it.
+     */
+    void assignAdaptiveRandomBucket(TPipelineFragmentParams params) {
+        TOlapTableSink sink = getOlapTableSink(params);
+        if (!OlapTableSink.shouldAssignAdaptiveRandomBucket(sink)) {
+            return;
+        }
+        long sinkBackendId = request.isSetBackendId() ? request.getBackendId() 
: -1L;
+        assignAdaptiveRandomBucket(params, sinkBackendId, request.getDb(), 
request.getTbl());
+    }
+
+    public static void assignAdaptiveRandomBucket(TPipelineFragmentParams 
params, long sinkBackendId,
+            String dbName, String tableName) {
+        TOlapTableSink sink = getOlapTableSink(params);
+        if (!OlapTableSink.shouldAssignAdaptiveRandomBucket(sink)) {
+            return;
+        }
+        if (sinkBackendId <= 0 || !sink.isSetLocation() || sink.getLocation() 
== null) {
+            // Old clients do not report the executing BE, and without the 
sink backend id or the
+            // tablet locations no assignment consistent with the receiver 
side can be computed
+            // here. Fall back to the non-adaptive per-batch routing, which 
never depends on the
+            // bucket owner. This is a normal compatibility fallback, not an 
error.
+            LOG.info("disable adaptive random bucket, stream load sink backend 
id is {}, db={}, table={}",
+                    sinkBackendId, dbName, tableName);
+            sink.unsetEnableAdaptiveRandomBucket();
+            return;
+        }
+        int sinkInstanceNum = Math.max(params.getLocalParamsSize(), 1);
+        Map<Long, Map<Long, OlapTableSink.AdaptiveBucketAssignment>> 
assignments =
+                OlapTableSink.computeAdaptiveRandomBucketAssignments(
+                        Lists.newArrayList(sinkBackendId), 
sink.getPartition().getPartitions(),
+                        sink.getLocation().getTablets(), sinkInstanceNum);
+        Map<Long, OlapTableSink.AdaptiveBucketAssignment> partitionAssignments 
= assignments.get(sinkBackendId);
+        if ((partitionAssignments == null || partitionAssignments.isEmpty())
+                && !isFakePartitionParam(sink.getPartition())) {
+            // Nothing could be assigned, so the sink would stay in adaptive 
mode without any
+            // routing and the BE would guess the bucket owner again.
+            LOG.warn("disable adaptive random bucket, no partition could be 
assigned for backend {}, "
+                            + "db={}, table={}", sinkBackendId, dbName, 
tableName);
+            sink.unsetEnableAdaptiveRandomBucket();
+            return;
+        }
+        OlapTableSink.applyAdaptiveRandomBucketAssignments(
+                sink.getPartition().getPartitions(), partitionAssignments);
+    }
+
+    /**
+     * Auto partition tables with no partition yet start with a fake partition 
list that is only
+     * used for tablet locations; their real partitions are created and 
assigned while the load
+     * runs, so an empty plan time assignment is expected and must keep 
adaptive mode enabled.
+     */
+    private static boolean isFakePartitionParam(TOlapTablePartitionParam 
partitionParam) {
+        return partitionParam.isSetPartitionsIsFake() && 
partitionParam.isPartitionsIsFake();
+    }
+
+    private static TOlapTableSink getOlapTableSink(TPipelineFragmentParams 
params) {
+        if (params == null || !params.isSetFragment() || params.getFragment() 
== null
+                || !params.getFragment().isSetOutputSink() || 
params.getFragment().getOutputSink() == null
+                || params.getFragment().getOutputSink().getType() != 
TDataSinkType.OLAP_TABLE_SINK) {
+            return null;
+        }
+        return params.getFragment().getOutputSink().getOlapTableSink();
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/load/routineload/KafkaTaskInfo.java 
b/fe/fe-core/src/main/java/org/apache/doris/load/routineload/KafkaTaskInfo.java
index 78d4312812f..375f96c5e98 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/load/routineload/KafkaTaskInfo.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/load/routineload/KafkaTaskInfo.java
@@ -25,6 +25,7 @@ import org.apache.doris.common.Config;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.util.DebugPointUtil;
 import org.apache.doris.common.util.DebugUtil;
+import org.apache.doris.load.StreamLoadHandler;
 import org.apache.doris.nereids.load.NereidsLoadTaskInfo;
 import org.apache.doris.nereids.load.NereidsStreamLoadPlanner;
 import org.apache.doris.qe.ConnectContext;
@@ -187,6 +188,8 @@ public class KafkaTaskInfo extends RoutineLoadTaskInfo {
                 (OlapTable) 
db.getTableOrMetaException(routineLoadJob.getTableId(),
                 Table.TableType.OLAP), taskInfo);
         TPipelineFragmentParams tExecPlanFragmentParams = 
routineLoadJob.plan(planner, loadId, txnId);
+        StreamLoadHandler.assignAdaptiveRandomBucket(tExecPlanFragmentParams, 
beId,
+                routineLoadJob.getDbFullName(), routineLoadJob.getTableName());
         TPlanFragment tPlanFragment = tExecPlanFragmentParams.getFragment();
         tPlanFragment.getOutputSink().getOlapTableSink().setTxnId(txnId);
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
index 8f266e81ab8..116375ee747 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
@@ -29,9 +29,20 @@ import org.apache.doris.mysql.privilege.Auth;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.system.Backend;
 import org.apache.doris.system.SystemInfoService;
+import org.apache.doris.thrift.TDataSink;
+import org.apache.doris.thrift.TDataSinkType;
+import org.apache.doris.thrift.TOlapTableIndexTablets;
+import org.apache.doris.thrift.TOlapTableLocationParam;
+import org.apache.doris.thrift.TOlapTablePartition;
+import org.apache.doris.thrift.TOlapTablePartitionParam;
+import org.apache.doris.thrift.TOlapTableSink;
+import org.apache.doris.thrift.TPipelineFragmentParams;
+import org.apache.doris.thrift.TPlanFragment;
 import org.apache.doris.thrift.TStreamLoadPutRequest;
 import org.apache.doris.thrift.TStreamLoadPutResult;
+import org.apache.doris.thrift.TTabletLocation;
 
+import com.google.common.collect.Lists;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.MockedStatic;
@@ -145,6 +156,151 @@ public class StreamLoadHandlerTest {
         }
     }
 
+    @Test
+    public void testAssignAdaptiveRandomBucketRoutesPartitionToTabletOwner() {
+        // The load runs on backend 20, but the only tablet of the partition 
lives on backend 10,
+        // so the sink has to route the partition to backend 10 instead of 
assuming self ownership.
+        TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+        request.setBackendId(20L);
+        StreamLoadHandler handler = new StreamLoadHandler(
+                request, null, new TStreamLoadPutResult(), "127.0.0.1");
+
+        TPipelineFragmentParams params = 
buildStreamLoadParams(createSingleBucketSink());
+        handler.assignAdaptiveRandomBucket(params);
+
+        TOlapTablePartition partition = getSinglePartition(params);
+        Assert.assertEquals(10L, partition.getBucketBeId());
+        Assert.assertEquals(0L, partition.getLoadTabletIdx());
+        Assert.assertEquals(Lists.newArrayList(0), 
partition.getLocalBucketSeqs());
+        Assert.assertEquals(10L, 
partition.getIndexes().get(0).getBucketBeId());
+        Assert.assertEquals(Lists.newArrayList(0), 
partition.getIndexes().get(0).getLocalBucketSeqs());
+    }
+
+    @Test
+    public void 
testAssignAdaptiveRandomBucketKeepsLocalBucketWhenSinkOwnsTablet() {
+        TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+        request.setBackendId(10L);
+        StreamLoadHandler handler = new StreamLoadHandler(
+                request, null, new TStreamLoadPutResult(), "127.0.0.1");
+
+        TPipelineFragmentParams params = 
buildStreamLoadParams(createSingleBucketSink());
+        handler.assignAdaptiveRandomBucket(params);
+
+        TOlapTablePartition partition = getSinglePartition(params);
+        Assert.assertEquals(10L, partition.getBucketBeId());
+        Assert.assertEquals(Lists.newArrayList(0), 
partition.getLocalBucketSeqs());
+    }
+
+    @Test
+    public void testAssignAdaptiveRandomBucketFallsBackWithoutBackendId() {
+        // An old client does not report the executing backend, so no 
assignment consistent with the
+        // receiver side can be computed. Adaptive mode must be turned off to 
keep the legacy routing.
+        TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+        request.setDb("test_db");
+        request.setTbl("test_tbl");
+        StreamLoadHandler handler = new StreamLoadHandler(
+                request, null, new TStreamLoadPutResult(), "127.0.0.1");
+
+        TPipelineFragmentParams params = 
buildStreamLoadParams(createSingleBucketSink());
+        handler.assignAdaptiveRandomBucket(params);
+
+        TOlapTableSink sink = 
params.getFragment().getOutputSink().getOlapTableSink();
+        Assert.assertFalse(sink.isSetEnableAdaptiveRandomBucket());
+        TOlapTablePartition partition = getSinglePartition(params);
+        Assert.assertFalse(partition.isSetBucketBeId());
+        Assert.assertFalse(partition.getIndexes().get(0).isSetBucketBeId());
+    }
+
+    @Test
+    public void 
testAssignAdaptiveRandomBucketFallsBackWhenNothingCanBeAssigned() {
+        // A partition the FE cannot assign (here: without a load tablet 
index) would leave the sink
+        // in adaptive mode without any routing, so adaptive mode has to be 
turned off.
+        TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+        request.setBackendId(20L);
+        StreamLoadHandler handler = new StreamLoadHandler(
+                request, null, new TStreamLoadPutResult(), "127.0.0.1");
+
+        TOlapTableSink sink = createSingleBucketSink();
+        sink.getPartition().getPartitions().get(0).unsetLoadTabletIdx();
+        TPipelineFragmentParams params = buildStreamLoadParams(sink);
+        handler.assignAdaptiveRandomBucket(params);
+
+        Assert.assertFalse(sink.isSetEnableAdaptiveRandomBucket());
+    }
+
+    @Test
+    public void testAssignAdaptiveRandomBucketKeepsAdaptiveForFakePartitions() 
{
+        // Auto partition tables get their real partitions, and their 
assignments, while the load
+        // runs, so an empty plan time assignment must keep adaptive mode 
enabled.
+        TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+        request.setBackendId(20L);
+        StreamLoadHandler handler = new StreamLoadHandler(
+                request, null, new TStreamLoadPutResult(), "127.0.0.1");
+
+        TOlapTableSink sink = createSingleBucketSink();
+        sink.getPartition().setPartitionsIsFake(true);
+        sink.getPartition().getPartitions().get(0).unsetLoadTabletIdx();
+        TPipelineFragmentParams params = buildStreamLoadParams(sink);
+        handler.assignAdaptiveRandomBucket(params);
+
+        Assert.assertTrue(sink.isSetEnableAdaptiveRandomBucket());
+        Assert.assertTrue(sink.isEnableAdaptiveRandomBucket());
+    }
+
+    @Test
+    public void testAssignAdaptiveRandomBucketIgnoresNonOlapTableSink() {
+        TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+        request.setBackendId(20L);
+        StreamLoadHandler handler = new StreamLoadHandler(
+                request, null, new TStreamLoadPutResult(), "127.0.0.1");
+
+        TPipelineFragmentParams params = new TPipelineFragmentParams();
+        TPlanFragment fragment = new TPlanFragment();
+        fragment.setOutputSink(new TDataSink(TDataSinkType.DATA_STREAM_SINK));
+        params.setFragment(fragment);
+
+        handler.assignAdaptiveRandomBucket(params);
+        
Assert.assertNull(params.getFragment().getOutputSink().getOlapTableSink());
+    }
+
+    /**
+     * Creates a random distribution sink with one bucket, whose only tablet 
is located on backend 10.
+     */
+    private static TOlapTableSink createSingleBucketSink() {
+        TOlapTablePartition partition = new TOlapTablePartition();
+        partition.setId(1000L);
+        partition.setNumBuckets(1);
+        partition.setLoadTabletIdx(0);
+        partition.addToIndexes(new TOlapTableIndexTablets(1L, 
Lists.newArrayList(100L)));
+        TOlapTablePartitionParam partitionParam = new 
TOlapTablePartitionParam();
+        partitionParam.addToPartitions(partition);
+
+        TOlapTableLocationParam locationParam = new TOlapTableLocationParam();
+        locationParam.addToTablets(new TTabletLocation(100L, 
Lists.newArrayList(10L)));
+
+        TOlapTableSink sink = new TOlapTableSink();
+        sink.setEnableAdaptiveRandomBucket(true);
+        sink.setLoadToSingleTablet(false);
+        sink.setPartition(partitionParam);
+        sink.setLocation(locationParam);
+        return sink;
+    }
+
+    private static TPipelineFragmentParams 
buildStreamLoadParams(TOlapTableSink sink) {
+        TPipelineFragmentParams params = new TPipelineFragmentParams();
+        TPlanFragment fragment = new TPlanFragment();
+        TDataSink tDataSink = new TDataSink(TDataSinkType.OLAP_TABLE_SINK);
+        tDataSink.setOlapTableSink(sink);
+        fragment.setOutputSink(tDataSink);
+        params.setFragment(fragment);
+        return params;
+    }
+
+    private static TOlapTablePartition 
getSinglePartition(TPipelineFragmentParams params) {
+        return params.getFragment().getOutputSink().getOlapTableSink()
+                .getPartition().getPartitions().get(0);
+    }
+
     private Backend createBackend(long id, String host) {
         Backend backend = new Backend(id, host, 9050);
         backend.setAlive(true);
diff --git 
a/regression-test/data/load_p0/routine_load/test_adaptive_random_bucket_routine_load.out
 
b/regression-test/data/load_p0/routine_load/test_adaptive_random_bucket_routine_load.out
new file mode 100644
index 00000000000..3bd9915d837
--- /dev/null
+++ 
b/regression-test/data/load_p0/routine_load/test_adaptive_random_bucket_routine_load.out
@@ -0,0 +1,23 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !first_batch --
+1      value_1
+2      value_2
+3      value_3
+4      value_4
+5      value_5
+6      value_6
+
+-- !second_batch --
+1      value_1
+10     value_10
+11     value_11
+12     value_12
+2      value_2
+3      value_3
+4      value_4
+5      value_5
+6      value_6
+7      value_7
+8      value_8
+9      value_9
+
diff --git 
a/regression-test/suites/load_p0/routine_load/test_adaptive_random_bucket_routine_load.groovy
 
b/regression-test/suites/load_p0/routine_load/test_adaptive_random_bucket_routine_load.groovy
new file mode 100644
index 00000000000..7099ce34797
--- /dev/null
+++ 
b/regression-test/suites/load_p0/routine_load/test_adaptive_random_bucket_routine_load.groovy
@@ -0,0 +1,139 @@
+// 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.apache.doris.regression.util.RoutineLoadTestUtils
+import org.apache.kafka.clients.admin.AdminClient
+import org.apache.kafka.clients.admin.NewTopic
+import org.apache.kafka.clients.producer.ProducerConfig
+import org.apache.kafka.clients.producer.ProducerRecord
+
+suite("test_adaptive_random_bucket_routine_load", "docker") {
+    if (!RoutineLoadTestUtils.isKafkaTestEnabled(context)) {
+        return
+    }
+
+    def kafkaBroker = RoutineLoadTestUtils.getKafkaBroker(context)
+    def topic = 
"test_adaptive_random_bucket_routine_load_${UUID.randomUUID()}".toString()
+    def job = "test_adaptive_random_bucket_routine_load_job"
+    def options = new ClusterOptions()
+    options.setFeNum(1)
+    options.setBeNum(2)
+    options.cloudMode = true
+    options.feConfigs += [
+        'enable_adaptive_random_bucket_load=true',
+        'max_routine_load_task_num_per_be=1'
+    ]
+
+    docker(options) {
+        sql "DROP TABLE IF EXISTS test_adaptive_random_bucket_routine_load"
+        // Two concurrent Kafka tasks must use different BEs. With only one 
tablet,
+        // one task has to route to a BE other than the one executing the task.
+        sql """
+            CREATE TABLE test_adaptive_random_bucket_routine_load (
+                k INT NOT NULL,
+                v STRING
+            )
+            DUPLICATE KEY(k)
+            DISTRIBUTED BY RANDOM BUCKETS 1
+            PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+        """
+
+        def adminProps = new Properties()
+        adminProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBroker)
+        def adminClient = AdminClient.create(adminProps)
+        try {
+            adminClient.createTopics([new NewTopic(topic, 2, (short) 
1)]).all().get()
+            try {
+                def producer = 
RoutineLoadTestUtils.createKafkaProducer(kafkaBroker)
+                try {
+                    def sendBatch = { int firstId ->
+                        for (int partition = 0; partition < 2; partition++) {
+                            for (int i = 0; i < 3; i++) {
+                                int id = firstId + partition * 3 + i
+                                producer.send(new ProducerRecord<>(topic, 
partition, null,
+                                        "${id},value_${id}".toString())).get()
+                            }
+                        }
+                        producer.flush()
+                    }
+
+                    sendBatch(1)
+                    sql """
+                        CREATE ROUTINE LOAD ${job} ON 
test_adaptive_random_bucket_routine_load
+                        COLUMNS TERMINATED BY ","
+                        PROPERTIES (
+                            "desired_concurrent_number" = "2",
+                            "max_batch_interval" = "5",
+                            "load_to_single_tablet" = "false"
+                        )
+                        FROM KAFKA (
+                            "kafka_broker_list" = "${kafkaBroker}",
+                            "kafka_topic" = "${topic}",
+                            "kafka_partitions" = "0,1",
+                            "kafka_offsets" = 
"OFFSET_BEGINNING,OFFSET_BEGINNING",
+                            "property.enable.partition.eof" = "false"
+                        )
+                    """
+                    try {
+                        def checkJob = {
+                            def state = sql_return_maparray "SHOW ROUTINE LOAD 
FOR ${job}"
+                            logger.info("Routine load status: ${state}")
+                            assertTrue(state[0].State in ["NEED_SCHEDULE", 
"RUNNING"],
+                                    "Routine load stopped unexpectedly: 
${state}")
+                            // Do not let a retry on the tablet owner hide a 
routing failure.
+                            
assertFalse(state[0].OtherMsg.toString().contains("unknown partition channel"),
+                                    "Adaptive routing failed: ${state}")
+                        }
+
+                        // Disable early EOF above so both tasks stay active 
for the batch interval,
+                        // allowing their BE assignments to be observed before 
they are renewed.
+                        awaitUntil(60) {
+                            checkJob()
+                            def tasks = sql_return_maparray "SHOW ROUTINE LOAD 
TASK WHERE JobName = '${job}'"
+                            def backendIds = tasks.collect { it.BeId as long 
}.findAll { it > 0 }.toSet()
+                            backendIds.size() == 2
+                        }
+
+                        def waitForRows = { int expected ->
+                            awaitUntil(120) {
+                                checkJob()
+                                def rows = sql "SELECT count(*) FROM 
test_adaptive_random_bucket_routine_load"
+                                rows[0][0] == expected
+                            }
+                        }
+                        waitForRows(6)
+                        order_qt_first_batch "SELECT k, v FROM 
test_adaptive_random_bucket_routine_load"
+
+                        // Renewed tasks must receive the adaptive assignments 
as well.
+                        sendBatch(7)
+                        waitForRows(12)
+                        order_qt_second_batch "SELECT k, v FROM 
test_adaptive_random_bucket_routine_load"
+                    } finally {
+                        sql "STOP ROUTINE LOAD FOR ${job}"
+                    }
+                } finally {
+                    producer.close()
+                }
+            } finally {
+                adminClient.deleteTopics([topic]).all().get()
+            }
+        } finally {
+            adminClient.close()
+        }
+    }
+}
diff --git 
a/regression-test/suites/load_p0/stream_load/test_adaptive_random_bucket_stream_load.groovy
 
b/regression-test/suites/load_p0/stream_load/test_adaptive_random_bucket_stream_load.groovy
new file mode 100644
index 00000000000..c07de6327fa
--- /dev/null
+++ 
b/regression-test/suites/load_p0/stream_load/test_adaptive_random_bucket_stream_load.groovy
@@ -0,0 +1,84 @@
+// 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.
+
+suite("test_adaptive_random_bucket_stream_load", "p0,nonConcurrent") {
+    if (!isCloudMode()) {
+        return
+    }
+
+    def enableAdaptiveRandomBucketConfig =
+            sql """ ADMIN SHOW FRONTEND CONFIG LIKE 
'enable_adaptive_random_bucket_load'; """
+    String oldEnableAdaptiveRandomBucket = 
enableAdaptiveRandomBucketConfig[0][1]
+
+    try {
+        sql """ ADMIN SET FRONTEND CONFIG 
('enable_adaptive_random_bucket_load' = 'true') """
+        sql """ DROP TABLE IF EXISTS test_adaptive_random_bucket_stream_load 
"""
+        // With a single bucket the partition has exactly one tablet, so all 
but one entry backend
+        // do not own it and have to route the partition to the owning backend.
+        sql """
+            CREATE TABLE test_adaptive_random_bucket_stream_load (
+                k int NOT NULL,
+                v string
+            )
+            DUPLICATE KEY(k)
+            DISTRIBUTED BY RANDOM BUCKETS 1
+            PROPERTIES (
+                "replication_allocation" = "tag.location.default: 1"
+            )
+        """
+
+        Map<String, String> backendIps = [:]
+        Map<String, String> backendHttpPorts = [:]
+        getBackendIpHttpPort(backendIps, backendHttpPorts)
+        assertTrue(backendIps.size() > 1, "adaptive random bucket routing 
needs at least two backends")
+
+        int rowsPerLoad = 10
+        int totalRows = 0
+        backendIps.each { backendId, backendIp ->
+            StringBuilder data = new StringBuilder()
+            for (int i = 0; i < rowsPerLoad; i++) {
+                data.append("${totalRows + i},value_${totalRows + i}\n")
+            }
+            totalRows += rowsPerLoad
+            // The entry backend is the sink backend. Only one of them owns 
the single tablet, the
+            // others must send the partition to the owning backend instead of 
failing with
+            // "unknown partition channel".
+            streamLoad {
+                table "test_adaptive_random_bucket_stream_load"
+                directToBe backendIp, backendHttpPorts.get(backendId) as int
+                set 'column_separator', ','
+                inputText data.toString()
+                time 60000
+
+                check { result, exception, startTime, endTime ->
+                    if (exception != null) {
+                        throw exception
+                    }
+                    def json = parseJson(result)
+                    
assertTrue(json.Status.toString().equalsIgnoreCase("success"), "load failed: 
${result}")
+                    assertEquals(rowsPerLoad, 
json.NumberLoadedRows.toString().toInteger(), "load failed: ${result}")
+                }
+            }
+            sql "sync"
+        }
+
+        def count = sql "SELECT count(*) FROM 
test_adaptive_random_bucket_stream_load"
+        assertEquals(totalRows, count[0][0] as int)
+    } finally {
+        sql """ ADMIN SET FRONTEND CONFIG 
('enable_adaptive_random_bucket_load' = '${oldEnableAdaptiveRandomBucket}') """
+    }
+}


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

Reply via email to