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

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


The following commit(s) were added to refs/heads/branch-3.1 by this push:
     new 43af1d4632b branch-3.1: [Debug](distribute) Check bucket hash table 
before quit #53661 (#54367)
43af1d4632b is described below

commit 43af1d4632bc1ed1b94c3e67551a27ad79ae3b03
Author: bobhan1 <[email protected]>
AuthorDate: Wed Aug 6 18:53:10 2025 +0800

    branch-3.1: [Debug](distribute) Check bucket hash table before quit #53661 
(#54367)
    
    pick #53661
---
 be/src/vec/functions/function_string.cpp           |   1 +
 be/src/vec/functions/function_string.h             |  55 ++++++++++
 .../doris/catalog/BuiltinScalarFunctions.java      |   2 +
 .../functions/scalar/Crc32Internal.java            |  77 ++++++++++++++
 .../expressions/visitor/ScalarFunctionVisitor.java |   5 +
 .../apache/doris/regression/RegressionTest.groovy  |  11 +-
 .../check_hash_bucket_table.groovy                 | 118 +++++++++++++++++++++
 7 files changed, 267 insertions(+), 2 deletions(-)

diff --git a/be/src/vec/functions/function_string.cpp 
b/be/src/vec/functions/function_string.cpp
index 97d8079985d..d1041b7b0ad 100644
--- a/be/src/vec/functions/function_string.cpp
+++ b/be/src/vec/functions/function_string.cpp
@@ -1381,6 +1381,7 @@ void register_function_string(SimpleFunctionFactory& 
factory) {
     factory.register_function<FunctionStrcmp>();
     factory.register_function<FunctionNgramSearch>();
     factory.register_function<FunctionXPathString>();
+    factory.register_function<FunctionCrc32Internal>();
 
     factory.register_alias(FunctionLeft::name, "strleft");
     factory.register_alias(FunctionRight::name, "strright");
diff --git a/be/src/vec/functions/function_string.h 
b/be/src/vec/functions/function_string.h
index 97c950d97b7..2e59dce22ef 100644
--- a/be/src/vec/functions/function_string.h
+++ b/be/src/vec/functions/function_string.h
@@ -49,6 +49,9 @@
 #include "gutil/strings/numbers.h"
 #include "gutil/strings/substitute.h"
 #include "runtime/decimalv2_value.h"
+#include "runtime/define_primitive_type.h"
+#include "runtime/primitive_type.h"
+#include "runtime/raw_value.h"
 #include "runtime/string_search.hpp"
 #include "util/sha.h"
 #include "util/string_util.h"
@@ -5021,4 +5024,56 @@ private:
     }
 };
 
