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 3c53ec749fc branch-4.1: [fix](docker)(case) Restore Kerberos/Paimon 
external env and fix flaky file cache cases (#66314)
3c53ec749fc is described below

commit 3c53ec749fc02694c7f24d4ac5ef933af5d95df7
Author: Socrates <[email protected]>
AuthorDate: Fri Jul 31 16:12:36 2026 +0800

    branch-4.1: [fix](docker)(case) Restore Kerberos/Paimon external env and 
fix flaky file cache cases (#66314)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66254
    
    Problem Summary:
    
    Forward-port the external environment and regression-case fixes from
    #66254 to branch-4.1. The branch inherited the same five defects:
    
    1. The lightweight Kerberos environment creates principals with random
    keys on every container start, so keytabs provisioned to Doris hosts no
    longer match after the KDC is rebuilt.
    2. A successful Hive baseline restore skips `init-hive-baseline.sh`,
    while the incremental module list does not include the Paimon HMS table
    definitions. The restored environment therefore misses the Paimon
    databases.
    3. `test_es_query` lives under `external_table_p2` but still switches to
    its old P0 database name.
    4. `test_file_cache_query_limit` sends BE brpc and HTTP requests to
    `externalEnvIp`, which identifies the third-party Docker host rather
    than the backend host in a multi-host deployment.
    5. `test_file_cache_statistics` treats each queue's `max_size` as a hard
    limit even though queues may borrow unused capacity from one another.
    The actual hard bound is the total file-cache capacity.
    
    This PR uses deterministic Kerberos keys, registers Paimon HMS as an
    incremental Hive module, uses the suite-derived ES database, resolves
    the single live backend before accessing BE endpoints, and validates
    normal-queue usage against the sum of all queue capacities.
    
    The patch was cherry-picked from commit
    `954fde35c03257208b993421fb0d80d8b88769df` and applied cleanly to the
    latest branch-4.1.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [ ] Unit Test
        - [x] Manual test
            - `bash -n` for all changed shell scripts
    -
    
`docker/thirdparties/test/run-thirdparties-docker-hive-bootstrap-groups-test.sh`
            - Paimon HMS module selection and refresh harness
    - Groovy CONVERSION-phase parse for all three changed regression suites
    - Full external regression was not run because it requires a multi-host
    Doris cluster and the third-party Docker stack
    - Behavior changed:
        - [x] No
        - [ ] Yes
    - Does this need documentation?
        - [x] No
        - [ ] Yes
    
    Co-authored-by: morningman <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 docker/thirdparties/docker-compose/hive/README.md  |  1 +
 .../thirdparties/docker-compose/hive/README_ZH.md  |  1 +
 .../docker-compose/hive/scripts/hive-module-lib.sh | 26 +++++++++++++--
 .../hive/scripts/init-hive-baseline.sh             |  8 +++--
 .../kerberos/entrypoint-hive-master.sh             | 11 +++++--
 .../cache/test_file_cache_query_limit.groovy       | 21 ++++++++++--
 .../cache/test_file_cache_statistics.groovy        | 37 +++++++++++++++++-----
 .../external_table_p2/es/test_es_query.groovy      |  7 ++--
 8 files changed, 92 insertions(+), 20 deletions(-)

diff --git a/docker/thirdparties/docker-compose/hive/README.md 
b/docker/thirdparties/docker-compose/hive/README.md
index 443b1d6ada7..57c6d414b1d 100644
--- a/docker/thirdparties/docker-compose/hive/README.md
+++ b/docker/thirdparties/docker-compose/hive/README.md
@@ -60,6 +60,7 @@ Modules are refreshed incrementally: only modules whose 
content SHA changed are
 | `test` | `scripts/data/test/` | Lightweight smoke-test datasets |
 | `preinstalled_hql` | `scripts/create_preinstalled_scripts/*.hql` | ~77 HQL 
files, executed in parallel via `xargs -P` |
 | `view` | `scripts/create_view_scripts/create_view.hql` | View definitions |
+| `paimon_hms` | 
`scripts/create_external_paimon_scripts/create_paimon_tables.hql` | Paimon 
tables registered in HMS (`hdfs_db`, `ali_db`, …); only selected when 
`enablePaimonHms=true` |
 
 ### Layer 3 — Version-Specific File Selection
 
diff --git a/docker/thirdparties/docker-compose/hive/README_ZH.md 
b/docker/thirdparties/docker-compose/hive/README_ZH.md
index 2b1423e387b..8cc32162dc6 100644
--- a/docker/thirdparties/docker-compose/hive/README_ZH.md
+++ b/docker/thirdparties/docker-compose/hive/README_ZH.md
@@ -60,6 +60,7 @@ Hive 启动被拆分为三层互相独立的抽象:
 | `test` | `scripts/data/test/` | 轻量级冒烟测试数据 |
 | `preinstalled_hql` | `scripts/create_preinstalled_scripts/*.hql` | 约 77 个 
HQL 文件,通过 `xargs -P` 并行执行 |
 | `view` | `scripts/create_view_scripts/create_view.hql` | View 定义 |
+| `paimon_hms` | 
`scripts/create_external_paimon_scripts/create_paimon_tables.hql` | 注册到 HMS 的 
Paimon 表(`hdfs_db`、`ali_db` 等),仅在 `enablePaimonHms=true` 时纳入 |
 
 ### Layer 3 — 按版本自动选文件
 
diff --git a/docker/thirdparties/docker-compose/hive/scripts/hive-module-lib.sh 
b/docker/thirdparties/docker-compose/hive/scripts/hive-module-lib.sh
index 1cd00099c9b..beb26b1ff2e 100644
--- a/docker/thirdparties/docker-compose/hive/scripts/hive-module-lib.sh
+++ b/docker/thirdparties/docker-compose/hive/scripts/hive-module-lib.sh
@@ -22,12 +22,19 @@ set -eo pipefail
 . /mnt/scripts/hive-common-lib.sh
 
 BOOTSTRAP_GROUPS="$(bootstrap_normalize_groups "${HIVE_BOOTSTRAP_GROUPS:-}")"
-DEFAULT_MODULES=(default multi_catalog partition_type statistics tvf 
regression test preinstalled_hql view)
+DEFAULT_MODULES=(default multi_catalog partition_type statistics tvf 
regression test preinstalled_hql view paimon_hms)
 LAST_REFRESH_DETAIL=""
 HIVE_HQL_PARALLEL="${HIVE_HQL_PARALLEL:-${LOAD_PARALLEL}}"
 
 ensure_hive_state_layout
 
+# The Paimon HMS tables need the Paimon storage handler and the object storage
+# credentials that only the Hive3 stack is configured with, so the module opts
+# in via the same flag the settings env files carry.
+paimon_hms_enabled() {
+    [[ "${enablePaimonHms:-false}" == "true" ]]
+}
+
 normalize_hive_modules() {
     local raw_modules="${1:-}"
     local cleaned_modules="${raw_modules// /}"
@@ -35,14 +42,19 @@ normalize_hive_modules() {
     local normalized=()
 
     if [[ -z "${cleaned_modules}" || "${cleaned_modules}" == "all" ]]; then
-        printf '%s\n' "${DEFAULT_MODULES[@]}"
+        for module in "${DEFAULT_MODULES[@]}"; do
+            if [[ "${module}" == "paimon_hms" ]] && ! paimon_hms_enabled; then
+                continue
+            fi
+            echo "${module}"
+        done
         return 0
     fi
 
     IFS=',' read -r -a normalized <<<"${cleaned_modules}"
     for module in "${normalized[@]}"; do
         case "${module}" in
-        
default|multi_catalog|partition_type|statistics|tvf|regression|test|preinstalled_hql|view)
+        
default|multi_catalog|partition_type|statistics|tvf|regression|test|preinstalled_hql|view|paimon_hms)
             echo "${module}"
             ;;
         *)
@@ -126,6 +138,9 @@ calc_module_sha() {
     view)
         files+=("/mnt/scripts/create_view_scripts/create_view.hql")
         ;;
+    paimon_hms)
+        
files+=("/mnt/scripts/create_external_paimon_scripts/create_paimon_tables.hql")
+        ;;
     *)
         echo "Unknown module for sha: ${module}" >&2
         return 1