+// ATTN: for debug only
+// compute crc32 hash value as the same way in 
`VOlapTablePartitionParam::find_tablets()`
+class FunctionCrc32Internal : public IFunction {
+public:
+    static constexpr auto name = "crc32_internal";
+    static FunctionPtr create() { return 
std::make_shared<FunctionCrc32Internal>(); }
+    String get_name() const override { return name; }
+    size_t get_number_of_arguments() const override { return 0; }
+    bool is_variadic() const override { return true; }
+    bool use_default_implementation_for_nulls() const override { return false; 
}
+    DataTypePtr get_return_type_impl(const DataTypes& arguments) const 
override {
+        return std::make_shared<DataTypeInt64>();
+    }
+
+    Status execute_impl(FunctionContext* context, Block& block, const 
ColumnNumbers& arguments,
+                        size_t result, size_t input_rows_count) const override 
{
+        DCHECK_GE(arguments.size(), 1);
+
+        auto argument_size = arguments.size();
+        std::vector<ColumnPtr> argument_columns(argument_size);
+        std::vector<PrimitiveType> argument_primitive_types(argument_size);
+
+        for (size_t i = 0; i < argument_size; ++i) {
+            argument_columns[i] =
+                    
block.get_by_position(arguments[i]).column->convert_to_full_column_if_const();
+            argument_primitive_types[i] =
+                    
block.get_by_position(arguments[i]).type->get_type_as_type_descriptor().type;
+        }
+
+        auto res_col = ColumnInt64::create();
+        auto& res_data = res_col->get_data();
+        res_data.resize_fill(input_rows_count, 0);
+
+        for (size_t i = 0; i < input_rows_count; ++i) {
+            uint32_t hash_val = 0;
+            for (size_t j = 0; j < argument_size; ++j) {
+                const auto& column = argument_columns[j];
+                auto primitive_type = argument_primitive_types[j];
+                auto val = column->get_data_at(i);
+                if (val.data != nullptr) {
+                    hash_val = RawValue::zlib_crc32(val.data, val.size, 
primitive_type, hash_val);
+                } else {
+                    hash_val = HashUtil::zlib_crc_hash_null(hash_val);
+                }
+            }
+            res_data[i] = hash_val;
+        }
+
+        block.replace_by_position(result, std::move(res_col));
+        return Status::OK();
+    }
+};
 } // namespace doris::vectorized
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
index ea49acefd67..579f3148dd5 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
@@ -129,6 +129,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CosineDistanc
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Cot;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.CountEqual;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Crc32;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.Crc32Internal;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.CreateMap;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CreateNamedStruct;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CreateStruct;
@@ -771,6 +772,7 @@ public class BuiltinScalarFunctions implements 
FunctionHelper {
             scalar(Left.class, "left", "strleft"),
             scalar(Length.class, "length"),
             scalar(Crc32.class, "crc32"),
+            scalar(Crc32Internal.class, "crc32_internal"),
             scalar(Like.class, "like"),
             scalar(Ln.class, "ln", "dlog1"),
             scalar(Locate.class, "locate"),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Crc32Internal.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Crc32Internal.java
new file mode 100644
index 00000000000..d42cfbeaa20
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Crc32Internal.java
@@ -0,0 +1,77 @@
+// 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.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
+import org.apache.doris.nereids.trees.expressions.functions.ComputePrecision;
+import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.coercion.AnyDataType;
+import org.apache.doris.nereids.util.ExpressionUtils;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * for debug only, compute crc32 hash value as the same way in 
`VOlapTablePartitionParam::find_tablets()`
+ */
+public class Crc32Internal extends ScalarFunction
+        implements UnaryExpression, ExplicitlyCastableSignature, 
AlwaysNotNullable, ComputePrecision {
+
+    public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+            
FunctionSignature.ret(BigIntType.INSTANCE).varArgs(AnyDataType.INSTANCE_WITHOUT_INDEX)
+    );
+
+    /**
+     * constructor with 1 or more arguments.
+     */
+    public Crc32Internal(Expression arg, Expression... varArgs) {
+        super("crc32_internal", ExpressionUtils.mergeArguments(arg, varArgs));
+    }
+
+    /**
+     * withChildren.
+     */
+    @Override
+    public Crc32Internal withChildren(List<Expression> children) {
+        Preconditions.checkArgument(children.size() >= 1);
+        return new Crc32Internal(children.get(0),
+                children.subList(1, children.size()).toArray(new 
Expression[0]));
+    }
+
+    @Override
+    public List<FunctionSignature> getSignatures() {
+        return SIGNATURES;
+    }
+
+    @Override
+    public FunctionSignature computePrecision(FunctionSignature signature) {
+        return signature;
+    }
+
+    @Override
+    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+        return visitor.visitCrc32Internal(this, context);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
index 0c49e99362f..c1725cd1658 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
@@ -137,6 +137,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CosineDistanc
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Cot;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.CountEqual;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Crc32;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.Crc32Internal;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.CreateMap;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CreateNamedStruct;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CreateStruct;
@@ -1546,6 +1547,10 @@ public interface ScalarFunctionVisitor<R, C> {
         return visitScalarFunction(crc32, context);
     }
 
+    default R visitCrc32Internal(Crc32Internal crc32Internal, C context) {
+        return visitScalarFunction(crc32Internal, context);
+    }
+
     default R visitLike(Like like, C context) {
         return visitStringRegexPredicate(like, context);
     }
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
index 65def1c50f1..700ff4add8b 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy
@@ -313,12 +313,19 @@ class RegressionTest {
         if (!config.withOutLoadData) {
             log.info('Start to run load scripts')
             runScripts(config, recorder, directoryFilter,
-                    { fileName -> fileName.substring(0, 
fileName.lastIndexOf(".")) == "load" })
+                    { fileName -> {
+                        def name = fileName.substring(0, 
fileName.lastIndexOf("."))
+                        return name == "load" && name != 
"check_hash_bucket_table"} })
         }
         log.info('Start to run scripts')
         runScripts(config, recorder, directoryFilter, 
-                { fileName -> fileName.substring(0, fileName.lastIndexOf(".")) 
!= "load" })
+                { fileName -> {
+                        def name = fileName.substring(0, 
fileName.lastIndexOf("."))
+                        return name != "load" && name != 
"check_hash_bucket_table"} })
 
+        log.info('Start to run check hash bucket table scripts')
+        runScripts(config, recorder, directoryFilter, 
+                { fileName -> fileName.substring(0, fileName.lastIndexOf(".")) 
== "check_hash_bucket_table" })
         return recorder
     }
 