@@ -314,6 +329,11 @@ refresh_module() {
         LAST_REFRESH_DETAIL="create_view.hql"
         run_hive_hql /mnt/scripts/create_view_scripts/create_view.hql 
"create_view.hql"
         ;;
+    paimon_hms)
+        LAST_REFRESH_DETAIL="create_paimon_tables.hql"
+        run_hive_hql 
/mnt/scripts/create_external_paimon_scripts/create_paimon_tables.hql \
+            "create_paimon_tables.hql"
+        ;;
     *)
         echo "Unknown module for refresh: ${module}" >&2
         return 1
diff --git 
a/docker/thirdparties/docker-compose/hive/scripts/init-hive-baseline.sh 
b/docker/thirdparties/docker-compose/hive/scripts/init-hive-baseline.sh
index 49424054fbd..02ce6c42f9c 100644
--- a/docker/thirdparties/docker-compose/hive/scripts/init-hive-baseline.sh
+++ b/docker/thirdparties/docker-compose/hive/scripts/init-hive-baseline.sh
@@ -31,6 +31,10 @@ copy_to_hdfs_if_selected "paimon1"
 copy_to_hdfs_if_selected "tvf_data"
 copy_to_hdfs_if_selected "preinstalled_data"
 
-if [[ ${enablePaimonHms:-false} == "true" ]]; then
-    run_hive_hql 
/mnt/scripts/create_external_paimon_scripts/create_paimon_tables.hql 
"create_paimon_table.hql"
+# Go through the module framework rather than calling the HQL directly: this
+# script only runs on the full-init path, while a baseline restore reaches the
+# Paimon tables through refresh-hive-modules.sh. Recording the module state
+# here also keeps the follow-up refresh pass from running the same HQL twice.
+if paimon_hms_enabled; then
+    refresh_module paimon_hms
 fi