diff --git 
a/regression-test/suites/check_hash_bucket_table/check_hash_bucket_table.groovy 
b/regression-test/suites/check_hash_bucket_table/check_hash_bucket_table.groovy
new file mode 100644
index 00000000000..e1b38057da3
--- /dev/null
+++ 
b/regression-test/suites/check_hash_bucket_table/check_hash_bucket_table.groovy
@@ -0,0 +1,118 @@
+// 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 java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+suite("check_hash_bucket_table") {
+
+    AtomicInteger dbNum = new AtomicInteger(0)
+    AtomicInteger tableNum = new AtomicInteger(0)
+    AtomicInteger partitionNum = new AtomicInteger(0)
+    def executor = Executors.newFixedThreadPool(30)
+    def futures = []
+
+    def excludedDbs = ["mysql", "information_schema", 
"__internal_schema"].toSet()
+
+    logger.info("===== [check] begin to check hash bucket tables")
+    def checkPartition = { String db, String tblName, def info ->
+        int bucketNum = info["Buckets"].toInteger()
+        if (bucketNum <= 1) { return false}
+
+        def bucketColumns = info["DistributionKey"]
+        if (bucketColumns == "RANDOM") {return false}
+        def columnsDetail = sql_return_maparray "desc ${tblName} all;"
+        def bucketCols = bucketColumns.split(",").collect { it.trim() }
+        def bucketColsStr = bucketCols.collect { "`${it}`" }.join(",")
+        def partitionName = info["PartitionName"]
+        try {
+            def tabletIdList = sql_return_maparray(""" show replica status 
from ${tblName} partition(${partitionName}); """).collect { it.TabletId 
}.toList()
+            def tabletIds = tabletIdList.toSet()
+            int replicaNum = tabletIdList.stream().filter { it == 
tabletIdList[0] }.count()
+            logger.info("""===== [check] Begin to check partition: 
${db}.${tblName}, partition name: ${partitionName}, bucket num: ${bucketNum}, 
replica num: ${replicaNum}, bucket columns: ${bucketColsStr}""")
+            (0..replicaNum-1).each { replica ->
+                sql "set use_fix_replica=${replica};"
+                tabletIds.each { it2 ->
+                    def tabletId = it2
+                    try {
+                        def res = sql "select crc32_internal(${bucketColsStr}) 
% ${bucketNum} from ${db}.${tblName} tablet(${tabletId}) group by 
crc32_internal(${bucketColsStr}) % ${bucketNum};"
+                        if (res.size() > 1) {
+                            logger.info("""===== [check] check failed: 
${db}.${tblName}, partition name: ${partitionName}, tabletId: ${tabletId}, 
bucket columns: ${bucketColsStr}, res.size()=${res.size()}, res=${res}""")
+                            assert res.size() == 1
+                        }
+                    } catch (AssertionError e) {
+                        throw e
+                    } catch (Throwable e) {
+                        logger.info("===== [check] catch exception, table: 
${db}.${tblName}, partition name: ${partitionName}, tabletId: ${tabletId}, 
e=${e}")
+                    }
+                }
+                sql "set use_fix_replica=-1;"
+            }
+            logger.info("""===== [check] Finish to check table partition: 
${db}.${tblName}, partitionName: ${partitionName}, replica num: ${replicaNum}, 
bucket num: ${bucketNum}, bucket columns: ${bucketColsStr}""")
+        } catch (AssertionError e) {
+            throw e
+        } catch (Throwable e) {
+            logger.info("===== [check] catch exception, table: 
${db}.${tblName}, partition name: ${partitionName}, e=${e}")
+        }
+        return true
+    }
+
+    def checkTable = { String db, String tblName ->
+        sql "use ${db};"
+        def showStmt = sql_return_maparray("show create table 
${tblName}")[0]["Create Table"]
+        def partitionInfo = sql_return_maparray """ show partitions from 
${tblName}; """
+        int checkedPartition = 0
+        partitionInfo.each {
+            if (checkPartition(db, tblName, it)) {
+                ++checkedPartition
+            }
+        }
+        logger.info("""===== [check] Finish to check table: 
${db}.${tblName}""")
+        partitionNum.addAndGet(checkedPartition)
+        return checkedPartition > 0
+    }
+
+    def checkDb = { String db ->
+        sql "use ${db};"
+        dbNum.incrementAndGet()
+        def tables = sql("show full tables").stream().filter{ it[1] == "BASE 
TABLE" }.collect{ it[0] }.toList()
+        def asyncMVs = sql_return_maparray("""select * from 
mv_infos("database"="${db}");""").collect{ it.Name }.toSet()
+        tables.each {
+            def tblName = it
+            if (!asyncMVs.contains(tblName)) {
+                futures << executor.submit({
+                    if (checkTable(db, tblName)) {
+                        tableNum.incrementAndGet()
+                    }
+                })
+            }
+        }
+    }
+
+    def allDbs = sql "show databases"
+    allDbs.each {
+        def db = it[0]
+        if (!excludedDbs.contains(db)) {
+            checkDb(db)
+        }
+    }
+    futures.each { it.get() }
+    executor.shutdown()
+    executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES)
+    logger.info("===== [check] finish to check hash bucket tables, db num: 
${dbNum}, table num: ${tableNum}, partition num: ${partitionNum}")
+}


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

Reply via email to