diff --git 
a/docker/thirdparties/docker-compose/kerberos/entrypoint-hive-master.sh 
b/docker/thirdparties/docker-compose/kerberos/entrypoint-hive-master.sh
index 5dd4adf46d4..6735ae7ee40 100644
--- a/docker/thirdparties/docker-compose/kerberos/entrypoint-hive-master.sh
+++ b/docker/thirdparties/docker-compose/kerberos/entrypoint-hive-master.sh
@@ -37,6 +37,7 @@ readonly HTTP_PRINCIPAL="HTTP/${HOST}@${REALM}"
 readonly HIVE_PRINCIPAL="hive/${HOST}@${REALM}"
 readonly HIVE_CLIENT_PRINCIPAL="hive/presto-master.docker.cluster@${REALM}"
 readonly 
PRESTO_CLIENT_PRINCIPAL="presto-server/presto-master.docker.cluster@${REALM}"
+readonly PRINCIPAL_PASSWORD="doris-kerberos-test"
 
 declare -a SERVICE_PIDS=()
 
@@ -70,12 +71,18 @@ wait_for_port() {
     return 1
 }
 
+# Keys must stay identical across container rebuilds. Deployments that run
+# Doris on separate hosts from this container provision /keytabs out of band,
+# so a key that is re-randomized on every start makes every such client fail
+# the AS-REP decryption with "GeneralSecurityException: Checksum failed".
+# A fixed password plus the single fixed enctype in kdc.conf yields a stable
+# key, and -norandkey exports that key instead of rolling a new one.
 create_keytab() {
     local principal=$1
     local keytab=$2
 
-    kadmin.local -r "${REALM}" -q "addprinc -randkey ${principal}"
-    kadmin.local -r "${REALM}" -q "ktadd -k ${keytab} ${principal}"
+    kadmin.local -r "${REALM}" -q "addprinc -pw ${PRINCIPAL_PASSWORD} 
${principal}"
+    kadmin.local -r "${REALM}" -q "ktadd -k ${keytab} -norandkey ${principal}"
 }
 
 report_stage() {
diff --git 
a/regression-test/suites/external_table_p0/cache/test_file_cache_query_limit.groovy
 
b/regression-test/suites/external_table_p0/cache/test_file_cache_query_limit.groovy
index 540c07fab66..7e81f220447 100644
--- 
a/regression-test/suites/external_table_p0/cache/test_file_cache_query_limit.groovy
+++ 
b/regression-test/suites/external_table_p0/cache/test_file_cache_query_limit.groovy
@@ -52,9 +52,24 @@ suite("test_file_cache_query_limit", 
"external_docker,hive,external_docker_hive,
 
     sql """set enable_file_cache=true"""
 
-    // Check backend configuration prerequisites
     // Note: This test case assumes a single backend scenario. Testing with 
single backend is logically equivalent
     // to testing with multiple backends having identical configurations, but 
simpler in logic.
+    // The assumption is load-bearing rather than cosmetic: the HTTP calls 
below clear and inspect ONE backend's
+    // file cache while the queries are served by the whole cluster, so with 
several backends the inspected cache
+    // never reflects what the query actually cached. Skip instead of 
reporting a false failure.
+    def aliveBackends = sql_return_maparray("show backends").findAll {
+        it.Alive.toString().equalsIgnoreCase("true")
+    }
+    if (aliveBackends.size() != 1) {
+        logger.info("skip test_file_cache_query_limit: it assumes a single 
backend, found ${aliveBackends.size()}")
+        return
+    }
+    // The backend HTTP/brpc endpoints must be addressed by the backend's own 
host. externalEnvIp is the
+    // third-party docker host (hive/es/...), which in a multi-host deployment 
runs no backend at all, so
+    // curling it silently yields no file cache metrics.
+    String beHost = aliveBackends[0].Host
+
+    // Check backend configuration prerequisites
     def enableFileCacheResult = sql """show backend config like 
'enable_file_cache';"""
     logger.info("enable_file_cache configuration: " + enableFileCacheResult)
     assertFalse(enableFileCacheResult.size() == 0 || 
!enableFileCacheResult[0][3].equalsIgnoreCase("true"),
@@ -139,7 +154,7 @@ suite("test_file_cache_query_limit", 
"external_docker,hive,external_docker_hive,
     String brpc_port = brpcPortResult[0][3]
 
     // Search file cache capacity
-    def command = ["curl", "-X", "POST", "${externalEnvIp}:${brpc_port}/vars"]
+    def command = ["curl", "-X", "POST", "${beHost}:${brpc_port}/vars"]
     def stringCommand = command.collect{it.toString()}
     def process = new ProcessBuilder(stringCommand as 
String[]).redirectErrorStream(true).start()
 
@@ -160,7 +175,7 @@ suite("test_file_cache_query_limit", 
"external_docker,hive,external_docker_hive,
     logger.info("========================= Start running file cache base test 
========================")
 
     // Clear file cache
-    command = ["curl", "-X", "POST", 
"${externalEnvIp}:${webserver_port}/api/file_cache?op=clear&sync=true"]
+    command = ["curl", "-X", "POST", 
"${beHost}:${webserver_port}/api/file_cache?op=clear&sync=true"]
     stringCommand = command.collect{it.toString()}
     process = new ProcessBuilder(stringCommand as 
String[]).redirectErrorStream(true).start()
 
diff --git 
a/regression-test/suites/external_table_p0/cache/test_file_cache_statistics.groovy
 
b/regression-test/suites/external_table_p0/cache/test_file_cache_statistics.groovy
index 6e16af8397c..34f4948c510 100644
--- 
a/regression-test/suites/external_table_p0/cache/test_file_cache_statistics.groovy
+++ 
b/regression-test/suites/external_table_p0/cache/test_file_cache_statistics.groovy
@@ -31,8 +31,8 @@ final String HIT_RATIO_5M_METRIC_FALSE_MSG = 
HIT_RATIO_CHECK_FAILED_PREFIX + "hi
 
 // Constants for normal queue check
 final String NORMAL_QUEUE_CHECK_FAILED_PREFIX = "Normal queue check failed: "
-final String NORMAL_QUEUE_SIZE_VALIDATION_FAILED_MSG = 
NORMAL_QUEUE_CHECK_FAILED_PREFIX + "size validation failed (curr_size should be 
> 0 and < max_size)"
-final String NORMAL_QUEUE_ELEMENTS_VALIDATION_FAILED_MSG = 
NORMAL_QUEUE_CHECK_FAILED_PREFIX + "elements validation failed (curr_elements 
should be > 0 and < max_elements)"
+final String NORMAL_QUEUE_SIZE_VALIDATION_FAILED_MSG = 
NORMAL_QUEUE_CHECK_FAILED_PREFIX + "size validation failed (curr_size should be 
> 0 and <= total cache capacity)"
+final String NORMAL_QUEUE_ELEMENTS_VALIDATION_FAILED_MSG = 
NORMAL_QUEUE_CHECK_FAILED_PREFIX + "elements validation failed (curr_elements 
should be > 0)"
 
 // Constants for hit and read counts check
 final String HIT_AND_READ_COUNTS_CHECK_FAILED_PREFIX = "Hit and read counts 
check failed: "
@@ -174,8 +174,20 @@ suite("test_file_cache_statistics", 
"external_docker,hive,external_docker_hive,p
     // ===== Normal Queue Metrics Check =====
     // curr_size / curr_elements are monitor-published; poll until populated 
(> 0) across paths.
     // max_size / max_elements come from the queue's static capacity (not 
monitor-published), so
-    // they are read once without polling. SUM across paths preserves the curr 
< max inequality
-    // (sum of per-path curr < sum of per-path max, since each curr < max).
+    // they are read once without polling.
+    //
+    // A queue's own max_size is a SOFT limit, not a bound to assert against: 
when a queue is over
+    // its share, BlockFileCache::try_reserve_from_other_queue lets it keep 
growing as long as the
+    // WHOLE cache still fits (`_cur_cache_size + size > _capacity && 
cur_queue_size + size >
+    // cur_queue_max_size` is the only rejection), evicting from the 
under-used queues instead --
+    // see the "Hit the soft limit by self" branch and is_overflow(), which 
compares against
+    // _capacity alone. So normal_queue_curr_size legitimately exceeds 
normal_queue_max_size
+    // whenever the ttl/index/disposable queues are not full, which depends on 
whichever cases ran
+    // before on this shared cache. Asserting curr < max encoded that 
non-invariant and was flaky.
+    //
+    // The hard bound the BE actually enforces is the per-cache _capacity, and 
by construction in
+    // get_file_cache_settings() capacity == normal + index + ttl + disposable 
max sizes (the
+    // normal/query queue is defined as the remainder). Sum across paths and 
assert against that.
     pollMetric('normal_queue_curr_size', { it > 0 }, metricPollTimeoutSeconds)
     pollMetric('normal_queue_curr_elements', { it > 0 }, 
metricPollTimeoutSeconds)
 
@@ -188,16 +200,25 @@ suite("test_file_cache_statistics", 
"external_docker,hive,external_docker_hive,p
     def normalQueueMaxElementsSum = cacheMetricSum('normal_queue_max_elements')
     logger.info("normal_queue_max_elements sum: " + normalQueueMaxElementsSum)
 
+    def indexQueueMaxSizeSum = cacheMetricSum('index_queue_max_size')
+    def ttlQueueMaxSizeSum = cacheMetricSum('ttl_queue_max_size')
+    def disposableQueueMaxSizeSum = cacheMetricSum('disposable_queue_max_size')
+    Double cacheCapacitySum = (normalQueueMaxSizeSum == null || 
indexQueueMaxSizeSum == null
+            || ttlQueueMaxSizeSum == null || disposableQueueMaxSizeSum == 
null) ? null
+            : normalQueueMaxSizeSum + indexQueueMaxSizeSum + 
ttlQueueMaxSizeSum + disposableQueueMaxSizeSum
+    logger.info("total file cache capacity sum (normal+index+ttl+disposable 
max_size): " + cacheCapacitySum)
+
     boolean hasNormalQueueCurrSize = normalQueueCurrSizeSum != null && 
normalQueueCurrSizeSum > 0
     boolean hasNormalQueueMaxSize = normalQueueMaxSizeSum != null && 
normalQueueMaxSizeSum > 0
     boolean hasNormalQueueCurrElements = normalQueueCurrElementsSum != null && 
normalQueueCurrElementsSum > 0
     boolean hasNormalQueueMaxElements = normalQueueMaxElementsSum != null && 
normalQueueMaxElementsSum > 0
 
-    // Check if current size is less than max size and current elements is 
less than max elements
+    // The queue must be in use and must stay within the cache's hard 
capacity. max_elements is only
+    // logged: element counts are not bounded by the sum of the per-queue 
element caps either, since
+    // a block may be smaller than max_file_block_size.
     boolean normalQueueSizeValid = hasNormalQueueCurrSize && 
hasNormalQueueMaxSize &&
-        normalQueueCurrSizeSum < normalQueueMaxSizeSum
-    boolean normalQueueElementsValid = hasNormalQueueCurrElements && 
hasNormalQueueMaxElements &&
-        normalQueueCurrElementsSum < normalQueueMaxElementsSum
+        cacheCapacitySum != null && normalQueueCurrSizeSum <= cacheCapacitySum
+    boolean normalQueueElementsValid = hasNormalQueueCurrElements && 
hasNormalQueueMaxElements
 
     logger.info("Normal queue metrics check result - size valid: 
${normalQueueSizeValid}, " +
         "elements valid: ${normalQueueElementsValid}")
diff --git a/regression-test/suites/external_table_p2/es/test_es_query.groovy 
b/regression-test/suites/external_table_p2/es/test_es_query.groovy
index bfe2ee7170b..c3d6b75ecca 100644
--- a/regression-test/suites/external_table_p2/es/test_es_query.groovy
+++ b/regression-test/suites/external_table_p2/es/test_es_query.groovy
@@ -193,9 +193,12 @@ suite("test_es_query", "p2,external") {
             }
         }
 
-        def query_catalogs = { -> 
+        def query_catalogs = { ->
             sql """switch internal"""
-            sql """use regression_test_external_table_p0_es"""
+            // test_v1/test_v2 live in this suite's own database, which the
+            // framework derives from the suite directory. Hardcoding the name
+            // breaks whenever the suite moves between p0 and p2.
+            sql """use ${context.dbName}"""
             executeWithRetry("""select * from test_v1 where test2='text#1'""", 
"sql01", 30)
             order_qt_sql01 """select * from test_v1 where test2='text#1'"""
             order_qt_sql02 """select * from test_v1 where esquery(test2, 
'{"match":{"test2":"text#1"}}')"""


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

Reply via email to