You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by yi...@apache.org on 2026/10/08 03:23:41 UTC

(doris) 10/35: [fix](regression) Backport regression stability fixes to branch-4.1 (#68449) (#68618)

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

commit a8f3b9dd7dc6dae943247fda7aea588929545e84
Author: chunping <zh...@selectdb.com>
AuthorDate: Tue Sep 29 21:36:07 2026 +0800

    [fix](regression) Backport regression stability fixes to branch-4.1 (#68449) (#68618)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #68449
    
    Problem Summary:
    
    Backport #68449 (`50a39819e185f54fbb3a82e67182b1ec8f78433b`) to
    `branch-4.1`. The changes stabilize regression clients and orchestration
    for TLS/mTLS, Docker isolation, multi-cluster state cleanup, cloud
    cache/compaction, asynchronous metadata visibility, and delete-bitmap
    restart/concurrency tests. Doris Compose uses exact container-name
    matching and retries concurrent removal races.
    
    Resolved 27 conflict paths with these branch adaptations:
    
    - Keep the `nereids_p0` suite paths, generated database names, planner
    settings, existing SHOW columns and 4.1-only test logic.
    - Preserve configured-master JDBC/HTTP endpoints in the
    insert-visible-timeout suite, including mapped ports from #65340.
    - Keep the routine-load WHERE expectation `(k00 = 8)`: 4.1's
    `PlanUtils.ExpressionToExpr` stores unquoted slot labels, unlike master.
    Retain the source topic fix, diagnostic fields and independent polling
    counters.
    - Keep `nereids_p0/hbo` in the non-concurrent exclusions while excluding
    all `cloud_p0` suites as intended by the source fix.
    - Combine the shared TLS/compaction helpers with 4.1's existing
    framework behavior; do not import unrelated scenarios present only in
    the source commit's parent.
    - Omit `test_partition_cloud_add_build_index_validation.groovy`: the
    partition index-format rollout feature from #66475 and its required
    properties/configuration are absent from 4.1.
    - Reuse the original PR's result files without manually creating
    expected output; retain the deterministic variant ordering fix.
    
    The resulting backport changes 350 files; no FE implementation,
    protocol, or storage-format change is added.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test:
    - [x] Regression test on an existing 4.1.4 cloud cluster (1 FE / 3 BE):
    127 passed, 3 failed, 186 skipped across all 316 changed suites.
    - Retained failures: `test_create_view_nereids` (SHOW VIEW column-name
    quoting mismatch), `test_streaming_insert_job_priv` (one successful task
    and one pending task after the 300-second wait), and
    `test_load_channel_profile` (S3 TVF query reports unknown column `c1`).
    Their failed assertions/queries are unchanged by this backport. Root
    causes are not established by this run.
    - Python support is disabled: 19 attempts hit that prerequisite and 23
    more suites were skipped. One JDBC fixture was unavailable, and the
    single-BE Java static-load case returned early on this three-BE cluster;
    these are counted as skipped, not passes.
    - Other skipped suites require dedicated Docker/multi-compute-group
    fixtures, restarts, TLS/external services, global configuration/fault
    injection, or stress capacity. Four packed-file injection suites were
    excluded before launch.
    - Arrow Flight authentication passed after correcting only the
    task-local endpoint configuration. Three small-table TTL/row-store
    compaction follow-ups also passed.
    - The failed streaming job and its test user were cleaned up. All FE/BE
    nodes remained alive and HTTP health returned 200 after execution.
    - [x] Unit Test: `run-regression-test.sh --compile` completed on the
    remote validation host; all 35 regression-framework tests passed (0
    failures, 0 errors, 0 skips).
        - [x] Manual test:
            - Diff whitespace and conflict-marker checks passed.
    - All 340 changed existing Groovy files passed conversion-phase parsing
    with Groovy 4.0.19 / JDK 17; both changed Python files passed
    `py_compile`.
    - `build-support/check-format.sh` passed with clang-format 16.0.6 (4,400
    C/C++ files).
    - Compared conflict resolutions with the source patch and branch-4.1
    implementations, including RPC/debug-point dependencies and routine-load
    expression serialization.
    - BE/FE module builds, deployment and clang-tidy were not run, per the
    requested existing-cluster validation scope. The live enterprise cluster
    is not a build of this backport, so BE debug-point additions and
    excluded scenarios still require CI.
    - Behavior changed: Yes. Regression framework/test behavior and Doris
    Compose discovery match the applicable source fixes; BE additions are
    gated debug points.
    - Does this need documentation: No.
    
    <details>
    <summary>Backport self-review checkpoints</summary>
    
    | Checkpoint | Conclusion |
    | --- | --- |
    | Goal and tests | Applicable source fixes preserved; syntax/static
    checks and 35 framework unit tests pass. Existing-cluster suite outcomes
    and remaining CI scope are listed above. |
    | Scope | Source patch plus documented branch adaptations; unsupported
    feature suite omitted. |
    | Concurrency and locking | Existing production lock order retained;
    debug-only post-calculation barrier releases the rowset lock before
    waiting. Suite worker errors propagate to their owners. |
    | Lifecycle | Source connection/process cleanup and shared-state
    restoration retained. |
    | Configuration | Existing TLS and debug configuration used; referenced
    cloud sync settings exist in 4.1. |
    | Compatibility | Target RPC/HTTP/debug APIs verified; branch endpoint
    mappings and result schemas retained. No wire/storage format changes. |
    | Parallel paths | Relevant plaintext/TLS and local/cloud helper paths
    retained. |
    | Conditional checks | Source bounded polling and negative assertions
    retained; branch adaptations checked against implementation. |
    | Test coverage | Source coverage retained; 35 framework tests and 127
    existing-cluster suites passed, with 3 retained failures and 186
    documented skips. |
    | Expected results | Source result files reused; no hand-written golden
    output. |
    | Observability | Source timeout/last-state diagnostics and
    token/tablet/transaction debug logs retained. |
    | Persistence and failover | No production persistence or replay change.
    |
    | Writes and atomicity | Production write behavior unchanged outside
    enabled test debug points. |
    | FE/BE propagation | Existing target sync-load RPC and metadata
    endpoints support new helpers. |
    | Performance | Production additions are gated debug checks; regression
    polling is bounded. |
    | Other findings | Focused framework/BE, TLS-suite and cloud-suite
    reviews found no unresolved backport-specific blocker. |
    
    </details>
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases and CI results
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../cloud/cloud_engine_calc_delete_bitmap_task.cpp |  26 +
 be/src/cloud/cloud_meta_mgr.cpp                    |  10 +
 docker/runtime/doris-compose/utils.py              |  43 +-
 .../cache/ttl/st04_alter_ttl_n_to_0_runtime.out    |   7 +-
 .../cache/ttl/st06_warmup_ttl_type_assert.out      |   4 +-
 .../data/cloud_p0/cache/ttl/test_show_ttl.out      |  16 -
 .../test_double_write_when_schema_change.out       |   4 +-
 .../org/apache/doris/regression/Config.groovy      | 139 ++--
 .../doris/regression/action/ProfileAction.groovy   |  38 +-
 .../doris/regression/suite/ScriptContext.groovy    |  11 +
 .../org/apache/doris/regression/suite/Suite.groovy | 231 ++++++-
 .../doris/regression/suite/SuiteContext.groovy     |  55 +-
 .../apache/doris/regression/util/DebugPoint.groovy |  12 +-
 .../org/apache/doris/regression/util/Http.groovy   |  32 +-
 .../regression/util/MultiClusterStateGuard.groovy  | 195 ++++++
 .../doris/regression/util/MySqlClient.groovy       |  70 ++
 .../doris/regression/util/RowsetMetaUtils.groovy   |  79 +++
 .../regression/util/WarmupMetricsUtils.groovy      |  96 ++-
 .../regression/ConfigArrowFlightSqlUrlTest.groovy  |  61 ++
 .../doris/regression/ConfigRootPasswordTest.groovy |  61 ++
 .../suite/CompactionPluginCompatibilityTest.groovy | 154 +++++
 .../regression/suite/SuiteSshExecTest.groovy}      |  24 +-
 .../regression/util/HttpAuthenticationTest.groovy  |  72 ++
 .../util/MultiClusterStateGuardTest.groovy         | 256 ++++++++
 .../doris/regression/util/MySqlClientTest.groovy   |  96 +++
 .../regression/util/RowsetMetaUtilsTest.groovy     |  81 +++
 .../nonConcurrent/conf/regression-conf.groovy      |   2 +-
 .../plugins/cloud_multi_cluster_state.groovy       | 136 ++++
 regression-test/plugins/plugin_compaction.groovy   | 134 ++--
 .../plugins/plugin_curl_requester.groovy           |   3 +-
 regression-test/plugins/plugin_index_change.groovy |   5 +-
 .../suites/account_p0/test_auth_show.groovy        |   4 +-
 .../suites/account_p0/test_grant_priv.groovy       |  10 +-
 .../account_p0/test_grant_priv_resource.groovy     |   4 +-
 .../account_p0/test_grant_priv_workload.groovy     |   4 +-
 .../account_p0/test_nereids_authentication.groovy  |  14 +-
 .../account_p0/test_nereids_row_policy.groovy      |   4 +-
 .../suites/account_p0/test_set_password.groovy     |   6 +-
 .../suites/account_p0/test_view_row_policy.groovy  |   6 +-
 .../ann_topn_small_candidate_fallback.groovy       |  21 +-
 .../arrow_flight_sql_p0/test_auth_remote_ip.groovy |   3 +-
 .../suites/audit/test_audit_log_queue_time.groovy  | 212 +++---
 .../auth_call/test_dml_select_udf_auth.groovy      |   1 +
 .../test_dml_stream_load_be_auth_docker.groovy     |  49 +-
 .../auth_p0/test_role_mapping_system_table.groovy  |   2 +-
 .../cloud_p0/auth/test_set_default_cluster.groovy  |   6 +-
 .../balance/test_cross_cg_bvar_metrics.groovy      |  27 +-
 .../multi_cluster/read_write/sync_insert.groovy    | 330 ++++++----
 .../read_write/test_multi_stale_rowset.groovy      |  70 +-
 .../cluster/test_warm_up_cluster_batch.groovy      | 409 +++++++-----
 ...est_warm_up_cluster_event_cancel_expired.groovy |  92 ++-
 .../cluster/test_warm_up_compute_group.groovy      | 209 +++---
 .../warm_up/table/test_warm_up_partition.groovy    | 399 +++++++-----
 .../test_warm_up_same_table_multi_times.groovy     |  43 +-
 .../warm_up/table/test_warm_up_table.groovy        | 200 ++++--
 .../warm_up/table/test_warm_up_tables.groovy       | 486 ++++++++------
 .../test_clear_file_cache_on_load_failure.groovy   | 121 ++--
 .../cloud_p0/cache/test_file_cache_info.groovy     | 318 +++++----
 .../suites/cloud_p0/cache/ttl/alter_ttl_2.groovy   | 231 +++----
 .../suites/cloud_p0/cache/ttl/alter_ttl_4.groovy   | 489 ++++++++------
 .../cloud_p0/cache/ttl/alter_ttl_random.groovy     |  21 +-
 .../cloud_p0/cache/ttl/alter_ttl_seconds.groovy    | 170 -----
 .../cache/ttl/st04_alter_ttl_n_to_0_runtime.groovy | 233 ++++---
 .../cache/ttl/st06_warmup_ttl_type_assert.groovy   | 231 ++++---
 .../suites/cloud_p0/cache/ttl/test_show_ttl.groovy |  57 ++
 .../suites/cloud_p0/cache/ttl/test_show_ttl.sql    |  40 --
 .../suites/cloud_p0/cache/ttl/test_ttl.groovy      | 433 +++++++-----
 ...st_file_cache_warmup_read_metrics_docker.groovy |   4 +-
 .../cache/warm_up/test_warmup_table_docker.groovy  |   6 +-
 ...test_cloud_alter_disable_auto_compaction.groovy |   8 +-
 .../async_copy_into/async_load.groovy              |  10 +-
 .../multi_cluster/copy_into/sync_laod.groovy       | 124 ++--
 .../cloud_p0/multi_cluster/default_cluster.groovy  |  38 +-
 .../multi_cluster_s3_load/load.groovy              |  66 +-
 .../cloud_p0/multi_cluster/rename_cluster.groovy   |  10 +-
 .../routine_load/test_routine_load.groovy          |   6 +-
 ...test_materialized_view_with_drop_cluster.groovy |   8 +-
 ...est_materialized_view_with_readd_cluster.groovy |   8 +-
 .../test_rollup_with_drop_cluster.groovy           |   8 +-
 .../test_rollup_with_readd_cluster.groovy          |   8 +-
 .../test_schema_change_with_drop_cluster.groovy    |   8 +-
 .../test_schema_change_with_readd_cluster.groovy   |   8 +-
 .../smooth_upgrade/smooth_upgrade.groovy           |  10 +
 .../multi_cluster/stream_load/stream_load.groovy   |  67 +-
 .../stream_load/stream_load_2pc.groovy             |   3 +
 .../stream_load/stream_load_lb.groovy              |  12 +-
 .../cloud_p0/multi_cluster/table_rebalance.groovy  |  30 +-
 .../test_apsaradb_internal_stage.groovy            |  31 +-
 .../multi_cluster/test_cloud_cluster.groovy        |  19 +-
 .../multi_cluster/test_drop_cluster.groovy         |  98 +--
 .../cloud_p0/multi_cluster/test_drop_userid.groovy |   2 +-
 .../test_group_commit_multi_cluster.groovy         |  12 +-
 .../multi_cluster/test_overdue_instance.groovy     |  54 +-
 .../suites/cloud_p0/multi_cluster/test_tvf.groovy  |  11 +-
 .../default_vcg_auto_failover.groovy               |  19 +-
 .../use_vcg_read_write_routine_load.groovy         | 722 +++++++++++----------
 .../cloud_p0/snapshot/test_snapshot_command.groovy |  12 +-
 .../test_clean_tablet_when_drop_force_table.groovy | 144 +++-
 .../test_clean_tablet_when_rebalance.groovy        | 109 ++--
 .../test_create_partition_idempotence.groovy       |   3 +-
 .../test_insert_overwrite_idempotence.groovy       |   3 +-
 .../test_schema_change_with_compaction10.groovy    |  66 +-
 .../test_schema_change_with_compaction11.groovy    |  91 +--
 .../test_schema_change_with_compaction5.groovy     |  25 +-
 .../test_schema_change_with_compaction6.groovy     |  67 +-
 .../test_schema_change_with_compaction9.groovy     |  24 +-
 .../test_inverted_index_sc_version1_race.groovy    |   2 +-
 .../test_sc_compaction_cross_v1_retry.groovy       |  73 ++-
 ...c_compaction_cross_v1_stale_base_refresh.groovy |  30 +-
 .../test_sc_compaction_optimization.groovy         |  50 +-
 ...est_sc_compaction_optimization_with_load.groovy |  52 +-
 .../compaction_width_array_column.groovy           |   2 +-
 .../compaction/test_admin_compact_tablet.groovy    |  88 ++-
 ...paction_with_dup_key_max_file_size_limit.groovy | 148 ++---
 .../test_compaction_uniq_keys_row_store.groovy     |   4 +-
 .../test_compaction_uniq_keys_row_store_ck.groovy  |   4 +-
 ...test_compaction_update_big_delete_bitmap.groovy |  17 +-
 .../test_compaction_with_visible_version.groovy    |   3 +
 .../test_config_prune_delete_sign.groovy           | 129 ++--
 ...ilecache_with_base_compaction_thresthold.groovy | 150 +++--
 .../test_mow_compact_multi_segments.groovy         |  48 +-
 ...paction_agg_and_remove_pre_delete_bitmap.groovy |  17 +-
 ...test_mow_compaction_and_rowset_not_exist.groovy |  17 +-
 .../test_mow_compaction_and_schema_change.groovy   |  17 +-
 .../test_mow_stale_rowset_delete_bitmap.groovy     |  17 +-
 .../test_non_mow_key_bounds_aggregation.groovy     |  25 +-
 .../test_create_view_nereids.groovy                |   1 +
 .../suites/ddl_p0/test_drop_view_nereids.groovy    |   1 +
 .../delete/batch-delete-manual.md.groovy           |  12 +-
 .../import-way/group-commit-manual.md.groovy       |  11 +-
 .../import/import-way/mysql-load-manual.md.groovy  |  20 +-
 .../update/unique-update-transaction.md.groovy     |   6 +-
 .../update/update-of-aggregate-model.md.groovy     |   2 +-
 .../update/update-of-unique-model.md.groovy        |   2 +-
 .../test_iceberg_arrow_flight_split_source.groovy  |   3 +-
 .../select/test_mysql_varbinary_with_udf.groovy    |   1 +
 .../test_cloud_full_compaction_do_lease.groovy     | 142 ++--
 .../test_cloud_mow_delete_bitmap_lock_case.groovy  | 412 +++++++++---
 .../test_cloud_mow_partial_update_retry.groovy     |  99 ++-
 ..._cloud_mow_stale_resp_load_load_conflict.groovy | 105 ++-
 .../test_delete_bitmap_lock_with_restart.groovy    | 545 ++++++++--------
 ...est_cold_data_compaction_fault_injection.groovy | 146 +++--
 ...ion_clone_missing_rowset_fault_injection.groovy |  29 +-
 .../test_incomplete_commit_info.groovy             | 117 ++--
 ...st_skip_index_compaction_fault_injection.groovy |  90 +--
 .../http_rest_api/get/test_schema_api.groovy       |   4 +-
 .../http_rest_api/post/test_query_stmt.groovy      |   3 +-
 .../insert_p0/insert_group_commit_into.groovy      |  10 +-
 .../mow_insert_with_partition_drop.groovy          |  24 +-
 .../test_insert_visible_timeout_return_mode.groovy |  16 +-
 regression-test/suites/insert_p0/test_jdbc.groovy  |   4 +-
 .../transaction/test_publish_exception.groovy      |   5 +
 .../suites/insert_p0/transaction/txn_insert.groovy |   5 +-
 .../operate_internal_schema.groovy                 |   2 +-
 .../show_data/test_show_index_data_p2.groovy       | 104 +--
 .../collect/test_javaudf_groupcount.groovy         |   1 +
 .../collect/test_javaudf_murmurhash3.groovy        |   1 +
 .../collect/test_javaudf_sessionize.groovy         |   2 +-
 .../javaudf_p0/date/test_javaudf_adddays.groovy    |   1 +
 .../date/test_javaudf_addisoperioud.groovy         |   1 +
 .../javaudf_p0/date/test_javaudf_daydiff.groovy    |   1 +
 .../function_meta/function_query_test.groovy       |  10 +-
 .../suites/javaudf_p0/function_meta/load.groovy    |  19 +-
 .../json/test_javaudf_convertfromcamelcase.groovy  |   1 +
 .../json/test_javaudf_converttocamelcase.groovy    |   1 +
 regression-test/suites/javaudf_p0/null/load.groovy |   1 +
 .../sanity/test_javaudf_assertequal.groovy         |   1 +
 .../sanity/test_javaudf_assertlessthan.groovy      |   1 +
 .../sanity/test_javaudf_assertudf.groovy           |   1 +
 .../javaudf_p0/sketch/test_javaudf_md5.groovy      |   1 +
 .../test_javaudaf_my_date_datetime.groovy          |   1 +
 .../test_javaudaf_mygroupconcat_string.groovy      |   1 +
 .../javaudf_p0/test_javaudaf_mysum_array.groovy    |   1 +
 .../javaudf_p0/test_javaudaf_mysum_decimal.groovy  |   1 +
 .../test_javaudaf_mysum_float_double.groovy        |   1 +
 .../javaudf_p0/test_javaudaf_mysum_int.groovy      |   1 +
 .../javaudf_p0/test_javaudaf_null_test.groovy      |   1 +
 .../javaudf_p0/test_javaudaf_return_map.groovy     |   1 +
 .../suites/javaudf_p0/test_javaudf_agg_map.groovy  |   1 +
 .../javaudf_p0/test_javaudf_all_types.groovy       |   1 +
 .../suites/javaudf_p0/test_javaudf_array.groovy    |   1 +
 .../suites/javaudf_p0/test_javaudf_auth.groovy     |   5 +-
 .../suites/javaudf_p0/test_javaudf_boolean.groovy  |   1 +
 .../suites/javaudf_p0/test_javaudf_case.groovy     |   1 +
 .../javaudf_p0/test_javaudf_const_test.groovy      |   1 +
 .../suites/javaudf_p0/test_javaudf_date.groovy     |   1 +
 .../suites/javaudf_p0/test_javaudf_decimal.groovy  |   1 +
 .../suites/javaudf_p0/test_javaudf_float.groovy    |   1 +
 .../suites/javaudf_p0/test_javaudf_int.groovy      |   1 +
 .../suites/javaudf_p0/test_javaudf_map.groovy      |   1 +
 .../javaudf_p0/test_javaudf_multi_evaluate.groovy  |   1 +
 .../suites/javaudf_p0/test_javaudf_no_input.groovy |   2 +-
 .../suites/javaudf_p0/test_javaudf_override.groovy |   1 +
 .../suites/javaudf_p0/test_javaudf_ret_map.groovy  |   1 +
 .../test_javaudf_static_load_test.groovy           |   2 +-
 .../suites/javaudf_p0/test_javaudf_string.groovy   |   1 +
 .../javaudf_p0/test_javaudf_struct_type.groovy     |   1 +
 .../javaudf_p0/test_javaudf_with_decimal.groovy    |   1 +
 .../javaudf_p0/test_javaudtf_all_types.groovy      |   1 +
 .../javaudf_p0/test_javaudtf_arrayint.groovy       |   1 +
 .../suites/javaudf_p0/test_javaudtf_decimal.groovy |   1 +
 .../suites/javaudf_p0/test_javaudtf_double.groovy  |   1 +
 .../suites/javaudf_p0/test_javaudtf_int.groovy     |   1 +
 .../suites/javaudf_p0/test_javaudtf_string.groovy  |   1 +
 .../test_streaming_insert_job_priv.groovy          |   8 +-
 .../test_insert_statistic_after_fe_restart.groovy  |   6 +-
 .../test_routine_load_advanced_mapping.groovy      |  56 +-
 .../routine_load/test_routine_load_with_udf.groovy |   1 +
 .../scripts/stream_load_redirect_chunked_e2e.py    |  16 +-
 .../test_packed_file_stream_load_case1.groovy      |   2 +-
 .../test_packed_file_stream_load_case2.groovy      |   2 +-
 .../test_packed_file_stream_load_case3.groovy      |   2 +-
 .../test_packed_file_stream_load_case4.groovy      |   2 +-
 .../test_packed_file_stream_load_case5.groovy      |  11 +-
 .../test_packed_file_stream_load_case6.groovy      |  11 +-
 .../test_packed_file_stream_load_case7.groovy      |  11 +-
 .../test_packed_file_stream_load_case8.groovy      |  11 +-
 .../load_p0/stream_load/test_sink_tolerate.groovy  |   3 +-
 .../load_p0/stream_load/test_stream_load.groovy    |  38 +-
 .../stream_load/test_stream_load_endpoint.groovy   | 141 ++--
 ...test_stream_load_fe_redirect_chunked_e2e.groovy |  10 +-
 .../stream_load/test_stream_load_with_udf.groovy   |   1 +
 .../suites/manager/test_manager_interface_3.groovy | 317 +++------
 .../suites/mtmv_p0/test_expand_star_mtmv.groovy    |   1 +
 .../mysql_ssl_p0/test_mysql_connection.groovy      |  66 +-
 .../cache/parse_sql_from_sql_cache.groovy          |  21 +-
 .../insert_into_table/insert_auth.groovy           |   6 +-
 .../javaudf/collect/test_javaudf_groupcount.groovy |   1 +
 .../collect/test_javaudf_murmurhash3.groovy        |   1 +
 .../javaudf/collect/test_javaudf_sessionize.groovy |   2 +-
 .../javaudf/date/test_javaudf_adddays.groovy       |   1 +
 .../javaudf/date/test_javaudf_addisoperioud.groovy |   1 +
 .../javaudf/date/test_javaudf_daydiff.groovy       |   1 +
 .../json/test_javaudf_convertfromcamelcase.groovy  |   1 +
 .../json/test_javaudf_converttocamelcase.groovy    |   1 +
 .../javaudf/sanity/test_javaudf_assertequal.groovy |   1 +
 .../sanity/test_javaudf_assertlessthan.groovy      |   1 +
 .../javaudf/sanity/test_javaudf_assertudf.groovy   |   1 +
 .../javaudf/sketch/test_javaudf_md5.groovy         |   1 +
 .../javaudf/test_javaudaf_my_date_datetime.groovy  |   1 +
 .../test_javaudaf_mygroupconcat_string.groovy      |   1 +
 .../javaudf/test_javaudaf_mysum_decimal.groovy     |   1 +
 .../test_javaudaf_mysum_float_double.groovy        |   1 +
 .../javaudf/test_javaudaf_mysum_int.groovy         |   1 +
 .../javaudf/test_javaudaf_null_test.groovy         |   1 +
 .../nereids_p0/javaudf/test_javaudf_boolean.groovy |   1 +
 .../nereids_p0/javaudf/test_javaudf_case.groovy    |   1 +
 .../nereids_p0/javaudf/test_javaudf_date.groovy    |   1 +
 .../nereids_p0/javaudf/test_javaudf_decimal.groovy |   1 +
 .../nereids_p0/javaudf/test_javaudf_float.groovy   |   1 +
 .../nereids_p0/javaudf/test_javaudf_int.groovy     |   1 +
 .../nereids_p0/javaudf/test_javaudf_ip.groovy      |   1 +
 .../nereids_p0/javaudf/test_javaudf_null.groovy    |   1 +
 .../nereids_p0/javaudf/test_javaudf_string.groovy  |   1 +
 .../agg_state/test_outfile_agg_state.groovy        |   2 +-
 .../agg_state_array/test_outfile_agg_array.groovy  |   2 +-
 .../test_outfile_agg_state_bitmap.groovy           |   2 +-
 .../nereids_p0/outfile/hll/test_outfile_hll.groovy |   2 +-
 .../test_outfile_quantile_state.groovy             |   2 +-
 .../show/test_nereids_show_functions.groovy        |  13 +-
 .../stats/col_stats/hot_value_analyze_sync.groovy  |   2 +-
 .../test_partition_create_tablet_rr.groovy         |   1 +
 .../auto_partition/sql/multi_thread_load.groovy    |  30 +-
 .../sql/stress_test_insert_into.groovy             |  22 +-
 .../diff_data/stress_test_diff_date_list.groovy    |  16 +-
 .../stress_test_high_concurrency_load.groovy       |  19 +-
 .../same_data/stress_test_same_date_range.groovy   |  19 +-
 .../stress_test_two_stream_load.groovy             |  19 +-
 .../suites/point_query_p0/test_point_query.groovy  |  10 +-
 .../point_query_p0/test_point_query_ck.groovy      |  17 +-
 .../test_point_query_partition.groovy              |   6 +-
 .../suites/point_query_p0/test_rowstore.groovy     |  14 +-
 .../suites/point_query_p0/test_rowstore_ck.groovy  |  14 +-
 .../test_python_raise_error_propagation.groovy     |   3 +
 .../pythonudaf_p0/test_pythonudaf_basic.groovy     |   1 +
 ...st_pythonudaf_complex_aggregation_module.groovy |   1 +
 ..._pythonudaf_complex_state_objects_module.groovy |   1 +
 .../pythonudaf_p0/test_pythonudaf_drop.groovy      |   2 +
 .../test_pythonudaf_forbidden_module.groovy        |   1 +
 .../test_pythonudaf_pkg_isolation.groovy           |   2 +
 .../pythonudaf_p0/test_pythonudaf_simple.groovy    |   1 +
 .../test_pythonudaf_window_advanced_module.groovy  |   1 +
 .../test_python_udaf_complex.groovy                |   1 +
 .../test_python_udf_business_logic.groovy          |   1 +
 .../test_python_udf_external_api.groovy            |   3 +-
 .../test_python_udf_nlp_chinese.groovy             |   1 +
 .../test_python_udtf_complex.groovy                |   1 +
 .../sanity/test_pythonudf_assertequal.groovy       |   1 +
 .../sanity/test_pythonudf_assertlessthan.groovy    |   1 +
 .../pythonudf_p0/test_pythonudf_array.groovy       |   1 +
 .../test_pythonudf_base_data_type.groovy           |   1 +
 .../pythonudf_p0/test_pythonudf_boolean.groovy     |   1 +
 .../test_pythonudf_complex_data_type.groovy        |   1 +
 .../suites/pythonudf_p0/test_pythonudf_drop.groovy |   2 +
 .../test_pythonudf_file_protocol.groovy            |   1 +
 .../pythonudf_p0/test_pythonudf_float.groovy       |   1 +
 .../test_pythonudf_forbidden_module.groovy         |   1 +
 .../suites/pythonudf_p0/test_pythonudf_int.groovy  |   1 +
 .../suites/pythonudf_p0/test_pythonudf_map.groovy  |   1 +
 .../pythonudf_p0/test_pythonudf_module.groovy      |   1 +
 .../test_pythonudf_module_advanced.groovy          |   1 +
 .../test_pythonudf_module_scalar.groovy            |   1 +
 .../test_pythonudf_module_vector.groovy            |   1 +
 .../test_pythonudf_pkg_isolation.groovy            |   2 +
 .../pythonudf_p0/test_pythonudf_ret_map.groovy     |   1 +
 .../pythonudf_p0/test_pythonudf_string.groovy      |   1 +
 .../test_pythonudtf_basic_module.groovy            |   1 +
 .../test_pythonudtf_data_types_module.groovy       |   1 +
 .../pythonudtf_p0/test_pythonudtf_drop.groovy      |   2 +
 .../test_pythonudtf_edge_cases_module.groovy       |   1 +
 .../test_pythonudtf_exceptions_module.groovy       |   1 +
 .../test_pythonudtf_forbidden_module.groovy        |   1 +
 .../test_pythonudtf_io_patterns_module.groovy      |   1 +
 .../test_pythonudtf_pkg_isolation.groovy           |   2 +
 .../test_pythonudtf_sql_integration_module.groovy  |   1 +
 .../query_p0/cache/query_cache_with_context.groovy |   3 +-
 .../query_p0/system/test_partitions_schema.groovy  |   6 +-
 .../query_p0/system/test_table_options.groovy      |   6 +-
 .../query_p0/system/test_table_properties.groovy   |   6 +-
 .../suites/query_p0/test_row_policy.groovy         |   4 +-
 .../query_p2/big_join_build/big_join_build.groovy  |   2 +-
 .../test_set_operation_large_string.groovy         |   2 +-
 .../suites/query_profile/profile_safe.groovy       |   5 +-
 .../query_profile/s3_load_profile_test.groovy      |  55 +-
 .../query_profile/test_load_channel_profile.groovy |   6 +-
 ...e_column_with_delete_drop_column_dup_key.groovy |  82 +--
 ...olumn_with_delete_drop_column_unique_key.groovy |  63 +-
 .../test_filecache_with_alter_table.groovy         |  36 +-
 .../test_alter_table_column_rename.groovy          |   2 +-
 .../test_schema_change_fail.groovy                 |  11 +-
 ...test_show_create_table_and_views_nereids.groovy |  89 ++-
 .../suites/show_p0/test_show_data.groovy           |   2 +-
 .../suites/show_p0/test_show_processlist.groovy    |   6 +-
 .../suites/statistics/test_analyze_all_null.groovy |   2 +-
 .../suites/statistics/test_hot_value.groovy        |   2 +-
 .../test_skip_agg_table_value_column.groovy        |   2 +-
 .../test_partial_update_2pc_schema_change.groovy   |  38 +-
 .../legacy/test_f_2pc_schema_change.groovy         |  38 +-
 .../publish/test_f_auto_inc_compaction.groovy      |  35 +-
 .../test_f_seq_publish_read_from_old.groovy        |  33 +-
 ...xble_partial_update_publish_conflict_seq.groovy |  35 +-
 ...flexible_partial_update_publish_conflict.groovy |  35 +-
 .../test_key_bounds_truncation_basic.groovy        |  27 +-
 ...est_key_bounds_truncation_read_scenarios.groovy |  39 +-
 .../test_partial_update_publish_all_del.groovy     |  35 +-
 .../publish/test_partial_update_publish_seq.groovy |  35 +-
 ...partial_update_delete_sign_with_conflict.groovy |  33 +-
 .../test_partial_update_multi_stmt.groovy          |  13 +-
 .../test_double_write_when_schema_change.groovy    |   4 +-
 .../suites/variant_p0/variant_with_rowstore.groovy |   4 +-
 350 files changed, 8633 insertions(+), 5157 deletions(-)

diff --git a/be/src/cloud/cloud_engine_calc_delete_bitmap_task.cpp b/be/src/cloud/cloud_engine_calc_delete_bitmap_task.cpp
index ffa7529d172..32c96474be7 100644
--- a/be/src/cloud/cloud_engine_calc_delete_bitmap_task.cpp
+++ b/be/src/cloud/cloud_engine_calc_delete_bitmap_task.cpp
@@ -213,6 +213,15 @@ Status CloudTabletCalcDeleteBitmapTask::handle(int64_t queue_time_us) const {
     }
 
     int64_t t3 = MonotonicMicros();
+    DBUG_EXECUTE_IF("CloudEngineCalcDeleteBitmapTask.handle.block_for_restart", {
+        if (dp->param<int64_t>("tablet_id", -1) == _tablet_id) {
+            // The restart regression pairs this marker with the persisted MS lock owner.
+            LOG(INFO) << "delete bitmap restart barrier token="
+                      << dp->param<std::string>("token", "") << " lock_id=" << _transaction_id
+                      << " tablet_id=" << _tablet_id;
+            DBUG_BLOCK;
+        }
+    });
     DBUG_EXECUTE_IF("CloudEngineCalcDeleteBitmapTask.handle.inject_sleep", {
         auto p = dp->param("percent", 0.01);
         // 100s > Config.calculate_delete_bitmap_task_timeout_seconds = 60s
@@ -275,6 +284,23 @@ Status CloudTabletCalcDeleteBitmapTask::handle(int64_t queue_time_us) const {
             DCHECK(invisible_rowsets.size() == i + 1 - empty_rowset_count);
         }
     }
+    DBUG_EXECUTE_IF("CloudTabletCalcDeleteBitmapTask.handle.block_after_calc", {
+        auto target_tablet_id = dp->param<int64_t>("tablet_id", 0);
+        auto target_transaction_id = dp->param<int64_t>("transaction_id", 0);
+        if (status.ok() && target_tablet_id == _tablet_id &&
+            (target_transaction_id == 0 || target_transaction_id == _transaction_id)) {
+            // Delay the response after calculation without preventing another transaction from
+            // calculating the delete bitmap on the same tablet.
+            wrlock.unlock();
+            LOG(INFO) << "start debug block after calculating delete bitmap"
+                      << ", tablet_id=" << _tablet_id << ", txn_id=" << _transaction_id;
+            while (DebugPoints::instance()->is_enable(DP_NAME)) {
+                std::this_thread::sleep_for(std::chrono::milliseconds(50));
+            }
+            LOG(INFO) << "finish debug block after calculating delete bitmap"
+                      << ", tablet_id=" << _tablet_id << ", txn_id=" << _transaction_id;
+        }
+    });
     DBUG_EXECUTE_IF("CloudCalcDbmTask.handle.return.block",
                     auto target_tablet_id = dp->param<int64_t>("tablet_id", 0);
                     if (target_tablet_id == tablet->tablet_id()) {DBUG_BLOCK});
diff --git a/be/src/cloud/cloud_meta_mgr.cpp b/be/src/cloud/cloud_meta_mgr.cpp
index d6293997e77..f9cf4b31609 100644
--- a/be/src/cloud/cloud_meta_mgr.cpp
+++ b/be/src/cloud/cloud_meta_mgr.cpp
@@ -2338,6 +2338,16 @@ Status CloudMetaMgr::get_delete_bitmap_update_lock(const CloudTablet& tablet, in
                 "initiator {}",
                 tablet.table_id(), lock_id, initiator);
     }
+    DBUG_EXECUTE_IF("CloudMetaMgr.get_delete_bitmap_update_lock.block_for_restart", {
+        if (st.ok() && res.status().code() == MetaServiceCode::OK &&
+            dp->param<int64_t>("table_id", -1) == tablet.table_id()) {
+            // Pause only a successful acquisition, never a conflict or failed RPC.
+            LOG(INFO) << "delete bitmap restart barrier token="
+                      << dp->param<std::string>("token", "") << " lock_id=" << lock_id
+                      << " tablet_id=" << tablet.tablet_id() << " initiator=" << initiator;
+            DBUG_BLOCK;
+        }
+    });
     return st;
 }
 
diff --git a/docker/runtime/doris-compose/utils.py b/docker/runtime/doris-compose/utils.py
index 0f41728d3f2..ff7f5e4b033 100644
--- a/docker/runtime/doris-compose/utils.py
+++ b/docker/runtime/doris-compose/utils.py
@@ -21,6 +21,7 @@ import jsonpickle
 import logging
 import os
 import pwd
+import re
 import socket
 import subprocess
 import sys
@@ -145,24 +146,46 @@ def is_container_running(container):
 
 # return all doris containers when cluster_names is empty
 def get_doris_containers(cluster_names):
-    if cluster_names:
-        if type(cluster_names) == type(""):
-            filter_names = "{}{}-*".format(DORIS_PREFIX, cluster_names)
-        else:
-            filter_names = "|".join([
-                "{}{}-*".format(DORIS_PREFIX, name) for name in cluster_names
-            ])
+    import cluster
+    if isinstance(cluster_names, str):
+        requested_names = {cluster_names} if cluster_names else set()
     else:
-        filter_names = "{}*".format(DORIS_PREFIX)
+        requested_names = set(cluster_names or [])
+
+    # Docker's name filter matches substrings, so anchor the complete service
+    # name before the SDK inspects each matching container. Otherwise a lookup
+    # for "test_vcg" also inspects "test_vcg_warmup..." containers.
+    name_pattern = ("|".join(re.escape(name) for name in sorted(requested_names))
+                    if requested_names else ".+")
+    node_pattern = "|".join(re.escape(node_type)
+                            for node_type in cluster.Node.TYPE_ALL)
+    filter_names = r"^/{}({})-({})-[0-9]+$".format(
+        re.escape(DORIS_PREFIX), name_pattern, node_pattern)
 
     clusters = {}
     client = docker.client.from_env()
-    containers = client.containers.list(filters={"name": filter_names})
+    max_list_attempts = 3
+    for attempt in range(1, max_list_attempts + 1):
+        try:
+            # A container may be removed between Docker's list and inspect.
+            containers = client.containers.list(filters={"name": filter_names},
+                                                ignore_removed=True)
+            break
+        except docker.errors.NotFound as err:
+            if attempt == max_list_attempts:
+                raise
+            LOG.warning(
+                "Docker returned 404 while listing Doris containers; "
+                "retrying enumeration (%d/%d): %s",
+                attempt,
+                max_list_attempts,
+                err,
+            )
     for container in containers:
         cluster_name, _, _ = parse_service_name(container.name)
         if not cluster_name:
             continue
-        if cluster_names and cluster_name not in cluster_names:
+        if requested_names and cluster_name not in requested_names:
             continue
         if cluster_name not in clusters:
             clusters[cluster_name] = []
diff --git a/regression-test/data/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.out b/regression-test/data/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.out
index 475b9ff01e9..eaa6918709a 100644
--- a/regression-test/data/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.out
+++ b/regression-test/data/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.out
@@ -1,4 +1,7 @@
 -- This file is automatically generated. You should know what you did if you want to edit this
--- !sql --
-400
+-- !before_alter --
+400	79800	3490
+
+-- !after_alter --
+400	79800	3490
 
diff --git a/regression-test/data/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.out b/regression-test/data/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.out
index a8419e02ed9..0db2a386610 100644
--- a/regression-test/data/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.out
+++ b/regression-test/data/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.out
@@ -1,7 +1,7 @@
 -- This file is automatically generated. You should know what you did if you want to edit this
 -- !source_preheat --
-200
+19900	2690
 
 -- !target_query --
-200
+19900	2690
 
diff --git a/regression-test/data/cloud_p0/cache/ttl/test_show_ttl.out b/regression-test/data/cloud_p0/cache/ttl/test_show_ttl.out
deleted file mode 100644
index 1d2ad674a77..00000000000
--- a/regression-test/data/cloud_p0/cache/ttl/test_show_ttl.out
+++ /dev/null
@@ -1,16 +0,0 @@
--- This file is automatically generated. You should know what you did if you want to edit this
--- !test_show_ttl --
-0
-
--- !test_show_ttl_2 --
-0
-
--- !test_show_ttl_3 --
-customer_ttl	CREATE TABLE `customer_ttl` (\n  `C_CUSTKEY` int NOT NULL,\n  `C_NAME` varchar(25) NOT NULL,\n  `C_ADDRESS` varchar(40) NOT NULL,\n  `C_NATIONKEY` int NOT NULL,\n  `C_PHONE` char(15) NOT NULL,\n  `C_ACCTBAL` decimal(15,2) NOT NULL,\n  `C_MKTSEGMENT` char(10) NOT NULL,\n  `C_COMMENT` varchar(117) NOT NULL\n) ENGINE=OLAP\nDUPLICATE KEY(`C_CUSTKEY`, `C_NAME`)\nDISTRIBUTED BY HASH(`C_CUSTKEY`) BUCKETS 32\nPROPERTIES (\n"file_cache_ttl_seconds" = "300",\n"is_being_synced" = "fals [...]
-
--- !test_show_ttl_4 --
-0
-
--- !test_show_ttl_5 --
-customer_ttl	CREATE TABLE `customer_ttl` (\n  `C_CUSTKEY` int NOT NULL,\n  `C_NAME` varchar(25) NOT NULL,\n  `C_ADDRESS` varchar(40) NOT NULL,\n  `C_NATIONKEY` int NOT NULL,\n  `C_PHONE` char(15) NOT NULL,\n  `C_ACCTBAL` decimal(15,2) NOT NULL,\n  `C_MKTSEGMENT` char(10) NOT NULL,\n  `C_COMMENT` varchar(117) NOT NULL\n) ENGINE=OLAP\nDUPLICATE KEY(`C_CUSTKEY`, `C_NAME`)\nDISTRIBUTED BY HASH(`C_CUSTKEY`) BUCKETS 32\nPROPERTIES (\n"file_cache_ttl_seconds" = "0",\n"is_being_synced" = "false" [...]
-
diff --git a/regression-test/data/variant_p0/schema_change/test_double_write_when_schema_change.out b/regression-test/data/variant_p0/schema_change/test_double_write_when_schema_change.out
index 1190a5f30bd..37aba8eb64c 100644
--- a/regression-test/data/variant_p0/schema_change/test_double_write_when_schema_change.out
+++ b/regression-test/data/variant_p0/schema_change/test_double_write_when_schema_change.out
@@ -28,7 +28,7 @@ PullRequestEvent	2489395770	2015-01-01T01:00:02Z
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/1310570?","gravatar_id":"","id":1310570,"login":"soumith","url":"https://api.github.com/users/soumith"},"created_at":"2015-01-01T01:00:00Z","id":"2489395767","payload":{"before":"2d06657267b32e0c8e193c617039da200f710195","commits":[{"sha":"dbd68d30ee1f7b60d404553fc1c6226ebb374c8e","author":{"email":"88de463b5797707cf3425f85a415c3d869db732b@gmail.com","name":"Soumith Chintala"},"message":"back to old structure, except lua f [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/6799218?","gravatar_id":"","id":6799218,"login":"gyuho","url":"https://api.github.com/users/gyuho"},"created_at":"2015-01-01T02:00:00Z","id":"2489418154","payload":{"before":"79d9868b8a1afbc5c6c42d33fcc7b97524f888ff","commits":[{"sha":"cf79996e894954b28db73a6bed54ba490c63f174","author":{"email":"0dfbe0912134938594f432abfeb9cb2f2e9092c9@gmail.com","name":"Gyu-Ho Lee"},"message":"Commit with markdownx at 2014-12-31 17:59:58" [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/4188511?","gravatar_id":"","id":4188511,"login":"iganari","url":"https://api.github.com/users/iganari"},"created_at":"2015-01-01T03:00:01Z","id":"2489436569","payload":{"action":"opened","number":44,"pull_request":{"_links":{"comments":{"href":"https://api.github.com/repos/iganari/mrtg/issues/44/comments"},"commits":{"href":"https://api.github.com/repos/iganari/mrtg/pulls/44/commits"},"html":{"href":"https://github.com/iga [...]
-1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
+1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
@@ -40,7 +40,7 @@ PullRequestEvent	2489395770	2015-01-01T01:00:02Z
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/1310570?","gravatar_id":"","id":1310570,"login":"soumith","url":"https://api.github.com/users/soumith"},"created_at":"2015-01-01T01:00:00Z","id":"2489395767","payload":{"before":"2d06657267b32e0c8e193c617039da200f710195","commits":[{"sha":"dbd68d30ee1f7b60d404553fc1c6226ebb374c8e","author":{"email":"88de463b5797707cf3425f85a415c3d869db732b@gmail.com","name":"Soumith Chintala"},"message":"back to old structure, except lua f [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/6799218?","gravatar_id":"","id":6799218,"login":"gyuho","url":"https://api.github.com/users/gyuho"},"created_at":"2015-01-01T02:00:00Z","id":"2489418154","payload":{"before":"79d9868b8a1afbc5c6c42d33fcc7b97524f888ff","commits":[{"sha":"cf79996e894954b28db73a6bed54ba490c63f174","author":{"email":"0dfbe0912134938594f432abfeb9cb2f2e9092c9@gmail.com","name":"Gyu-Ho Lee"},"message":"Commit with markdownx at 2014-12-31 17:59:58" [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/4188511?","gravatar_id":"","id":4188511,"login":"iganari","url":"https://api.github.com/users/iganari"},"created_at":"2015-01-01T03:00:01Z","id":"2489436569","payload":{"action":"opened","number":44,"pull_request":{"_links":{"comments":{"href":"https://api.github.com/repos/iganari/mrtg/issues/44/comments"},"commits":{"href":"https://api.github.com/repos/iganari/mrtg/pulls/44/commits"},"html":{"href":"https://github.com/iga [...]
-1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
+1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
 1	{"actor":{"avatar_url":"https://avatars.githubusercontent.com/u/100067519?","display_login":"savorfamily","gravatar_id":"123","id":100067519,"login":"savorfamily","url":"https://api.github.com/users/savorfamily"},"created_at":"2022-11-07T02:00:00Z","id":"25061216922","payload":{"before":"2bf823a4febcf809da126828ecef7617c8cc48ea","commits":[{"sha":"81106d369f763cb729d9d77610ace252c9db53f0","author":{"email":"73812536+upptime-bot@users.noreply.github.com","name":"Upptime Bot"},"message": [...]
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
index b4be1a88c21..85fa5a3ef4e 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
@@ -1145,7 +1145,9 @@ class Config {
     void fetchCloudMode() {
         if (runMode == RunMode.UNKNOWN) {
             try {
-                def result = JdbcUtils.executeToMapArray(getRootConnection(), "SHOW FRONTEND CONFIG LIKE 'cloud_unique_id'")
+                def result = getRootConnection().withCloseable { conn ->
+                    JdbcUtils.executeToMapArray(conn, "SHOW FRONTEND CONFIG LIKE 'cloud_unique_id'")
+                }
                 runMode = result[0].Value.toString().isEmpty() ? RunMode.NOT_CLOUD : RunMode.CLOUD
             } catch (Throwable t) {
                 throw new IllegalStateException("Fetch server config 'cloud_unique_id' failed, jdbcUrl: ${jdbcUrl}", t)
@@ -1155,7 +1157,9 @@ class Config {
 
     boolean isClusterKeyEnabled() {
         try {
-            def result = JdbcUtils.executeToMapArray(getRootConnection(), "SHOW FRONTEND CONFIG LIKE 'random_add_order_by_keys_for_mow'")
+            def result = getRootConnection().withCloseable { conn ->
+                JdbcUtils.executeToMapArray(conn, "SHOW FRONTEND CONFIG LIKE 'random_add_order_by_keys_for_mow'")
+            }
             log.info("show random_add_order_by_keys_for_mow config: ${result}".toString())
             return result[0].Value.toString().equalsIgnoreCase("true")
         } catch (Throwable t) {
@@ -1199,8 +1203,12 @@ class Config {
         return DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword)
     }
 
+    String getRootPassword() {
+        return (jdbcUser == 'root' && jdbcPassword) ? jdbcPassword : ''
+    }
+
     Connection getRootConnection() {
-        return DriverManager.getConnection(jdbcUrl, 'root', '')
+        return DriverManager.getConnection(jdbcUrl, 'root', getRootPassword())
     }
 
     Connection getConnectionByDbName(String dbName) {
@@ -1219,14 +1227,7 @@ class Config {
         Class.forName("org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver")
         String arrowFlightSqlHost = otherConfigs.get("extArrowFlightSqlHost")
         String arrowFlightSqlPort = otherConfigs.get("extArrowFlightSqlPort")
-        String arrowFlightSqlUrl = "jdbc:arrow-flight-sql://${arrowFlightSqlHost}:${arrowFlightSqlPort}" +
-                "/?useServerPrepStmts=false&useSSL=false&useEncryption=false"
-        // Arrow 17.0.0-rc03 support jdbc:arrow-flight-sql connect db
-        // https://github.com/apache/arrow/issues/41947
-        if (dbName?.trim()) {
-            arrowFlightSqlUrl = "jdbc:arrow-flight-sql://${arrowFlightSqlHost}:${arrowFlightSqlPort}" +
-                "/catalog=" + dbName + "?useServerPrepStmts=false&useSSL=false&useEncryption=false"
-        }
+        String arrowFlightSqlUrl = getArrowFlightSqlJdbcUrl(arrowFlightSqlHost, arrowFlightSqlPort, dbName)
         tryCreateDbIfNotExist(dbName)
         log.info("connect to ${arrowFlightSqlUrl}".toString())
         String arrowFlightSqlJdbcUser = otherConfigs.get("extArrowFlightSqlUser")
@@ -1234,6 +1235,27 @@ class Config {
         return DriverManager.getConnection(arrowFlightSqlUrl, arrowFlightSqlJdbcUser, arrowFlightSqlJdbcPassword)
     }
 
+    String getArrowFlightSqlJdbcUrl(String host, String port, String dbName = null) {
+        // Arrow 17.0.0-rc03 supports selecting the database with /catalog=<dbName>.
+        // Flight JDBC uses useEncryption and PEM paths rather than MySQL's useSSL/key stores.
+        String catalog = dbName?.trim() ? "catalog=${dbName}" : ""
+        boolean enableTls = otherConfigs.get("enableTLS")?.toString()?.equalsIgnoreCase("true") ?: false
+        String url = "jdbc:arrow-flight-sql://${host}:${port}/${catalog}?useServerPrepStmts=false&useEncryption=${enableTls}"
+        if (enableTls) {
+            String verifyMode = otherConfigs.get("tlsVerifyMode")?.toString()?.toLowerCase() ?: "strict"
+            if (verifyMode == "none") {
+                url += "&disableCertificateVerification=true"
+            } else if (otherConfigs.get("trustCACert")) {
+                url += "&tlsRootCerts=${otherConfigs.get('trustCACert')}"
+            }
+            if (otherConfigs.get("trustCert") && otherConfigs.get("trustCAKey")) {
+                url += "&clientCertificate=${otherConfigs.get('trustCert')}" +
+                        "&clientKey=${otherConfigs.get('trustCAKey')}"
+            }
+        }
+        return url
+    }
+
     Connection getDownstreamConnection() {
         return DriverManager.getConnection(ccrDownstreamUrl, ccrDownstreamUser, ccrDownstreamPassword)
     }
@@ -1300,24 +1322,17 @@ class Config {
             return jdbcUrl
         }
 
-        String urlWithDb = jdbcUrl
-        String urlWithoutSchema = jdbcUrl.substring(jdbcUrl.indexOf("://") + 3)
-        if (urlWithoutSchema.indexOf("/") >= 0) {
-            if (jdbcUrl.contains("?")) {
-                // e.g: jdbc:mysql://locahost:8080/?a=b
-                urlWithDb = jdbcUrl.substring(0, jdbcUrl.lastIndexOf("?"))
-                urlWithDb = urlWithDb.substring(0, urlWithDb.lastIndexOf("/"))
-                urlWithDb += ("/" + dbName) + jdbcUrl.substring(jdbcUrl.lastIndexOf("?"))
-            } else {
-                // e.g: jdbc:mysql://locahost:8080/
-                urlWithDb += dbName
-            }
-        } else {
-            // e.g: jdbc:mysql://locahost:8080
-            urlWithDb += ("/" + dbName)
+        int authorityStart = jdbcUrl.indexOf("://") + 3
+        int queryStart = jdbcUrl.indexOf("?", authorityStart)
+        int pathStart = jdbcUrl.indexOf("/", authorityStart)
+        if (pathStart >= 0 && (queryStart < 0 || pathStart < queryStart)) {
+            return jdbcUrl.substring(0, pathStart + 1) + dbName +
+                    (queryStart >= 0 ? jdbcUrl.substring(queryStart) : "")
         }
-
-        return urlWithDb
+        if (queryStart >= 0) {
+            return jdbcUrl.substring(0, queryStart) + "/" + dbName + jdbcUrl.substring(queryStart)
+        }
+        return jdbcUrl + "/" + dbName
     }
 
     public static String buildUrlWithDb(String jdbcUrl, String dbName) {
@@ -1330,8 +1345,7 @@ class Config {
 
     public static String buildUrlWithDb (String jdbcUrl, String dbName, String keyStorePath, String keyStorePassword, String trustStorePath, String trustStorePassword) {
         String urlWithDb = buildUrlWithDbImpl(jdbcUrl, dbName);
-        urlWithDb = addTlsUrl(urlWithDb, keyStorePath, keyStorePassword, trustStorePath, trustStorePassword);
-        urlWithDb = addTimeoutUrl(urlWithDb);
+        urlWithDb = buildTlsJdbcUrl(urlWithDb, keyStorePath, keyStorePassword, trustStorePath, trustStorePassword);
         return urlWithDb
     }
 
@@ -1343,8 +1357,27 @@ class Config {
         return url
     }
 
+    public static String buildUrlWithDb(String host, int queryPort, String dbName,
+                                        String keyStorePath, String keyStorePassword,
+                                        String trustStorePath, String trustStorePassword) {
+        def url = String.format(
+            "jdbc:mysql://%s:%s/?useLocalSessionState=true&allowLoadLocalInfile=false",
+            host, queryPort)
+        url = buildUrlWithDb(url, dbName, keyStorePath, keyStorePassword, trustStorePath, trustStorePassword)
+        return url
+    }
+
+    public static String buildTlsJdbcUrl(String jdbcUrl, String keyStorePath, String keyStorePassword,
+                                         String trustStorePath, String trustStorePassword) {
+        String tlsUrl = addTlsUrl(jdbcUrl, keyStorePath, keyStorePassword, trustStorePath, trustStorePassword)
+        return addTimeoutUrl(tlsUrl)
+    }
+
     private static String addSslUrl(String url) {
-        if (url.contains("TLS")) {
+        // A configured TLS URL already has its own verification policy and key stores.
+        // Appending the optional-SSL defaults would override requireSSL and certificate verification.
+        if (url.contains("useSSL=") || url.contains("sslMode=") ||
+                url.contains("clientCertificateKeyStoreUrl=") || url.contains("trustCertificateKeyStoreUrl=")) {
             return url
         }
         // ssl-mode = PREFERRED
@@ -1366,25 +1399,33 @@ class Config {
     }
 
     private static String addTlsUrl(String url, String keyStorePath, String keyStorePassword, String trustStorePath, String trustStorePassword) {
-        // ssl-mode = PREFERRED
-        String useSsl = "true"
-        String requireSsl = "true"
-        String useSslconfig = "useSSL=" + useSsl + "&requireSSL=" + requireSsl + "&verifyServerCertificate=true"
-        String clientCAKey = "clientCertificateKeyStoreUrl=file:" + keyStorePath
-        String clientCAPwd = "clientCertificateKeyStorePassword=" + keyStorePassword
-        String trustCAKey = "trustCertificateKeyStoreUrl=file:" + trustStorePath
-        String trustCAPwd = "trustCertificateKeyStorePassword=" + trustStorePassword
-        String tlsUrl = useSslconfig + "&" + clientCAKey + "&" + clientCAPwd + "&" +  trustCAKey + "&" + trustCAPwd
-        // e.g: jdbc:mysql://locahost:8080/dbname?
-        if (url.charAt(url.length() - 1) == '?') {
-            return url + tlsUrl
-            // e.g: jdbc:mysql://locahost:8080/dbname?a=b
-        } else if (url.contains('?')) {
-            return url + '&' + tlsUrl
-            // e.g: jdbc:mysql://locahost:8080/dbname
-        } else {
-            return url + '?' + tlsUrl
+        int queryStart = url.indexOf('?')
+        String baseUrl = queryStart >= 0 ? url.substring(0, queryStart) : url
+        List<String> options = new ArrayList<>()
+        Set<String> tlsOptions = new HashSet<>(Arrays.asList(
+                "sslMode", "useSSL", "requireSSL", "verifyServerCertificate",
+                "clientCertificateKeyStoreUrl", "clientCertificateKeyStorePassword",
+                "trustCertificateKeyStoreUrl", "trustCertificateKeyStorePassword"))
+        if (queryStart >= 0) {
+            for (String option : url.substring(queryStart + 1).split('&')) {
+                if (option.isEmpty()) {
+                    continue
+                }
+                int equals = option.indexOf('=')
+                String name = equals >= 0 ? option.substring(0, equals) : option
+                if (!tlsOptions.contains(name)) {
+                    options.add(option)
+                }
+            }
         }
+        options.add("useSSL=true")
+        options.add("requireSSL=true")
+        options.add("verifyServerCertificate=true")
+        options.add("clientCertificateKeyStoreUrl=file:" + keyStorePath)
+        options.add("clientCertificateKeyStorePassword=" + keyStorePassword)
+        options.add("trustCertificateKeyStoreUrl=file:" + trustStorePath)
+        options.add("trustCertificateKeyStorePassword=" + trustStorePassword)
+        return baseUrl + "?" + String.join("&", options)
     }
 
     private static String addTimeoutUrl(String url) {
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ProfileAction.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ProfileAction.groovy
index b73ef8e196c..40deeec8930 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ProfileAction.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/action/ProfileAction.groovy
@@ -36,6 +36,7 @@ class ProfileAction implements SuiteAction {
     private Runnable runCallback
     private Closure<String> check
     private SuiteContext context
+    private InetSocketAddress profileAddress
 
     ProfileAction(SuiteContext context, String tag) {
         this.context = context
@@ -47,6 +48,13 @@ class ProfileAction implements SuiteAction {
         this.tag = null
     }
 
+    /** Query profiles are local to the FE that executed the SQL. */
+    ProfileAction(SuiteContext context, InetSocketAddress profileAddress) {
+        this.context = context
+        this.tag = null
+        this.profileAddress = Objects.requireNonNull(profileAddress, "profileAddress can not be null")
+    }
+
     void run(@ClosureParams(value = FromString, options = []) Runnable run) {
         runCallback = run
     }
@@ -58,17 +66,13 @@ class ProfileAction implements SuiteAction {
 
     List getProfileList() {
         def httpCli = new HttpCliAction(context)
-        def addr = context.getFeHttpAddress()
+        def addr = profileAddress ?: context.getFeHttpAddress()
         httpCli.endpoint("${addr.hostString}:${addr.port}")
         httpCli.uri("/rest/v1/query_profile")
         httpCli.op("get")
         httpCli.printResponse(false)
 
-        if (context.config.isCloudMode()) {
-            httpCli.basicAuthorization(context.config.feCloudHttpUser, context.config.feCloudHttpPassword)
-        } else {
-            httpCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
-        }
+        httpCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
         List profileData = []
         httpCli.check { code, body ->
             if (code != 200) {
@@ -104,17 +108,13 @@ class ProfileAction implements SuiteAction {
 
     String getProfile(String profileId) {
         def profileCli = new HttpCliAction(context)
-        def addr = context.getFeHttpAddress()
+        def addr = profileAddress ?: context.getFeHttpAddress()
         profileCli.endpoint("${addr.hostString}:${addr.port}")
         profileCli.uri("/rest/v1/query_profile/${profileId}")
         profileCli.op("get")
         profileCli.printResponse(false)
 
-        if (context.config.isCloudMode()) {
-            profileCli.basicAuthorization(context.config.feCloudHttpUser, context.config.feCloudHttpPassword)
-        } else {
-            profileCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
-        }
+        profileCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
         def result = [text: ""]
         profileCli.check { profileCode, profileResp ->
             if (profileCode != 200) {
@@ -222,17 +222,13 @@ class ProfileAction implements SuiteAction {
             }
 
             def httpCli = new HttpCliAction(context)
-            def addr = context.getFeHttpAddress()
+            def addr = profileAddress ?: context.getFeHttpAddress()
             httpCli.endpoint("${addr.hostString}:${addr.port}")
             httpCli.uri("/rest/v1/query_profile")
             httpCli.op("get")
             httpCli.printResponse(false)
 
-            if (context.config.isCloudMode()) {
-                httpCli.basicAuthorization(context.config.feCloudHttpUser, context.config.feCloudHttpPassword)
-            } else {
-                httpCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
-            }
+            httpCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
             httpCli.check { code, body ->
                 if (code != 200) {
                     throw new IllegalStateException("Get profile list failed, code: ${code}, body:\n${body}")
@@ -252,11 +248,7 @@ class ProfileAction implements SuiteAction {
                         profileCli.op("get")
                         profileCli.printResponse(false)
 
-                        if (context.config.isCloudMode()) {
-                            profileCli.basicAuthorization(context.config.feCloudHttpUser, context.config.feCloudHttpPassword)
-                        } else {
-                            profileCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
-                        }
+                        profileCli.basicAuthorization(context.config.feHttpUser, context.config.feHttpPassword)
                         profileCli.check { profileCode, profileResp ->
                             if (profileCode != 200) {
                                 throw new IllegalStateException("Get profile failed, url: ${"/rest/v1/query_profile/${profileId}"}, code: ${profileCode}, body:\n${profileResp}")
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/ScriptContext.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/ScriptContext.groovy
index c8b64fb9807..16790e43309 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/ScriptContext.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/ScriptContext.groovy
@@ -21,6 +21,7 @@ import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 import org.apache.doris.regression.Config
 import org.apache.doris.regression.suite.event.EventListener
+import org.apache.doris.regression.util.Http
 
 import java.lang.reflect.UndeclaredThrowableException
 import java.util.concurrent.ExecutorService
@@ -62,6 +63,16 @@ class ScriptContext implements Closeable {
         def outputRelativePath = path.substring(0, path.lastIndexOf(".")) + ".out"
         this.outputFile = new File(new File(config.dataPath), outputRelativePath)
         this.dataPath = this.outputFile.getParentFile().getCanonicalFile()
+        Http.configure(
+                (config.otherConfigs.get("enableTLS")?.toString()?.equalsIgnoreCase("true")) ?: false,
+                config.otherConfigs.get("tlsVerifyMode")?.toString()?.toLowerCase() ?: "strict",
+                config.otherConfigs.get("trustStorePath")?.toString(),
+                config.otherConfigs.get("trustStorePassword")?.toString(),
+                config.otherConfigs.get("trustStoreType")?.toString() ?: "PKCS12",
+                config.otherConfigs.get("keyStorePath")?.toString(),
+                config.otherConfigs.get("keyStorePassword")?.toString(),
+                config.otherConfigs.get("keyStoreType")?.toString() ?: "PKCS12"
+        )
     }
 
     private final synchronized Suite newSuite(String suiteName, String group) {
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
index d280c880874..9899da208c1 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
@@ -35,6 +35,10 @@ import groovy.json.JsonSlurper
 import groovy.util.logging.Slf4j
 import org.apache.commons.lang3.ObjectUtils
 import org.apache.doris.regression.Config
+import org.apache.doris.regression.suite.client.BackendClientImpl
+import org.apache.doris.regression.util.RowsetMetaUtils
+import org.apache.doris.thrift.TNetworkAddress
+import org.apache.doris.thrift.TSyncLoadForTabletsRequest
 import org.apache.doris.regression.RegressionTest
 import org.apache.doris.regression.action.FlightRecordAction
 import org.apache.doris.regression.action.BenchmarkAction
@@ -134,6 +138,30 @@ class Suite implements GroovyInterceptable {
         return getConf("suites." + name + "." + key, defaultValue)
     }
 
+    boolean isDorisTlsEnabled() {
+        return Boolean.parseBoolean(getConf("enableTLS", "false"))
+    }
+
+    String getDorisHttpScheme() {
+        return isDorisTlsEnabled() ? "https" : "http"
+    }
+
+    String getDorisCurlTlsOptions() {
+        if (!isDorisTlsEnabled()) {
+            return ""
+        }
+        return " --cert ${quoteShellArgument(getConf('trustCert'))}" +
+                " --key ${quoteShellArgument(getConf('trustCAKey'))}" +
+                " --cacert ${quoteShellArgument(getConf('trustCACert'))}"
+    }
+
+    private static String quoteShellArgument(String value) {
+        if (!value) {
+            throw new IllegalArgumentException("Missing TLS certificate path for curl")
+        }
+        return "'" + value.replace("'", "'\"'\"'") + "'"
+    }
+
     List<String> getDorisConnectorTlsArgs() {
         if (!Boolean.parseBoolean(getConf("enableTLS", "false"))) {
             return Collections.emptyList()
@@ -299,6 +327,21 @@ class Suite implements GroovyInterceptable {
         return context.connect(user, password, url, actionSupplier)
     }
 
+    public <T> T connectToDoris(String user = context.config.jdbcUser, String password = context.config.jdbcPassword,
+                                String url = context.getJdbcUrl(), Closure<T> actionSupplier) {
+        return context.connectToDoris(user, password, url, actionSupplier)
+    }
+
+    public <T> T connectWithDockerCluster(
+            SuiteCluster cluster,
+            Boolean connectToFollower = false,
+            String user = context.config.jdbcUser,
+            String password = context.config.jdbcPassword,
+            Closure<T> actionSupplier) {
+        def jdbcUrl = cluster.getJdbcUrl(connectToFollower)
+        return context.connect(user, password, jdbcUrl, actionSupplier)
+    }
+
     public <T> T connectInDocker(String user = context.config.jdbcUser, String password = context.config.jdbcPassword,
                         Closure<T> actionSupplier) {
         def connInfo = context.threadLocalConn.get()
@@ -314,6 +357,119 @@ class Suite implements GroovyInterceptable {
     //         }
     //     )
     // }
+    /** Wait for continuous version coverage on each tablet's serving BE, with lazy commit enabled. */
+    void syncAndWaitTabletVersion(Collection<Map> tablets, long version, int timeoutSeconds = 60) {
+        Assertions.assertFalse(tablets.isEmpty(), "no tablets to synchronize")
+        List<Map> tabletList = tablets.toList()
+        Map<String, List<Map>> tabletGroups = tabletList.groupBy { it.BackendId.toString() }
+        Map<String, BackendClientImpl> backendClients = [:]
+        Set<Integer> ready = [] as Set
+        Map<String, Object> lastStates = [:]
+        int pollCount = 0
+        try {
+            if (isCloudMode()) {
+                def backendById = sql_return_maparray("SHOW BACKENDS").collectEntries {
+                    [(it.BackendId.toString()): it]
+                }
+                tabletGroups.keySet().each { backendId ->
+                    def backend = backendById[backendId]
+                    Assertions.assertNotNull(backend,
+                            "backend ${backendId} for tablets ${tabletGroups[backendId]*.TabletId} was not found")
+                    backendClients[backendId] = new BackendClientImpl(
+                            new TNetworkAddress(backend.Host.toString(), backend.BePort as int),
+                            backend.HttpPort as int)
+                }
+            }
+
+            awaitUntil(timeoutSeconds, 0.5) {
+                // The BE RPC is asynchronous. Retry it periodically in case an earlier queued
+                // task observed no advancement while Meta Service was finalizing lazy commit.
+                if (!backendClients.isEmpty() && pollCount++ % 10 == 0) {
+                    tabletGroups.each { backendId, backendTablets ->
+                        backendClients[backendId].client.syncLoadForTablets(
+                                new TSyncLoadForTabletsRequest(
+                                        backendTablets.collect { it.TabletId as long }))
+                    }
+                }
+                tabletList.eachWithIndex { tablet, index ->
+                    if (!ready.contains(index)) {
+                        def status = Http.GET(tablet.CompactionStatus.toString(), true, false,
+                                context.config.feHttpUser, context.config.feHttpPassword)
+                        lastStates["${tablet.TabletId}@${tablet.BackendId}"] = status
+                        if (RowsetMetaUtils.coversVersion(status, version)) {
+                            ready.add(index)
+                        }
+                    }
+                }
+                return ready.size() == tabletList.size()
+            }
+        } catch (org.awaitility.core.ConditionTimeoutException e) {
+            throw new IllegalStateException(
+                    "Waiting for tablet version ${version} timed out; last BE states: ${lastStates}", e)
+        } finally {
+            backendClients.values().each { it.close() }
+        }
+    }
+
+    /** Fetch an exact physical rowset after the serving BE has caught up. */
+    Map syncAndWaitCloudRowsetMeta(Map tablet, long version, int timeoutSeconds = 60) {
+        syncAndWaitTabletVersion([tablet], version, timeoutSeconds)
+        return getRowsetMetaAtVersion(tablet, version)
+    }
+
+    /**
+     * Call after syncAndWaitTabletVersion. Cloud headers omit rs_metas, so read the committed
+     * MS rowset key instead. The caller must prevent compaction from removing the exact version.
+     */
+    Map getRowsetMetaAtVersion(Map tablet, long version) {
+        if (!isCloudMode()) {
+            String metaUrl = tablet.MetaUrl.toString()
+            metaUrl += (metaUrl.contains('?') ? '&' : '?') + 'byte_to_base64=true'
+            def header = Http.GET(metaUrl, true, false,
+                    context.config.feHttpUser, context.config.feHttpPassword)
+            Assertions.assertTrue(header.rs_metas instanceof List, "tablet header is missing rs_metas")
+            def meta = header.rs_metas.find { (it.end_version as long) == version }
+            Assertions.assertNotNull(meta, "rowset not found: tablet=${tablet.TabletId}, version=${version}")
+            return RowsetMetaUtils.decodeKeyBounds(meta)
+        }
+        def endpoint = context.config.metaServiceHttpAddress
+        def token = context.config.metaServiceToken
+        // CI deployment writes multiClusterInstanceId as a custom property; the legacy
+        // multiClusterInstance can still contain the template's default_instance_id.
+        def instanceId = context.config.otherConfigs.get("multiClusterInstanceId")?.toString()?.trim() ?:
+                context.config.multiClusterInstance?.trim()
+        Assertions.assertTrue(endpoint?.trim() && token?.trim() && instanceId?.trim(),
+                "metaServiceHttpAddress, metaServiceToken and multiClusterInstanceId (or multiClusterInstance) must be configured")
+        def params = [token: token, key_type: "MetaRowsetKey", instance_id: instanceId,
+                      tablet_id: tablet.TabletId, version: version]
+        def query = params.collect { key, value ->
+            "${key}=${java.net.URLEncoder.encode(value.toString(), 'UTF-8')}"
+        }.join('&')
+        // Do not use Http.GET here: it logs the URL, including the MS token.
+        def baseUrl = endpoint.contains('://') ? endpoint : "http://${endpoint}"
+        HttpURLConnection conn = new URL("${baseUrl}/MetaService/http/get_value?${query}").openConnection()
+        conn.connectTimeout = 5000
+        conn.readTimeout = 10000
+        conn.instanceFollowRedirects = false
+        try {
+            int code = conn.responseCode
+            Assertions.assertEquals(200, code,
+                    "MS rowset read failed: instance=${instanceId}, tablet=${tablet.TabletId}, version=${version}")
+            def meta = new JsonSlurper().parseText(conn.inputStream.getText('UTF-8'))
+            Assertions.assertNotNull(meta.end_version,
+                    "MS response is not rowset metadata: tablet=${tablet.TabletId}, version=${version}, code=${meta.code}")
+            Assertions.assertEquals(version, meta.end_version as long)
+            Assertions.assertEquals(tablet.TabletId as long, meta.tablet_id as long)
+            return RowsetMetaUtils.decodeKeyBounds(meta)
+        } catch (IOException e) {
+            // Network exception messages may contain the credential-bearing URL.
+            throw new IOException("MS rowset read failed: tablet=${tablet.TabletId}, version=${version}, " +
+                    "error=${e.class.simpleName}")
+        } finally {
+            conn.disconnect()
+        }
+    }
+
     public void awaitUntil(int atMostSeconds, double intervalSecond = 1, Closure actionSupplier) {
         Awaitility
             .with().pollInSameThread()
@@ -1453,8 +1609,12 @@ class Suite implements GroovyInterceptable {
         return outBuf.toString()
     }
 
+    static String buildSshCommand(String username, String host, String cmd) {
+        return "ssh -o StrictHostKeyChecking=no ${username}@${host} '${cmd}'"
+    }
+
     void sshExec(String username, String host, String cmd, boolean alert=true) {
-        String command = "ssh ${username}@${host} '${cmd}'"
+        String command = buildSshCommand(username, host, cmd)
         def cmds = ["/bin/bash", "-c", command]
         logger.info("Execute: ${cmds}".toString())
         Process p = cmds.execute()
@@ -1980,27 +2140,34 @@ class Suite implements GroovyInterceptable {
     }
 
     String getServerPrepareJdbcUrl(String jdbcUrl, String database, boolean useMasterIp) {
-        String urlWithoutSchema = jdbcUrl.substring(jdbcUrl.indexOf("://") + 3)
-        def sql_ip = useMasterIp ? getMasterIp() : urlWithoutSchema.substring(0, urlWithoutSchema.indexOf(":"))
-        def sql_port
-        if (urlWithoutSchema.indexOf("/") >= 0) {
-            // e.g: jdbc:mysql://locahost:8080/?a=b
-            sql_port = urlWithoutSchema.substring(urlWithoutSchema.indexOf(":") + 1, urlWithoutSchema.indexOf("/"))
-        } else {
-            // e.g: jdbc:mysql://locahost:8080
-            sql_port = urlWithoutSchema.substring(urlWithoutSchema.indexOf(":") + 1)
-        }
-        String tlsUrl = ""
-        // set server side prepared statement url
+        String scheme = "jdbc:mysql://"
+        if (!jdbcUrl.startsWith(scheme)) {
+            throw new IllegalArgumentException("Expected a MySQL JDBC URL")
+        }
+        String endpointAndPath = jdbcUrl.substring(scheme.length())
+        int pathStart = endpointAndPath.indexOf("/")
+        int queryStart = endpointAndPath.indexOf("?")
+        int endpointEnd = pathStart >= 0 && (queryStart < 0 || pathStart < queryStart)
+                ? pathStart : (queryStart >= 0 ? queryStart : endpointAndPath.length())
+        String endpoint = endpointAndPath.substring(0, endpointEnd)
+        int portStart = endpoint.lastIndexOf(":")
+        if (portStart < 0) {
+            throw new IllegalArgumentException("MySQL JDBC URL has no port")
+        }
+        String host = useMasterIp ? getMasterIp() : endpoint.substring(0, portStart)
+        String suffix = endpointAndPath.substring(endpointEnd)
+        if (!suffix.startsWith("/")) {
+            suffix = "/" + suffix
+        }
+        String url = Config.buildUrlWithDbImpl(scheme + host + endpoint.substring(portStart) + suffix, database)
         if ((context.config.otherConfigs.get("enableTLS")?.toString()?.equalsIgnoreCase("true")) ?: false) {
-            String useSslconfig = "useSSL=true&requireSSL=true&verifyServerCertificate=true"
-            String clientCAKey = "clientCertificateKeyStoreUrl=file:" + context.config.otherConfigs.get("keyStorePath")
-            String clientCAPwd = "clientCertificateKeyStorePassword=" + context.config.otherConfigs.get("keyStorePassword")
-            String trustCAKey = "trustCertificateKeyStoreUrl=file:" + context.config.otherConfigs.get("trustStorePath")
-            String trustCAPwd = "trustCertificateKeyStorePassword=" + context.config.otherConfigs.get("trustStorePassword")
-            tlsUrl = "&" + useSslconfig + "&" + clientCAKey + "&" + clientCAPwd + "&" +  trustCAKey + "&" + trustCAPwd
+            url = Config.buildTlsJdbcUrl(url,
+                    context.config.otherConfigs.get("keyStorePath")?.toString(),
+                    context.config.otherConfigs.get("keyStorePassword")?.toString(),
+                    context.config.otherConfigs.get("trustStorePath")?.toString(),
+                    context.config.otherConfigs.get("trustStorePassword")?.toString())
         }
-        return "jdbc:mysql://" + sql_ip + ":" + sql_port + "/" + database + "?&useServerPrepStmts=true" + tlsUrl
+        return url + (url.contains("?") ? "&" : "?") + "useServerPrepStmts=true"
     }
 
     DebugPoint GetDebugPoint() {
@@ -3394,7 +3561,7 @@ class Suite implements GroovyInterceptable {
                 endpoint feEndPoint
                 uri "/rest/v1/query_profile"
                 check check_func
-                basicAuthorization "${context.config.feCloudHttpUser}","${context.config.feCloudHttpPassword}"
+                basicAuthorization "${context.config.feHttpUser}","${context.config.feHttpPassword}"
             }
         }
 
@@ -3417,7 +3584,7 @@ class Suite implements GroovyInterceptable {
                 endpoint feEndPoint
                 uri "/api/profile?query_id=${query_id}"
                 check check_func
-                basicAuthorization "${context.config.feCloudHttpUser}","${context.config.feCloudHttpPassword}"
+                basicAuthorization "${context.config.feHttpUser}","${context.config.feHttpPassword}"
             }
         }
 
@@ -3626,6 +3793,26 @@ class Suite implements GroovyInterceptable {
         }
     }
 
+    def scp_udf_file_to_all_fe = { udf_file_path ->
+        def udf_file = new File(udf_file_path).absoluteFile
+        assertTrue(udf_file.isFile(), "UDF file does not exist: ${udf_file}")
+        def fe_hosts = sql_return_maparray("SHOW FRONTENDS").collect { it.Host }.unique()
+        assertTrue(!fe_hosts.isEmpty(), "No frontend found to copy UDF file to")
+        if (fe_hosts.size() == 1) {
+            def feAddress = java.net.InetAddress.getByName(fe_hosts[0].toString())
+            if (feAddress.isAnyLocalAddress() || feAddress.isLoopbackAddress() ||
+                    java.net.NetworkInterface.getByInetAddress(feAddress) != null) {
+                logger.info("Only one local frontend, skip scp udf file")
+                return
+            }
+        }
+
+        fe_hosts.each { fe_host ->
+            sshExec("root", fe_host, "mkdir -p ${udf_file.parent}")
+            scpFiles("root", fe_host, udf_file.path, udf_file.path, false)
+        }
+    }
+
     def check_fold_consistency = { test_sql ->
         def re_fe = order_sql "select /*+SET_VAR(enable_fold_constant_by_be=false)*/ ${test_sql}"
         def re_be = order_sql "select /*+SET_VAR(enable_fold_constant_by_be=true)*/ ${test_sql}"
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
index dd360b353b8..fb33da6865d 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy
@@ -160,6 +160,13 @@ class SuiteContext implements Closeable {
 
     Connection getConnectionByDbName(String dbName) {
         def jdbcUrl = getJdbcUrl()
+        if (isTlsEnabled()) {
+            jdbcUrl = Config.buildTlsJdbcUrl(jdbcUrl,
+                    config.otherConfigs.get("keyStorePath")?.toString(),
+                    config.otherConfigs.get("keyStorePassword")?.toString(),
+                    config.otherConfigs.get("trustStorePath")?.toString(),
+                    config.otherConfigs.get("trustStorePassword")?.toString())
+        }
         def jdbcConn = DriverManager.getConnection(jdbcUrl, config.jdbcUser, config.jdbcPassword)
         try {
             String sql = "CREATE DATABASE IF NOT EXISTS ${dbName}"
@@ -170,7 +177,9 @@ class SuiteContext implements Closeable {
         } catch (Throwable t) {
             throw new IllegalStateException("Create database failed, jdbcUrl: ${jdbcUrl}", t)
         }
-        def dbUrl = Config.buildUrlWithDb(jdbcUrl, dbName)
+        def dbUrl = isTlsEnabled()
+                ? Config.buildUrlWithDbImpl(jdbcUrl, dbName)
+                : Config.buildUrlWithDb(jdbcUrl, dbName)
         log.info("connect to ${dbUrl}".toString())
         return DriverManager.getConnection(dbUrl, config.jdbcUser, config.jdbcPassword)
     }
@@ -362,6 +371,22 @@ class SuiteContext implements Closeable {
         }
     }
 
+    // Use this explicitly for Doris FE connections. Generic connect must also support external
+    // databases and TLS tests that intentionally supply their own (or no) certificates.
+    public <T> T connectToDoris(String user, String password, String url, Closure<T> actionSupplier) {
+        if (!url.startsWith("jdbc:mysql:")) {
+            throw new IllegalArgumentException("Doris FE connections require a MySQL JDBC URL")
+        }
+        if (isTlsEnabled()) {
+            url = Config.buildTlsJdbcUrl(url,
+                    config.otherConfigs.get("keyStorePath")?.toString(),
+                    config.otherConfigs.get("keyStorePassword")?.toString(),
+                    config.otherConfigs.get("trustStorePath")?.toString(),
+                    config.otherConfigs.get("trustStorePassword")?.toString())
+        }
+        return connect(user, password, url, actionSupplier)
+    }
+
     public <T> T connect(String user, String password, String url, Closure<T> actionSupplier) {
         def originConnection = threadLocalConn.get()
         if ((config.otherConfigs.get("enableTLS")?.toString()?.equalsIgnoreCase("true")) ?: false) {
@@ -403,6 +428,10 @@ class SuiteContext implements Closeable {
         }
     }
 
+    private boolean isTlsEnabled() {
+        return (config.otherConfigs.get("enableTLS")?.toString()?.equalsIgnoreCase("true")) ?: false
+    }
+
     Connection getMasterConnectionByDbName(String dbName) {
         def result = JdbcUtils.executeToMapArray(getConnection(), "SHOW FRONTENDS")
         def master = null
@@ -414,7 +443,16 @@ class SuiteContext implements Closeable {
         }
         if (master) {
             log.info("master found: ${master.Host}:${master.HttpPort}")
-            def url = Config.buildUrlWithDb(master.Host as String, master.QueryPort as Integer, dbName)
+            def url = isTlsEnabled()
+                    ? Config.buildUrlWithDb(
+                            master.Host as String,
+                            master.QueryPort as Integer,
+                            dbName,
+                            config.otherConfigs.get("keyStorePath")?.toString(),
+                            config.otherConfigs.get("keyStorePassword")?.toString(),
+                            config.otherConfigs.get("trustStorePath")?.toString(),
+                            config.otherConfigs.get("trustStorePassword")?.toString())
+                    : Config.buildUrlWithDb(master.Host as String, master.QueryPort as Integer, dbName)
             def username = config.jdbcUser
             def password = config.jdbcPassword
 
@@ -436,7 +474,16 @@ class SuiteContext implements Closeable {
         }
         if (master) {
             log.info("master found: ${master.Host}:${master.HttpPort}")
-            def url = Config.buildUrlWithDb(master.Host as String, master.QueryPort as Integer, dbName)
+            def url = isTlsEnabled()
+                    ? Config.buildUrlWithDb(
+                            master.Host as String,
+                            master.QueryPort as Integer,
+                            dbName,
+                            config.otherConfigs.get("keyStorePath")?.toString(),
+                            config.otherConfigs.get("keyStorePassword")?.toString(),
+                            config.otherConfigs.get("trustStorePath")?.toString(),
+                            config.otherConfigs.get("trustStorePassword")?.toString())
+                    : Config.buildUrlWithDb(master.Host as String, master.QueryPort as Integer, dbName)
             ConnectionInfo connInfo = threadLocalConn.get()
             def userName = null
             def userPass = null
@@ -447,7 +494,7 @@ class SuiteContext implements Closeable {
                 userName = config.jdbcUser
                 userPass = config.jdbcPassword
             }
-            connectTo(url, connInfo.username, connInfo.password)
+            connectTo(url, userName, userPass)
             log.info("Successfully reconnected to the master")
         } else {
             throw new Exception("No master found to reconnect")
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy
index cbf4cda6f8d..58ca44abdf5 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/DebugPoint.groovy
@@ -38,6 +38,11 @@ class DebugPoint {
         this.suite = suite
     }
 
+    private static String buildDebugPointUrl(String host, int httpPort, String path) {
+        String scheme = Http.enableTls ? "https" : "http"
+        return "${scheme}://${host}:${httpPort}${path}"
+    }
+
     /* Enable debug point in regression
      * Note: set BE config::enable_debug_points = true to take effect
      * Parameters:
@@ -48,7 +53,7 @@ class DebugPoint {
      *    params:      timeout, execute, or other customized input params
      */
     static def enableDebugPoint(String host, int httpPort, NodeType type, String name, Map<String, String> params = null) {
-        def url = 'http://' + host + ':' + httpPort + '/api/debug_point/add/' + name
+        def url = buildDebugPointUrl(host, httpPort, '/api/debug_point/add/' + name)
         if (params != null && params.size() > 0) {
             url += '?' + params.collect((k, v) -> k + '=' + v).join('&')
         }
@@ -64,7 +69,7 @@ class DebugPoint {
      *    name:        debug point name
      */
     static def disableDebugPoint(String host, int httpPort, NodeType type, String name) {
-        def url = 'http://' + host + ':' + httpPort + '/api/debug_point/remove/' + name
+        def url = buildDebugPointUrl(host, httpPort, '/api/debug_point/remove/' + name)
         def result = Http.POST(url, null, true)
         Http.checkHttpResult(result, type)
     }
@@ -76,7 +81,7 @@ class DebugPoint {
      *    type:        NodeType.BE or NodeType.FE
      */
     static def clearDebugPoints(String host, int httpPort, NodeType type) {
-        def url = 'http://' + host + ':' + httpPort + '/api/debug_point/clear'
+        def url = buildDebugPointUrl(host, httpPort, '/api/debug_point/clear')
         def result = Http.POST(url, null, true)
         Http.checkHttpResult(result, type)
     }
@@ -197,4 +202,3 @@ class DebugPoint {
         }
     }
 }
-
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy
index 8ee33b5ecba..e902a345a66 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/Http.groovy
@@ -66,6 +66,9 @@ class Http {
         keyStorePath = keyPath
         keyStorePassword = keyPassword
         keyStoreType = keystoreType
+        if (!enableTls) {
+            return
+        }
         initSSLContext()
     }
 
@@ -110,13 +113,26 @@ class Http {
         }
     }
 
-    static Object GET(url, isJson = false, printText = true) {
-        if (enableTls) {
-            url = url.replace("http://", "https://")
+    static String basicAuthorization(String user, String password) {
+        String credentials = "${user}:${password ?: ''}"
+        return 'Basic ' + credentials.getBytes('UTF-8').encodeBase64().toString()
+    }
+
+    static URLConnection openConnection(String url) {
+        if (enableTls && url.startsWith('http://')) {
+            url = 'https://' + url.substring('http://'.length())
         }
-        def conn = new URL(url).openConnection()
+        return new URL(url).openConnection()
+    }
+
+    static Object GET(url, isJson = false, printText = true) {
+        return GET(url, isJson, printText, 'root', '')
+    }
+
+    static Object GET(url, isJson, printText, String user, String password) {
+        def conn = openConnection(url)
         conn.setRequestMethod('GET')
-        conn.setRequestProperty('Authorization', 'Basic cm9vdDo=') //token for root
+        conn.setRequestProperty('Authorization', basicAuthorization(user, password))
         def code = conn.responseCode
         def text = conn.content.text
         if (printText) {
@@ -135,12 +151,16 @@ class Http {
     }
 
     static Object POST(url, data = null, isJson = false) {
+        return POST(url, data, isJson, 'root', '')
+    }
+
+    static Object POST(url, data, isJson, String user, String password) {
         if (enableTls) {
             url = url.replace("http://", "https://")
         }
         def conn = new URL(url).openConnection()
         conn.setRequestMethod('POST')
-        conn.setRequestProperty('Authorization', 'Basic cm9vdDo=') //token for root
+        conn.setRequestProperty('Authorization', basicAuthorization(user, password))
         if (data) {
             if (isJson) {
                 conn.setRequestProperty('Content-Type', 'application/json')
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/MultiClusterStateGuard.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/MultiClusterStateGuard.groovy
new file mode 100644
index 00000000000..2594c43d02b
--- /dev/null
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/MultiClusterStateGuard.groovy
@@ -0,0 +1,195 @@
+// 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.regression.util
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+
+/** Restores the dedicated multi-cluster test instance; never used for docker/upgrade tests. */
+class MultiClusterStateGuard {
+    Closure listClusters
+    Closure dropCluster
+    Closure addCluster
+    Closure readDefault
+    Closure setDefault
+    Closure awaitState
+    Closure restoreInstance = {}
+
+    /** Parse legacy IP:heartbeat:http:cloud-id entries with an optional brpc port. */
+    static List<Map> configuredBackends(String configuredBes) {
+        if (!configuredBes?.trim()) {
+            throw new IllegalArgumentException('Missing multiClusterBes configuration')
+        }
+        def nodes = []
+        configuredBes.split(',').eachWithIndex { entry, index ->
+            def parts = entry.trim().split(':', -1)
+            if (!(parts.length in [4, 5]) || parts.any { !it.trim() }) {
+                throw new IllegalArgumentException("Invalid multiClusterBes entry at index ${index}")
+            }
+            def ports = [parts[1], parts[2]] + (parts.length == 5 ? [parts[4]] : [])
+            if (ports.any { !it.isInteger() || it.toInteger() < 1 || it.toInteger() > 65535 }) {
+                throw new IllegalArgumentException("Invalid BE port at index ${index}")
+            }
+            nodes << [ip: parts[0], heartbeat_port: parts[1].toInteger(), http_port: parts[2].toInteger(),
+             cloud_unique_id: parts[3]]
+        }
+        // cloud_unique_id identifies a cloud deployment, not an individual BE process.
+        if (nodes.collect { [it.ip, it.heartbeat_port] }.toSet().size() != nodes.size()) {
+            throw new IllegalArgumentException('multiClusterBes contains duplicate BE endpoints')
+        }
+        return nodes
+    }
+
+    /** Shared fixture: the first two configured BEs each belong to one compute group. */
+    static List<Map> sharedBaseline(String configuredBes) {
+        def nodes = configuredBackends(configuredBes)
+        if (nodes.size() < 2) {
+            throw new IllegalArgumentException('The shared multi-cluster baseline requires at least two BEs')
+        }
+        return (0..1).collect { index ->
+            [type: 'COMPUTE', cluster_name: "regression_cluster_name${index}".toString(),
+             cluster_id: "regression_cluster_id${index}".toString(), cluster_status: 'NORMAL',
+             nodes: [nodes[index]]]
+        }
+    }
+
+    private static Object copy(Object value) {
+        return new JsonSlurper().parseText(JsonOutput.toJson(value))
+    }
+
+    // MS refreshes these timestamps when recreating a cluster or node.
+    static Object topology(Object value) {
+        if (value instanceof Map) {
+            def result = new TreeMap()
+            value.each { key, item ->
+                if (!(key in ['ctime', 'mtime'])) {
+                    result[key] = topology(item)
+                }
+            }
+            return result
+        }
+        if (value instanceof List) {
+            return value.collect { topology(it) }.sort { a, b ->
+                JsonOutput.toJson(a) <=> JsonOutput.toJson(b)
+            }
+        }
+        return value
+    }
+
+    static boolean frontendMatches(List<Map> expected, List<Map> groups, List<Map> backends) {
+        if ((groups.collect { it.cluster } as Set) != (expected.collect { it.cluster_name } as Set)) {
+            return false
+        }
+        def expectedNodes = expected.collectMany { cluster ->
+            (cluster.nodes ?: []).collect { node ->
+                [cluster.cluster_id.toString(), (node.ip ?: node.host).toString(), node.heartbeat_port.toString(),
+                        cluster.cluster_name.toString(), cluster.cluster_status.toString(), node.cloud_unique_id.toString()]
+            }
+        }.sort { a, b -> a.toString() <=> b.toString() }
+        // Backend.getCloudClusterStatus treats an absent status tag as NORMAL.
+        def actualNodes = backends.collect { backend ->
+            def tag = backend.Tag instanceof Map ? backend.Tag : new JsonSlurper().parseText(backend.Tag.toString())
+            [(tag.compute_group_id ?: tag.cloud_cluster_id)?.toString(), backend.Host.toString(),
+                    backend.HeartbeatPort.toString(), (tag.compute_group_name ?: tag.cloud_cluster_name)?.toString(),
+                    (tag.compute_group_status ?: tag.cloud_cluster_status ?: 'NORMAL').toString(), tag.cloud_unique_id?.toString()]
+        }.sort { a, b -> a.toString() <=> b.toString() }
+        return expectedNodes == actualNodes && backends.every {
+            it.Alive.toString().toBoolean() && !it.SystemDecommissioned?.toString()?.toBoolean()
+        }
+    }
+
+    private List<Map> computeClusters() {
+        List<Map> clusters = listClusters.call()
+        if (clusters.any { it.type == 'VIRTUAL' }) {
+            throw new IllegalStateException('Legacy multi-cluster state restoration does not support virtual clusters')
+        }
+        return clusters.findAll { it.type == 'COMPUTE' }
+    }
+
+    /** initializeBaseline keeps a successful load fixture, and rolls back a failed one. */
+    def run(boolean initializeBaseline, Closure body) {
+        List<Map> original = copy(computeClusters()) as List<Map>
+        if (original.any { it.cluster_status != 'NORMAL' ||
+                (it.nodes ?: []).any { n -> n.status && n.status != 'NODE_STATUS_RUNNING' } }) {
+            throw new IllegalStateException('Multi-cluster tests require normal clusters and running nodes')
+        }
+        String originalDefault = readDefault.call()
+        // A stale default cannot be restored with SET PROPERTY. Reject it before destructive tests.
+        if (originalDefault && !original.any { it.cluster_name == originalDefault }) {
+            throw new IllegalStateException("Initial default compute group does not exist: ${originalDefault}")
+        }
+        Throwable failure = null
+        try {
+            // Tests can replace the groups. New connections must not inherit a deleted group.
+            setDefault.call('')
+            awaitState.call(original, '')
+            return body.call()
+        } catch (Throwable t) {
+            failure = t
+            throw t
+        } finally {
+            def errors = []
+            def attempt = { Closure action ->
+                try {
+                    action.call()
+                } catch (Throwable t) {
+                    errors.add(t)
+                }
+            }
+            attempt { restoreInstance.call() }
+            List<Map> expected = original
+            String expectedDefault = originalDefault
+            attempt {
+                if (initializeBaseline && failure == null) {
+                    // The load suite intentionally leaves its successful setup for subsequent suites.
+                    expected = copy(computeClusters()) as List<Map>
+                    expectedDefault = readDefault.call()
+                } else {
+                    def expectedById = original.collectEntries { [(it.cluster_id): it] }
+                    computeClusters().each { current ->
+                        def saved = expectedById[current.cluster_id]
+                        if (saved == null || topology(saved) != topology(current)) {
+                            attempt { dropCluster.call(current) }
+                        }
+                    }
+                    // Re-read after deletion; a failed drop must not prevent restoring other groups.
+                    def currentById = computeClusters().collectEntries { [(it.cluster_id): it] }
+                    original.each { saved ->
+                        if (!currentById.containsKey(saved.cluster_id)) {
+                            attempt { addCluster.call(copy(saved)) }
+                        }
+                    }
+                }
+            }
+            // Restore metadata and FE node maps before restoring the default property.
+            // Even if a wait fails, attempt the remaining cleanup and report every failure.
+            attempt { awaitState.call(expected, null) }
+            attempt { setDefault.call(expectedDefault) }
+            attempt { awaitState.call(expected, expectedDefault) }
+            if (!errors.isEmpty()) {
+                Throwable restoreFailure = new IllegalStateException('Failed to restore multi-cluster test state')
+                errors.each { restoreFailure.addSuppressed(it) }
+                if (failure != null) {
+                    failure.addSuppressed(restoreFailure)
+                } else {
+                    throw restoreFailure
+                }
+            }
+        }
+    }
+}
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/MySqlClient.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/MySqlClient.groovy
new file mode 100644
index 00000000000..e0de281b8f3
--- /dev/null
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/MySqlClient.groovy
@@ -0,0 +1,70 @@
+// 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.regression.util
+
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.attribute.PosixFilePermissions
+
+class MySqlClient {
+    // MySQL option files interpret backslash escapes even inside quoted values.
+    private static String quoteOption(String value) {
+        String text = value ?: ''
+        if (text.indexOf(0) >= 0) {
+            throw new IllegalArgumentException('MySQL credentials must not contain NUL')
+        }
+        return '"' + text.replace('\\', '\\\\').replace('"', '\\"')
+                .replace('\n', '\\n').replace('\r', '\\r')
+                .replace('\t', '\\t').replace('\b', '\\b') + '"'
+    }
+
+    static Map execute(String user, String password, List<String> options, String sql,
+                       File directory = null, String executable = 'mysql') {
+        def credentials = Files.createTempFile('doris-mysql-', '.cnf',
+                PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString('rw-------')))
+        Process process = null
+        try {
+            String config = "[client]\nuser=${quoteOption(user)}\npassword=${quoteOption(password)}\n"
+            Files.write(credentials, config.getBytes(StandardCharsets.UTF_8))
+            List<String> command = [executable, "--defaults-extra-file=${credentials}".toString()]
+            command.addAll(options.collect { it.toString() })
+            ProcessBuilder builder = new ProcessBuilder(command)
+            if (directory != null) {
+                builder.directory(directory)
+            }
+            process = builder.start()
+            StringBuilder stdout = new StringBuilder()
+            StringBuilder stderr = new StringBuilder()
+            Thread outputReader = process.consumeProcessOutputStream(stdout)
+            Thread errorReader = process.consumeProcessErrorStream(stderr)
+            process.outputStream.withWriter('UTF-8') { writer -> writer.write(sql) }
+            int exitCode = process.waitFor()
+            outputReader.join()
+            errorReader.join()
+            return [exitCode: exitCode, stdout: stdout.toString(), stderr: stderr.toString()]
+        } finally {
+            try {
+                if (process != null && process.isAlive()) {
+                    process.destroyForcibly().waitFor()
+                }
+            } finally {
+                Files.deleteIfExists(credentials)
+            }
+        }
+    }
+}
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/RowsetMetaUtils.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/RowsetMetaUtils.groovy
new file mode 100644
index 00000000000..26b48e23f48
--- /dev/null
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/RowsetMetaUtils.groovy
@@ -0,0 +1,79 @@
+// 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.regression.util
+
+import java.nio.charset.StandardCharsets
+
+class RowsetMetaUtils {
+    /** Only active rowsets count; a high max version must not hide a gap in the version chain. */
+    static boolean coversVersion(Map status, long version) {
+        if (version < 0) {
+            throw new IllegalArgumentException("negative target version: ${version}")
+        }
+        if (!(status.rowsets instanceof List)) {
+            throw new IllegalArgumentException("compaction response is missing rowsets: ${status}")
+        }
+        def ranges = status.rowsets.collect { rowset ->
+            def match = rowset.toString() =~ /^\[(\d+)-(\d+)\](?:\s.*)?$/
+            if (!match.matches()) {
+                throw new IllegalArgumentException("invalid compaction rowset: ${rowset}")
+            }
+            long start = match.group(1).toLong()
+            long end = match.group(2).toLong()
+            if (start > end) {
+                throw new IllegalArgumentException("invalid rowset version range: ${rowset}")
+            }
+            [start, end]
+        }.sort { a, b -> a[0] <=> b[0] }
+        long covered = -1
+        for (def range : ranges) {
+            if (range[0] > covered + 1) {
+                return false
+            }
+            covered = Math.max(covered, range[1] as long)
+            if (covered >= version) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /** MS protobuf JSON encodes bytes as base64. Preserve one character per key byte. */
+    static Map decodeKeyBounds(Map meta) {
+        Map result = new LinkedHashMap(meta)
+        if (meta.segments_key_bounds instanceof List) {
+            result.segments_key_bounds = meta.segments_key_bounds.collect { bounds ->
+                Map decoded = new LinkedHashMap(bounds)
+                ['min_key', 'max_key'].each { key ->
+                    if (!(bounds[key] instanceof String)) {
+                        throw new IllegalArgumentException("key bounds are missing ${key}")
+                    }
+                    decoded[key] = new String(Base64.decoder.decode(bounds[key]), StandardCharsets.ISO_8859_1)
+                }
+                decoded
+            }
+        }
+        // These optional protobuf flags default to false when omitted from MS JSON.
+        ['segments_key_bounds_truncated', 'segments_key_bounds_aggregated'].each { key ->
+            if (!result.containsKey(key)) {
+                result[key] = false
+            }
+        }
+        return result
+    }
+}
diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
index 846ce4ac34b..e765c55a508 100644
--- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
+++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
@@ -44,7 +44,7 @@ class WarmupMetricsUtils {
      */
     static long getBrpcMetric(String ip, String port, String metricName) {
         def url = "http://${ip}:${port}/brpc_metrics"
-        def text = new URL(url).text
+        def text = readText(url)
         def matcher = text =~ ~"${metricName}\\s+(\\d+)"
         if (matcher.find()) {
             return matcher[0][1] as long
@@ -53,7 +53,7 @@ class WarmupMetricsUtils {
     }
 
     static String getPrometheusMetrics(String ip, Object port) {
-        return new URL("http://${ip}:${port}/metrics").text
+        return readText("http://${ip}:${port}/metrics")
     }
 
     static BigDecimal findPrometheusMetricValue(String metricsText, String metricName, Map labels) {
@@ -101,7 +101,7 @@ class WarmupMetricsUtils {
     }
 
     static void clearFileCache(String ip, String httpPort) {
-        def response = new URL("http://${ip}:${httpPort}/api/file_cache?op=clear&sync=true").text
+        def response = readText("http://${ip}:${httpPort}/api/file_cache?op=clear&sync=true")
         def json = new JsonSlurper().parseText(response)
         if (json.status != "OK") {
             throw new RuntimeException("Clear cache on ${ip}:${httpPort} failed: ${json.status}")
@@ -115,6 +115,96 @@ class WarmupMetricsUtils {
         Thread.sleep(waitMs)
     }
 
+    private static String backendIp(Object backend) {
+        return backend instanceof Map ? backend.ip.toString() : backend[1].toString()
+    }
+
+    private static String backendHttpPort(Object backend) {
+        return backend instanceof Map ? backend.httpPort.toString() : backend[4].toString()
+    }
+
+    private static String backendBrpcPort(Object backend) {
+        return backend instanceof Map ? backend.brpcPort.toString() : backend[5].toString()
+    }
+
+    private static String readText(String url) {
+        def connection = Http.openConnection(url)
+        connection.connectTimeout = 5000
+        connection.readTimeout = 60000
+        try {
+            return connection.inputStream.withCloseable { it.getText("UTF-8") }
+        } finally {
+            connection.disconnect()
+        }
+    }
+
+    static Map getBackendMetricValues(Collection backends, String metricName) {
+        Map values = [:]
+        for (be in backends) {
+            String key = "${backendIp(be)}:${backendBrpcPort(be)}"
+            values[key] = getBrpcMetric(backendIp(be), backendBrpcPort(be), metricName)
+        }
+        return values
+    }
+
+    static long getBackendMetricSum(Collection backends, String metricName) {
+        return getBackendMetricValues(backends, metricName).values().sum(0L) as long
+    }
+
+    static long waitForBackendMetricSum(Collection backends, String metricName,
+                                        Closure<Boolean> predicate, long timeoutMs = 60000,
+                                        String message = null) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        long value = 0L
+        while (System.currentTimeMillis() < deadline) {
+            value = getBackendMetricSum(backends, metricName)
+            if (predicate(value)) {
+                return value
+            }
+            logger.info("waiting ${metricName} sum, value=${value}")
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException((message ?: "Metric ${metricName} sum did not satisfy predicate") +
+                " within ${timeoutMs}ms, last=${value}")
+    }
+
+    static long waitForBackendMetricSumStable(Collection backends, String metricName,
+                                              long stableMs = 10000, long timeoutMs = 60000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        long lastValue = getBackendMetricSum(backends, metricName)
+        long stableSince = System.currentTimeMillis()
+        while (System.currentTimeMillis() < deadline) {
+            Thread.sleep(1000)
+            long currentValue = getBackendMetricSum(backends, metricName)
+            if (currentValue != lastValue) {
+                lastValue = currentValue
+                stableSince = System.currentTimeMillis()
+            } else if (System.currentTimeMillis() - stableSince >= stableMs) {
+                return currentValue
+            }
+            logger.info("waiting ${metricName} sum stable, value=${currentValue}")
+        }
+        throw new RuntimeException("Metric ${metricName} sum did not stay stable for ${stableMs}ms " +
+                "within ${timeoutMs}ms, last=${lastValue}")
+    }
+
+    static long waitForBrpcMetric(String ip, String port, String metricName,
+                                  Closure<Boolean> predicate, long timeoutMs = 60000,
+                                  String message = null) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        long value = 0L
+        while (System.currentTimeMillis() < deadline) {
+            value = getBrpcMetric(ip, port, metricName)
+            if (predicate(value)) {
+                return value
+            }
+            logger.info("waiting ${metricName} for ${ip}:${port}, value=${value}")
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException((message ?: "Metric ${metricName} did not satisfy predicate") +
+                " within ${timeoutMs}ms, last=${value}")
+    }
+
     static long sumProfileCounter(String profileText, String counterName) {
         def matcher = profileText =~ ~"(?m)(?<![A-Za-z0-9_])${Pattern.quote(counterName)}\\s*:\\s*([0-9,]+)"
         long sum = 0
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/ConfigArrowFlightSqlUrlTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/ConfigArrowFlightSqlUrlTest.groovy
new file mode 100644
index 00000000000..e04fcaba42d
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/ConfigArrowFlightSqlUrlTest.groovy
@@ -0,0 +1,61 @@
+// 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.regression
+
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertFalse
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+class ConfigArrowFlightSqlUrlTest {
+    @Test
+    void plaintextFlightUrlRemainsAvailableWhenTlsIsDisabled() {
+        Config config = new Config()
+        assertEquals("jdbc:arrow-flight-sql://localhost:8070/?useServerPrepStmts=false&useEncryption=false",
+                config.getArrowFlightSqlJdbcUrl("localhost", "8070"))
+    }
+
+    @Test
+    void mtlsFlightUrlUsesCaAndClientPemFiles() {
+        Config config = new Config()
+        config.otherConfigs.put("enableTLS", "true")
+        config.otherConfigs.put("trustCACert", "/test/ca.crt")
+        config.otherConfigs.put("trustCert", "/test/client.crt")
+        config.otherConfigs.put("trustCAKey", "/test/client.key")
+
+        String url = config.getArrowFlightSqlJdbcUrl("localhost", "8070", "test_db")
+        assertTrue(url.startsWith("jdbc:arrow-flight-sql://localhost:8070/catalog=test_db?"))
+        assertTrue(url.contains("useEncryption=true"))
+        assertTrue(url.contains("tlsRootCerts=/test/ca.crt"))
+        assertTrue(url.contains("clientCertificate=/test/client.crt"))
+        assertTrue(url.contains("clientKey=/test/client.key"))
+        assertFalse(url.contains("disableCertificateVerification=true"))
+    }
+
+    @Test
+    void flightUrlHonorsDisabledCertificateVerification() {
+        Config config = new Config()
+        config.otherConfigs.put("enableTLS", "true")
+        config.otherConfigs.put("tlsVerifyMode", "none")
+
+        String url = config.getArrowFlightSqlJdbcUrl("localhost", "8070")
+        assertTrue(url.contains("useEncryption=true"))
+        assertTrue(url.contains("disableCertificateVerification=true"))
+    }
+}
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/ConfigRootPasswordTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/ConfigRootPasswordTest.groovy
new file mode 100644
index 00000000000..cd908606cba
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/ConfigRootPasswordTest.groovy
@@ -0,0 +1,61 @@
+// 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.regression
+
+import org.junit.jupiter.api.Test
+
+import java.sql.Connection
+import java.sql.Driver
+import java.sql.DriverManager
+
+import static org.junit.jupiter.api.Assertions.*
+
+class ConfigRootPasswordTest {
+    @Test
+    void rootConnectionsOnlyReusePasswordsConfiguredForRoot() {
+        def calls = []
+        boolean closed = false
+        Connection connection = [close: { closed = true }] as Connection
+        Driver driver = [connect: { String url, Properties properties ->
+            if (url != 'jdbc:root-credentials:test') return null
+            calls.add(new HashMap(properties))
+            return connection
+        }] as Driver
+        DriverManager.registerDriver(driver)
+        try {
+            def cases = [
+                    ['root', 'fake password \u5bc6\u78bc', 'fake password \u5bc6\u78bc'],
+                    ['root', '', ''], ['root', null, ''], ['root', ' ', ' '],
+                    ['test_user', 'other-user-password', ''], ['test_user', '', ''],
+                    [null, 'other-user-password', '']
+            ]
+            cases.each { user, password, expected ->
+                Config config = new Config(jdbcUrl: 'jdbc:root-credentials:test',
+                        jdbcUser: user, jdbcPassword: password)
+                assertEquals(expected, config.getRootPassword())
+                closed = false
+                config.getRootConnection().withCloseable { assertSame(connection, it) }
+                assertTrue(closed)
+                assertEquals('root', calls.last().user)
+                assertEquals(expected, calls.last().password)
+            }
+        } finally {
+            DriverManager.deregisterDriver(driver)
+        }
+    }
+}
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/CompactionPluginCompatibilityTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/CompactionPluginCompatibilityTest.groovy
new file mode 100644
index 00000000000..c8106d9e7bf
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/CompactionPluginCompatibilityTest.groovy
@@ -0,0 +1,154 @@
+// 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.regression.suite
+
+import groovy.lang.Binding
+import groovy.lang.GroovyShell
+import groovy.lang.GroovySystem
+import groovy.json.JsonSlurper
+import org.apache.doris.regression.Config
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import org.slf4j.LoggerFactory
+
+import java.nio.file.Path
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertNotNull
+import static org.junit.jupiter.api.Assertions.assertThrows
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+class CompactionPluginCompatibilityTest {
+    @TempDir
+    Path tempDir
+
+    @AfterEach
+    void removePluginMethods() {
+        GroovySystem.metaClassRegistry.removeMetaClass(Suite)
+    }
+
+    @Test
+    void tabletStatusAndCompactionStatusSupportDefaultAndBoundedProbes() {
+        File plugin = [new File("../plugins/plugin_compaction.groovy"),
+                       new File("regression-test/plugins/plugin_compaction.groovy")]
+                .find { it.isFile() }
+        assertNotNull(plugin, "plugin_compaction.groovy must be available to this test")
+
+        def logger = LoggerFactory.getLogger(CompactionPluginCompatibilityTest)
+        new GroovyShell(getClass().classLoader, new Binding(logger: logger)).evaluate(plugin)
+
+        def requests = []
+        Suite.metaClass.curl = { String method, String url, String body, Integer timeoutSec,
+                                 String user, String password, Integer maxRetries ->
+            requests.add([method, url, body, timeoutSec, user, password, maxRetries])
+            return [0, "{}", ""]
+        }
+
+        File suiteDir = tempDir.resolve("suites/compaction").toFile()
+        assertTrue(suiteDir.mkdirs())
+        File suiteFile = new File(suiteDir, "compaction_plugin_compatibility.groovy")
+        assertTrue(suiteFile.createNewFile())
+        Config config = new Config()
+        config.suitePath = tempDir.resolve("suites").toString()
+        config.dataPath = tempDir.resolve("data").toString()
+        config.realDataPath = tempDir.resolve("real-data").toString()
+        config.defaultDb = "regression_test"
+        config.feHttpUser = "http_user"
+        config.feHttpPassword = "http_password"
+        ScriptContext scriptContext = new ScriptContext(
+                suiteFile, null, null, config, Collections.emptyList(), { true })
+        SuiteCluster cluster = new SuiteCluster("compaction_plugin_compatibility", config)
+        SuiteContext context = new SuiteContext(
+                suiteFile, "compaction_plugin_compatibility", "p0", scriptContext,
+                cluster, null, null, config)
+        Suite suite = new Suite("compaction_plugin_compatibility", "p0", context, cluster)
+        assertEquals([0, "{}", ""], suite.be_show_tablet_status("127.0.0.1", "8040", "42"))
+        assertEquals([0, "{}", ""], suite.be_show_tablet_status("127.0.0.1", "8040", "42", 5, 1))
+        assertEquals([0, "{}", ""], suite.be_get_compaction_status("127.0.0.1", "8040", "42"))
+        assertEquals([0, "{}", ""], suite.be_get_compaction_status("127.0.0.1", "8040", "42", 5, 1))
+
+        assertEquals([
+                ["GET", "http://127.0.0.1:8040/api/compaction/show?tablet_id=42",
+                 null, 10, "http_user", "http_password", 10],
+                ["GET", "http://127.0.0.1:8040/api/compaction/show?tablet_id=42",
+                 null, 5, "http_user", "http_password", 1],
+                ["GET", "http://127.0.0.1:8040/api/compaction/run_status?tablet_id=42",
+                 null, 10, "http_user", "http_password", 10],
+                ["GET", "http://127.0.0.1:8040/api/compaction/run_status?tablet_id=42",
+                 null, 5, "http_user", "http_password", 1]
+        ], requests)
+
+        def defaultCalls = []
+        Suite.metaClass.curl = { String method, String url, String body, Integer timeoutSec,
+                                 String user, String password, Integer maxRetries ->
+            defaultCalls.add([method, url, body, timeoutSec, user, password, maxRetries])
+            return [0, "{}", ""]
+        }
+        assertEquals([0, "{}", ""], suite.be_get_overall_compaction_status("127.0.0.1", "8040"))
+        assertEquals([0, "{}", ""], suite.be_run_base_compaction("127.0.0.1", "8040", "42"))
+        assertEquals([0, "{}", ""], suite.be_run_cumulative_compaction("127.0.0.1", "8040", "42"))
+        assertEquals([0, "{}", ""], suite.be_run_full_compaction("127.0.0.1", "8040", "42"))
+        assertEquals([0, "{}", ""], suite.be_run_full_compaction_by_table_id("127.0.0.1", "8040", "7"))
+        assertEquals([
+                ["GET", "http://127.0.0.1:8040/api/compaction/run_status",
+                 null, 10, "http_user", "http_password", 10],
+                ["POST", "http://127.0.0.1:8040/api/compaction/run?tablet_id=42&compact_type=base",
+                 null, 10, "http_user", "http_password", 10],
+                ["POST", "http://127.0.0.1:8040/api/compaction/run?tablet_id=42&compact_type=cumulative",
+                 null, 10, "http_user", "http_password", 10],
+                ["POST", "http://127.0.0.1:8040/api/compaction/run?tablet_id=42&compact_type=full",
+                 null, 10, "http_user", "http_password", 10],
+                ["POST", "http://127.0.0.1:8040/api/compaction/run?table_id=7&compact_type=full",
+                 null, 10, "http_user", "http_password", 10]
+        ], defaultCalls)
+
+        // This reproduces the plugin-internal three-argument call that failed in TeamCity 212706.
+        Suite.metaClass.getBackendIpHttpPort = { Map hosts, Map ports ->
+            hosts["1"] = "127.0.0.1"
+            ports["1"] = "8040"
+        }
+        Suite.metaClass.sql_return_maparray = { String ignored -> [[BackendId: "1", TabletId: "42"]] }
+        Suite.metaClass.sql = { String ignored -> [[null, '"disable_auto_compaction" = "true"']] }
+        Suite.metaClass.curl = { String method, String url, String body, Integer timeoutSec,
+                                 String user, String password, Integer maxRetries ->
+            throw new IllegalStateException("tablet probe reached")
+        }
+        IllegalStateException probe = assertThrows(IllegalStateException) {
+            suite.trigger_and_wait_compaction("t", "full")
+        }
+        assertEquals("tablet probe reached", probe.message)
+
+        Suite.metaClass.parseJson = { String value -> new JsonSlurper().parseText(value) }
+        int showCount = 0
+        Suite.metaClass.curl = { String method, String url, String body = null, Integer timeoutSec = 10,
+                                 String user = "", String password = "", Integer maxRetries = 10 ->
+            if (url.contains("/api/compaction/show?")) {
+                showCount++
+                return [0, '{"last full success time":"' + (showCount == 1 ? "old" : "new")
+                        + '","last full failure time":"old"}', ""]
+            }
+            if (url.contains("/api/compaction/run_status?")) {
+                return [0, '{"status":"success","run_status":false}', ""]
+            }
+            return [0, '{"status":"success"}', ""]
+        }
+        suite.trigger_and_wait_compaction("t", "full")
+        assertTrue(showCount >= 2)
+    }
+}
diff --git a/regression-test/suites/show_p0/test_show_data.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/SuiteSshExecTest.groovy
similarity index 61%
copy from regression-test/suites/show_p0/test_show_data.groovy
copy to regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/SuiteSshExecTest.groovy
index 20adc299d55..c7fe5e33f20 100644
--- a/regression-test/suites/show_p0/test_show_data.groovy
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/SuiteSshExecTest.groovy
@@ -15,17 +15,17 @@
 // specific language governing permissions and limitations
 // under the License.
 
-suite("test_show_data") {
-    def result = sql """show data;"""
-    logger.info("show data result:${result}");
-    assertTrue(result.size() > 0);
+package org.apache.doris.regression.suite
 
-    def jdbcUrlWithoutDbStr = (context.config.jdbcUrl).split(context.config.defaultDb)
-    logger.info("jdbcUrlWithoutDbStr:${jdbcUrlWithoutDbStr}");
-    def result2 = connect(context.config.jdbcUser, context.config.jdbcPassword, jdbcUrlWithoutDbStr[0]) {
-        sql """show data;"""
-    }
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
 
-    assertTrue(result2.size() > 0);
-    assertTrue(result2[0].size() == 6);
-}
\ No newline at end of file
+class SuiteSshExecTest {
+    @Test
+    void sshCommandAcceptsNewRegressionHosts() {
+        assertEquals(
+                "ssh -o StrictHostKeyChecking=no root@172.17.0.1 'mkdir -p /tmp/udf'",
+                Suite.buildSshCommand("root", "172.17.0.1", "mkdir -p /tmp/udf"))
+    }
+}
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/HttpAuthenticationTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/HttpAuthenticationTest.groovy
new file mode 100644
index 00000000000..a1542b24d13
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/HttpAuthenticationTest.groovy
@@ -0,0 +1,72 @@
+// 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.regression.util
+
+import com.sun.net.httpserver.HttpServer
+import groovy.json.JsonOutput
+import org.junit.jupiter.api.Test
+
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+import static org.junit.jupiter.api.Assertions.*
+
+class HttpAuthenticationTest {
+    @Test
+    void credentialsAreRequestScopedAndLegacyCallsStillUseEmptyRootPassword() {
+        def server = HttpServer.create(new InetSocketAddress('127.0.0.1', 0), 0)
+        def serverThreads = Executors.newCachedThreadPool()
+        def callers = Executors.newFixedThreadPool(6)
+        boolean originalTls = Http.enableTls
+        server.executor = serverThreads
+        server.createContext('/') { exchange ->
+            def response = [authorization: exchange.requestHeaders.getFirst('Authorization'),
+                            body: exchange.requestBody.getText('UTF-8')]
+            byte[] bytes = JsonOutput.toJson(response).getBytes('UTF-8')
+            exchange.responseHeaders.set('Content-Type', 'application/json; charset=UTF-8')
+            exchange.sendResponseHeaders(200, bytes.length)
+            exchange.responseBody.withCloseable { it.write(bytes) }
+        }
+        try {
+            Http.enableTls = false
+            server.start()
+            String url = "http://127.0.0.1:${server.address.port}/"
+            def passwords = ['', null, 'test-password', '密碼 \\" :#; $()`\\\\\n']
+            def futures = (0..<24).collect { index ->
+                callers.submit({ ->
+                    String user = "user${index}"
+                    String password = passwords[index % passwords.size()]
+                    def response = index % 2 == 0
+                            ? Http.GET(url, true, false, user, password)
+                            : Http.POST(url, [index: index], true, user, password)
+                    String decoded = new String(Base64.decoder.decode(response.authorization.substring(6)), 'UTF-8')
+                    assertEquals(user + ':' + (password ?: ''), decoded)
+                    if (index % 2 != 0) assertEquals(JsonOutput.toJson([index: index]), response.body)
+                } as Runnable)
+            }
+            futures.each { it.get(10, TimeUnit.SECONDS) }
+            assertEquals('Basic cm9vdDo=', Http.GET(url, true, false).authorization)
+            assertEquals('Basic cm9vdDo=', Http.POST(url, null, true).authorization)
+        } finally {
+            Http.enableTls = originalTls
+            server.stop(0)
+            callers.shutdownNow()
+            serverThreads.shutdownNow()
+        }
+    }
+}
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/MultiClusterStateGuardTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/MultiClusterStateGuardTest.groovy
new file mode 100644
index 00000000000..1cdc9a17d06
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/MultiClusterStateGuardTest.groovy
@@ -0,0 +1,256 @@
+// 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.regression.util
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.*
+
+class MultiClusterStateGuardTest {
+    private static def copy(value) {
+        new JsonSlurper().parseText(JsonOutput.toJson(value))
+    }
+
+    private static Map cluster(String id) {
+        [type: 'COMPUTE', cluster_id: id, cluster_name: "group_${id}".toString(),
+         cluster_status: 'NORMAL', public_endpoint: "public_${id}".toString(),
+         private_endpoint: "private_${id}".toString(), properties: [key: 'value'], ctime: 1,
+         nodes: [[cloud_unique_id: "be_${id}".toString(), ip: '127.0.0.1', heartbeat_port: 9050,
+                  status: 'NODE_STATUS_RUNNING', ctime: 1],
+                 [cloud_unique_id: "be2_${id}".toString(), ip: '127.0.0.2', heartbeat_port: 9051,
+                  status: 'NODE_STATUS_RUNNING', ctime: 1]]]
+    }
+
+    private static Map fixture(List<Map> clusters = [cluster('a'), cluster('b')], String defaultGroup = 'group_a') {
+        def state = [clusters: copy(clusters), defaultGroup: defaultGroup, events: [],
+                     failDrop: [], failAdd: [], failWait: false, instanceStatus: 'NORMAL']
+        state.guard = new MultiClusterStateGuard(
+            listClusters: { state.clusters },
+            restoreInstance: { state.events << 'instance'; state.instanceStatus = 'NORMAL' },
+            dropCluster: { c ->
+                state.events << "drop:${c.cluster_id}".toString()
+                if (c.cluster_id in state.failDrop) { throw new IOException('drop failed') }
+                state.clusters.removeAll { it.cluster_id == c.cluster_id }
+            },
+            addCluster: { c ->
+                state.events << "add:${c.cluster_id}".toString()
+                if (c.cluster_id in state.failAdd) { throw new IOException('add failed') }
+                c.ctime = 100
+                c.nodes.each { it.ctime = 100 }
+                state.clusters << c
+            },
+            readDefault: { state.defaultGroup },
+            setDefault: { saved -> state.events << 'default'; state.defaultGroup = saved },
+            awaitState: { expected, expectedDefault ->
+                state.events << "wait:${expectedDefault}".toString()
+                if (state.failWait) { throw new IOException('FE timeout') }
+                assertEquals(MultiClusterStateGuard.topology(expected), MultiClusterStateGuard.topology(state.clusters))
+                if (expectedDefault != null) { assertEquals(expectedDefault, state.defaultGroup) }
+            }
+        )
+        state
+    }
+
+    @Test
+    void sharedBaselineUsesFirstTwoConfiguredNodesWithoutSharingMutableSnapshots() {
+        String config = 'be0:9050:8040:id0,be1:9051:8041:id1,be2:9052:8042:id2'
+        def baseline = MultiClusterStateGuard.sharedBaseline(config)
+        assertEquals(['regression_cluster_name0', 'regression_cluster_name1'], baseline*.cluster_name)
+        assertEquals(['regression_cluster_id0', 'regression_cluster_id1'], baseline*.cluster_id)
+        assertEquals(['id0', 'id1'], baseline.collect { it.nodes[0].cloud_unique_id })
+        assertEquals(8041, baseline[1].nodes[0].http_port)
+        baseline[0].nodes.clear()
+        assertEquals(1, MultiClusterStateGuard.sharedBaseline(config)[0].nodes.size())
+    }
+
+    @Test
+    void acceptsOptionalBrpcPortAndSharedCloudId() {
+        def nodes = MultiClusterStateGuard.configuredBackends('be0:9050:8040:shared:8060,be1:9050:8040:shared')
+        assertEquals(['shared', 'shared'], nodes*.cloud_unique_id)
+        assertEquals(['be0', 'be1'], nodes*.ip)
+        assertEquals(2, MultiClusterStateGuard.sharedBaseline('be0:9050:8040:shared:8060,be1:9050:8040:shared:8060').size())
+    }
+
+    @Test
+    void initializerReplacesExistingFixtureAndRollsBackFailedReplacement() {
+        def state = fixture()
+        state.guard.run(true) {
+            assertEquals('', state.defaultGroup)
+            state.clusters = [cluster('canonical')]
+            state.defaultGroup = 'group_canonical'
+        }
+        assertEquals(['canonical'], state.clusters*.cluster_id)
+        assertEquals('group_canonical', state.defaultGroup)
+        state = fixture()
+        def original = copy(state.clusters)
+        assertThrows(IOException) {
+            state.guard.run(true) {
+                state.clusters = [cluster('canonical')]
+                state.defaultGroup = 'group_canonical'
+                throw new IOException('load failed')
+            }
+        }
+        assertEquals(MultiClusterStateGuard.topology(original), MultiClusterStateGuard.topology(state.clusters))
+        assertEquals('group_a', state.defaultGroup)
+    }
+
+    @Test
+    void sharedBaselineRejectsMissingMalformedOrDuplicateNodes() {
+        [null, '', 'be0:9050:8040:id0', 'be0:9050:8040:id0,invalid',
+         'be0:0:8040:id0,be1:9050:8040:id1', 'be0:9050:8040:id0:invalid,be1:9050:8040:id0',
+         'be0:9050:8040:id0,be0:9050:8041:id1'].each { config ->
+            assertThrows(IllegalArgumentException) { MultiClusterStateGuard.sharedBaseline(config) }
+        }
+    }
+
+    @Test
+    void successRestoresFullMetadataAndDefaultAfterTopology() {
+        def state = fixture()
+        def original = copy(state.clusters)
+        assertEquals(42, state.guard.run(false) {
+            state.clusters[0].nodes.remove(0)
+            state.clusters[0].properties.key = 'changed'
+            state.clusters << cluster('temporary')
+            state.defaultGroup = 'group_temporary'
+            state.instanceStatus = 'OVERDUE'
+            42
+        })
+        assertEquals(MultiClusterStateGuard.topology(original), MultiClusterStateGuard.topology(state.clusters))
+        assertEquals('group_a', state.defaultGroup)
+        assertEquals('NORMAL', state.instanceStatus)
+        assertEquals(['default', 'wait:', 'instance', 'drop:a', 'drop:temporary', 'add:a', 'wait:null', 'default', 'wait:group_a'], state.events)
+    }
+
+    @Test
+    void failureAfterDeletingGroupsRestoresSnapshotAndRethrowsOriginal() {
+        def state = fixture()
+        def original = copy(state.clusters)
+        def failure = new AssertionError('original failure')
+        assertSame(failure, assertThrows(AssertionError) {
+            state.guard.run(false) {
+                state.clusters.clear()
+                state.clusters << cluster('temporary')
+                state.defaultGroup = 'group_temporary'
+                throw failure
+            }
+        })
+        assertEquals(MultiClusterStateGuard.topology(original), MultiClusterStateGuard.topology(state.clusters))
+        assertEquals('group_a', state.defaultGroup)
+        assertEquals(0, failure.suppressed.length)
+    }
+
+    @Test
+    void unchangedTopologyIsNotRecreated() {
+        def state = fixture()
+        state.guard.run(false) {
+            state.clusters.reverse(true)
+            state.clusters[0].ctime = 123
+            state.clusters[0].nodes.reverse(true)
+        }
+        assertEquals(['default', 'wait:', 'instance', 'wait:null', 'default', 'wait:group_a'], state.events)
+    }
+
+    @Test
+    void cleanupFailuresAreSuppressedAndOtherGroupsAreStillRestored() {
+        def state = fixture()
+        def failure = new AssertionError('query failed')
+        state.failDrop = ['temporary']
+        state.failAdd = ['a']
+        assertSame(failure, assertThrows(AssertionError) {
+            state.guard.run(false) {
+                state.failWait = true
+                state.clusters = [cluster('temporary')]
+                throw failure
+            }
+        })
+        assertTrue(state.events.contains('add:b'))
+        assertTrue(state.events.contains('default'))
+        assertEquals(1, failure.suppressed.length)
+        assertEquals(4, failure.suppressed[0].suppressed.length)
+    }
+
+    @Test
+    void cleanupFailureFailsAnOtherwiseSuccessfulSuite() {
+        def state = fixture()
+        def failure = assertThrows(IllegalStateException) { state.guard.run(false) { state.failWait = true; 42 } }
+        assertEquals(2, failure.suppressed.length)
+        assertTrue(state.events.contains('default'))
+    }
+
+    @Test
+    void bootstrapKeepsSuccessfulInitialSetupButRollsBackFailure() {
+        def state = fixture([], '')
+        state.guard.run(true) { state.clusters << cluster('a'); state.defaultGroup = 'group_a' }
+        assertEquals(['a'], state.clusters*.cluster_id)
+        assertEquals('group_a', state.defaultGroup)
+
+        state = fixture([], '')
+        assertThrows(IOException) {
+            state.guard.run(true) { state.clusters << cluster('a'); throw new IOException('load failed') }
+        }
+        assertTrue(state.clusters.isEmpty())
+        assertEquals('', state.defaultGroup)
+
+        state = fixture([], '')
+        state.guard.run(false) { state.clusters << cluster('a') }
+        assertTrue(state.clusters.isEmpty())
+    }
+
+    @Test
+    void rejectsInvalidInitialStateBeforeRunningBody() {
+        def suspended = cluster('a'); suspended.cluster_status = 'SUSPENDED'
+        def decommissioning = cluster('a'); decommissioning.nodes[0].status = 'NODE_STATUS_DECOMMISSIONING'
+        [fixture([cluster('a')], 'missing'), fixture([suspended]), fixture([decommissioning]),
+         fixture([[type: 'VIRTUAL']])].each { state ->
+            assertThrows(IllegalStateException) { state.guard.run(false) { fail('body must not run') } }
+            assertTrue(state.events.isEmpty())
+        }
+    }
+
+    @Test
+    void feMustSeeExactNodeMappingAndAllHeartbeats() {
+        def expected = [cluster('a')]
+        def groups = [[cluster: 'group_a']]
+        def nodes = [[Host: '127.0.0.1', HeartbeatPort: 9050, Alive: 'true', Tag: JsonOutput.toJson([cloud_cluster_id: 'a', cloud_cluster_name: 'group_a',
+                        cloud_cluster_status: 'NORMAL', cloud_unique_id: 'be_a'])],
+                     [Host: '127.0.0.2', HeartbeatPort: 9051, Alive: true, Tag: [compute_group_id: 'a',
+                        compute_group_name: 'group_a', compute_group_status: 'NORMAL', cloud_unique_id: 'be2_a']]]
+        assertTrue(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, [], nodes))
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, groups, nodes.take(1)))
+        nodes[1].Tag.remove('compute_group_status')
+        assertTrue(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+        nodes[1].Tag.compute_group_status = 'NORMAL'
+        nodes[1].Alive = false
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+        nodes[1].Alive = true
+        nodes[1].HeartbeatPort = 9052
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+        nodes[1].HeartbeatPort = 9051
+        nodes[1].Tag.compute_group_status = 'SUSPENDED'
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+        nodes[1].Tag.compute_group_status = 'NORMAL'
+        nodes[1].SystemDecommissioned = true
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+        nodes[1].SystemDecommissioned = false
+        nodes[1].Tag.compute_group_id = 'wrong'
+        assertFalse(MultiClusterStateGuard.frontendMatches(expected, groups, nodes))
+    }
+}
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/MySqlClientTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/MySqlClientTest.groovy
new file mode 100644
index 00000000000..1444dc54ec7
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/MySqlClientTest.groovy
@@ -0,0 +1,96 @@
+// 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.regression.util
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+
+import java.nio.file.Files
+import java.nio.file.Path
+
+import static org.junit.jupiter.api.Assertions.*
+
+class MySqlClientTest {
+    @TempDir
+    Path tempDir
+
+    private File client(int exitCode) {
+        File script = tempDir.resolve('fake mysql client').toFile()
+        script.text = """#!/bin/sh
+file=\${1#--defaults-extra-file=}
+printf '%s\\n' "\$file"
+ls -l "\$file"
+printf '%s\\n' 'CONFIG'
+cat "\$file"
+printf '%s\\n' 'ARGS'
+printf '%s\\n' "\$@"
+printf '%s\\n' 'SQL'
+cat
+printf '%s\\n' 'DIR'
+pwd
+printf '%s' 'test diagnostic' >&2
+exit ${exitCode}
+"""
+        assertTrue(script.setExecutable(true))
+        return script
+    }
+
+    @Test
+    void passesOptionsAndSqlLiterallyAndCleansPrivateCredentials() {
+        File executable = client(0)
+        File directory = Files.createDirectory(tempDir.resolve('working directory')).toFile()
+        String sql = 'select "$HOME", "`id`", "\\n";\n'
+        ['', null, 'fake secret "\\\\#;\n\t密碼'].each { password ->
+            def result = MySqlClient.execute('root', password,
+                    ['--ssl-mode=VERIFY_CA', '--ssl-ca=/path with spaces/ca.pem'], sql, directory, executable.path)
+            assertEquals(0, result.exitCode)
+            assertEquals('test diagnostic\n', result.stderr)
+            def lines = result.stdout.readLines()
+            assertFalse(new File(lines[0]).exists())
+            assertTrue(lines[1].startsWith('-rw-------'))
+            String args = result.stdout.split('ARGS\n', 2)[1].split('SQL\n', 2)[0]
+            assertEquals(["--defaults-extra-file=${lines[0]}".toString(), '--ssl-mode=VERIFY_CA',
+                          '--ssl-ca=/path with spaces/ca.pem'], args.readLines())
+            assertTrue(result.stdout.contains('SQL\n' + sql))
+            assertTrue(result.stdout.endsWith('DIR\n' + directory.canonicalPath + '\n'))
+            String config = result.stdout.split('CONFIG\n', 2)[1].split('ARGS\n', 2)[0]
+            assertTrue(config.startsWith('[client]\nuser="root"\npassword="'))
+            if (!password) assertTrue(config.endsWith('password=""\n'))
+            else assertEquals(3, config.readLines().size())
+        }
+    }
+
+    @Test
+    void preservesNonRootUserAndFailureExitCodeAndStillCleansFile() {
+        def result = MySqlClient.execute('load_user', 'fake-secret', [], 'select 1;', null, client(7).path)
+        assertEquals(7, result.exitCode)
+        assertTrue(result.stdout.contains('user="load_user"'))
+        assertFalse(new File(result.stdout.readLines()[0]).exists())
+    }
+
+    @Test
+    void removesCredentialsWhenProcessCannotStart() {
+        def directory = new File(System.getProperty('java.io.tmpdir'))
+        def credentialFiles = { directory.listFiles().findAll { it.name.startsWith('doris-mysql-') }.collect { it.name }.toSet() }
+        def before = credentialFiles()
+        assertThrows(IOException) {
+            MySqlClient.execute('root', 'fake-secret', [], '', null, tempDir.resolve('missing-client').toString())
+        }
+        assertEquals(before, credentialFiles())
+    }
+}
diff --git a/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/RowsetMetaUtilsTest.groovy b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/RowsetMetaUtilsTest.groovy
new file mode 100644
index 00000000000..baa3984591d
--- /dev/null
+++ b/regression-test/framework/src/test/groovy/org/apache/doris/regression/util/RowsetMetaUtilsTest.groovy
@@ -0,0 +1,81 @@
+// 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.regression.util
+
+import org.junit.jupiter.api.Test
+
+import java.nio.charset.StandardCharsets
+
+import static org.junit.jupiter.api.Assertions.*
+
+class RowsetMetaUtilsTest {
+    @Test
+    void mergedAndUnsortedRowsetsCoverCommittedVersion() {
+        assertTrue(RowsetMetaUtils.coversVersion(
+                [rowsets: ['[32-61] 3 DATA', '[0-1] 0 DATA', '[2-31] 2 DATA']], 61))
+    }
+
+    @Test
+    void maximumVersionCannotHideGapOrMissingInitialRowset() {
+        assertFalse(RowsetMetaUtils.coversVersion([rowsets: ['[0-1]', '[3-100]']], 11))
+        assertFalse(RowsetMetaUtils.coversVersion([rowsets: ['[2-100]']], 11))
+        assertFalse(RowsetMetaUtils.coversVersion([rowsets: ['[0-1]']], 2))
+        assertFalse(RowsetMetaUtils.coversVersion([rowsets: []], 2))
+    }
+
+    @Test
+    void gapAfterTargetDoesNotInvalidateTargetCoverage() {
+        assertTrue(RowsetMetaUtils.coversVersion([rowsets: ['[0-1]', '[2-6]', '[8-9]']], 6))
+    }
+
+    @Test
+    void malformedMetadataFailsInsteadOfTimingOut() {
+        assertThrows(IllegalArgumentException) { RowsetMetaUtils.coversVersion([:], 2) }
+        assertThrows(IllegalArgumentException) {
+            RowsetMetaUtils.coversVersion([rowsets: ['not a rowset']], 2)
+        }
+        assertThrows(IllegalArgumentException) {
+            RowsetMetaUtils.coversVersion([rowsets: ['[5-2]']], 2)
+        }
+        assertThrows(IllegalArgumentException) { RowsetMetaUtils.coversVersion([rowsets: []], -1) }
+    }
+
+    @Test
+    void keyBoundsPreserveAllBytesAndDoNotMutateInput() {
+        byte[] bytes = [0, 2, 127, -128, -1] as byte[]
+        String encoded = Base64.encoder.encodeToString(bytes)
+        def input = [segments_key_bounds: [[min_key: encoded, max_key: encoded]],
+                     segments_key_bounds_truncated: true]
+        def decoded = RowsetMetaUtils.decodeKeyBounds(input)
+        assertArrayEquals(bytes, decoded.segments_key_bounds[0].min_key.getBytes(StandardCharsets.ISO_8859_1))
+        assertEquals(encoded, input.segments_key_bounds[0].min_key)
+        assertTrue(decoded.segments_key_bounds_truncated)
+        assertFalse(decoded.segments_key_bounds_aggregated)
+        assertFalse(input.containsKey('segments_key_bounds_aggregated'))
+    }
+
+    @Test
+    void missingOrInvalidKeyBoundsAreRejected() {
+        assertThrows(IllegalArgumentException) {
+            RowsetMetaUtils.decodeKeyBounds([segments_key_bounds: [[max_key: 'AA==']]])
+        }
+        assertThrows(IllegalArgumentException) {
+            RowsetMetaUtils.decodeKeyBounds([segments_key_bounds: [[min_key: '***', max_key: 'AA==']]])
+        }
+    }
+}
diff --git a/regression-test/pipeline/nonConcurrent/conf/regression-conf.groovy b/regression-test/pipeline/nonConcurrent/conf/regression-conf.groovy
index 66024db5649..9e514f98073 100644
--- a/regression-test/pipeline/nonConcurrent/conf/regression-conf.groovy
+++ b/regression-test/pipeline/nonConcurrent/conf/regression-conf.groovy
@@ -74,7 +74,7 @@ excludeDirectories = "000_the_start_sentinel_do_not_touch," + // keep this line
     "variant_github_events_new_p2," +
     "hdfs_vault_p2," +
     "nereids_p0/hbo," +
-    "cloud_p0/multi_cluster," +
+    "cloud_p0," +
     "zzz_the_end_sentinel_do_not_touch"// keep this line as the last line
 
 // for test csv with header
diff --git a/regression-test/plugins/cloud_multi_cluster_state.groovy b/regression-test/plugins/cloud_multi_cluster_state.groovy
new file mode 100644
index 00000000000..977410fc03e
--- /dev/null
+++ b/regression-test/plugins/cloud_multi_cluster_state.groovy
@@ -0,0 +1,136 @@
+// 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 groovy.json.JsonOutput
+import org.apache.doris.regression.Config
+import org.apache.doris.regression.suite.Suite
+import org.apache.doris.regression.util.MultiClusterStateGuard
+
+// Shared by the load fixture and cases that reuse or deliberately recreate its groups.
+Suite.metaClass.multiClusterBaseline = { ->
+    MultiClusterStateGuard.sharedBaseline(delegate.context.config.multiClusterBes)
+}
+
+Suite.metaClass.checkMultiClusterBaseline = { List<Map> baseline ->
+    def suite = delegate
+    suite.assertTrue(MultiClusterStateGuard.frontendMatches(baseline,
+            suite.sql_return_maparray('SHOW CLUSTERS'), suite.sql_return_maparray('SHOW BACKENDS')),
+            'Expected shared regression_cluster_name0/1 groups with their configured BEs alive; ' +
+                    'run the multi_cluster_s3_load/load fixture first and check prior suite cleanup')
+}
+
+// Only for serial legacy suites on the dedicated multiClusterInstance.
+// Docker/VCG/upgrade suites have their own cluster lifecycle.
+Suite.metaClass.withRestoredMultiClusterState = { boolean initializeBaseline, Closure action ->
+    def suite = delegate
+    def config = suite.context.config
+    def nodes = MultiClusterStateGuard.configuredBackends(config.multiClusterBes)
+    if (!config.multiClusterInstance) {
+        throw new IllegalStateException('Missing dedicated multi-cluster test instance/BE configuration')
+    }
+    def request = { String operation, Map payload ->
+        def result
+        suite.httpTest {
+            printResponse false
+            endpoint config.metaServiceHttpAddress
+            uri "/MetaService/http/${operation}?token=${config.metaServiceToken}" +
+                    "&instance_id=${java.net.URLEncoder.encode(config.multiClusterInstance, 'UTF-8')}"
+            body JsonOutput.toJson([instance_id: config.multiClusterInstance] + payload)
+            check { code, response ->
+                result = suite.parseJson(response)
+                suite.assertTrue(code == 200 && result.code == 'OK',
+                        "Multi-cluster ${operation} failed: ${result.code}")
+            }
+        }
+        result
+    }
+    def readInstanceStatus = { request('get_instance', [:]).result.status }
+    if (readInstanceStatus() != 'NORMAL') {
+        throw new IllegalStateException('Multi-cluster tests require a normal dedicated instance')
+    }
+    def fetchClusters = { request('get_cluster', [cloud_unique_id: nodes[0].cloud_unique_id]).result.cluster }
+    def frontends = suite.sql_return_maparray('SHOW FRONTENDS')
+    if (frontends.isEmpty() || frontends.any { !it.Alive.toString().toBoolean() }) {
+        throw new IllegalStateException('All configured frontends must be alive before changing cluster state')
+    }
+    def readDefault = {
+        def rows = suite.sql_return_maparray('SHOW PROPERTY')
+        def row = rows.find { it.Key == 'default_cloud_cluster' }
+        if (row == null) {
+            throw new IllegalStateException('SHOW PROPERTY did not return default_cloud_cluster')
+        }
+        row.Value?.toString() ?: ''
+    }
+    def guard = new MultiClusterStateGuard(
+        listClusters: fetchClusters,
+        restoreInstance: {
+            if (readInstanceStatus() != 'NORMAL') {
+                request('set_instance_status', [op: 'SET_NORMAL'])
+            }
+        },
+        dropCluster: { saved ->
+            request('drop_cluster', [cluster: [type: 'COMPUTE',
+                    cluster_name: saved.cluster_name, cluster_id: saved.cluster_id, nodes: []]])
+        },
+        addCluster: { saved -> request('add_cluster', [cluster: saved]) },
+        readDefault: readDefault,
+        setDefault: { String saved ->
+            def literal = saved.replace('\\', '\\\\').replace("'", "''")
+            suite.sql("SET PROPERTY 'default_cloud_cluster' = '${literal}'")
+        },
+        awaitState: { List<Map> expected, String expectedDefault ->
+            long deadline = System.nanoTime() + 120_000_000_000L
+            Throwable lastError = null
+            while (System.nanoTime() < deadline) {
+                try {
+                    if (readInstanceStatus() != 'NORMAL') {
+                        throw new IllegalStateException('MS instance status has not returned to normal')
+                    }
+                    def actual = fetchClusters().findAll { it.type == 'COMPUTE' }
+                    if (MultiClusterStateGuard.topology(actual) != MultiClusterStateGuard.topology(expected)) {
+                        throw new IllegalStateException('MS compute groups do not match the saved topology')
+                    }
+                    for (def frontend : frontends) {
+                        boolean tls = config.otherConfigs.get('enableTLS')?.toString()?.toBoolean() ?: false
+                        def url = tls ? Config.buildUrlWithDb(frontend.Host as String, frontend.QueryPort as int,
+                                suite.context.dbName, config.otherConfigs.get('keyStorePath')?.toString(),
+                                config.otherConfigs.get('keyStorePassword')?.toString(),
+                                config.otherConfigs.get('trustStorePath')?.toString(),
+                                config.otherConfigs.get('trustStorePassword')?.toString()) :
+                                Config.buildUrlWithDb(frontend.Host as String, frontend.QueryPort as int, suite.context.dbName)
+                        url += '&connectTimeout=5000&socketTimeout=5000'
+                        suite.connect(config.jdbcUser, config.jdbcPassword, url) {
+                            def visibleGroups = suite.sql_return_maparray('SHOW CLUSTERS')
+                            def visibleNodes = suite.sql_return_maparray('SHOW BACKENDS')
+                            if (!MultiClusterStateGuard.frontendMatches(expected, visibleGroups, visibleNodes)) {
+                                throw new IllegalStateException("FE ${frontend.Host} has not restored groups/nodes/heartbeats")
+                            }
+                            if (expectedDefault != null && readDefault() != expectedDefault) {
+                                throw new IllegalStateException("FE ${frontend.Host} has not restored the user default group")
+                            }
+                        }
+                    }
+                    return
+                } catch (Exception e) {
+                    lastError = e
+                }
+                Thread.sleep(500)
+            }
+            throw new IllegalStateException('Timed out restoring multi-cluster state on MS and all frontends', lastError)
+        }
+    )
+    guard.run(initializeBaseline, action)
+}
diff --git a/regression-test/plugins/plugin_compaction.groovy b/regression-test/plugins/plugin_compaction.groovy
index 8cf6b1f0f87..b3e600ad958 100644
--- a/regression-test/plugins/plugin_compaction.groovy
+++ b/regression-test/plugins/plugin_compaction.groovy
@@ -19,45 +19,57 @@ import org.apache.doris.regression.suite.Suite
 import java.util.concurrent.TimeUnit
 import org.awaitility.Awaitility;
 
-Suite.metaClass.be_get_compaction_status{ String ip, String port, String tablet_id  /* param */->
-    return curl("GET", String.format("http://%s:%s/api/compaction/run_status?tablet_id=%s", ip, port, tablet_id))
+Suite.metaClass.be_get_compaction_status{ String ip, String port, String tablet_id,
+                                          Integer timeout_sec = 10, Integer max_retries = 10  /* param */->
+    return delegate.curl("GET", String.format("http://%s:%s/api/compaction/run_status?tablet_id=%s", ip, port, tablet_id),
+            null, timeout_sec, delegate.context.config.feHttpUser,
+            delegate.context.config.feHttpPassword, max_retries)
 }
 
 Suite.metaClass.be_get_overall_compaction_status{ String ip, String port  /* param */->
-    return curl("GET", String.format("http://%s:%s/api/compaction/run_status", ip, port))
+    return delegate.curl("GET", String.format("http://%s:%s/api/compaction/run_status", ip, port),
+            null, 10, delegate.context.config.feHttpUser, delegate.context.config.feHttpPassword, 10)
 }
 
-Suite.metaClass.be_show_tablet_status{ String ip, String port, String tablet_id  /* param */->
-    return curl("GET", String.format("http://%s:%s/api/compaction/show?tablet_id=%s", ip, port, tablet_id))
+Suite.metaClass.be_show_tablet_status{ String ip, String port, String tablet_id,
+                                      Integer timeout_sec = 10, Integer max_retries = 10  /* param */->
+    return delegate.curl("GET", String.format("http://%s:%s/api/compaction/show?tablet_id=%s", ip, port, tablet_id),
+            null, timeout_sec, delegate.context.config.feHttpUser,
+            delegate.context.config.feHttpPassword, max_retries)
 }
 
 Suite.metaClass._be_run_compaction = { String ip, String port, String tablet_id, String compact_type ->
-    return curl("POST", String.format("http://%s:%s/api/compaction/run?tablet_id=%s&compact_type=%s",
-            ip, port, tablet_id, compact_type))
+    return delegate.curl("POST", String.format("http://%s:%s/api/compaction/run?tablet_id=%s&compact_type=%s",
+            ip, port, tablet_id, compact_type), null, 10, delegate.context.config.feHttpUser,
+            delegate.context.config.feHttpPassword, 10)
 }
 
 Suite.metaClass.be_run_base_compaction = { String ip, String port, String tablet_id  /* param */->
-    return _be_run_compaction(ip, port, tablet_id, "base")
+    return delegate._be_run_compaction(ip, port, tablet_id, "base")
 }
 
 logger.info("Added 'be_run_base_compaction' function to Suite")
 
 Suite.metaClass.be_run_cumulative_compaction = { String ip, String port, String tablet_id  /* param */->
-    return _be_run_compaction(ip, port, tablet_id, "cumulative")
+    return delegate._be_run_compaction(ip, port, tablet_id, "cumulative")
 }
 
 logger.info("Added 'be_run_cumulative_compaction' function to Suite")
 
 Suite.metaClass.be_run_full_compaction = { String ip, String port, String tablet_id  /* param */->
-    return _be_run_compaction(ip, port, tablet_id, "full")
+    return delegate._be_run_compaction(ip, port, tablet_id, "full")
 }
 
 Suite.metaClass.be_run_full_compaction_by_table_id = { String ip, String port, String table_id  /* param */->
-    return curl("POST", String.format("http://%s:%s/api/compaction/run?table_id=%s&compact_type=full", ip, port, table_id))
+    return delegate.curl("POST", String.format("http://%s:%s/api/compaction/run?table_id=%s&compact_type=full",
+            ip, port, table_id), null, 10, delegate.context.config.feHttpUser,
+            delegate.context.config.feHttpPassword, 10)
 }
 
 logger.info("Added 'be_run_full_compaction' function to Suite")
-Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compaction_type, int timeout_seconds=300, String[] ignored_errors=[] ->
+Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compaction_type, int timeout_seconds=300,
+                                                String[] ignored_errors=[], Collection tablet_ids=[],
+                                                String[] retryable_errors=[] ->
     if (!(compaction_type in ["cumulative", "base", "full"])) {
         throw new IllegalArgumentException("invalid compaction type: ${compaction_type}, supported types: cumulative, base, full")
     }
@@ -66,6 +78,13 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
     def backendId_to_backendHttpPort = [:]
     getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort);
     def tablets = sql_return_maparray """show tablets from ${table_name}"""
+    if (!tablet_ids.isEmpty()) {
+        def requestedTabletIds = tablet_ids.collect { "${it}" }.toSet()
+        tablets = tablets.findAll { requestedTabletIds.contains("${it.TabletId}") }
+        def foundTabletIds = tablets.collect { "${it.TabletId}" }.toSet()
+        assert foundTabletIds == requestedTabletIds:
+                "Unable to find all requested tablets for ${table_name}, requested: ${requestedTabletIds}, found: ${foundTabletIds}"
+    }
     def exit_code, stdout, stderr
 
     def auto_compaction_disabled = sql("show create table ${table_name}")[0][1].contains('"disable_auto_compaction" = "true"')
@@ -76,7 +95,7 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
     for (tablet in tablets) {
         def be_host = backendId_to_backendIP["${tablet.BackendId}"]
         def be_port = backendId_to_backendHttpPort["${tablet.BackendId}"]
-        (exit_code, stdout, stderr) = be_show_tablet_status(be_host, be_port, tablet.TabletId)
+        (exit_code, stdout, stderr) = delegate.be_show_tablet_status(be_host, be_port, tablet.TabletId)
         assert exit_code == 0: "get tablet status failed, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}"
 
         def tabletStatus = parseJson(stdout.trim())
@@ -87,23 +106,38 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
     for (tablet in tablets) {
         def be_host = backendId_to_backendIP["${tablet.BackendId}"]
         def be_port = backendId_to_backendHttpPort["${tablet.BackendId}"]
-        switch (compaction_type) {
-            case "cumulative":
-                (exit_code, stdout, stderr) = be_run_cumulative_compaction(be_host, be_port, tablet.TabletId)
-                break
-            case "base":
-                (exit_code, stdout, stderr) = be_run_base_compaction(be_host, be_port, tablet.TabletId)
-                break
-            case "full":
-                (exit_code, stdout, stderr) = be_run_full_compaction(be_host, be_port, tablet.TabletId)
-                break
-        }
-        assert exit_code == 0: "trigger compaction failed, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}"
-        def trigger_status = parseJson(stdout.trim())
-        if (trigger_status.status.toLowerCase() != "success") {
+        long triggerDeadline = System.currentTimeMillis() + timeout_seconds * 1000L
+        while (true) {
+            switch (compaction_type) {
+                case "cumulative":
+                    (exit_code, stdout, stderr) = delegate.be_run_cumulative_compaction(be_host, be_port, tablet.TabletId)
+                    break
+                case "base":
+                    (exit_code, stdout, stderr) = delegate.be_run_base_compaction(be_host, be_port, tablet.TabletId)
+                    break
+                case "full":
+                    (exit_code, stdout, stderr) = delegate.be_run_full_compaction(be_host, be_port, tablet.TabletId)
+                    break
+            }
+            assert exit_code == 0: "trigger compaction failed, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}"
+            def trigger_status = parseJson(stdout.trim())
             def status_lower = trigger_status.status.toLowerCase()
-            if (status_lower == "already_exist") {
-                triggered_tablets.add(tablet) // compaction already in queue, treat it as successfully triggered
+            if (status_lower == "success" || status_lower == "already_exist") {
+                triggered_tablets.add(tablet)
+                break
+            } else if (retryable_errors.any { error -> status_lower.contains(error.toLowerCase()) }) {
+                assert System.currentTimeMillis() < triggerDeadline:
+                        "retry trigger compaction timeout, be host: ${be_host}, tablet id: ${tablet.TabletId}, status: ${trigger_status.status}"
+                logger.info("retry transient compaction trigger failure, be host: ${be_host}, tablet id: ${tablet.TabletId}, status: ${trigger_status.status}")
+                // The transient trigger failure updates the tablet's failure timestamp.
+                // Refresh the baseline so it cannot be mistaken for completion of the
+                // later successful asynchronous trigger.
+                (exit_code, stdout, stderr) = delegate.be_show_tablet_status(be_host, be_port, tablet.TabletId)
+                assert exit_code == 0:
+                        "refresh tablet status failed, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}"
+                be_tablet_compaction_status.put("${be_host}-${tablet.TabletId}", parseJson(stdout.trim()))
+                sleep(1000)
+                continue
             } else if (!auto_compaction_disabled) {
                 // ignore the error if auto compaction enabled
             } else if (status_lower.contains("e-2000") || status_lower.contains("e-2010")
@@ -119,8 +153,7 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
             } else {
                 throw new Exception("trigger compaction failed, be host: ${be_host}, tablet id: ${tablet.TabletId}, status: ${trigger_status.status}")
             }
-        } else {
-            triggered_tablets.add(tablet)
+            break
         }
     }
 
@@ -136,24 +169,34 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
             return null
         }
     }
-    Awaitility.await().atMost(timeout_seconds, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).until(() -> {
+    // Parallel suites own their worker failures; do not capture uncaught exceptions from other suites.
+    Awaitility.await().dontCatchUncaughtExceptions().atMost(timeout_seconds, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).until(() -> {
         for (tablet in triggered_tablets) {
             def be_host = backendId_to_backendIP["${tablet.BackendId}"]
             def be_port = backendId_to_backendHttpPort["${tablet.BackendId}"]
 
-            (exit_code, stdout, stderr) = be_get_compaction_status(be_host, be_port, tablet.TabletId)
-            assert exit_code == 0: "get compaction status failed, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}"
+            // Awaitility owns the retry loop. Keep each HTTP probe bounded so an
+            // inner curl retry cannot consume the entire compaction timeout.
+            (exit_code, stdout, stderr) = delegate.be_get_compaction_status(be_host, be_port, tablet.TabletId, 5, 1)
+            if (exit_code != 0) {
+                logger.warn("get compaction status failed, will retry, be host: ${be_host}, tablet id: ${tablet.TabletId}, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}")
+                return false
+            }
             def compactionStatus = parseJson(stdout.trim())
             assert compactionStatus.status.toLowerCase() == "success": "compaction failed, be host: ${be_host}, tablet id: ${tablet.TabletId}, status: ${compactionStatus.status}"
             // running is true means compaction is still running
             running = compactionStatus.run_status
 
             if (!is_time_series_compaction) {
-                (exit_code, stdout, stderr) = be_show_tablet_status(be_host, be_port, tablet.TabletId)
-                assert exit_code == 0: "get tablet status failed, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}"
+                (exit_code, stdout, stderr) = delegate.be_show_tablet_status(be_host, be_port, tablet.TabletId, 5, 1)
+                if (exit_code != 0) {
+                    logger.warn("get tablet status failed, will retry, be host: ${be_host}, tablet id: ${tablet.TabletId}, exit code: ${exit_code}, stdout: ${stdout}, stderr: ${stderr}")
+                    return false
+                }
                 def tabletStatus = parseJson(stdout.trim())
                 def oldStatus = be_tablet_compaction_status.get("${be_host}-${tablet.TabletId}")
-                // last compaction success/failure time isn't updated, indicates compaction is not started(so we treat it as running and wait)
+                // The HTTP trigger is asynchronous in cloud mode. run_status may already be
+                // false at the first poll, so also require a completion marker for this run.
                 def handedOffToBaseCompactionAfterDeleteVersion = false
                 def completedByBaseCompactionAfterDeleteVersion = false
                 if (compaction_type == "cumulative") {
@@ -163,9 +206,6 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
                     def baseSuccessTimeChanged = oldStatus["last base success time"] != tabletStatus["last base success time"]
                     def cumulativeSuccessTimeChanged =
                             oldStatus["last cumulative success time"] != tabletStatus["last cumulative success time"]
-                    // E-2010 advances the cumulative point and lets base compaction handle delete-version rowsets.
-                    // In some timing windows, base success is already visible in the cached old status while
-                    // cumulative success advances later, so accept either success signal but not failure time alone.
                     handedOffToBaseCompactionAfterDeleteVersion = lastCumulativeStatus.contains("e-2010") &&
                             oldCumulativePoint != null && newCumulativePoint != null &&
                             newCumulativePoint > oldCumulativePoint
@@ -173,19 +213,21 @@ Suite.metaClass.trigger_and_wait_compaction = { String table_name, String compac
                             handedOffToBaseCompactionAfterDeleteVersion &&
                             (baseSuccessTimeChanged || cumulativeSuccessTimeChanged)
                 }
-                def success_time_unchanged = (oldStatus["last ${compaction_type} success time"] == tabletStatus["last ${compaction_type} success time"])
-                def failure_time_unchanged = (oldStatus["last ${compaction_type} failure time"] == tabletStatus["last ${compaction_type} failure time"])
-                def currentCompactionTimestampChanged = !success_time_unchanged || !failure_time_unchanged
+                def successTimeUnchanged = oldStatus["last ${compaction_type} success time"] ==
+                        tabletStatus["last ${compaction_type} success time"]
+                def failureTimeUnchanged = oldStatus["last ${compaction_type} failure time"] ==
+                        tabletStatus["last ${compaction_type} failure time"]
+                def completionTimestampChanged = !successTimeUnchanged || !failureTimeUnchanged
                 def compactionFinished = completedByBaseCompactionAfterDeleteVersion ||
-                        (!handedOffToBaseCompactionAfterDeleteVersion && currentCompactionTimestampChanged)
+                        (!handedOffToBaseCompactionAfterDeleteVersion && completionTimestampChanged)
                 running = running || !compactionFinished
                 if (running) {
                     logger.info("compaction is still running, be host: ${be_host}, tablet id: ${tablet.TabletId}, run status: ${compactionStatus.run_status}, old status: ${oldStatus}, new status: ${tabletStatus}")
                     return false
                 }
             } else {
-                // time series compaction sometimes doesn't update compaction success time
-                // so we solely check run_status for it
+                // Time series compaction sometimes doesn't update compaction success time,
+                // so solely check run_status for it.
                 if (running) {
                     logger.info("compaction is still running, be host: ${be_host}, tablet id: ${tablet.TabletId}")
                     return false
diff --git a/regression-test/plugins/plugin_curl_requester.groovy b/regression-test/plugins/plugin_curl_requester.groovy
index a0a813fb800..e16945616db 100644
--- a/regression-test/plugins/plugin_curl_requester.groovy
+++ b/regression-test/plugins/plugin_curl_requester.groovy
@@ -146,7 +146,7 @@ Suite.metaClass.http_client = { String method, String url /* param */ ->
 
 logger.info("Added 'http_client' function to Suite")
 
-Suite.metaClass.curl = { String method, String url, String body = null, Integer timeoutSec = 10, String user = "", String pwd = ""->
+Suite.metaClass.curl = { String method, String url, String body = null, Integer timeoutSec = 10, String user = "", String pwd = "", Integer maxRetries = 10 ->
     Suite suite = delegate as Suite
     if (method != "GET" && method != "POST") {
         throw new Exception(String.format("invalid curl method: %s", method))
@@ -155,7 +155,6 @@ Suite.metaClass.curl = { String method, String url, String body = null, Integer
         throw new Exception("invalid curl url, blank")
     }
 
-    Integer maxRetries = 10; // Maximum number of retries
     Integer retryCount = 0; // Current retry count
     Integer sleepTime = 5000; // Sleep time in milliseconds
 
diff --git a/regression-test/plugins/plugin_index_change.groovy b/regression-test/plugins/plugin_index_change.groovy
index eb47798f274..ad7edd1a12c 100644
--- a/regression-test/plugins/plugin_index_change.groovy
+++ b/regression-test/plugins/plugin_index_change.groovy
@@ -16,6 +16,7 @@
 // under the License.
 
 import org.apache.doris.regression.suite.Suite
+import org.apache.doris.regression.util.Http
 
 import groovy.json.JsonSlurper
 import java.util.regex.Matcher;
@@ -75,7 +76,7 @@ Suite.metaClass.wait_for_last_schema_change_finish = {table_name, OpTimeout ->
 
 Suite.metaClass.http_get = { url ->
     def dst = 'http://' + context.config.feHttpAddress
-    def conn = new URL(dst + url).openConnection()
+    def conn = Http.openConnection(dst + url)
     conn.setRequestMethod("GET")
     def encoding = Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" +
             (context.config.feHttpPassword == null ? "" : context.config.feHttpPassword)).getBytes("UTF-8"))
@@ -137,4 +138,4 @@ Suite.metaClass.check_bf_index_filter_rows = { sql, expectedRowsBfFiltered ->
         log.info("filter number:{}", number)
         assertEquals(expectedRowsBfFiltered, number)
     }
-}
\ No newline at end of file
+}
diff --git a/regression-test/suites/account_p0/test_auth_show.groovy b/regression-test/suites/account_p0/test_auth_show.groovy
index d9d4f3fa7f1..80050aa06c8 100644
--- a/regression-test/suites/account_p0/test_auth_show.groovy
+++ b/regression-test/suites/account_p0/test_auth_show.groovy
@@ -45,7 +45,7 @@ suite("test_auth_show", "account") {
 
     // With select priv for table, should be able to see db
     sql """GRANT SELECT_PRIV ON ${dbName}.${tableName} TO ${user}"""
-    def result1 = connect(user, "${pwd}", url) {
+    def result1 = connectToDoris(user, "${pwd}", url) {
         sql """show databases like '${dbName}'"""
     }
     assertEquals(result1.size(), 1)
@@ -53,7 +53,7 @@ suite("test_auth_show", "account") {
 
     // With show_view priv for table, should be able to see db
     sql """GRANT SHOW_VIEW_PRIV ON ${dbName}.${tableName} TO ${user}"""
-    def result2 = connect(user, "${pwd}", url) {
+    def result2 = connectToDoris(user, "${pwd}", url) {
         sql """show databases like '${dbName}'"""
     }
     assertEquals(result2.size(), 1)
diff --git a/regression-test/suites/account_p0/test_grant_priv.groovy b/regression-test/suites/account_p0/test_grant_priv.groovy
index 21d26dd30d8..f23134bb214 100644
--- a/regression-test/suites/account_p0/test_grant_priv.groovy
+++ b/regression-test/suites/account_p0/test_grant_priv.groovy
@@ -40,7 +40,7 @@ suite("test_grant_priv") {
 
     // test only have select_priv, can not grant to other user
     sql """grant select_priv on ${dbName}.* to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         try {
             sql """grant select_priv on ${dbName}.* to ${user2}"""
             Assert.fail("can not grant to other user");
@@ -51,7 +51,7 @@ suite("test_grant_priv") {
 
     // test both have select_priv and grant_priv , can grant to other user
     sql """grant grant_priv on ${dbName}.* to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         try {
             sql """grant select_priv on ${dbName}.* to ${user2}"""
         } catch (Exception e) {
@@ -96,7 +96,7 @@ suite("test_grant_priv") {
     sql """grant select_priv on *.*.* to role ${role2}"""
     sql """grant '${role1}' to ${user1}"""
     // test only have role1 can not grant
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         test {
                  sql """grant select_priv on *.*.* to ${user2}"""
                   exception "denied"
@@ -105,7 +105,7 @@ suite("test_grant_priv") {
     sql """revoke '${role1}' from ${user1}"""
     sql """grant '${role2}' to ${user1}"""
     // test only have role2 can not grant
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         test {
                   sql """grant select_priv on *.*.* to ${user2}"""
                   exception "denied"
@@ -113,7 +113,7 @@ suite("test_grant_priv") {
     }
     // test both have role1 and role2 can grant to other
     sql """grant '${role1}' to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
             sql """grant select_priv on *.*.* to ${user2}"""
         }
     sql """drop user if exists ${user1}"""
diff --git a/regression-test/suites/account_p0/test_grant_priv_resource.groovy b/regression-test/suites/account_p0/test_grant_priv_resource.groovy
index c8eea500a07..a9c10da6ef7 100644
--- a/regression-test/suites/account_p0/test_grant_priv_resource.groovy
+++ b/regression-test/suites/account_p0/test_grant_priv_resource.groovy
@@ -33,7 +33,7 @@ suite("test_grant_priv_resource") {
 
     // test only have USAGE_PRIV, can not grant to other user
     sql """grant USAGE_PRIV on RESOURCE ${resource1} to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         try {
             sql """grant USAGE_PRIV on RESOURCE ${resource1} to ${user2}"""
             Assert.fail("can not grant to other user");
@@ -44,7 +44,7 @@ suite("test_grant_priv_resource") {
 
     // test both have USAGE_PRIV and grant_priv , can grant to other user
     sql """grant grant_priv on RESOURCE * to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         try {
             sql """grant USAGE_PRIV on RESOURCE ${resource1} to ${user2}"""
         } catch (Exception e) {
diff --git a/regression-test/suites/account_p0/test_grant_priv_workload.groovy b/regression-test/suites/account_p0/test_grant_priv_workload.groovy
index 8fa9f901a3e..f0f831a55cb 100644
--- a/regression-test/suites/account_p0/test_grant_priv_workload.groovy
+++ b/regression-test/suites/account_p0/test_grant_priv_workload.groovy
@@ -45,7 +45,7 @@ suite("test_grant_priv_workload") {
 
     // test only have USAGE_PRIV, can not grant to other user
     sql """grant USAGE_PRIV on WORKLOAD GROUP ${workload1} to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         try {
             sql """grant USAGE_PRIV on WORKLOAD GROUP ${workload1} to ${user2}"""
             Assert.fail("can not grant to other user");
@@ -56,7 +56,7 @@ suite("test_grant_priv_workload") {
 
     // test both have USAGE_PRIV and grant_priv , can grant to other user
     sql """grant grant_priv on WORKLOAD GROUP ${workload1} to ${user1}"""
-    connect(user1, "${pwd}", url) {
+    connectToDoris(user1, "${pwd}", url) {
         try {
            sql """grant USAGE_PRIV on WORKLOAD GROUP ${workload1} to ${user2}"""
         } catch (Exception e) {
diff --git a/regression-test/suites/account_p0/test_nereids_authentication.groovy b/regression-test/suites/account_p0/test_nereids_authentication.groovy
index 4b8c644495c..70648ee27b4 100644
--- a/regression-test/suites/account_p0/test_nereids_authentication.groovy
+++ b/regression-test/suites/account_p0/test_nereids_authentication.groovy
@@ -54,26 +54,26 @@ suite("test_nereids_authentication", "query") {
     
     def tokens = context.config.jdbcUrl.split('/')
     def url=tokens[0] + "//" + tokens[2] + "/" + dbName + "?"
-    def result = connect(user, 'Doris_123456', url) {
+    def result = connectToDoris(user, 'Doris_123456', url) {
         sql "SELECT * FROM ${tableName1}"
     }
     assertEquals(result.size(), 0)
 
-    connect(user, 'Doris_123456', url) {
+    connectToDoris(user, 'Doris_123456', url) {
         test {
             sql "SELECT * FROM ${tableName2}"
             exception "denied"
         }
     }
 
-    connect(user, 'Doris_123456', url) {
+    connectToDoris(user, 'Doris_123456', url) {
         test {
             sql "SELECT count(*) FROM ${tableName2}"
             exception "denied"
         }
     }
 
-    connect(user, 'Doris_123456', url) {
+    connectToDoris(user, 'Doris_123456', url) {
         test {
             sql "SELECT * FROM ${tableName1}, ${tableName2} WHERE ${tableName1}.`key` = ${tableName2}.`key`"
             exception "denied"
@@ -81,14 +81,14 @@ suite("test_nereids_authentication", "query") {
     }
 
     sql "GRANT SELECT_PRIV ON internal.${dbName}.${tableName2} TO ${user}"
-    connect(user, 'Doris_123456', url) {
+    connectToDoris(user, 'Doris_123456', url) {
         sql "SELECT * FROM ${tableName2}"
     }
     assertEquals(result.size(), 0)
-    connect(user, 'Doris_123456', url) {
+    connectToDoris(user, 'Doris_123456', url) {
         sql "SELECT count(*) FROM ${tableName2}"
     }
-    connect(user, 'Doris_123456', url) {
+    connectToDoris(user, 'Doris_123456', url) {
         sql "SELECT * FROM ${tableName1}, ${tableName2} WHERE ${tableName1}.`key` = ${tableName2}.`key`"
     }
     assertEquals(result.size(), 0)
diff --git a/regression-test/suites/account_p0/test_nereids_row_policy.groovy b/regression-test/suites/account_p0/test_nereids_row_policy.groovy
index 4463a41c90b..0ed3e8dc9b5 100644
--- a/regression-test/suites/account_p0/test_nereids_row_policy.groovy
+++ b/regression-test/suites/account_p0/test_nereids_row_policy.groovy
@@ -32,10 +32,10 @@ suite("test_nereids_row_policy") {
     }
 
     def assertQueryResult = { size ->
-        def result = connect(user, '123abc!@#', url) {
+        def result = connectToDoris(user, '123abc!@#', url) {
             sql "SELECT * FROM ${tableName}"
         }
-        connect(user, '123abc!@#', url) {
+        connectToDoris(user, '123abc!@#', url) {
             test {
                 sql "SELECT * FROM ${viewName}"
                 exception "does not have privilege for"
diff --git a/regression-test/suites/account_p0/test_set_password.groovy b/regression-test/suites/account_p0/test_set_password.groovy
index 65f08d5f40e..8ef1bbc2aa3 100644
--- a/regression-test/suites/account_p0/test_set_password.groovy
+++ b/regression-test/suites/account_p0/test_set_password.groovy
@@ -38,7 +38,7 @@ suite("test_set_password", "account") {
     def tokens = context.config.jdbcUrl.split('/')
     def url = tokens[0] + "//" + tokens[2] + "/" + "information_schema" + "?"
 
-    connect(user, password1, url) {
+    connectToDoris(user, password1, url) {
         def result = sql "SELECT 1"
         assertEquals(1, result[0][0])
     }
@@ -48,14 +48,14 @@ suite("test_set_password", "account") {
     sql "SET PASSWORD FOR '${user}'@'%' = PASSWORD('${password2}')"
 
     // verify login with new password
-    connect(user, password2, url) {
+    connectToDoris(user, password2, url) {
         def result = sql "SELECT 1"
         assertEquals(1, result[0][0])
     }
 
     // verify old password no longer works
     try {
-        connect(user, password1, url) {
+        connectToDoris(user, password1, url) {
             sql "SELECT 1"
         }
         assertTrue(false, "Old password should not work after SET PASSWORD")
diff --git a/regression-test/suites/account_p0/test_view_row_policy.groovy b/regression-test/suites/account_p0/test_view_row_policy.groovy
index 6a7b3d02109..45e5b70fcfa 100644
--- a/regression-test/suites/account_p0/test_view_row_policy.groovy
+++ b/regression-test/suites/account_p0/test_view_row_policy.groovy
@@ -65,7 +65,7 @@ suite("test_view_row_policy") {
     }
 
     // no policy
-    connect(user, "${pwd}", url) {
+    connectToDoris(user, "${pwd}", url) {
         order_qt_no_policy "SELECT * FROM ${viewName}"
     }
 
@@ -74,7 +74,7 @@ suite("test_view_row_policy") {
         CREATE ROW POLICY IF NOT EXISTS ${tablePolcyName} ON ${dbName}.${tableName}
         AS RESTRICTIVE TO ${user} USING (k=1)
     """
-    connect(user, "${pwd}", url) {
+    connectToDoris(user, "${pwd}", url) {
         order_qt_table_policy "SELECT * FROM ${viewName}"
     }
 
@@ -83,7 +83,7 @@ suite("test_view_row_policy") {
         CREATE ROW POLICY IF NOT EXISTS ${viewPolcyName} ON ${dbName}.${viewName}
         AS RESTRICTIVE TO ${user} USING (v=2)
     """
-    connect(user, "${pwd}", url) {
+    connectToDoris(user, "${pwd}", url) {
         order_qt_view_policy "SELECT * FROM ${viewName}"
     }
 
diff --git a/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy b/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy
index b4f0e29085b..92e9fa56d0e 100644
--- a/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy
+++ b/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy
@@ -16,27 +16,16 @@
 // under the License.
 
 import groovy.json.JsonSlurper
+import org.apache.doris.regression.util.Http
 
 def getProfileList = {
-    def dst = "http://" + context.config.feHttpAddress
-    def conn = new URL(dst + "/rest/v1/query_profile").openConnection()
-    conn.setRequestMethod("GET")
-    def encoding = Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" +
-            (context.config.feHttpPassword == null ? "" : context.config.feHttpPassword))
-            .getBytes("UTF-8"))
-    conn.setRequestProperty("Authorization", "Basic ${encoding}")
-    return conn.getInputStream().getText()
+    return Http.GET("http://${context.config.feHttpAddress}/rest/v1/query_profile", false, false,
+            context.config.feHttpUser, context.config.feHttpPassword)
 }
 
 def getProfile = { id ->
-    def dst = "http://" + context.config.feHttpAddress
-    def conn = new URL(dst + "/api/profile/text/?query_id=$id").openConnection()
-    conn.setRequestMethod("GET")
-    def encoding = Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" +
-            (context.config.feHttpPassword == null ? "" : context.config.feHttpPassword))
-            .getBytes("UTF-8"))
-    conn.setRequestProperty("Authorization", "Basic ${encoding}")
-    return conn.getInputStream().getText()
+    return Http.GET("http://${context.config.feHttpAddress}/api/profile/text/?query_id=$id", false, false,
+            context.config.feHttpUser, context.config.feHttpPassword)
 }
 
 def extractCounterValue = { String profileText, String counterName ->
diff --git a/regression-test/suites/arrow_flight_sql_p0/test_auth_remote_ip.groovy b/regression-test/suites/arrow_flight_sql_p0/test_auth_remote_ip.groovy
index 1d9f02260da..707dff1e1bf 100644
--- a/regression-test/suites/arrow_flight_sql_p0/test_auth_remote_ip.groovy
+++ b/regression-test/suites/arrow_flight_sql_p0/test_auth_remote_ip.groovy
@@ -57,8 +57,7 @@ suite("test_auth_remote_ip", "arrow_flight_sql") {
         Class.forName("org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver")
         String arrowFlightSqlHost = context.config.otherConfigs.get("extArrowFlightSqlHost")
         String arrowFlightSqlPort = context.config.otherConfigs.get("extArrowFlightSqlPort")
-        String arrowFlightSqlUrl = "jdbc:arrow-flight-sql://${arrowFlightSqlHost}:${arrowFlightSqlPort}" +
-                "/?useServerPrepStmts=false&useSSL=false&useEncryption=false"
+        String arrowFlightSqlUrl = context.config.getArrowFlightSqlJdbcUrl(arrowFlightSqlHost, arrowFlightSqlPort)
 
         Connection conn = DriverManager.getConnection(arrowFlightSqlUrl, user, password)
         try {
diff --git a/regression-test/suites/audit/test_audit_log_queue_time.groovy b/regression-test/suites/audit/test_audit_log_queue_time.groovy
index e40d1688c42..e4cc20e9c52 100644
--- a/regression-test/suites/audit/test_audit_log_queue_time.groovy
+++ b/regression-test/suites/audit/test_audit_log_queue_time.groovy
@@ -17,6 +17,7 @@
 
 suite("test_audit_log_queue_time", "nonConcurrent") {
     // Check admin privilege
+    def originalAuditPlugin = sql("show global variables like 'enable_audit_plugin'")[0][1]
     try {
         sql "set global enable_audit_plugin = true"
     } catch (Exception e) {
@@ -27,103 +28,148 @@ suite("test_audit_log_queue_time", "nonConcurrent") {
 
     def tableName = "audit_queue_time_test"
     def wgName = "test_queue_time_wg"
-    def testMarker = UUID.randomUUID().toString().substring(0, 8)
+    def maxConcurrency = 1
+    def queueTimeoutMs = 30000
+    def guaranteedQueueTimeMs = 1000
+    def blockerSleepTime = 10
+    def markerPrefix = UUID.randomUUID().toString().substring(0, 8)
+    def blockerMarker = "${markerPrefix}_blocker"
+    def queuedMarker = "${markerPrefix}_queued"
+    def threads = []
+    def queryErrors = Collections.synchronizedList([])
 
-    // Cleanup environment
-    sql "drop table if exists ${tableName}"
-    sql "drop workload group if exists ${wgName}"
+    def getQueueState = {
+        def row = sql("show workload groups").find { it[1].toString() == wgName }
+        if (row == null) {
+            return null
+        }
+        return [
+                running: row[row.size() - 2] as int,
+                waiting: row[row.size() - 1] as int
+        ]
+    }
 
-    // Create test table
-    sql """
-        CREATE TABLE `${tableName}` (
-          `id` bigint,
-          `name` varchar(32)
-        ) ENGINE=OLAP
-        DUPLICATE KEY(`id`)
-        DISTRIBUTED BY HASH(`id`) BUCKETS 1
-        PROPERTIES ("replication_allocation" = "tag.location.default: 1")
-    """
+    def waitForQueueState = { int expectedRunning, int expectedWaiting ->
+        def state = null
+        for (int i = 0; i < 100; i++) {
+            state = getQueueState()
+            if (state != null
+                    && state.running == expectedRunning
+                    && state.waiting == expectedWaiting) {
+                return
+            }
+            sleep(100)
+        }
+        throw new RuntimeException("workload group ${wgName} did not reach "
+                + "running=${expectedRunning}, waiting=${expectedWaiting}; last state=${state}")
+    }
+
+    try {
+        // Cleanup environment
+        sql "drop table if exists ${tableName}"
+        sql "drop workload group if exists ${wgName}"
+
+        // Create test table
+        sql """
+            CREATE TABLE `${tableName}` (
+              `id` bigint,
+              `name` varchar(32)
+            ) ENGINE=OLAP
+            DUPLICATE KEY(`id`)
+            DISTRIBUTED BY HASH(`id`) BUCKETS 1
+            PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+        """
+
+        sql "insert into ${tableName} values (1, 'test')"
+
+        // max_concurrency=1 ensures that the second query enters the queue.
+        sql """
+            create workload group ${wgName}
+            properties (
+                'max_concurrency' = '${maxConcurrency}',
+                'max_queue_size' = '10',
+                'queue_timeout' = '${queueTimeoutMs}'
+            )
+        """
+
+        // Wait for workload group to take effect.
+        Thread.sleep(5000)
+
+        // Truncate audit_log for easier testing.
+        sql "truncate table __internal_schema.audit_log"
+
+        // Occupy the only running slot before submitting the query whose queue time is audited.
+        threads << Thread.start {
+            try {
+                sql "set workload_group=${wgName}"
+                sql """
+                    select sleep(${blockerSleepTime}), '${blockerMarker}' as marker
+                    from ${tableName} limit 1
+                """
+            } catch (Throwable t) {
+                queryErrors.add(t)
+            }
+        }
 
-    sql "insert into ${tableName} values (1, 'test')"
+        waitForQueueState(maxConcurrency, 0)
 
-    def maxConcurrency = 1
-    // Create workload group: max_concurrency=1 ensures queries queue up
-    sql """
-        create workload group ${wgName}
-        properties (
-            'max_concurrency' = '${maxConcurrency}',
-            'max_queue_size' = '10',
-            'queue_timeout' = '30000'
-        )
-    """
-
-    // Wait for workload group to take effect
-    Thread.sleep(5000)
-
-    // Truncate audit_log for easier testing
-    sql "truncate table __internal_schema.audit_log"
-
-    // Submit concurrent queries with marker for later lookup
-    def sqlSleepTime = 5
-    def queuedSqlCnt = 1
-    def threads = []
-    for (int i = 0; i < maxConcurrency + queuedSqlCnt; i++) {
-        def idx = i
         threads << Thread.start {
             try {
                 sql "set workload_group=${wgName}"
-                // Use sleep function to simulate long query, ensuring subsequent queries need to queue
                 sql """
-                    select sleep(${sqlSleepTime}), '${testMarker}_${idx}' as marker
+                    select id, '${queuedMarker}' as marker
                     from ${tableName} limit 1
                 """
-            } catch (Exception e) {
-                log.warn("Query ${idx} failed: ${e.getMessage()}")
+            } catch (Throwable t) {
+                queryErrors.add(t)
             }
         }
-    }
 
-    // Wait for all queries to complete
-    threads.each { it.join() }
-
-    // Wait for audit log to flush
-    Thread.sleep(5000)
-    sql "call flush_audit_log()"
-    Thread.sleep(5000)
-
-    // Verify queue_time_ms column exists
-    def schemaResult = sql "desc internal.__internal_schema.audit_log"
-    def hasQueueTimeMs = schemaResult.any { it[0] == "queue_time_ms" }
-    assertTrue(hasQueueTimeMs)
-
-    // check result
-    def retry = 10
-    def query = """
-        select query_id, queue_time_ms, stmt
-        from __internal_schema.audit_log
-        where stmt like '%${testMarker}%'
-        and queue_time_ms > 0
-        order by time
-    """
-    def auditResult = sql "${query}"
-
-    while (auditResult.isEmpty()) {
-        if (retry-- < 0) {
-            throw new RuntimeException("It has retried a few but still failed, you need to check it")
+        waitForQueueState(maxConcurrency, 1)
+        sleep(guaranteedQueueTimeMs)
+        waitForQueueState(maxConcurrency, 1)
+
+        threads.each { it.join() }
+        assertTrue(queryErrors.isEmpty(), "query failures: ${queryErrors}")
+
+        // Verify queue_time_ms column exists.
+        def schemaResult = sql "desc internal.__internal_schema.audit_log"
+        def hasQueueTimeMs = schemaResult.any { it[0] == "queue_time_ms" }
+        assertTrue(hasQueueTimeMs)
+
+        // Match only the queued query. Excluding audit_log statements prevents the lookup
+        // queries themselves from matching the marker during retries.
+        def query = """
+            select query_id, queue_time_ms, stmt
+            from __internal_schema.audit_log
+            where stmt like '%${queuedMarker}%'
+            and stmt not like '%__internal_schema.audit_log%'
+            order by time
+        """
+        def auditResult = []
+        for (int retry = 0; retry < 10 && auditResult.isEmpty(); retry++) {
+            sql "call flush_audit_log()"
+            sleep(1000)
+            auditResult = sql "${query}"
         }
-        sql "call flush_audit_log()"
-        sleep(3000)
-        auditResult = sql "${query}"
-    }
 
-    auditResult.each { row ->
-        assertTrue(row[1] >= sqlSleepTime * 1000)
+        assertFalse(auditResult.isEmpty(), "queued query was not found in audit log")
+        logger.info("Queued query audit result: ${auditResult}")
+        auditResult.each { row ->
+            def queueTimeMs = row[1] as long
+            assertTrue(queueTimeMs >= guaranteedQueueTimeMs,
+                    "queue_time_ms ${queueTimeMs} is less than guaranteed wait ${guaranteedQueueTimeMs}")
+            assertTrue(queueTimeMs < queueTimeoutMs,
+                    "queue_time_ms ${queueTimeMs} reached queue timeout ${queueTimeoutMs}")
+        }
+    } finally {
+        threads.each { thread ->
+            if (thread.isAlive()) {
+                thread.join(blockerSleepTime * 1000 + 5000)
+            }
+        }
+        sql "drop table if exists ${tableName}"
+        sql "drop workload group if exists ${wgName}"
+        sql "set global enable_audit_plugin = ${originalAuditPlugin}"
     }
-
-    assertTrue(auditResult.size() >= queuedSqlCnt)
-
-    // Cleanup
-    sql "drop table if exists ${tableName}"
-    sql "drop workload group if exists ${wgName}"
-    sql "set global enable_audit_plugin = false"
 }
diff --git a/regression-test/suites/auth_call/test_dml_select_udf_auth.groovy b/regression-test/suites/auth_call/test_dml_select_udf_auth.groovy
index f09d28b167c..1845304d3b3 100644
--- a/regression-test/suites/auth_call/test_dml_select_udf_auth.groovy
+++ b/regression-test/suites/auth_call/test_dml_select_udf_auth.groovy
@@ -44,6 +44,7 @@ suite("test_dml_select_udf_auth","p0,auth_call") {
 
     def jarPath = """${context.file.parent}/../javaudf_p0/jars/java-udf-case-jar-with-dependencies.jar"""
     scp_udf_file_to_all_be(jarPath)
+    scp_udf_file_to_all_fe(jarPath)
     log.info("Jar path: ${jarPath}".toString())
 
     sql """ CREATE FUNCTION ${dbName}.${udfName}(string) RETURNS int PROPERTIES (
diff --git a/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy b/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy
index 46d06e5df81..10d0388ea0c 100644
--- a/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy
+++ b/regression-test/suites/auth_call/test_dml_stream_load_be_auth_docker.groovy
@@ -16,6 +16,27 @@
 // under the License.
 
 import org.apache.doris.regression.suite.ClusterOptions
+import java.util.concurrent.TimeUnit
+
+// Drain both pipes while curl runs. Reading process.text after waitForOrKill can read an
+// already closed stream and also risks blocking when the child fills a pipe.
+def executeCurl = { List command ->
+    def process = command.execute()
+    def stdout = new ByteArrayOutputStream()
+    def stderr = new ByteArrayOutputStream()
+    def stdoutThread = process.consumeProcessOutputStream(stdout)
+    def stderrThread = process.consumeProcessErrorStream(stderr)
+    boolean completed = process.waitFor(7200, TimeUnit.SECONDS)
+    if (!completed) {
+        process.destroyForcibly()
+        process.waitFor()
+    }
+    stdoutThread.join()
+    stderrThread.join()
+    assert completed : "curl timed out: ${stderr.toString('UTF-8')}"
+    assert process.exitValue() == 0 : "curl failed: ${stderr.toString('UTF-8')}"
+    return [stdout.toString('UTF-8').trim(), stderr.toString('UTF-8').trim()]
+}
 
 suite("test_dml_stream_load_be_auth_off_docker", "docker,auth_call") {
     def options = new ClusterOptions()
@@ -40,7 +61,7 @@ suite("test_dml_stream_load_be_auth_off_docker", "docker,auth_call") {
 
         def parseCurlResult = { String out, String err ->
             def lines = out.readLines()
-            assertTrue("curl output should include http code, output=${out}, err=${err}", !lines.isEmpty())
+            assertTrue(!lines.isEmpty(), "curl output should include http code, output=${out}, err=${err}")
             def httpCode = lines.last() as int
             def body = lines.size() == 1 ? "" : lines[0..-2].join("\n")
             return [httpCode, body, err]
@@ -63,10 +84,7 @@ suite("test_dml_stream_load_be_auth_off_docker", "docker,auth_call") {
                     "http://${endpoint}/api/${dbName}/${table}/_stream_load"
             ])
             logger.info("stream load target: ${endpoint}, table: ${table}, label: ${label}, user: ${authUser}")
-            def process = command.execute()
-            process.waitForOrKill(7200000)
-            def out = process.text.trim()
-            def err = process.errorStream.text.trim()
+            def (out, err) = executeCurl(command)
             logger.info("stream load out: ${out}, err: ${err}")
             return parseCurlResult(out, err)
         }
@@ -82,10 +100,7 @@ suite("test_dml_stream_load_be_auth_off_docker", "docker,auth_call") {
             ]
             logger.info("stream load 2pc target: ${beHttpAddress}, table: ${table}, "
                     + "txn operation: ${txnOperation}, user: ${authUser}")
-            def process = command.execute()
-            process.waitForOrKill(7200000)
-            def out = process.text.trim()
-            def err = process.errorStream.text.trim()
+            def (out, err) = executeCurl(command)
             logger.info("stream load 2pc out: ${out}, err: ${err}")
             return parseCurlResult(out, err)
         }
@@ -93,7 +108,7 @@ suite("test_dml_stream_load_be_auth_off_docker", "docker,auth_call") {
         def assertLoadFailed = { def result ->
             assertEquals(200, result[0])
             def json = parseJson(result[1])
-            assertTrue("stream load should fail, body=${result[1]}", json.Status != "Success")
+            assertTrue(json.Status != "Success", "stream load should fail, body=${result[1]}")
         }
 
         def assertLoadSuccess = { def result ->
@@ -192,7 +207,7 @@ suite("test_dml_stream_load_be_auth_on_docker", "docker,auth_call") {
 
         def parseCurlResult = { String out, String err ->
             def lines = out.readLines()
-            assertTrue("curl output should include http code, output=${out}, err=${err}", !lines.isEmpty())
+            assertTrue(!lines.isEmpty(), "curl output should include http code, output=${out}, err=${err}")
             def httpCode = lines.last() as int
             def body = lines.size() == 1 ? "" : lines[0..-2].join("\n")
             return [httpCode, body, err]
@@ -215,10 +230,7 @@ suite("test_dml_stream_load_be_auth_on_docker", "docker,auth_call") {
                     "http://${endpoint}/api/${dbName}/${table}/_stream_load"
             ])
             logger.info("stream load target: ${endpoint}, table: ${table}, label: ${label}, user: ${authUser}")
-            def process = command.execute()
-            process.waitForOrKill(7200000)
-            def out = process.text.trim()
-            def err = process.errorStream.text.trim()
+            def (out, err) = executeCurl(command)
             logger.info("stream load out: ${out}, err: ${err}")
             return parseCurlResult(out, err)
         }
@@ -234,10 +246,7 @@ suite("test_dml_stream_load_be_auth_on_docker", "docker,auth_call") {
             ]
             logger.info("stream load 2pc target: ${beHttpAddress}, table: ${table}, "
                     + "txn operation: ${txnOperation}, user: ${authUser}")
-            def process = command.execute()
-            process.waitForOrKill(7200000)
-            def out = process.text.trim()
-            def err = process.errorStream.text.trim()
+            def (out, err) = executeCurl(command)
             logger.info("stream load 2pc out: ${out}, err: ${err}")
             return parseCurlResult(out, err)
         }
@@ -245,7 +254,7 @@ suite("test_dml_stream_load_be_auth_on_docker", "docker,auth_call") {
         def assertLoadFailed = { def result ->
             assertEquals(200, result[0])
             def json = parseJson(result[1])
-            assertTrue("stream load should fail, body=${result[1]}", json.Status != "Success")
+            assertTrue(json.Status != "Success", "stream load should fail, body=${result[1]}")
         }
 
         def assertLoadSuccess = { def result ->
diff --git a/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy b/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy
index 284b533a352..95680a8df28 100644
--- a/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy
+++ b/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy
@@ -101,7 +101,7 @@ suite("test_role_mapping_system_table", "p0,auth") {
         assertTrue(result[0][6] != null && result[0][6].length() > 0)
         assertEquals(result[0][5], result[0][7])
 
-        connect(user, "${pwd}", jdbcUrlWithoutSchema) {
+        connectToDoris(user, "${pwd}", jdbcUrlWithoutSchema) {
             test {
                 sql """
                     SELECT NAME
diff --git a/regression-test/suites/cloud_p0/auth/test_set_default_cluster.groovy b/regression-test/suites/cloud_p0/auth/test_set_default_cluster.groovy
index ddcf6cd28f4..5193c3e5af6 100644
--- a/regression-test/suites/cloud_p0/auth/test_set_default_cluster.groovy
+++ b/regression-test/suites/cloud_p0/auth/test_set_default_cluster.groovy
@@ -42,8 +42,10 @@ suite("test_default_cluster", "docker") {
         def user1 = "default_user1"
         // admin role
         def user2 = "default_user2"
-        // domain user
-        def user3 = "default_user3@'175.%'"
+        // The Docker client address depends on the network allocated to this run.
+        def clientHost = (sql "SELECT USER()")[0][0].toString().split('@').last().replace("'", "")
+        assertTrue(clientHost ==~ /[0-9A-Za-z.:_-]+/, "Unexpected Docker client host: ${clientHost}")
+        def user3 = "default_user3@'${clientHost}'"
 
         sql """CREATE USER $user1 IDENTIFIED BY 'Cloud123456' DEFAULT ROLE 'admin'"""
         sql """CREATE USER $user2 IDENTIFIED BY 'Cloud123456'"""
diff --git a/regression-test/suites/cloud_p0/balance/test_cross_cg_bvar_metrics.groovy b/regression-test/suites/cloud_p0/balance/test_cross_cg_bvar_metrics.groovy
index b21b347e84e..fdc7a2b4202 100644
--- a/regression-test/suites/cloud_p0/balance/test_cross_cg_bvar_metrics.groovy
+++ b/regression-test/suites/cloud_p0/balance/test_cross_cg_bvar_metrics.groovy
@@ -232,22 +232,26 @@ suite('test_cross_cg_bvar_metrics', 'docker') {
         assert beA2 != null : "Could not identify BE_A2"
         logger.info("T_bvar2: BE_A2 host={} brpcPort={}", beA2.Host, beA2.BrpcPort)
 
-        // Wait until BE_A2 has both accepted tablets and registered same-CG candidates.
+        // Wait until this table has at least one tablet routed to BE_A2. TabletNum and the
+        // process-wide mapping gauge may become positive before FE updates this table's route.
+        def routedTabletIds = []
         awaitUntil(120) {
-            def latest = sql_return_maparray('show backends').find {
+            def routedTablets = sql_return_maparray("SHOW TABLETS FROM ${tbl}").findAll {
                 it.BackendId.toLong() == beA2.BackendId.toLong()
             }
-            latest != null && latest.TabletNum.toInteger() > 0
+            def candidateReady =
+                getBrpcMetric(beA2.Host, beA2.BrpcPort, "balance_tablet_be_mapping_size") > 0
+            routedTabletIds = routedTablets.collect { it.TabletId.toLong() }
+            logger.info("T_bvar2 readiness: BE_A2 routed tablets={}, candidateReady={}",
+                routedTabletIds, candidateReady)
+            !routedTabletIds.isEmpty() && candidateReady
         }
-        awaitUntil(120) {
-            getBrpcMetric(beA2.Host, beA2.BrpcPort, "balance_tablet_be_mapping_size") > 0
-        }
-
-        // Give the rebalance warmup workflow a short window to submit and finish the skipped
-        // warmup tasks before query traffic starts.
-        sleep(2000)
 
         sql "use @compute_cluster"
+        // Retries must reach BE_A2 after its tablet route becomes active. Otherwise the first
+        // result can be served repeatedly from FE SQL cache without exercising same-CG reads.
+        sql "SET enable_sql_cache = false"
+        sql "SET enable_query_cache = false"
         def t2_sameBefore_A1 = getBrpcMetric(beA.Host,  beA.BrpcPort,  "peer_same_compute_group_read")
         def t2_sameBefore_A2 = getBrpcMetric(beA2.Host, beA2.BrpcPort, "peer_same_compute_group_read")
 
@@ -255,7 +259,8 @@ suite('test_cross_cg_bvar_metrics', 'docker') {
             // Run query iteratively; full-table scans should cover tablets rebalanced to BE_A2.
             // Those scans see: (a) same-CG candidate (BE_A1) registered, (b) empty local cache
             // after warmup timeout -> same-CG winner race with peer win -> peer_same_compute_group_read++.
-            sql "SELECT * FROM ${tbl} ORDER BY k1"
+            def result = sql "SELECT * FROM ${tbl} ORDER BY k1"
+            assertEquals(6, result.size(), "T_bvar2 query should return all rows")
             def sameAfterA1 = getBrpcMetric(beA.Host,  beA.BrpcPort,  "peer_same_compute_group_read")
             def sameAfterA2 = getBrpcMetric(beA2.Host, beA2.BrpcPort, "peer_same_compute_group_read")
             ((sameAfterA1 - t2_sameBefore_A1) + (sameAfterA2 - t2_sameBefore_A2)) > 0
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/sync_insert.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/sync_insert.groovy
index a80dab3105a..c653462c57d 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/sync_insert.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/sync_insert.groovy
@@ -15,148 +15,228 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.WarmupMetricsUtils
 
-suite("sync_insert") {
-    List<String> ipList = new ArrayList<>();
-    List<String> hbPortList = new ArrayList<>()
-    List<String> httpPortList = new ArrayList<>()
-    List<String> brpcPortList = new ArrayList<>()
-    List<String> beUniqueIdList = new ArrayList<>()
-
-    String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
-    int num = 0
-    for(String values : bes) {
-        if (num++ == 2) break;
-        println("the value is " + values);
-        String[] beInfo = values.split(':');
-        ipList.add(beInfo[0]);
-        hbPortList.add(beInfo[1]);
-        httpPortList.add(beInfo[2]);
-        beUniqueIdList.add(beInfo[3]);
-        brpcPortList.add(beInfo[4]);
+suite("sync_insert", "nonConcurrent") {
+    String sourceCluster = "regression_cluster_name0"
+    String targetCluster = "regression_cluster_name1"
+    String tableName = "test_dup_tab_basic_int_tab_nullable"
+    def values = [
+        [9,10,11,12], [9,10,11,12], [21,null,23,null], [1,2,3,4],
+        [1,2,3,4], [13,14,15,16], [13,21,22,16], [13,14,15,16],
+        [13,21,22,16], [17,18,19,20], [17,18,19,20], [null,21,null,23],
+        [22,null,24,25], [26,27,null,29], [5,6,7,8], [5,6,7,8]
+    ]
+    def backends = sql_return_maparray("SHOW BACKENDS")
+    def clusterBackends = [sourceCluster, targetCluster].collectEntries { clusterName ->
+        def members = backends.findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collect { be ->
+            [id: be.BackendId as Long, ip: be.Host.toString(),
+             httpPort: be.HttpPort.toString(), brpcPort: be.BrpcPort.toString()]
+        }
+        assertTrue(!members.isEmpty(), "No alive backends in ${clusterName}")
+        [(clusterName): members]
     }
-
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
-    println("the brpc port is " + brpcPortList);
-
-    def clearFileCache = { ip, port ->
-        httpTest {
-            endpoint ""
-            uri ip + ":" + port + """/api/file_cache?op=clear&sync=true"""
-            op "get"
-            body ""
+    logger.info("Sync insert backends: ${clusterBackends}")
+    def tabletIds = []
+    def primaryBackends = [:]
+    def waitUntil = { String description, Closure condition ->
+        long deadline = System.currentTimeMillis() + 120000L
+        while (System.currentTimeMillis() < deadline) {
+            if (condition()) {
+                return
+            }
+            sleep(1000)
         }
+        assertTrue(false, "Timeout after 120000ms: ${description}; see last state in log")
     }
-
-    def getMetricsMethod = { ip, port, check_func ->
+    def getGlobalBytes = {
+        [source: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[sourceCluster], "file_cache_cache_size"),
+         target: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[targetCluster], "file_cache_cache_size")]
+    }
+    def getCache = { String clusterName ->
+        sql "use @${clusterName}"
+        def beIds = clusterBackends[clusterName].collect { it.id }
+        def rows = sql """select tablet_id, be_id, lower(type), sum(size), count(*)
+            from information_schema.file_cache_info
+            where tablet_id in (${tabletIds.join(',')}) and be_id in (${beIds.join(',')})
+            group by tablet_id, be_id, lower(type)"""
+        rows.groupBy { "${it[0]}:${it[1]}".toString() }.collectEntries { key, entries ->
+            [(key): [normalBytes: entries.findAll { it[2]?.toString() == "normal" }.sum(0L) { it[3] as Long },
+                     otherBlocks: entries.findAll { it[2]?.toString() != "normal" }.sum(0L) { it[4] as Long },
+                     blocks: entries.sum(0L) { it[4] as Long }]]
+        }
+    }
+    def getHeader = { be, long tabletId ->
+        def meta = null
         httpTest {
-            endpoint ip + ":" + port
-            uri "/brpc_metrics"
+            endpoint "${be.ip}:${be.httpPort}"
+            uri "/api/meta/header/${tabletId}"
             op "get"
-            check check_func
+            check { code, body ->
+                if ((code as int) == 200) {
+                    meta = parseJson(body.toString())
+                }
+            }
         }
+        meta
     }
 
-    def table1 = "test_dup_tab_basic_int_tab_nullable"
-    sql """ drop table if exists ${table1} """
+    try {
+        sql "use @${sourceCluster}"
+        String dbName = sql("select database()")[0][0].toString()
+        sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+        sql """CREATE TABLE ${tableName} (
+            siteid INT NULL, citycode INT NULL, userid INT NULL, pv INT NULL
+        ) ENGINE=OLAP DUPLICATE KEY(siteid)
+        DISTRIBUTED BY HASH(siteid) BUCKETS 1
+        PROPERTIES("disable_auto_compaction"="true", "file_cache_ttl_seconds"="0")"""
+        tabletIds = sql("show tablets from ${tableName}").collect { it[0] as Long }.unique()
+        assertTrue(tabletIds.size() == 1, "Expected one tablet: ${tabletIds}")
+        def initialPartitions = sql_return_maparray("show partitions from ${tableName}")
+        assertTrue(initialPartitions.size() == 1, "Expected one partition: ${initialPartitions}")
+        long initialVersion = initialPartitions[0].VisibleVersion as Long
 
-    // clear all file cache on all BEs of all clusters
-    clearFileCache.call(ipList[0], httpPortList[0]);
-    clearFileCache.call(ipList[1], httpPortList[1]);
-
-    sql """
-CREATE TABLE IF NOT EXISTS `${table1}` (
-  `siteid` int(11) NULL COMMENT "",
-  `citycode` int(11) NULL COMMENT "",
-  `userid` int(11) NULL COMMENT "",
-  `pv` int(11) NULL COMMENT ""
-) ENGINE=OLAP
-DUPLICATE KEY(`siteid`)
-COMMENT "OLAP"
-DISTRIBUTED BY HASH(`siteid`) BUCKETS 1
-"""
-
-    long insertedSizeBytes = 795
-    connect('root') {
-        sql "use @regression_cluster_name0"
-        sql "use regression_test_cloud_p0_cache_multi_cluster_read_write"
-        sql """ set enable_multi_cluster_sync_load = true """
-        // all the inserted values will result in insertedSizeBytes bytes of size
-        sql """insert into test_dup_tab_basic_int_tab_nullable values
-            (9,10,11,12),
-            (9,10,11,12),
-            (21,null,23,null),
-            (1,2,3,4),
-            (1,2,3,4),
-            (13,14,15,16),
-            (13,21,22,16),
-            (13,14,15,16),
-            (13,21,22,16),
-            (17,18,19,20),
-            (17,18,19,20),
-            (null,21,null,23),
-            (22,null,24,25),
-            (26,27,null,29),
-            (5,6,7,8),
-            (5,6,7,8)
-        """
-        sql """ set enable_multi_cluster_sync_load = false """
-    }
+        waitUntil("source and target primary tablet routes") {
+            [sourceCluster, targetCluster].each { clusterName ->
+                sql "use @${clusterName}"
+                def members = clusterBackends[clusterName].collectEntries { [(it.id): it] }
+                def routes = [:]
+                sql_return_maparray("show tablets from ${tableName}").each { tablet ->
+                    long id = tablet.TabletId as Long
+                    long primary = tablet.PrimaryBackendId as Long
+                    if (id in tabletIds && members.containsKey(primary) && (tablet.BackendId as Long) == primary) {
+                        routes[id] = members[primary]
+                    }
+                }
+                primaryBackends[clusterName] = routes
+            }
+            logger.info("Sync insert primary routes: ${primaryBackends}")
+            [sourceCluster, targetCluster].every { primaryBackends[it].keySet() == tabletIds.toSet() }
+        }
+        def beforeClear = getGlobalBytes()
+        clusterBackends.values().flatten().each { be ->
+            WarmupMetricsUtils.clearFileCache(be.ip, be.httpPort)
+        }
+        def afterClear = getGlobalBytes()
+        logger.info("Sync insert clear: before=${beforeClear}, after=${afterClear}, " +
+                "source_minus_target_after_clear=${afterClear.source - afterClear.target}, " +
+                "source_change_after_clear=${afterClear.source - beforeClear.source}, " +
+                "target_change_after_clear=${afterClear.target - beforeClear.target}")
+        [sourceCluster, targetCluster].each { clusterName ->
+            def cache = getCache(clusterName)
+            assertTrue(cache.isEmpty(), "New test tablet must be cold in ${clusterName}: ${cache}")
+        }
 
-    sleep(30000) // wait for download from remote
+        connect('root') {
+            sql "use @${sourceCluster}"
+            sql "use `${dbName.replace('`', '``')}`"
+            String previousSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+            assertTrue(previousSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+                    "Unexpected sync-load setting: ${previousSyncLoad}")
+            try {
+                sql "set enable_multi_cluster_sync_load = true"
+                def tuples = values.collect { row ->
+                    "(" + row.collect { it == null ? "NULL" : it.toString() }.join(',') + ")"
+                }.join(',')
+                sql "INSERT INTO ${tableName} VALUES ${tuples}"
+            } finally {
+                sql "set enable_multi_cluster_sync_load = ${previousSyncLoad}"
+            }
+        }
 
-    // get insert size of first cluster, may contain system tables
-    long srcClusterSize = 0
-    getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("file_cache_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+        sql "use @${sourceCluster}"
+        long loadedVersion = -1L
+        waitUntil("INSERT visible version") {
+            def partitions = sql_return_maparray("show partitions from ${tableName}")
+            logger.info("Sync insert visible partition state: ${partitions}")
+            if (partitions.size() == 1) {
+                loadedVersion = partitions[0].VisibleVersion as Long
+            }
+            loadedVersion > initialVersion
+        }
+        def expectedBytes = [:]
+        waitUntil("source rowset metadata for the inserted rows") {
+            def ready = [:]
+            tabletIds.each { id ->
+                try {
+                    def meta = getHeader(primaryBackends[sourceCluster][id], id)
+                    if (meta == null || (meta.tablet_id as Long) != id) {
+                        return
                     }
-                    def i = line.indexOf(' ')
-                    srcClusterSize = line.substring(i).toLong()
-                    flag = true
-                    break
+                    // Cloud /api/meta/header omits rs_metas. This metadata scanner reads the
+                    // source BE's rowset map without scanning data or filling target cache.
+                    def rowsets = sql("""select rowset_id, start_version, end_version, rowset_num_rows,
+                            data_disk_size, index_disk_size, num_segments
+                        from information_schema.rowsets
+                        where tablet_id = ${id} and backend_id = ${primaryBackends[sourceCluster][id].id}
+                            and start_version > ${initialVersion} and end_version <= ${loadedVersion}""")
+                            .collect { row ->
+                                [rowset_id_v2: row[0], start_version: row[1] as Long, end_version: row[2] as Long,
+                                 num_rows: row[3] as Long, data_disk_size: row[4] as Long,
+                                 index_disk_size: row[5] as Long, num_segments: row[6] as Long]
+                            }
+                    logger.info("Inserted rowsets for ${id}: " + rowsets.collect {
+                        [rowset: it.rowset_id_v2, start: it.start_version, end: it.end_version,
+                         rows: it.num_rows, dataBytes: it.data_disk_size, indexBytes: it.index_disk_size,
+                         segments: it.num_segments]
+                    })
+                    if (!rowsets.isEmpty() && rowsets.max { it.end_version as Long }.end_version as Long == loadedVersion &&
+                            rowsets.sum(0L) { (it.num_rows ?: 0L) as Long } == values.size()) {
+                        // This table has no inverted indexes. data_disk_size sums actual segment file sizes,
+                        // including packed slices, without depending on a particular encoding or file format.
+                        long bytes = rowsets.sum(0L) { (it.data_disk_size ?: 0L) as Long }
+                        assertTrue(rowsets.every { ((it.index_disk_size ?: 0L) as Long) == 0L },
+                                "Unexpected external index files for ${id}: ${rowsets}")
+                        if (bytes > 0L) {
+                            ready[id] = bytes
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.info("Waiting for inserted rowset metadata on ${id}: ${e.message}")
                 }
             }
-            assertTrue(flag)
-    }
+            if (ready.keySet() == tabletIds.toSet()) {
+                expectedBytes = ready
+                return true
+            }
+            false
+        }
+        logger.info("Sync insert actual segment bytes: ${expectedBytes}")
 
-    // get synced insert size of first cluster
-    long dstClusterSize = 0
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("file_cache_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    dstClusterSize = line.substring(i).toLong()
-                    flag = true
-                    break
+        waitUntil("source and target NORMAL cache to match actual inserted segment bytes") {
+            def source = getCache(sourceCluster)
+            def target = getCache(targetCluster)
+            logger.info("Sync insert cache: expected=${expectedBytes}, source=${source}, target=${target}, " +
+                    "primary=${primaryBackends}")
+            // Inspect one complete copy on each cluster's primary BE. Do not add replicated copies:
+            // syncLoadForTablets can send the same tablet to multiple BEs in a target cluster.
+            [sourceCluster, targetCluster].every { clusterName ->
+                def cache = clusterName == sourceCluster ? source : target
+                tabletIds.every { id ->
+                    String key = "${id}:${primaryBackends[clusterName][id].id}".toString()
+                    def copy = cache[key]
+                    copy != null && copy.blocks > 0L && copy.otherBlocks == 0L &&
+                            copy.normalBytes == expectedBytes[id]
                 }
             }
-            assertTrue(flag)
+        }
+
+        // Query only after proving automatic download, so reads cannot mask a failed sync load.
+        connect('root') {
+            sql "use @${targetCluster}"
+            sql "use `${dbName.replace('`', '``')}`"
+            sql "set enable_sql_cache = false"
+            sql "set enable_query_cache = false"
+            def actual = sql("select siteid, citycode, userid, pv from ${tableName}")
+                    .collect { row -> row.collect { it == null ? null : it as Long } }
+            def expected = values.collect { row -> row.collect { it == null ? null : it as Long } }
+            assertTrue(actual.countBy { it } == expected.countBy { it },
+                    "Target data differs, including duplicate/NULL rows: actual=${actual}, expected=${expected}")
+        }
+    } finally {
+        sql "DROP TABLE IF EXISTS ${tableName} FORCE"
     }
-    // FIXME(gavin): this is a strong assertion, make it weaker and robuster
-    assertEquals(insertedSizeBytes, dstClusterSize)
-    org.junit.Assert.assertTrue("insertedSizeBytes ${insertedSizeBytes} <= dstClusterSize ${dstClusterSize}", insertedSizeBytes <= dstClusterSize)
-    org.junit.Assert.assertTrue("insertedSizeBytes ${insertedSizeBytes} <= srcClusterSize ${dstClusterSize}", insertedSizeBytes <= srcClusterSize)
-    org.junit.Assert.assertTrue("dstClusterSize ${insertedSizeBytes} <= srcClusterSize ${dstClusterSize}", dstClusterSize <= srcClusterSize)
-    sql "drop table if exists ${table1}"
 }
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/test_multi_stale_rowset.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/test_multi_stale_rowset.groovy
index b2bc06ffd81..0b7b2e24430 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/test_multi_stale_rowset.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/read_write/test_multi_stale_rowset.groovy
@@ -17,7 +17,16 @@
 
 import org.codehaus.groovy.runtime.IOGroovyMethods
 
-suite("test_multi_stale_rowset") {
+suite("test_multi_stale_rowset", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
+    }
+
     def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="1200", "disable_auto_compaction"="true") """
     List<String> ipList = new ArrayList<>();
     List<String> hbPortList = new ArrayList<>()
@@ -138,6 +147,10 @@ suite("test_multi_stale_rowset") {
     load_customer_once()
     sleep(30000);
     def backendIdToAfterLoadCacheSize = getCurCacheSize()
+    assertTrue(backendIdToAfterLoadCacheSize.get(beUniqueIdList[0]) > 0,
+            "Source backend should cache rowsets generated by load")
+    assertTrue(backendIdToAfterLoadCacheSize.get(beUniqueIdList[1]) > 0,
+            "Target backend should cache rowsets synchronized by multi-cluster sync load")
 
     sql "use @regression_cluster_name0"
     String[][] tablets = sql """ show tablets from ${tableName}; """
@@ -147,50 +160,17 @@ suite("test_multi_stale_rowset") {
 
     sleep(90000);
     def backendIdToAfterCompactionCacheSize = getCurCacheSize()
-    assertTrue(backendIdToAfterCompactionCacheSize.get(beUniqueIdList[1]) < backendIdToAfterLoadCacheSize.get(beUniqueIdList[1]));
-
+    logger.info("File cache size after load: ${backendIdToAfterLoadCacheSize}, " +
+            "after compaction: ${backendIdToAfterCompactionCacheSize}")
+    assertTrue(backendIdToAfterCompactionCacheSize.get(beUniqueIdList[0]) > 0,
+            "Source backend should cache the compaction output rowsets")
+    assertTrue(backendIdToAfterCompactionCacheSize.get(beUniqueIdList[1]) <
+            backendIdToAfterLoadCacheSize.get(beUniqueIdList[1]),
+            "Target backend should recycle the stale rowset cache")
+
+    // Multi-cluster sync load only synchronizes rowsets generated by load. It does not
+    // synchronize subsequent compaction output rowsets, so the source and target cache
+    // sizes are not expected to remain equal after stale rowset compaction.
     sql "use @regression_cluster_name1"
-
-    long ttl_cache_size = 0
-    getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    ttl_cache_size = line.substring(i).toLong()
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
-    }
-
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    assertEquals(ttl_cache_size, line.substring(i).toLong())
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
-    }
     sql new File("""${context.file.parent}/ddl/${table}_delete.sql""").text
-    sql """ set enable_multi_cluster_sync_load=false """
 }
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
index 21a3773b262..9244e7f9e24 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
@@ -15,177 +15,280 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.WarmupMetricsUtils
 
-suite("test_warm_up_cluster_batch") {
-    def getJobState = { jobId ->
-         def jobStateResult = sql """  SHOW WARM UP JOB WHERE ID = ${jobId} """
-         return jobStateResult[0]
+suite("test_warm_up_cluster_batch", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
     }
-    def table = "customer"
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
 
-    List<String> ipList = new ArrayList<>();
-    List<String> hbPortList = new ArrayList<>()
-    List<String> httpPortList = new ArrayList<>()
-    List<String> brpcPortList = new ArrayList<>()
-    List<String> beUniqueIdList = new ArrayList<>()
-
-    String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
-    int num = 0
-    for(String values : bes) {
-        if (num++ == 2) break;
-        println("the value is " + values);
-        String[] beInfo = values.split(':');
-        ipList.add(beInfo[0]);
-        hbPortList.add(beInfo[1]);
-        httpPortList.add(beInfo[2]);
-        beUniqueIdList.add(beInfo[3]);
-        brpcPortList.add(beInfo[4]);
-    }
-
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
-    println("the brpc port is " + brpcPortList);
-
-    sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
-    sql new File("""${context.file.parent}/../ddl/supplier_delete.sql""").text
-    // create table if not exists
-    sql new File("""${context.file.parent}/../ddl/${table}.sql""").text
-    sql new File("""${context.file.parent}/../ddl/supplier.sql""").text
-
-    sql """ TRUNCATE TABLE __internal_schema.cloud_cache_hotspot; """
-    sleep(30000)
-
-    def s3BucketName = getS3BucketName()
-    def s3WithProperties = """WITH S3 (
-        |"AWS_ACCESS_KEY" = "${getS3AK()}",
-        |"AWS_SECRET_KEY" = "${getS3SK()}",
-        |"AWS_ENDPOINT" = "${getS3Endpoint()}",
-        |"AWS_REGION" = "${getS3Region()}",
-        |"provider" = "${getS3Provider()}")
-        |PROPERTIES(
-        |"exec_mem_limit" = "8589934592",
-        |"load_parallelism" = "3")""".stripMargin()
-    
-    
-
-    def clearFileCache = { ip, port ->
-        httpTest {
-            endpoint ""
-            uri ip + ":" + port + """/api/file_cache?op=clear&sync=true"""
-            op "get"
-            body ""
-        }
-    }
-
-    def getMetricsMethod = { ip, port, check_func ->
-        httpTest {
-            endpoint ip + ":" + port
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
+    String sourceCluster = "regression_cluster_name0"
+    String targetCluster = "regression_cluster_name1"
+    def tableNames = ["customer", "supplier"]
+    def dataQueries = [
+        customer: "select sum(C_CUSTKEY), sum(length(C_COMMENT)) from customer",
+        supplier: "select sum(s_suppkey), sum(length(s_address)) from supplier"
+    ]
+    def backends = sql_return_maparray("SHOW BACKENDS")
+    def clusterBackends = [sourceCluster, targetCluster].collectEntries { clusterName ->
+        def members = backends.findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collect { be ->
+            [id: be.BackendId as Long, ip: be.Host.toString(),
+             httpPort: be.HttpPort.toString(), brpcPort: be.BrpcPort.toString()]
         }
+        assertTrue(!members.isEmpty(), "No alive backends in ${clusterName}")
+        [(clusterName): members]
     }
+    logger.info("Batch warmup backends: ${clusterBackends}")
+    assertTrue(getFeConfig("enable_fetch_cluster_cache_hotspot").equalsIgnoreCase("true"),
+            "Cluster warmup requires the FE cache hotspot collector to be enabled at startup")
+    def hotspotInterval = getFeConfig("fetch_cluster_cache_hotspot_interval_ms")
 
-    clearFileCache.call(ipList[0], httpPortList[0]);
-    clearFileCache.call(ipList[1], httpPortList[1]);
-
-    def load_customer_once =  { 
-        def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
-        def loadLabel = table + "_" + uniqueID
-        // load data from cos
-        def loadSql = new File("""${context.file.parent}/../ddl/${table}_load.sql""").text.replaceAll("\\\$\\{s3BucketName\\}", s3BucketName)
-        loadSql = loadSql.replaceAll("\\\$\\{loadLabel\\}", loadLabel) + s3WithProperties
-        sql loadSql
-
-        // check load state
-        while (true) {
-            def stateResult = sql "show load where Label = '${loadLabel}'"
-            def loadState = stateResult[stateResult.size() - 1][2].toString()
-            if ("CANCELLED".equalsIgnoreCase(loadState)) {
-                throw new IllegalStateException("load ${loadLabel} failed.")
-            } else if ("FINISHED".equalsIgnoreCase(loadState)) {
-                break
+    def waitUntil = { String description, long timeoutMs, Closure condition ->
+        long deadline = System.currentTimeMillis() + timeoutMs
+        while (System.currentTimeMillis() < deadline) {
+            if (condition()) {
+                return
             }
-            sleep(5000)
-        }
-    }
-    
-    sql "use @regression_cluster_name0"
-    def waitJobDone = { jobId ->
-        int retryTime = 120
-        int i = 0
-        for (; i < retryTime; i++) {
             sleep(1000)
-            def statuses = getJobState(jobId[0][0])
-            if (statuses.any { it != null && it.equals("CANCELLED") }) {
-                assertTrue(false);
-            }
-            if (statuses.any { it != null && it.equals("FINISHED") }) {
-                break;
-            }
-        }
-        if (i == retryTime) {
-            sql "cancel warm up job where id = ${jobId[0][0]}"
-            assertTrue(false);
         }
+        assertTrue(false, "Timeout after ${timeoutMs}ms: ${description}")
+    }
+    def pendingLoads = [] as Set
+    def warmupJobs = [] as Set
+    def tableInfo = [:]
+    def getGlobalTtlBytes = {
+        [source: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[sourceCluster], "ttl_cache_size"),
+         target: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[targetCluster], "ttl_cache_size")]
     }
-    for (int k = 0; k < 10; k++) {
-        load_customer_once()
-        for (int i = 0; i < 10; i++) {
-            sql "select count(*) from customer"
+    def getNormalCache = { String clusterName ->
+        sql "use @${clusterName}"
+        def beIds = clusterBackends[clusterName].collect { it.id }.join(',')
+        tableInfo.collectEntries { tableName, info ->
+            def rows = sql """select tablet_id, sum(size)
+                from information_schema.file_cache_info
+                where tablet_id in (${info.tabletIds.join(',')})
+                  and be_id in (${beIds}) and lower(type) = 'normal'
+                group by tablet_id"""
+            def bytesByTablet = rows.collectEntries { [(it[0] as Long): it[1] as Long] }
+            [(tableName): [bytes: bytesByTablet.values().sum(0L) as Long,
+                          complete: info.tabletIds.every { (bytesByTablet[it] ?: 0L) > 0L }]]
         }
-        if (k % 2 == 1) {
-            sleep(40000)
-            def jobId_ = sql "WARM UP CLUSTER regression_cluster_name1 WITH CLUSTER regression_cluster_name0"
-            waitJobDone(jobId_)
-            sleep(30000)
-            long ttl_cache_size = 0
-            getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-                respCode, body ->
-                    assertEquals("${respCode}".toString(), "200")
-                    String out = "${body}".toString()
-                    def strs = out.split('\n')
-                    Boolean flag = false;
-                    for (String line in strs) {
-                        if (line.contains("ttl_cache_size")) {
-                            if (line.startsWith("#")) {
-                                continue
+    }
+    def getHotspotCounts = {
+        sql "use @${sourceCluster}"
+        def rows = sql """select table_id, partition_id, sum(query_per_day)
+            from __internal_schema.cloud_cache_hotspot
+            where cluster_name = '${sourceCluster}'
+              and table_id in (${tableInfo.values().collect { it.id }.join(',')})
+              and insert_day = current_date()
+            group by table_id, partition_id"""
+        rows.collectEntries { [("${it[0]}:${it[1]}".toString()): it[2] as Long] }
+    }
+    def waitForTargetTablets = {
+        sql "use @${targetCluster}"
+        def targetBes = clusterBackends[targetCluster].collectEntries { [(it.id): it] }
+        def missing = []
+        waitUntil("target primary routing and tablet metadata; see missing tablet log", 120000L) {
+            missing = []
+            tableInfo.each { tableName, info ->
+                def ready = [] as Set
+                def tablets = sql_return_maparray("show tablets from ${tableName}")
+                tablets.each { tablet ->
+                    long id = tablet.TabletId as Long
+                    long primary = tablet.PrimaryBackendId as Long
+                    if (info.tabletIds.contains(id) && targetBes.containsKey(primary) &&
+                            (tablet.BackendId as Long) == primary) {
+                        def be = targetBes[primary]
+                        try {
+                            // Load metadata only; querying the target table could mask a failed warmup.
+                            httpTest {
+                                endpoint "${be.ip}:${be.httpPort}"
+                                uri "/api/meta/header/${id}"
+                                op "get"
+                                check { code, body ->
+                                    if ((code as int) == 200) {
+                                        def meta = parseJson(body.toString())
+                                        // Cloud header JSON omits rowsets; data readiness is
+                                        // established by the source scan and cache baseline.
+                                        if ((meta.tablet_id as Long) == id &&
+                                                (meta.ttl_seconds as Long) == 0L &&
+                                                meta.tablet_state == "PB_RUNNING" && meta.schema != null) {
+                                            ready.add(id)
+                                        }
+                                    }
+                                }
                             }
-                            def i = line.indexOf(' ')
-                            ttl_cache_size = line.substring(i).toLong()
-                            flag = true
-                            break
+                        } catch (Exception e) {
+                            logger.info("Waiting for target tablet ${id} on BE ${primary}: ${e.message}")
                         }
                     }
-                    assertTrue(flag)
+                }
+                missing.addAll(info.tabletIds - ready)
             }
+            logger.info("Target tablet readiness: missing=${missing}")
+            missing.isEmpty()
+        }
+    }
 
-            getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-                respCode, body ->
-                    assertEquals("${respCode}".toString(), "200")
-                    String out = "${body}".toString()
-                    def strs = out.split('\n')
-                    Boolean flag = false;
-                    for (String line in strs) {
-                        if (line.contains("ttl_cache_size")) {
-                            if (line.startsWith("#")) {
-                                continue
-                            }
-                            def i = line.indexOf(' ')
-                            assertEquals(ttl_cache_size, line.substring(i).toLong())
-                            flag = true
-                            break
+    // This case must exercise hotspot-based selection, not the all-partitions bypass.
+    setFeConfigTemporary([cloud_warm_up_force_all_partitions: false]) {
+        try {
+            sql "use @${sourceCluster}"
+            sql "set enable_sql_cache = false"
+            sql "set enable_query_cache = false"
+            // Only disabling SQL/query cache does not disable COUNT(*) constant rewriting.
+            // The SUM expressions above force scans of actual data columns on the source BE.
+            tableNames.each { tableName ->
+                sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+                sql (new File("${context.file.parent}/../ddl/${tableName}.sql").text + """
+                    PROPERTIES("disable_auto_compaction"="true", "file_cache_ttl_seconds"="0")""")
+                def tabletIds = sql("show tablets from ${tableName}").collect { it[0] as Long }.unique()
+                def partitionIds = sql("show partitions from ${tableName}").collect { it[0] as Long }.unique()
+                long tableId = getTableId(tableName)
+                assertTrue(tableId > 0L && !tabletIds.isEmpty() && !partitionIds.isEmpty(),
+                        "Missing table/partition/tablet IDs for ${tableName}")
+                tableInfo[tableName] = [id: tableId, tabletIds: tabletIds, partitionIds: partitionIds]
+            }
+            logger.info("Batch warmup tables: ${tableInfo}")
+            sql "TRUNCATE TABLE __internal_schema.cloud_cache_hotspot"
+
+            def ttlBeforeClear = getGlobalTtlBytes()
+            clusterBackends.values().flatten().each { be ->
+                WarmupMetricsUtils.clearFileCache(be.ip, be.httpPort)
+            }
+            // Held blocks can survive sync=true. Keep their baseline for diagnosis only.
+            def ttlAfterClear = getGlobalTtlBytes()
+            logger.info("TTL cache clear: before=${ttlBeforeClear}, after=${ttlAfterClear}, " +
+                    "source_minus_target_after_clear=${ttlAfterClear.source - ttlAfterClear.target}, " +
+                    "source_change_after_clear=${ttlAfterClear.source - ttlBeforeClear.source}, " +
+                    "target_change_after_clear=${ttlAfterClear.target - ttlBeforeClear.target}")
+
+            def s3BucketName = getS3BucketName()
+            def s3Properties = """WITH S3 (
+                "AWS_ACCESS_KEY" = "${getS3AK()}",
+                "AWS_SECRET_KEY" = "${getS3SK()}",
+                "AWS_ENDPOINT" = "${getS3Endpoint()}",
+                "AWS_REGION" = "${getS3Region()}",
+                "provider" = "${getS3Provider()}")
+                PROPERTIES("exec_mem_limit"="8589934592", "load_parallelism"="3")"""
+            def loadOnce = { String tableName ->
+                String label = "${tableName}_${UUID.randomUUID().toString().replace('-', '')}"
+                String loadSql = new File("${context.file.parent}/../ddl/${tableName}_load.sql").text
+                        .replace('${s3BucketName}', s3BucketName).replace('${loadLabel}', label)
+                sql (loadSql + s3Properties)
+                pendingLoads.add(label)
+                waitUntil("load ${label} to finish", 300000L) {
+                    def rows = sql "show load where Label = '${label}'"
+                    if (rows.isEmpty()) {
+                        return false
+                    }
+                    String state = rows.last()[2].toString()
+                    assertTrue(!state.equalsIgnoreCase("CANCELLED"), "Load ${label} cancelled: ${rows.last()}")
+                    if (state.equalsIgnoreCase("FINISHED")) {
+                        pendingLoads.remove(label)
+                        return true
+                    }
+                    false
+                }
+            }
+            def previousBytes = tableNames.collectEntries { [(it): 0L] }
+            for (int round = 0; round < 10; round++) {
+                sql "use @${sourceCluster}"
+                tableNames.each { loadOnce(it) }
+                if (round % 2 == 0) {
+                    continue
+                }
+                int batch = (round + 1) / 2
+                def hotspotBeforeQueries = getHotspotCounts()
+                // One real scan per table is enough to advance the hotspot counters for this batch.
+                tableNames.each { tableName ->
+                    def result = sql(dataQueries[tableName])
+                    assertTrue(!result.isEmpty() && result[0].every { it != null },
+                            "Data query should read nonempty ${tableName}")
+                }
+                // Require hotspot counters for these new table/partition IDs to advance beyond
+                // the pre-query snapshot, without assuming identical FE and BE timestamps.
+                waitUntil("batch ${batch} hotspot refresh (configured interval=${hotspotInterval}ms); " +
+                        "run with a short FE startup hotspot interval", 180000L) {
+                    def counts = getHotspotCounts()
+                    logger.info("Batch ${batch} hotspot counts: before=${hotspotBeforeQueries}, current=${counts}")
+                    tableInfo.values().every { info ->
+                        info.partitionIds.every { partitionId ->
+                            String key = "${info.id}:${partitionId}"
+                            (counts[key] ?: 0L) > (hotspotBeforeQueries[key] ?: 0L)
                         }
                     }
-                    assertTrue(flag)
+                }
+                waitForTargetTablets()
+                def sourceCache = [:]
+                waitUntil("batch ${batch} source NORMAL cache to include new data", 120000L) {
+                    sourceCache = getNormalCache(sourceCluster)
+                    logger.info("Batch ${batch} source NORMAL cache=${sourceCache}, previous=${previousBytes}")
+                    tableNames.every { sourceCache[it].complete && sourceCache[it].bytes > previousBytes[it] }
+                }
+                def job = sql "WARM UP CLUSTER ${targetCluster} WITH CLUSTER ${sourceCluster}"
+                def jobId = job[0][0]
+                warmupJobs.add(jobId)
+                waitUntil("warmup job ${jobId} to finish", 300000L) {
+                    def rows = sql "SHOW WARM UP JOB WHERE ID = ${jobId}"
+                    if (rows.isEmpty()) {
+                        return false
+                    }
+                    def states = rows[0].collect { it?.toString() }
+                    assertTrue(!states.contains("CANCELLED"), "Warmup ${jobId} cancelled: ${rows[0]}")
+                    states.contains("FINISHED")
+                }
+                def targetCache = [:]
+                waitUntil("batch ${batch}: per-table NORMAL cache bytes must be positive and equal", 180000L) {
+                    sourceCache = getNormalCache(sourceCluster)
+                    targetCache = getNormalCache(targetCluster)
+                    logger.info("Batch ${batch} NORMAL cache: source=${sourceCache}, target=${targetCache}")
+                    tableNames.every { tableName ->
+                        sourceCache[tableName].complete && targetCache[tableName].complete &&
+                                sourceCache[tableName].bytes > previousBytes[tableName] &&
+                                sourceCache[tableName].bytes == targetCache[tableName].bytes
+                    }
+                }
+                previousBytes = sourceCache.collectEntries { name, cache -> [(name): cache.bytes] }
+                def currentTtl = getGlobalTtlBytes()
+                logger.info("Batch ${batch} global TTL=${currentTtl}, baseline_after_clear=${ttlAfterClear}, " +
+                        "source_minus_target=${currentTtl.source - currentTtl.target}")
+            }
+        } finally {
+            warmupJobs.each { jobId ->
+                try {
+                    def rows = sql "SHOW WARM UP JOB WHERE ID = ${jobId}"
+                    if (!rows.isEmpty() && rows[0].any { it?.toString() in ["PENDING", "RUNNING"] }) {
+                        sql "CANCEL WARM UP JOB WHERE ID = ${jobId}"
+                    }
+                } catch (Exception e) {
+                    logger.warn("Failed to clean up warmup ${jobId}: ${e.message}")
+                }
+            }
+            sql "use @${sourceCluster}"
+            pendingLoads.each { label ->
+                try {
+                    sql "CANCEL LOAD WHERE LABEL = '${label}'"
+                } catch (Exception e) {
+                    logger.warn("Failed to clean up load ${label}: ${e.message}")
+                }
+            }
+            try {
+                sql "DROP TABLE IF EXISTS customer FORCE"
+            } finally {
+                sql "DROP TABLE IF EXISTS supplier FORCE"
             }
-            sql "use @regression_cluster_name0"
         }
     }
-    sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
-    sql new File("""${context.file.parent}/../ddl/supplier_delete.sql""").text
-}   
+}
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_cancel_expired.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_cancel_expired.groovy
index 6114892039a..5cfef65570a 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_cancel_expired.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_cancel_expired.groovy
@@ -16,6 +16,7 @@
 // under the License.
 
 import org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.NodeType
 import groovy.json.JsonSlurper
 
 // Covers a two-part bug on the event-driven warm-up path:
@@ -43,7 +44,11 @@ suite('test_warm_up_cluster_event_cancel_expired', 'docker') {
         'file_cache_enter_disk_resource_limit_mode_percent=99',
         'enable_evict_file_cache_in_advance=false',
         'file_cache_background_monitor_interval_ms=1000',
+        // Expire cached replica locations before the post-cancel INSERTs so
+        // source BE must ask FE again and consume the CANCELLED status.
+        'warmup_tablet_replica_info_cache_ttl_sec=5',
     ]
+    options.enableDebugPoints()
     options.cloudMode = true
 
     def clearFileCache = { ip, port ->
@@ -77,18 +82,22 @@ suite('test_warm_up_cluster_event_cancel_expired', 'docker') {
         }
     }
 
-    def getSkippedRowsetSum = { cluster ->
+    def getMetricSum = { cluster, metric ->
         def backends = sql """SHOW BACKENDS"""
         def cluster_bes = backends.findAll {
             it[19].contains("""\"compute_group_name\" : \"${cluster}\"""")
         }
         long sum = 0
         for (be in cluster_bes) {
-            sum += getBrpcMetrics(be[1], be[5], "file_cache_event_driven_warm_up_skipped_rowset_num")
+            sum += getBrpcMetrics(be[1], be[5], metric)
         }
         return sum
     }
 
+    def getSkippedRowsetSum = { cluster ->
+        return getMetricSum(cluster, "file_cache_event_driven_warm_up_skipped_rowset_num")
+    }
+
     docker(options) {
         def clusterSrc = "warmup_source"
         def clusterDst = "warmup_target"
@@ -124,27 +133,55 @@ suite('test_warm_up_cluster_event_cancel_expired', 'docker') {
         }
         sleep(5000)
 
-        // 3. Cancel the job. After the fix, BE sees TStatus.CANCELLED on
-        //    the next get_replica_info and drops the job from
-        //    _tablet_replica_cache; before the fix (CANCELED typo) it
-        //    would keep the entry.
-        sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+        // 3. Ignore the proactive CLEAR_JOB on source BE so this case
+        //    exercises the passive cleanup path: after replica-info cache
+        //    expiry, get_replica_info asks FE and receives TStatus.CANCELLED.
+        def sourceBes = sql("""SHOW BACKENDS""").findAll {
+            it[19].contains("""\"compute_group_name\" : \"${clusterSrc}\"""")
+        }
+        assertFalse(sourceBes.isEmpty(), "source cluster should have at least one BE")
+        def activeJobsBeforeCancel = getMetricSum(clusterSrc, "file_cache_warm_up_job_num")
+        assertTrue(activeJobsBeforeCancel > 0, "source BE should hold the event-driven job")
+        try {
+            for (be in sourceBes) {
+                GetDebugPoint().enableDebugPoint(be[1].toString(), be[4] as int, NodeType.BE,
+                        "CloudWarmUpManager.set_event.ignore_all")
+            }
+            sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+        } finally {
+            for (be in sourceBes) {
+                GetDebugPoint().disableDebugPoint(be[1].toString(), be[4] as int, NodeType.BE,
+                        "CloudWarmUpManager.set_event.ignore_all")
+            }
+        }
         def st = sql """SHOW WARM UP JOB WHERE ID = ${jobId}"""
         assertEquals("CANCELLED", st[0][3])
+        def activeJobsAfterCancel = getMetricSum(clusterSrc, "file_cache_warm_up_job_num")
+        assertEquals(activeJobsBeforeCancel, activeJobsAfterCancel,
+                "ignored CLEAR_JOB should leave the source BE cache intact")
+        sleep(6000)
 
-        // 4. One more batch so BE actually sees the CANCELLED status
-        //    and (with the fix) purges its cache entry.
+        // 4. Waited past the 5s replica-info TTL after cancellation.
+        //    The next batch makes source BE observe CANCELLED and purge the job.
         for (int i = 0; i < 20; i++) {
             sql """INSERT INTO t_exp VALUES (${100 + i}, 'y')"""
         }
-        sleep(5000)
+        def activeJobsAfterPassiveCleanup = getMetricSum(clusterSrc, "file_cache_warm_up_job_num")
+        for (int i = 0; i < 20 && activeJobsAfterPassiveCleanup >= activeJobsAfterCancel; i++) {
+            sleep(500)
+            activeJobsAfterPassiveCleanup = getMetricSum(clusterSrc, "file_cache_warm_up_job_num")
+        }
+        assertTrue(activeJobsAfterPassiveCleanup < activeJobsAfterCancel,
+                "source BE should remove the job after receiving TStatus.CANCELLED, " +
+                "before=${activeJobsAfterCancel} after=${activeJobsAfterPassiveCleanup}")
 
         // 5. Baseline for the skipped-rowset counter. After BE has
         //    cleaned its cache, subsequent warm_up_rowset calls return
         //    early (empty replicas -> "skipping rowset") and bump this
-        //    counter on every commit. If the typo is unfixed the counter
-        //    stays flat because BE keeps calling FE.
-        def skippedBaseline = getSkippedRowsetSum(clusterDst)
+        //    counter on every commit.
+        // warm_up_rowset is invoked by the BE committing source rowsets, so
+        // this metric belongs to the source cluster rather than the target.
+        def skippedBaseline = getSkippedRowsetSum(clusterSrc)
         logger.info("skipped_rowset baseline=${skippedBaseline}")
 
         // 6. Wait past history_cloud_warm_up_job_keep_max_second plus one
@@ -160,13 +197,16 @@ suite('test_warm_up_cluster_event_cancel_expired', 'docker') {
                     logger.info("job ${jobId} removed from FE after ${i}s")
                     break
                 }
-            } catch (Exception e) {
-                if (!e.getMessage().contains("cloud warm up with job ${jobId} does not exist")) {
-                    throw e
+            } catch (Throwable t) {
+                def message = (t.getMessage() ?: t.toString()).toLowerCase()
+                if (message.contains("warm up") && message.contains("does not exist")) {
+                    // FE may report an expired job as "does not exist" instead of an empty result.
+                    // Both states mean the cancelled job has been removed from FE.
+                    logger.info("job ${jobId} removed from FE after ${i}s: ${t.getMessage()}")
+                    removed = true
+                    break
                 }
-                removed = true
-                logger.info("job ${jobId} removed from FE after ${i}s: ${e.getMessage()}")
-                break
+                throw t
             }
             sleep(1000)
         }
@@ -197,11 +237,15 @@ suite('test_warm_up_cluster_event_cancel_expired', 'docker') {
                 "post-removal inserts should not be blocked by NPE sleeps, " +
                 "took ${elapsedMs}ms")
 
-        // 8. On the fixed path every commit short-circuits through
-        //    g_file_cache_event_driven_warm_up_skipped_rowset_num.
-        //    We expect it to grow; on the buggy path it would be flat
-        //    since BE never stopped pursuing FE replicas.
-        def skippedAfter = getSkippedRowsetSum(clusterDst)
+        // 8. After passive cleanup, source commits short-circuit through
+        //    g_file_cache_event_driven_warm_up_skipped_rowset_num. The
+        //    warm-up tasks are asynchronous, so allow a short observation
+        //    window before checking that source-side traffic was counted.
+        def skippedAfter = getSkippedRowsetSum(clusterSrc)
+        for (int i = 0; i < 20 && skippedAfter <= skippedBaseline; i++) {
+            sleep(500)
+            skippedAfter = getSkippedRowsetSum(clusterSrc)
+        }
         logger.info("skipped_rowset after=${skippedAfter}")
         assertTrue(skippedAfter > skippedBaseline,
                 "BE should skip warm_up_rowset for tablets after cancel+expire, " +
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_compute_group.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_compute_group.groovy
index 19ca1958e2e..66e5b8c5019 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_compute_group.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_compute_group.groovy
@@ -15,9 +15,21 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite("test_warm_up_compute_group", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
+    }
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
 
-suite("test_warm_up_compute_group") {
     def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="12000") """
     def getJobState = { jobId ->
          def jobStateResult = sql """  SHOW WARM UP JOB WHERE ID = ${jobId} """
@@ -25,31 +37,24 @@ suite("test_warm_up_compute_group") {
     }
     def table = "customer"
 
-    List<String> ipList = new ArrayList<>();
-    List<String> hbPortList = new ArrayList<>()
-    List<String> httpPortList = new ArrayList<>()
-    List<String> brpcPortList = new ArrayList<>()
-    List<String> beUniqueIdList = new ArrayList<>()
-
-    String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
-    int num = 0
-    for(String values : bes) {
-        if (num++ == 2) break;
-        println("the value is " + values);
-        String[] beInfo = values.split(':');
-        ipList.add(beInfo[0]);
-        hbPortList.add(beInfo[1]);
-        httpPortList.add(beInfo[2]);
-        beUniqueIdList.add(beInfo[3]);
-        brpcPortList.add(beInfo[4]);
+    String sourceCluster = "regression_cluster_name0"
+    String targetCluster = "regression_cluster_name1"
+    def backends = sql_return_maparray("SHOW BACKENDS")
+    def clusterBackends = [sourceCluster, targetCluster].collectEntries { clusterName ->
+        def members = backends.findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collect { be ->
+            [id: be.BackendId as Long, ip: be.Host.toString(),
+             httpPort: be.HttpPort.toString(), brpcPort: be.BrpcPort.toString()]
+        }
+        assertTrue(!members.isEmpty(), "No alive backends in ${clusterName}")
+        [(clusterName): members]
     }
-
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
-    println("the brpc port is " + brpcPortList);
+    logger.info("Warmup source=${sourceCluster}, target=${targetCluster}, backends=${clusterBackends}")
+    sql "use @${sourceCluster}"
+    sql "set enable_sql_cache = false"
+    sql "set enable_query_cache = false"
 
     sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
     sql new File("""${context.file.parent}/../ddl/supplier_delete.sql""").text
@@ -73,26 +78,46 @@ suite("test_warm_up_compute_group") {
     
     
 
-    def clearFileCache = { ip, port ->
-        httpTest {
-            endpoint ""
-            uri ip + ":" + port + """/api/file_cache?op=clear&sync=true"""
-            op "get"
-            body ""
-        }
+    // Capture the IDs before DROP so the final check can still address this case's tablets.
+    def tableTabletIds = [table, "supplier"].collectEntries { tableName ->
+        def ids = sql("show tablets from ${tableName}").collect { it[0] as Long }.unique()
+        assertTrue(!ids.isEmpty(), "No tablets found for ${tableName}")
+        [(tableName): ids]
     }
+    logger.info("Case table tablets: ${tableTabletIds}")
 
-    def getMetricsMethod = { ip, port, check_func ->
-        httpTest {
-            endpoint ip + ":" + port
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
+    def getGlobalTtlBytes = {
+        [source: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[sourceCluster], "ttl_cache_size"),
+         target: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[targetCluster], "ttl_cache_size")]
+    }
+    def clearCacheAndLogTtl = { List<String> clustersToClear, String phase ->
+        def before = getGlobalTtlBytes()
+        clustersToClear.each { clusterName ->
+            clusterBackends[clusterName].each { be ->
+                WarmupMetricsUtils.clearFileCache(be.ip, be.httpPort)
+            }
         }
+        // sync=true can leave referenced blocks pending recycle. Record the residual baseline;
+        // it is diagnostic data, not an assertion that either BE's entire cache must be empty.
+        def after = getGlobalTtlBytes()
+        logger.info("TTL cache clear phase=${phase}, bytes_before=${before}, bytes_after=${after}, " +
+                "source_minus_target_after_clear=${after.source - after.target}, " +
+                "source_change_after_clear=${after.source - before.source}, " +
+                "target_change_after_clear=${after.target - before.target}")
+        return after
     }
-
-    clearFileCache.call(ipList[0], httpPortList[0]);
-    clearFileCache.call(ipList[1], httpPortList[1]);
+    def getTableTtlBytes = { String clusterName ->
+        sql "use @${clusterName}"
+        def backendIds = clusterBackends[clusterName].collect { it.id }.join(',')
+        tableTabletIds.collectEntries { tableName, ids ->
+            def rows = sql """select coalesce(sum(size), 0)
+                from information_schema.file_cache_info
+                where tablet_id in (${ids.join(',')})
+                  and be_id in (${backendIds}) and lower(type) = 'ttl'"""
+            [(tableName): rows[0][0] as Long]
+        }
+    }
+    def initialTtlAfterClear = clearCacheAndLogTtl([sourceCluster, targetCluster], "before_load")
 
     def load_customer_once =  { 
         def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
@@ -172,47 +197,33 @@ suite("test_warm_up_compute_group") {
     }
     waitJobDone(jobId_)
     
-    sleep(30000)
-    long ttl_cache_size = 0
-    getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    ttl_cache_size = line.substring(i).toLong()
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
-    }
-
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    assertEquals(ttl_cache_size, line.substring(i).toLong())
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
+    // Cache metadata is published asynchronously. Compare each table independently so that
+    // missing bytes in one table cannot be hidden by extra bytes in the other table.
+    long cacheDeadline = System.currentTimeMillis() + 120000L
+    def sourceTableBytes = [:]
+    def targetTableBytes = [:]
+    boolean cacheMatches = false
+    while (System.currentTimeMillis() < cacheDeadline) {
+        sourceTableBytes = getTableTtlBytes(sourceCluster)
+        targetTableBytes = getTableTtlBytes(targetCluster)
+        logger.info("Case table TTL cache bytes: source=${sourceTableBytes}, target=${targetTableBytes}")
+        cacheMatches = tableTabletIds.keySet().every { tableName ->
+            sourceTableBytes[tableName] > 0L &&
+                    sourceTableBytes[tableName] == targetTableBytes[tableName]
+        }
+        if (cacheMatches) {
+            break
+        }
+        sleep(1000)
     }
+    def ttlAfterWarmup = getGlobalTtlBytes()
+    logger.info("Global TTL cache bytes after warmup=${ttlAfterWarmup}, " +
+            "source_minus_target=${ttlAfterWarmup.source - ttlAfterWarmup.target}, " +
+            "baseline_after_clear=${initialTtlAfterClear}, " +
+            "source_change_since_clear=${ttlAfterWarmup.source - initialTtlAfterClear.source}, " +
+            "target_change_since_clear=${ttlAfterWarmup.target - initialTtlAfterClear.target}")
+    assertTrue(cacheMatches, "Case table TTL cache bytes must be positive and match on both clusters: " +
+            "source=${sourceTableBytes}, target=${targetTableBytes}, tablets=${tableTabletIds}")
 
     try {
         sql "WARM UP COMPUTE GROUP regression_cluster_name1 WITH COMPUTE GROUP regression_cluster_name2"
@@ -238,27 +249,21 @@ suite("test_warm_up_compute_group") {
     sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
     sql new File("""${context.file.parent}/../ddl/supplier_delete.sql""").text
 
-    clearFileCache.call(ipList[1], httpPortList[1]);
+    clearCacheAndLogTtl([targetCluster], "after_drop")
     jobId_ = sql "WARM UP COMPUTE GROUP regression_cluster_name1 WITH COMPUTE GROUP regression_cluster_name0"
     waitJobDone(jobId_)
-    sleep(40000)
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def j = line.indexOf(' ')
-                    assertEquals(0, line.substring(j).toLong())
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
+    long droppedCacheDeadline = System.currentTimeMillis() + 120000L
+    def droppedTableBytes = [:]
+    boolean droppedCacheCleared = false
+    while (System.currentTimeMillis() < droppedCacheDeadline) {
+        droppedTableBytes = getTableTtlBytes(targetCluster)
+        droppedCacheCleared = droppedTableBytes.values().every { it == 0L }
+        if (droppedCacheCleared) {
+            break
+        }
+        logger.info("Waiting for dropped table TTL cache to clear on ${targetCluster}: ${droppedTableBytes}")
+        sleep(1000)
     }
+    assertTrue(droppedCacheCleared,
+            "Dropped tables must have no TTL cache on ${targetCluster}: ${droppedTableBytes}")
 }
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_partition.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_partition.groovy
index fce44d37e36..c73c818a05c 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_partition.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_partition.groovy
@@ -15,184 +15,271 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.WarmupMetricsUtils
 
-suite("test_warm_up_partition") {
-    def custoBeConfig = [
-        enable_evict_file_cache_in_advance : false,
-        file_cache_enter_disk_resource_limit_mode_percent : 99
-    ]
-    setBeConfigTemporary(custoBeConfig) {
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="12000") """
-    def getJobState = { jobId ->
-         def jobStateResult = sql """  SHOW WARM UP JOB WHERE ID = ${jobId} """
-         return jobStateResult[0]
+suite("test_warm_up_partition", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
     }
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
 
-    List<String> ipList = new ArrayList<>();
-    List<String> hbPortList = new ArrayList<>()
-    List<String> httpPortList = new ArrayList<>()
-    List<String> brpcPortList = new ArrayList<>()
-    List<String> beUniqueIdList = new ArrayList<>()
-
-    String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
-    int num = 0
-    for(String values : bes) {
-        if (num++ == 2) break;
-        println("the value is " + values);
-        String[] beInfo = values.split(':');
-        ipList.add(beInfo[0]);
-        hbPortList.add(beInfo[1]);
-        httpPortList.add(beInfo[2]);
-        beUniqueIdList.add(beInfo[3]);
-        brpcPortList.add(beInfo[4]);
+    String sourceCluster = "regression_cluster_name0"
+    String targetCluster = "regression_cluster_name1"
+    String tableName = "customer"
+    def partitionNames = ["p1", "p2", "p3"]
+    long ttlSeconds = 12000L
+    def partitionInfo = [:]
+    def warmupJobs = [] as Set
+    def pendingLoads = [] as Set
+    def backends = sql_return_maparray("SHOW BACKENDS")
+    def clusterBackends = [sourceCluster, targetCluster].collectEntries { clusterName ->
+        def members = backends.findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collect { be ->
+            [id: be.BackendId as Long, ip: be.Host.toString(),
+             httpPort: be.HttpPort.toString(), brpcPort: be.BrpcPort.toString()]
+        }
+        assertTrue(!members.isEmpty(), "No alive backends in ${clusterName}")
+        [(clusterName): members]
     }
+    logger.info("Partition warmup backends: ${clusterBackends}")
 
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
-    println("the brpc port is " + brpcPortList);
-    
-    def s3BucketName = getS3BucketName()
-    def s3WithProperties = """WITH S3 (
-        |"AWS_ACCESS_KEY" = "${getS3AK()}",
-        |"AWS_SECRET_KEY" = "${getS3SK()}",
-        |"AWS_ENDPOINT" = "${getS3Endpoint()}",
-        |"AWS_REGION" = "${getS3Region()}",
-        |"provider" = "${getS3Provider()}")
-        |PROPERTIES(
-        |"exec_mem_limit" = "8589934592",
-        |"load_parallelism" = "3")""".stripMargin()
-    
-    
-
-    sql "use @regression_cluster_name0"
-
-    def table = "customer"
-    sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
-    // create table if not exists
-    sql (new File("""${context.file.parent}/../ddl/customer_with_partition.sql""").text + ttlProperties)
-    sleep(10000)
-
-    def load_customer_once =  { 
-        def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
-        def loadLabel = table + "_" + uniqueID
-        // load data from cos
-        def loadSql = new File("""${context.file.parent}/../ddl/${table}_load.sql""").text.replaceAll("\\\$\\{s3BucketName\\}", s3BucketName)
-        loadSql = loadSql.replaceAll("\\\$\\{loadLabel\\}", loadLabel) + s3WithProperties
-        sql loadSql
-
-        // check load state
-        while (true) {
-            def stateResult = sql "show load where Label = '${loadLabel}'"
-            def loadState = stateResult[stateResult.size() - 1][2].toString()
-            if ("CANCELLED".equalsIgnoreCase(loadState)) {
-                throw new IllegalStateException("load ${loadLabel} failed.")
-            } else if ("FINISHED".equalsIgnoreCase(loadState)) {
-                break
+    def waitUntil = { String description, long timeoutMs, Closure condition ->
+        long deadline = System.currentTimeMillis() + timeoutMs
+        while (System.currentTimeMillis() < deadline) {
+            if (condition()) {
+                return
             }
-            sleep(5000)
+            sleep(1000)
         }
+        assertTrue(false, "Timeout after ${timeoutMs}ms: ${description}; see last state in log")
     }
-
-    def clearFileCache = { ip, port ->
-        httpTest {
-            endpoint ""
-            uri ip + ":" + port + """/api/file_cache?op=clear&sync=true"""
-            op "get"
-            body ""
+    def getGlobalTtlBytes = {
+        [source: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[sourceCluster], "ttl_cache_size"),
+         target: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[targetCluster], "ttl_cache_size")]
+    }
+    def getPartitionCache = { String clusterName ->
+        sql "use @${clusterName}"
+        def tabletIds = partitionInfo.values().collectMany { it.tabletIds }
+        def beIds = clusterBackends[clusterName].collect { it.id }
+        // Read all cache types: a NORMAL block in an untouched partition is also unexpected.
+        def rows = sql """select tablet_id, lower(type), sum(size), count(*)
+            from information_schema.file_cache_info
+            where tablet_id in (${tabletIds.join(',')}) and be_id in (${beIds.join(',')})
+            group by tablet_id, lower(type)"""
+        partitionInfo.collectEntries { name, info ->
+            def partitionRows = rows.findAll { (it[0] as Long) in info.tabletIds }
+            def ttlRows = partitionRows.findAll { it[1]?.toString() == "ttl" }
+            def ttlByTablet = ttlRows.collectEntries { [(it[0] as Long): it[2] as Long] }
+            [(name): [ttlBytes: ttlByTablet.values().sum(0L) as Long, ttlByTablet: ttlByTablet,
+                      complete: info.tabletIds.every { (ttlByTablet[it] ?: 0L) > 0L },
+                      blocks: partitionRows.sum(0L) { it[3] as Long },
+                      nonTtlBlocks: partitionRows.findAll { it[1]?.toString() != "ttl" }
+                              .sum(0L) { it[3] as Long }]]
         }
     }
-
-    def getMetricsMethod = { ip, port, check_func ->
-        httpTest {
-            endpoint ip + ":" + port
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
+    def waitForTargetTablets = {
+        sql "use @${targetCluster}"
+        def targetBes = clusterBackends[targetCluster].collectEntries { [(it.id): it] }
+        waitUntil("target primary routing and tablet metadata", 120000L) {
+            def missing = [:]
+            partitionInfo.each { name, info ->
+                def ready = [] as Set
+                def tablets = sql_return_maparray("show tablets from ${tableName} partition ${name}")
+                tablets.each { tablet ->
+                    long id = tablet.TabletId as Long
+                    long primary = tablet.PrimaryBackendId as Long
+                    if (info.tabletIds.contains(id) && targetBes.containsKey(primary) &&
+                            (tablet.BackendId as Long) == primary) {
+                        def be = targetBes[primary]
+                        try {
+                            // Fetch metadata only; a target data query could populate the cache itself.
+                            httpTest {
+                                endpoint "${be.ip}:${be.httpPort}"
+                                uri "/api/meta/header/${id}"
+                                op "get"
+                                check { code, body ->
+                                    if ((code as int) == 200) {
+                                        def meta = parseJson(body.toString())
+                                        // Cloud header JSON omits rowsets. The source baseline and
+                                        // per-partition warm-up assertions verify the loaded data.
+                                        if ((meta.tablet_id as Long) == id &&
+                                                (meta.ttl_seconds as Long) == ttlSeconds &&
+                                                meta.tablet_state == "PB_RUNNING" && meta.schema != null) {
+                                            ready.add(id)
+                                        }
+                                    }
+                                }
+                            }
+                        } catch (Exception e) {
+                            logger.info("Waiting for ${name} tablet ${id} on BE ${primary}: ${e.message}")
+                        }
+                    }
+                }
+                missing[name] = [tablets: info.tabletIds - ready, loadedSourceVersion: info.version]
+            }
+            logger.info("Target tablet readiness: ${missing}")
+            missing.values().every { it.tablets.isEmpty() }
         }
     }
 
-    clearFileCache.call(ipList[0], httpPortList[0]);
-    clearFileCache.call(ipList[1], httpPortList[1]);
-    
-    load_customer_once()
+    setBeConfigTemporary([
+        enable_evict_file_cache_in_advance: false,
+        file_cache_enter_disk_resource_limit_mode_percent: 99
+    ]) {
+        try {
+            sql "use @${sourceCluster}"
+            sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+            sql (new File("${context.file.parent}/../ddl/customer_with_partition.sql").text + """
+                PROPERTIES("file_cache_ttl_seconds"="${ttlSeconds}", "disable_auto_compaction"="true")""")
+            partitionNames.each { name ->
+                def ids = sql("show tablets from ${tableName} partition ${name}")
+                        .collect { it[0] as Long }.unique()
+                assertTrue(!ids.isEmpty(), "Missing tablets for ${name}")
+                partitionInfo[name] = [tabletIds: ids]
+            }
 
-    def jobId_ = sql "warm up cluster regression_cluster_name1 with table customer partition p1;"
-    def waitJobDone = { jobId ->
-        int retryTime = 120
-        int i = 0
-        for (; i < retryTime; i++) {
-            sleep(1000)
-            def statuses = getJobState(jobId[0][0])
-            if (statuses.any { it != null && it.equals("CANCELLED") }) {
-                assertTrue(false);
+            def ttlBeforeClear = getGlobalTtlBytes()
+            clusterBackends.values().flatten().each { be ->
+                WarmupMetricsUtils.clearFileCache(be.ip, be.httpPort)
             }
-            if (statuses.any { it != null && it.equals("FINISHED") }) {
-                break;
+            // sync=true can leave held blocks from other tables. Keep global totals for diagnosis only.
+            def ttlAfterClear = getGlobalTtlBytes()
+            logger.info("TTL cache clear: before=${ttlBeforeClear}, after=${ttlAfterClear}, " +
+                    "source_minus_target_after_clear=${ttlAfterClear.source - ttlAfterClear.target}, " +
+                    "source_change_after_clear=${ttlAfterClear.source - ttlBeforeClear.source}, " +
+                    "target_change_after_clear=${ttlAfterClear.target - ttlBeforeClear.target}")
+
+            String loadLabel = "customer_${UUID.randomUUID().toString().replace('-', '')}"
+            String loadSql = new File("${context.file.parent}/../ddl/customer_load.sql").text
+                    .replace('${s3BucketName}', getS3BucketName()).replace('${loadLabel}', loadLabel)
+            sql (loadSql + """WITH S3 (
+                "AWS_ACCESS_KEY" = "${getS3AK()}", "AWS_SECRET_KEY" = "${getS3SK()}",
+                "AWS_ENDPOINT" = "${getS3Endpoint()}", "AWS_REGION" = "${getS3Region()}",
+                "provider" = "${getS3Provider()}")
+                PROPERTIES("exec_mem_limit"="8589934592", "load_parallelism"="3")""")
+            pendingLoads.add(loadLabel)
+            waitUntil("load ${loadLabel} to finish", 300000L) {
+                def rows = sql "show load where Label = '${loadLabel}'"
+                logger.info("Partition warmup load state: ${rows}")
+                if (rows.isEmpty()) {
+                    return false
+                }
+                String state = rows.last()[2].toString()
+                assertTrue(!state.equalsIgnoreCase("CANCELLED"), "Load ${loadLabel} cancelled: ${rows.last()}")
+                if (state.equalsIgnoreCase("FINISHED")) {
+                    pendingLoads.remove(loadLabel)
+                    return true
+                }
+                false
             }
-        }
-        if (i == retryTime) {
-            sql "cancel warm up job where id = ${jobId[0][0]}"
-            assertTrue(false);
-        }
-    }
-    waitJobDone(jobId_)
-    jobId_ = sql "warm up cluster regression_cluster_name1 with table customer partition p2;"
-    waitJobDone(jobId_)
-    jobId_ = sql "warm up cluster regression_cluster_name1 with table customer partition p3;"
-    waitJobDone(jobId_)
+            def partitions = sql_return_maparray("show partitions from ${tableName}")
+            partitionNames.each { name ->
+                def partition = partitions.find { it.PartitionName.toString() == name }
+                assertTrue(partition != null && (partition.VisibleVersion as Long) > 1L,
+                        "Missing loaded version for ${name}: ${partitions}")
+                partitionInfo[name].version = partition.VisibleVersion as Long
+            }
+            logger.info("Loaded partitions: ${partitionInfo}")
+            waitForTargetTablets()
 
-    sleep(30000)
-    long ttl_cache_size = 0
-    getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    ttl_cache_size = line.substring(i).toLong()
-                    flag = true
-                    break
+            def sourceBaseline = [:]
+            long stableSince = System.currentTimeMillis()
+            waitUntil("source partitions to have stable nonempty TTL cache", 120000L) {
+                def current = getPartitionCache(sourceCluster)
+                logger.info("Source partition cache before warmup: ${current}")
+                if (current != sourceBaseline) {
+                    sourceBaseline = current
+                    stableSince = System.currentTimeMillis()
                 }
+                current.values().every { it.complete && it.nonTtlBlocks == 0L } &&
+                        System.currentTimeMillis() - stableSince >= 3000L
             }
-            assertTrue(flag)
-    }
+            def targetBefore = getPartitionCache(targetCluster)
+            assertTrue(targetBefore.values().every { it.blocks == 0L },
+                    "Target partitions must be cold before warmup: ${targetBefore}")
 
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+            def warmedPartitions = [] as Set
+            partitionNames.each { name ->
+                // Explicit partition warmup does not require hotspot queries or hotspot collection.
+                def job = sql "WARM UP CLUSTER ${targetCluster} WITH TABLE ${tableName} PARTITION ${name}"
+                assertTrue(!job.isEmpty(), "No warmup job returned for ${name}")
+                def jobId = job[0][0]
+                warmupJobs.add(jobId)
+                waitUntil("warmup ${name} job ${jobId} to finish", 120000L) {
+                    def rows = sql "SHOW WARM UP JOB WHERE ID = ${jobId}"
+                    logger.info("Partition ${name} warmup job ${jobId}: ${rows}")
+                    if (rows.isEmpty()) {
+                        return false
+                    }
+                    def states = rows[0].collect { it?.toString() }
+                    assertTrue(!states.contains("CANCELLED"), "Warmup ${name} job ${jobId} cancelled: ${rows[0]}")
+                    states.contains("FINISHED")
+                }
+                warmedPartitions.add(name)
+                waitUntil("after ${name}: partition TTL cache must match source and untouched partitions stay cold",
+                        120000L) {
+                    def source = getPartitionCache(sourceCluster)
+                    def target = getPartitionCache(targetCluster)
+                    logger.info("After ${name} job ${jobId}: warmed=${warmedPartitions}, " +
+                            "source=${source}, target=${target}, baseline=${sourceBaseline}")
+                    // Fail immediately on scope/type violations; waiting must not hide unintended warmup.
+                    partitionNames.each { partitionName ->
+                        if (!warmedPartitions.contains(partitionName)) {
+                            assertTrue(target[partitionName].blocks == 0L,
+                                    "Untouched partition ${partitionName} has cache after ${name}: ${target}")
+                        } else {
+                            assertTrue(target[partitionName].nonTtlBlocks == 0L,
+                                    "Expected only TTL cache for ${partitionName}: ${target[partitionName]}")
+                        }
+                    }
+                    partitionNames.every { partitionName ->
+                        def src = source[partitionName]
+                        def dst = target[partitionName]
+                        src.complete && src.nonTtlBlocks == 0L &&
+                                src.ttlByTablet == sourceBaseline[partitionName].ttlByTablet &&
+                                (!warmedPartitions.contains(partitionName) ||
+                                 (dst.complete && dst.ttlBytes == src.ttlBytes && dst.ttlByTablet == src.ttlByTablet))
                     }
-                    def i = line.indexOf(' ')
-                    assertEquals(ttl_cache_size, line.substring(i).toLong())
-                    flag = true
-                    break
                 }
             }
-            assertTrue(flag)
-    }
-    
-    try {
-        sql "warm up cluster regression_cluster_name1 with table customer partition p5;"
-        assertTrue(false)
-    } catch (Exception e) {
-        assertTrue(true)
-    }
-    sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
+            sql "use @${targetCluster}"
+            test {
+                sql "WARM UP CLUSTER ${targetCluster} WITH TABLE ${tableName} PARTITION p5"
+                exception "The partition p5 doesn't exist"
+            }
+        } finally {
+            warmupJobs.each { jobId ->
+                try {
+                    def rows = sql "SHOW WARM UP JOB WHERE ID = ${jobId}"
+                    if (!rows.isEmpty() && rows[0].any { it?.toString() in ["PENDING", "RUNNING"] }) {
+                        sql "CANCEL WARM UP JOB WHERE ID = ${jobId}"
+                    }
+                } catch (Exception e) {
+                    logger.warn("Failed to clean up warmup ${jobId}: ${e.message}")
+                }
+            }
+            // Keep DROP in finally so even a cleanup SQL failure does not leave the test table behind.
+            try {
+                sql "use @${sourceCluster}"
+                pendingLoads.each { label ->
+                    try {
+                        sql "CANCEL LOAD WHERE LABEL = '${label}'"
+                    } catch (Exception e) {
+                        logger.warn("Failed to clean up load ${label}: ${e.message}")
+                    }
+                }
+            } finally {
+                sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+            }
+        }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_same_table_multi_times.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_same_table_multi_times.groovy
index b774273f952..cc09dabff4e 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_same_table_multi_times.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_same_table_multi_times.groovy
@@ -17,7 +17,19 @@
 
 import org.codehaus.groovy.runtime.IOGroovyMethods
 
-suite("test_warm_up_same_table_multi_times") {
+suite("test_warm_up_same_table_multi_times", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
+    }
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
+
     def custoBeConfig = [
         enable_evict_file_cache_in_advance : false,
         file_cache_enter_disk_resource_limit_mode_percent : 99
@@ -115,6 +127,26 @@ suite("test_warm_up_same_table_multi_times") {
         }
     }
 
+    def createdWarmUpJobIds = [] as Set
+    def recordWarmUpJob = { jobId ->
+        if (jobId != null && !jobId.isEmpty()) {
+            createdWarmUpJobIds.add(jobId[0][0])
+        }
+    }
+    def cleanupWarmUpJobs = {
+        createdWarmUpJobIds.each { jobId ->
+            try {
+                def statuses = getJobState(jobId)
+                if (statuses.any { it != null && (it.equals("PENDING") || it.equals("RUNNING")) }) {
+                    sql "cancel warm up job where id = ${jobId}"
+                }
+            } catch (Exception e) {
+                logger.info("ignore warm up job cleanup failure, job id: ${jobId}, error: ${e.getMessage()}")
+            }
+        }
+    }
+
+    try {
     clearFileCache.call();
     sleep(30000)
 
@@ -124,11 +156,12 @@ suite("test_warm_up_same_table_multi_times") {
     load_customer_once()
 
     def jobId = sql "warm up cluster ${validCluster} with table customer;"
+    recordWarmUpJob(jobId)
     try {
         sql "warm up cluster ${validCluster} with table customer;"
-        assertTrue(true) // dup warm up command can be send to fe queue now
-    } catch (Exception e) {
         assertTrue(false)
+    } catch (Exception e) {
+        assertTrue("${e.getMessage()}".contains("already has a pending job"))
     }
     int retryTime = 120
     int j = 0
@@ -191,6 +224,7 @@ suite("test_warm_up_same_table_multi_times") {
 
     // AGAIN!
     jobId = sql "warm up cluster ${validCluster} with table customer;"
+    recordWarmUpJob(jobId)
 
     retryTime = 120
     j = 0
@@ -273,5 +307,8 @@ suite("test_warm_up_same_table_multi_times") {
     long diff = skip_io_bytes_end - skip_io_bytes_start;
     println("skip_io_bytes diff: " + diff);
     assertTrue(diff > 1000);
+    } finally {
+        cleanupWarmUpJobs()
+    }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_table.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_table.groovy
index 4e4979ab79c..0b5da4bcdd3 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_table.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_table.groovy
@@ -15,9 +15,22 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.Http
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite("test_warm_up_table", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
+    }
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
 
-suite("test_warm_up_table") {
     def custoBeConfig = [
         enable_evict_file_cache_in_advance : false,
         file_cache_enter_disk_resource_limit_mode_percent : 99
@@ -113,17 +126,104 @@ suite("test_warm_up_table") {
         }
     }
 
-    def getMetricsMethod = { ip, port, check_func ->
-        httpTest {
-            endpoint ip + ":" + port
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
+    def clusterBackends = ["regression_cluster_name0", "regression_cluster_name1"].collectEntries { name ->
+        def members = sql_return_maparray("SHOW BACKENDS").findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == name
+        }.collect { be ->
+            [id: be.BackendId as Long, ip: be.Host.toString(),
+             httpPort: be.HttpPort.toString(), brpcPort: be.BrpcPort.toString()]
+        }
+        assertTrue(!members.isEmpty(), "No alive backends in ${name}")
+        [(name): members]
+    }
+    def tabletIds = []
+    def waitUntil = { String stage, long timeoutMs, Closure ready ->
+        long deadline = System.currentTimeMillis() + timeoutMs
+        while (System.currentTimeMillis() < deadline) {
+            if (ready()) { return }
+            sleep(1000)
+        }
+        assertTrue(false, "Timeout waiting for ${stage}; see last cache state in log")
+    }
+    def getGlobalTtl = {
+        clusterBackends.collectEntries { name, members ->
+            [(name): WarmupMetricsUtils.getBackendMetricSum(members, "ttl_cache_size")]
+        }
+    }
+    def getTableCache = { String clusterName ->
+        sql "use @${clusterName}"
+        def beIds = clusterBackends[clusterName].collect { it.id }
+        def rows = sql """select tablet_id, lower(type), sum(size)
+            from information_schema.file_cache_info
+            where tablet_id in (${tabletIds.join(',')}) and be_id in (${beIds.join(',')})
+            group by tablet_id, lower(type)"""
+        def ttlByTablet = rows.findAll { it[1]?.toString() == "ttl" }
+                .collectEntries { [(it[0] as Long): it[2] as Long] }
+        [ttlByTablet: ttlByTablet, complete: tabletIds.every { (ttlByTablet[it] ?: 0L) > 0L },
+         nonTtlBytes: rows.findAll { it[1]?.toString() != "ttl" }.sum(0L) { it[2] as Long }]
+    }
+    def waitForTargetTablets = {
+        sql "use @regression_cluster_name1"
+        def targetBes = clusterBackends.regression_cluster_name1.collectEntries { [(it.id): it] }
+        waitUntil("target primary tablet metadata", 120000L) {
+            def ready = [] as Set
+            sql_return_maparray("show tablets from ${table}").each { tablet ->
+                long id = tablet.TabletId as Long
+                long primary = tablet.PrimaryBackendId as Long
+                if (id in tabletIds && targetBes.containsKey(primary) && (tablet.BackendId as Long) == primary) {
+                    def be = targetBes[primary]
+                    try {
+                        // Metadata only: a business read here would populate the target cache.
+                        def connection = Http.openConnection(
+                                "http://${be.ip}:${be.httpPort}/api/meta/header/${id}")
+                        connection.connectTimeout = 5000
+                        connection.readTimeout = 5000
+                        try {
+                            def meta = connection.inputStream.withCloseable {
+                                parseJson(it.getText("UTF-8"))
+                            }
+                            if ((meta.tablet_id as Long) == id && (meta.ttl_seconds as Long) == 12000L &&
+                                    meta.tablet_state == "PB_RUNNING" && meta.schema != null) {
+                                ready.add(id)
+                            }
+                        } finally {
+                            connection.disconnect()
+                        }
+                    } catch (Exception e) {
+                        logger.info("Waiting for target tablet ${id} on BE ${primary}: ${e.message}")
+                    }
+                }
+            }
+            logger.info("Target tablet metadata: missing=${tabletIds - ready}")
+            ready.size() == tabletIds.size()
         }
     }
 
-    clearFileCache.call(ipList[0], httpPortList[0]);
-    clearFileCache.call(ipList[1], httpPortList[1]);
+    def createdWarmUpJobIds = [] as Set
+    def recordWarmUpJob = { jobId ->
+        if (jobId != null && !jobId.isEmpty()) {
+            createdWarmUpJobIds.add(jobId[0][0])
+        }
+    }
+    def cleanupWarmUpJobs = {
+        createdWarmUpJobIds.each { jobId ->
+            try {
+                def statuses = getJobState(jobId)
+                if (statuses.any { it != null && (it.equals("PENDING") || it.equals("RUNNING")) }) {
+                    sql "cancel warm up job where id = ${jobId}"
+                }
+            } catch (Exception e) {
+                logger.info("ignore warm up job cleanup failure, job id: ${jobId}, error: ${e.getMessage()}")
+            }
+        }
+    }
+
+    try {
+    def ttlBeforeClear = getGlobalTtl()
+    clusterBackends.values().flatten().each { be -> clearFileCache.call(be.ip, be.httpPort) }
+    def ttlAfterClear = getGlobalTtl()
+    logger.info("Single-table global TTL clear: before=${ttlBeforeClear}, after=${ttlAfterClear}")
     sleep(30000)
 
     load_customer_once()
@@ -140,12 +240,30 @@ suite("test_warm_up_table") {
     load_customer_once()
     load_customer_once()
 
+    tabletIds = sql_return_maparray("show tablets from ${table}")
+            .collect { it.TabletId as Long }.unique().sort()
+    assertTrue(!tabletIds.isEmpty(), "No tablets for ${table}")
+    logger.info("Single-table warmup tablets=${tabletIds}, backends=${clusterBackends}")
+    def sourceCache = null
+    def candidate = null
+    long stableSince = 0L
+    waitUntil("stable nonempty source TTL cache", 120000L) {
+        sourceCache = getTableCache("regression_cluster_name0")
+        boolean complete = sourceCache.complete && sourceCache.nonTtlBytes == 0L
+        if (!complete || sourceCache != candidate) { stableSince = System.currentTimeMillis() }
+        candidate = sourceCache
+        logger.info("Single-table source TTL baseline=${sourceCache}")
+        complete && System.currentTimeMillis() - stableSince >= 3000L
+    }
+    waitForTargetTablets()
+
     def jobId = sql "warm up cluster regression_cluster_name1 with table customer;"
+    recordWarmUpJob(jobId)
     try {
         sql "warm up cluster regression_cluster_name1 with table customer;"
-        assertTrue(true)
-    } catch (Exception e) {
         assertTrue(false)
+    } catch (Exception e) {
+        assertTrue("${e.getMessage()}".contains("already has a pending job"))
     }
     int retryTime = 120
     int j = 0
@@ -166,46 +284,20 @@ suite("test_warm_up_table") {
     def tablesString = getTablesFromShowCommand(jobId[0][0])
 
     assertTrue(tablesString.any { it != null && it.contains("customer") })
-    sleep(30000)
-    long ttl_cache_size = 0
-    getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    ttl_cache_size = line.substring(i).toLong()
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
-    }
-
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    assertEquals(ttl_cache_size, line.substring(i).toLong())
-                    flag = true
-                    break
-                }
-            }
-            assertTrue(flag)
+    // Global TTL includes residual blocks from other tables; assert this table on the target BEs.
+    try {
+        waitUntil("target per-tablet TTL cache to equal the nonzero source baseline", 180000L) {
+            def currentSource = getTableCache("regression_cluster_name0")
+            def targetCache = getTableCache("regression_cluster_name1")
+            logger.info("Single-table TTL cache: source=${currentSource}, target=${targetCache}")
+            currentSource == sourceCache && targetCache.complete && targetCache.nonTtlBytes == 0L &&
+                    targetCache.ttlByTablet == sourceCache.ttlByTablet
+        }
+    } finally {
+        def ttlAfterWarmup = getGlobalTtl()
+        def delta = ttlAfterWarmup.collectEntries { name, bytes -> [(name): bytes - ttlAfterClear[name]] }
+        logger.info("Single-table global TTL after warmup=${ttlAfterWarmup}, " +
+                "baseline_after_clear=${ttlAfterClear}, delta_since_clear=${delta}")
     }
 
     try {
@@ -222,5 +314,9 @@ suite("test_warm_up_table") {
     } catch (Exception e) {
         assertTrue(true)
     }
+    } finally {
+        cleanupWarmUpJobs()
+        sql "DROP TABLE IF EXISTS ${table} FORCE"
+    }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_tables.groovy b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_tables.groovy
index be4521544fe..9a8e1281deb 100644
--- a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_tables.groovy
+++ b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/table/test_warm_up_tables.groovy
@@ -15,233 +15,325 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.WarmupMetricsUtils
 
-suite("test_warm_up_tables") {
-    def custoBeConfig = [
-        enable_evict_file_cache_in_advance : false,
-        file_cache_enter_disk_resource_limit_mode_percent : 99
-    ]
-
-    setBeConfigTemporary(custoBeConfig) {
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="12000") """
-    def getJobState = { jobId ->
-         def jobStateResult = sql """  SHOW WARM UP JOB WHERE ID = ${jobId} """
-         return jobStateResult[0]
-    }
-    def getTablesFromShowCommand = { jobId ->
-         def jobStateResult = sql """  SHOW WARM UP JOB WHERE ID = ${jobId} """
-         return jobStateResult[0]
+suite("test_warm_up_tables", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
     }
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
 
-    List<String> ipList = new ArrayList<>();
-    List<String> hbPortList = new ArrayList<>()
-    List<String> httpPortList = new ArrayList<>()
-    List<String> brpcPortList = new ArrayList<>()
-    List<String> beUniqueIdList = new ArrayList<>()
-
-    String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
-    int num = 0
-    for(String values : bes) {
-        if (num++ == 2) break;
-        println("the value is " + values);
-        String[] beInfo = values.split(':');
-        ipList.add(beInfo[0]);
-        hbPortList.add(beInfo[1]);
-        httpPortList.add(beInfo[2]);
-        beUniqueIdList.add(beInfo[3]);
-        brpcPortList.add(beInfo[4]);
+    String sourceCluster = "regression_cluster_name0"
+    String targetCluster = "regression_cluster_name1"
+    def tableDefinitions = [customer: "customer_with_partition", supplier: "supplier"]
+    long ttlSeconds = 12000L
+    def partitionInfo = [
+        "customer.p1": [table: "customer", partition: "p1"],
+        "customer.p2": [table: "customer", partition: "p2"],
+        "customer.p3": [table: "customer", partition: "p3"],
+        "supplier": [table: "supplier", partition: null]
+    ]
+    def showTabletsSql = { info ->
+        "show tablets from ${info.table}" + (info.partition ? " partition ${info.partition}" : "")
     }
-
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
-    println("the brpc port is " + brpcPortList);
-
-    def s3BucketName = getS3BucketName()
-    def s3WithProperties = """WITH S3 (
-        |"AWS_ACCESS_KEY" = "${getS3AK()}",
-        |"AWS_SECRET_KEY" = "${getS3SK()}",
-        |"AWS_ENDPOINT" = "${getS3Endpoint()}",
-        |"AWS_REGION" = "${getS3Region()}",
-        |"provider" = "${getS3Provider()}")
-        |PROPERTIES(
-        |"exec_mem_limit" = "8589934592",
-        |"load_parallelism" = "3")""".stripMargin()
-    
-    
-    sql """ SET PROPERTY 'default_cloud_cluster' = "regression_cluster_name0"; """
-    sql "use @regression_cluster_name0"
-
-    def table = "customer"
-    sql new File("""${context.file.parent}/../ddl/${table}_delete.sql""").text
-    sql new File("""${context.file.parent}/../ddl/supplier_delete.sql""").text
-    // create table if not exists
-    sql (new File("""${context.file.parent}/../ddl/customer_with_partition.sql""").text + ttlProperties)
-    sql (new File("""${context.file.parent}/../ddl/supplier.sql""").text + ttlProperties)
-    sleep(10000)
-
-    def load_customer_once =  { 
-        def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
-        def loadLabel = table + "_" + uniqueID
-        // load data from cos
-        def loadSql = new File("""${context.file.parent}/../ddl/${table}_load.sql""").text.replaceAll("\\\$\\{s3BucketName\\}", s3BucketName)
-        loadSql = loadSql.replaceAll("\\\$\\{loadLabel\\}", loadLabel) + s3WithProperties
-        sql loadSql
-
-        // check load state
-        while (true) {
-            def stateResult = sql "show load where Label = '${loadLabel}'"
-            def loadState = stateResult[stateResult.size() - 1][2].toString()
-            if ("CANCELLED".equalsIgnoreCase(loadState)) {
-                throw new IllegalStateException("load ${loadLabel} failed.")
-            } else if ("FINISHED".equalsIgnoreCase(loadState)) {
-                break
-            }
-            sleep(5000)
+    def warmupJobs = [] as Set
+    def pendingLoads = [] as Set
+    def backends = sql_return_maparray("SHOW BACKENDS")
+    def clusterBackends = [sourceCluster, targetCluster].collectEntries { clusterName ->
+        def members = backends.findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collect { be ->
+            [id: be.BackendId as Long, ip: be.Host.toString(),
+             httpPort: be.HttpPort.toString(), brpcPort: be.BrpcPort.toString()]
         }
+        assertTrue(!members.isEmpty(), "No alive backends in ${clusterName}")
+        [(clusterName): members]
     }
+    logger.info("Multi-table warmup backends: ${clusterBackends}")
 
-    def load_supplier_once =  { 
-        def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
-        def loadLabel = "supplier_" + uniqueID
-        // load data from cos
-        def loadSql = new File("""${context.file.parent}/../ddl/supplier_load.sql""").text.replaceAll("\\\$\\{s3BucketName\\}", s3BucketName)
-        loadSql = loadSql.replaceAll("\\\$\\{loadLabel\\}", loadLabel) + s3WithProperties
-        sql loadSql
-
-        // check load state
-        while (true) {
-            def stateResult = sql "show load where Label = '${loadLabel}'"
-            def loadState = stateResult[stateResult.size() - 1][2].toString()
-            if ("CANCELLED".equalsIgnoreCase(loadState)) {
-                throw new IllegalStateException("load ${loadLabel} failed.")
-            } else if ("FINISHED".equalsIgnoreCase(loadState)) {
-                break
+    def waitUntil = { String description, long timeoutMs, Closure condition ->
+        long deadline = System.currentTimeMillis() + timeoutMs
+        while (System.currentTimeMillis() < deadline) {
+            if (condition()) {
+                return
             }
-            sleep(5000)
+            sleep(1000)
         }
+        assertTrue(false, "Timeout after ${timeoutMs}ms: ${description}; see last state in log")
     }
-
-    def clearFileCache = { ip, port ->
-        httpTest {
-            endpoint ""
-            uri ip + ":" + port + """/api/file_cache?op=clear&sync=true"""
-            op "get"
-            body ""
+    def getGlobalTtlBytes = {
+        [source: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[sourceCluster], "ttl_cache_size"),
+         target: WarmupMetricsUtils.getBackendMetricSum(clusterBackends[targetCluster], "ttl_cache_size")]
+    }
+    def getPartitionCache = { String clusterName ->
+        sql "use @${clusterName}"
+        def tabletIds = partitionInfo.values().collectMany { it.tabletIds }
+        def beIds = clusterBackends[clusterName].collect { it.id }
+        // Read all cache types: a NORMAL block in an untouched partition is also unexpected.
+        def rows = sql """select tablet_id, lower(type), sum(size), count(*)
+            from information_schema.file_cache_info
+            where tablet_id in (${tabletIds.join(',')}) and be_id in (${beIds.join(',')})
+            group by tablet_id, lower(type)"""
+        partitionInfo.collectEntries { name, info ->
+            def partitionRows = rows.findAll { (it[0] as Long) in info.tabletIds }
+            def ttlRows = partitionRows.findAll { it[1]?.toString() == "ttl" }
+            def ttlByTablet = ttlRows.collectEntries { [(it[0] as Long): it[2] as Long] }
+            [(name): [ttlBytes: ttlByTablet.values().sum(0L) as Long, ttlByTablet: ttlByTablet,
+                      bytesByType: partitionRows.groupBy { it[1]?.toString() }.collectEntries { type, entries ->
+                          [(type): entries.sum(0L) { it[2] as Long }]
+                      },
+                      complete: info.tabletIds.every { (ttlByTablet[it] ?: 0L) > 0L },
+                      blocks: partitionRows.sum(0L) { it[3] as Long },
+                      nonTtlBlocks: partitionRows.findAll { it[1]?.toString() != "ttl" }
+                              .sum(0L) { it[3] as Long }]]
         }
     }
-
-    def getMetricsMethod = { ip, port, check_func ->
-        httpTest {
-            endpoint ip + ":" + port
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
+    def waitForTargetTablets = {
+        sql "use @${targetCluster}"
+        def targetBes = clusterBackends[targetCluster].collectEntries { [(it.id): it] }
+        waitUntil("target primary routing and tablet metadata", 120000L) {
+            def missing = [:]
+            partitionInfo.each { name, info ->
+                def ready = [] as Set
+                def tablets = sql_return_maparray(showTabletsSql(info))
+                tablets.each { tablet ->
+                    long id = tablet.TabletId as Long
+                    long primary = tablet.PrimaryBackendId as Long
+                    if (info.tabletIds.contains(id) && targetBes.containsKey(primary) &&
+                            (tablet.BackendId as Long) == primary) {
+                        def be = targetBes[primary]
+                        try {
+                            // Fetch metadata only; a target data query could populate the cache itself.
+                            httpTest {
+                                endpoint "${be.ip}:${be.httpPort}"
+                                uri "/api/meta/header/${id}"
+                                op "get"
+                                check { code, body ->
+                                    if ((code as int) == 200) {
+                                        def meta = parseJson(body.toString())
+                                        // Cloud header JSON omits rowsets. Source data and cache
+                                        // baselines are checked separately from target readiness.
+                                        if ((meta.tablet_id as Long) == id &&
+                                                (meta.ttl_seconds as Long) == ttlSeconds &&
+                                                meta.tablet_state == "PB_RUNNING" && meta.schema != null) {
+                                            ready.add(id)
+                                        }
+                                    }
+                                }
+                            }
+                        } catch (Exception e) {
+                            logger.info("Waiting for ${name} tablet ${id} on BE ${primary}: ${e.message}")
+                        }
+                    }
+                }
+                missing[name] = [tablets: info.tabletIds - ready, loadedSourceVersion: info.version]
+            }
+            logger.info("Target tablet readiness: ${missing}")
+            missing.values().every { it.tablets.isEmpty() }
         }
     }
 
-    clearFileCache.call(ipList[0], httpPortList[0]);
-    clearFileCache.call(ipList[1], httpPortList[1]);
-    
-    load_customer_once()
-    load_supplier_once()
-
-    def jobId_ = sql "warm up cluster regression_cluster_name1 with table customer partition p1 and table customer partition p2;"
-    def waitJobDone = { jobId ->
-        int retryTime = 120
-        int i = 0
-        for (; i < retryTime; i++) {
-            sleep(1000)
-            def statuses = getJobState(jobId[0][0])
-            if (statuses.any { it != null && it.equals("CANCELLED") }) {
-                assertTrue(false);
-            }
-            if (statuses.any { it != null && it.equals("FINISHED") }) {
-                break;
+    def clearCaches = { String phase, List members ->
+        def before = getGlobalTtlBytes()
+        members.each { be ->
+            httpTest {
+                endpoint "${be.ip}:${be.httpPort}"
+                uri "/api/file_cache?op=clear&sync=true"
+                op "get"
+                check { code, body ->
+                    assertTrue((code as int) == 200, "Clear cache failed on BE ${be.id}: ${code}, ${body}")
+                    def response = parseJson(body.toString())
+                    assertTrue(response.status == "OK", "Clear cache failed on BE ${be.id}: ${body}")
+                    logger.info("${phase} clear BE ${be.id}: ${body}")
+                }
             }
         }
-        if (i == retryTime) {
-            sql "cancel warm up job where id = ${jobId[0][0]}"
-            assertTrue(false);
+        def after = getGlobalTtlBytes()
+        // Busy blocks belonging to other tables must not determine whether this case passes.
+        logger.info("${phase} global TTL: before=${before}, after=${after}, " +
+                "source_minus_target_after_clear=${after.source - after.target}, " +
+                "source_change_after_clear=${after.source - before.source}, " +
+                "target_change_after_clear=${after.target - before.target}")
+    }
+    def waitForTargetCold = { String phase ->
+        waitUntil("${phase}: target test-table cache cleanup incomplete", 120000L) {
+            def cache = getPartitionCache(targetCluster)
+            logger.info("${phase} target cache: ${cache}")
+            cache.values().every { it.blocks == 0L }
         }
     }
-    waitJobDone(jobId_)
 
-    jobId_ = sql "warm up cluster regression_cluster_name1 with table customer partition p3 and table supplier;"
-    waitJobDone(jobId_);
-
-    def tablesString = getTablesFromShowCommand(jobId_[0][0])
-    assertTrue(tablesString.any { it != null && it.contains("customer") })
-    assertTrue(tablesString.any { it != null && it.contains("supplier") })
+    setBeConfigTemporary([
+        enable_evict_file_cache_in_advance: false,
+        file_cache_enter_disk_resource_limit_mode_percent: 99
+    ]) {
+        try {
+            sql "use @${sourceCluster}"
+            String dbName = sql("select database()")[0][0].toString()
+            assertTrue(!dbName.isEmpty(), "Missing database for warmup Tables assertions")
+            tableDefinitions.each { tableName, ddlName ->
+                sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+                sql (new File("${context.file.parent}/../ddl/${ddlName}.sql").text + """
+                    PROPERTIES("file_cache_ttl_seconds"="${ttlSeconds}", "disable_auto_compaction"="true")""")
+            }
+            partitionInfo.each { name, info ->
+                def ids = sql(showTabletsSql(info)).collect { it[0] as Long }.unique()
+                assertTrue(!ids.isEmpty(), "Missing tablets for ${name}")
+                info.tabletIds = ids
+            }
+            clearCaches("initial", clusterBackends.values().flatten())
 
-    sleep(30000)
-    long ttl_cache_size = 0
-    getMetricsMethod.call(ipList[0], brpcPortList[0]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+            tableDefinitions.keySet().each { tableName ->
+                String label = "${tableName}_${UUID.randomUUID().toString().replace('-', '')}"
+                String loadSql = new File("${context.file.parent}/../ddl/${tableName}_load.sql").text
+                        .replace('${s3BucketName}', getS3BucketName()).replace('${loadLabel}', label)
+                sql (loadSql + """WITH S3 (
+                    "AWS_ACCESS_KEY" = "${getS3AK()}", "AWS_SECRET_KEY" = "${getS3SK()}",
+                    "AWS_ENDPOINT" = "${getS3Endpoint()}", "AWS_REGION" = "${getS3Region()}",
+                    "provider" = "${getS3Provider()}")
+                    PROPERTIES("exec_mem_limit"="8589934592", "load_parallelism"="3")""")
+                pendingLoads.add(label)
+                waitUntil("load ${label} to finish", 300000L) {
+                    def rows = sql "show load where Label = '${label}'"
+                    logger.info("Multi-table warmup load state: ${rows}")
+                    if (rows.isEmpty()) {
+                        return false
                     }
-                    def i = line.indexOf(' ')
-                    ttl_cache_size = line.substring(i).toLong()
-                    flag = true
-                    break
+                    String state = rows.last()[2].toString()
+                    assertTrue(!state.equalsIgnoreCase("CANCELLED"), "Load ${label} cancelled: ${rows.last()}")
+                    if (state.equalsIgnoreCase("FINISHED")) {
+                        pendingLoads.remove(label)
+                        return true
+                    }
+                    false
                 }
-            }
-            assertTrue(flag)
-    }
-
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+                def partitions = sql_return_maparray("show partitions from ${tableName}")
+                partitionInfo.findAll { name, info -> info.table == tableName }.each { name, info ->
+                    def matching = partitions.findAll {
+                        info.partition == null || it.PartitionName.toString() == info.partition
                     }
-                    def i = line.indexOf(' ')
-                    assertEquals(ttl_cache_size, line.substring(i).toLong())
-                    flag = true
-                    break
+                    assertTrue(matching.size() == 1 && (matching[0].VisibleVersion as Long) > 1L,
+                            "Missing loaded version for ${name}: ${partitions}")
+                    info.version = matching[0].VisibleVersion as Long
                 }
             }
-            assertTrue(flag)
-    }
+            logger.info("Loaded table/partition tablets: ${partitionInfo}")
+            waitForTargetTablets()
 
-    clearFileCache.call(ipList[1], httpPortList[1]);
-    jobId_ = sql "warm up cluster regression_cluster_name1 with table customer and table supplier;"
-    waitJobDone(jobId_);
+            def sourceBaseline = [:]
+            long stableSince = System.currentTimeMillis()
+            waitUntil("source tables to have stable nonempty TTL cache", 120000L) {
+                def current = getPartitionCache(sourceCluster)
+                logger.info("Source cache before warmup: ${current}")
+                if (current != sourceBaseline) {
+                    sourceBaseline = current
+                    stableSince = System.currentTimeMillis()
+                }
+                current.values().every { it.complete && it.nonTtlBlocks == 0L } &&
+                        System.currentTimeMillis() - stableSince >= 3000L
+            }
+            waitForTargetCold("before first warmup")
 
-    sleep(30000)
-    getMetricsMethod.call(ipList[1], brpcPortList[1]) {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+            def stages = [
+                [name: "customer_p1_p2",
+                 clause: "TABLE customer PARTITION p1 AND TABLE customer PARTITION p2",
+                 tables: ["customer.p1", "customer.p2"], warmed: ["customer.p1", "customer.p2"]],
+                [name: "customer_p3_and_supplier",
+                 clause: "TABLE customer PARTITION p3 AND TABLE supplier",
+                 tables: ["customer.p3", "supplier"], warmed: partitionInfo.keySet().toList()],
+                [name: "whole_tables", clause: "TABLE customer AND TABLE supplier",
+                 tables: ["customer", "supplier"], warmed: partitionInfo.keySet().toList()]
+            ]
+            stages.each { stage ->
+                if (stage.name == "whole_tables") {
+                    // Keep the source baseline, but require both target tables to become cold again.
+                    clearCaches("before whole_tables", clusterBackends[targetCluster])
+                    waitForTargetCold("before whole_tables")
+                }
+                sql "use @${targetCluster}"
+                // These explicit table/partition jobs do not depend on hotspot collection.
+                def job = sql "WARM UP CLUSTER ${targetCluster} WITH ${stage.clause}"
+                assertTrue(!job.isEmpty(), "No warmup job returned for ${stage.name}")
+                def jobId = job[0][0]
+                warmupJobs.add(jobId)
+                def expectedTables = stage.tables.collect { "${dbName}.${it}".toString() }.sort()
+                waitUntil("warmup ${stage.name} job ${jobId} to finish", 120000L) {
+                    def rows = sql_return_maparray("SHOW WARM UP JOB WHERE ID = ${jobId}")
+                    logger.info("${stage.name} warmup job ${jobId}: ${rows}")
+                    if (rows.isEmpty()) {
+                        return false
+                    }
+                    assertTrue(rows.size() == 1, "Unexpected warmup rows for ${jobId}: ${rows}")
+                    def state = rows[0]
+                    assertTrue(state.JobId.toString() == jobId.toString(), "Wrong warmup job: ${state}")
+                    assertTrue(state.DstComputeGroup == targetCluster, "Wrong warmup target: ${state}")
+                    assertTrue(state.Type == "TABLE", "Wrong warmup type: ${state}")
+                    def actualTables = (state.Tables ?: "").toString().split(',').collect { it.trim() }.sort()
+                    assertTrue(actualTables == expectedTables,
+                            "Wrong Tables for ${stage.name}: expected=${expectedTables}, actual=${actualTables}")
+                    assertTrue(state.Status != "CANCELLED", "Warmup ${stage.name} cancelled: ${state}")
+                    state.Status == "FINISHED"
+                }
+                waitUntil("after ${stage.name}: per-tablet TTL must match source and untouched tables stay cold",
+                        120000L) {
+                    def source = getPartitionCache(sourceCluster)
+                    def target = getPartitionCache(targetCluster)
+                    logger.info("After ${stage.name} job ${jobId}: source=${source}, target=${target}, " +
+                            "baseline=${sourceBaseline}")
+                    partitionInfo.keySet().each { name ->
+                        if (!stage.warmed.contains(name)) {
+                            assertTrue(target[name].blocks == 0L,
+                                    "Untouched ${name} has cache after ${stage.name}: ${target[name]}")
+                        } else {
+                            assertTrue(target[name].nonTtlBlocks == 0L,
+                                    "Expected only TTL cache for ${name}: ${target[name]}")
+                        }
+                    }
+                    partitionInfo.keySet().every { name ->
+                        def src = source[name]
+                        def dst = target[name]
+                        src.complete && src.nonTtlBlocks == 0L &&
+                                src.ttlByTablet == sourceBaseline[name].ttlByTablet &&
+                                (!stage.warmed.contains(name) ||
+                                 (dst.complete && dst.ttlBytes == src.ttlBytes && dst.ttlByTablet == src.ttlByTablet))
                     }
-                    def i = line.indexOf(' ')
-                    assertEquals(ttl_cache_size, line.substring(i).toLong())
-                    flag = true
-                    break
                 }
             }
-            assertTrue(flag)
-    }
+        } finally {
+            warmupJobs.each { jobId ->
+                try {
+                    def rows = sql_return_maparray("SHOW WARM UP JOB WHERE ID = ${jobId}")
+                    if (!rows.isEmpty() && rows[0].Status in ["PENDING", "RUNNING"]) {
+                        sql "CANCEL WARM UP JOB WHERE ID = ${jobId}"
+                    }
+                } catch (Exception e) {
+                    logger.warn("Failed to clean up warmup ${jobId}: ${e.message}")
+                }
+            }
+            try {
+                sql "use @${sourceCluster}"
+                pendingLoads.each { label ->
+                    try {
+                        sql "CANCEL LOAD WHERE LABEL = '${label}'"
+                    } catch (Exception e) {
+                        logger.warn("Failed to clean up load ${label}: ${e.message}")
+                    }
+                }
+            } finally {
+                try {
+                    sql "DROP TABLE IF EXISTS customer FORCE"
+                } finally {
+                    sql "DROP TABLE IF EXISTS supplier FORCE"
+                }
+            }
+        }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/test_clear_file_cache_on_load_failure.groovy b/regression-test/suites/cloud_p0/cache/test_clear_file_cache_on_load_failure.groovy
index 88d89eea0bf..a9705f6c03f 100644
--- a/regression-test/suites/cloud_p0/cache/test_clear_file_cache_on_load_failure.groovy
+++ b/regression-test/suites/cloud_p0/cache/test_clear_file_cache_on_load_failure.groovy
@@ -15,71 +15,38 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import groovy.json.JsonSlurper
+import org.apache.doris.regression.util.WarmupMetricsUtils
 
 suite("test_clear_file_cache_on_load_failure", "nonConcurrent") {
     if (!isCloudMode()) {
         return
     }
 
+    String originalAutoAnalyze = sql("SHOW GLOBAL VARIABLES LIKE 'enable_auto_analyze'")[0][1].toString()
+    assertTrue(originalAutoAnalyze.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected auto-analyze setting: ${originalAutoAnalyze}")
+    onFinish {
+        sql "SET GLOBAL enable_auto_analyze = ${originalAutoAnalyze}"
+        logger.info("Restored enable_auto_analyze=${originalAutoAnalyze}")
+    }
+
     // Clear any existing debug points
     GetDebugPoint().clearDebugPointsForAllFEs()
     GetDebugPoint().clearDebugPointsForAllBEs()
 
-    // Helper function to clear file cache on all backends
-    def clearFileCache = { ip, port ->
-        def url = "http://${ip}:${port}/api/file_cache?op=clear&sync=true"
-        def response = new URL(url).text
-        def json = new JsonSlurper().parseText(response)
-        if (json.status != "OK") {
-            throw new RuntimeException("Clear cache on ${ip}:${port} failed: ${json.status}")
-        }
-    }
-
-    def clearFileCacheOnAllBackends = {
-        def backends = sql """SHOW BACKENDS"""
-        for (be in backends) {
-            def ip = be[1]
-            def port = be[4]
-            clearFileCache(ip, port)
+    def getCacheBackends = {
+        return (sql """SHOW BACKENDS""").collect { be ->
+            [ip: be[1], httpPort: be[4], brpcPort: be[5]]
         }
-        // Wait for async clear to complete
-        sleep(5000)
     }
 
-    // Helper function to get brpc metrics
-    def getBrpcMetrics = { ip, port, name ->
-        def url = "http://${ip}:${port}/brpc_metrics"
-        try {
-            def metrics = new URL(url).text
-            def matcher = metrics =~ ~"${name}\\s+(\\d+)"
-            if (matcher.find()) {
-                return matcher[0][1] as long
-            }
-        } catch (Exception e) {
-            logger.warn("Failed to get brpc metrics from ${ip}:${port}: ${e.message}")
+    def requestFileCacheClearOnAllBackends = {
+        getCacheBackends().each { be ->
+            WarmupMetricsUtils.clearFileCache(be.ip.toString(), be.httpPort.toString())
         }
-        return 0L
-    }
-
-    // Helper function to get index queue cache size
-    def getIndexQueueSize = { ip, port ->
-        return getBrpcMetrics(ip, port, "file_cache_index_queue_cache_size")
     }
 
-    // Helper function to get total index queue cache size across all backends
-    def getTotalIndexQueueSize = {
-        def backends = sql """SHOW BACKENDS"""
-        long totalSize = 0
-        for (be in backends) {
-            def ip = be[1]
-            def brpcPort = be[5]
-            def size = getIndexQueueSize(ip, brpcPort)
-            totalSize += size
-            logger.info("BE ${ip}:${brpcPort} index_queue_size = ${size}")
-        }
-        return totalSize
-    }
+    def cacheSizeMetric = "file_cache_cache_size"
 
     // Create test table with file cache enabled
     def tableName = "test_load_failure_cache"
@@ -101,30 +68,37 @@ suite("test_clear_file_cache_on_load_failure", "nonConcurrent") {
         // Disable auto analyze to avoid internal loads affecting cache size
         sql """SET GLOBAL enable_auto_analyze = false"""
 
-        // Clear file cache and wait for it to complete
-        clearFileCacheOnAllBackends()
-        sleep(3000)
+        requestFileCacheClearOnAllBackends()
 
-        // Get initial cache size
-        def initialCacheSize = getTotalIndexQueueSize()
+        // sync=true synchronously removes releasable blocks, but it does not guarantee that
+        // blocks still held by readers or writers have all been recycled. Use a stable metric
+        // snapshot as the baseline and verify cache-size increments instead of requiring zero.
+        def initialCacheSize = WarmupMetricsUtils.waitForBackendMetricSumStable(
+                getCacheBackends(),
+                cacheSizeMetric,
+                10000,
+                60000)
         logger.info("Initial file cache size: ${initialCacheSize}")
 
         // First, do a successful load to establish baseline
         sql """INSERT INTO ${tableName} VALUES (1, 'test1', 100)"""
-        // Wait for cache metrics to update
-        sleep(5000)
 
-        def afterSuccessfulLoadSize = getTotalIndexQueueSize()
+        def afterSuccessfulLoadSize = WarmupMetricsUtils.waitForBackendMetricSum(
+                getCacheBackends(),
+                cacheSizeMetric,
+                { value -> value > initialCacheSize },
+                120000,
+                "Cache should increase after successful load. Initial: ${initialCacheSize}")
         logger.info("Cache size after successful load: ${afterSuccessfulLoadSize}")
-        assertTrue(afterSuccessfulLoadSize > initialCacheSize,
-            "Cache should increase after successful load. Initial: ${initialCacheSize}, After: ${afterSuccessfulLoadSize}")
-
-        // Clear cache again to reset
-        clearFileCacheOnAllBackends()
-        sleep(3000)
 
-        def afterClearSize = getTotalIndexQueueSize()
-        logger.info("Cache size after clear: ${afterClearSize}")
+        // The successful load may populate the cache asynchronously. Wait until it is stable,
+        // then use that value as the baseline for the failed-load cache-size increment.
+        afterSuccessfulLoadSize = WarmupMetricsUtils.waitForBackendMetricSumStable(
+                getCacheBackends(),
+                cacheSizeMetric,
+                10000,
+                60000)
+        logger.info("Stable cache size before failed load: ${afterSuccessfulLoadSize}")
 
         // Enable debug point to make commit_rowset return error
         GetDebugPoint().enableDebugPointForAllBEs("LoadChannel.add_batch.failed")
@@ -137,17 +111,22 @@ suite("test_clear_file_cache_on_load_failure", "nonConcurrent") {
         }
 
         // Wait for cleanup to complete and cache metrics to update
-        sleep(5000)
+        def afterFailedLoadSize = WarmupMetricsUtils.waitForBackendMetricSumStable(
+                getCacheBackends(),
+                cacheSizeMetric,
+                10000,
+                60000)
 
         // Get cache size after failed load
-        def afterFailedLoadSize = getTotalIndexQueueSize()
         logger.info("Cache size after failed load: ${afterFailedLoadSize}")
 
-        // Verify cache size has not increased
-        assertTrue(afterFailedLoadSize == afterClearSize,
+        // Pre-existing deleting blocks may be recycled during this interval, so a negative
+        // delta is valid. A failed load must not add cache bytes over the stable baseline.
+        def failedLoadCacheSizeDelta = afterFailedLoadSize - afterSuccessfulLoadSize
+        assertTrue(failedLoadCacheSizeDelta <= 0,
             "Cache should not increase after failed load. " +
-            "Before: ${afterClearSize}, After: ${afterFailedLoadSize}, " +
-            "Difference: ${afterFailedLoadSize - afterClearSize}")
+            "Before: ${afterSuccessfulLoadSize}, After: ${afterFailedLoadSize}, " +
+            "Difference: ${failedLoadCacheSizeDelta}")
 
         logger.info("Test passed: File cache was properly cleared after load failure")
     } finally {
diff --git a/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy b/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy
index 7c8841f4544..26c4964bb72 100644
--- a/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy
+++ b/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy
@@ -15,120 +15,216 @@
 // specific language governing permissions and limitations
 // under the License.
 
-suite("test_file_cache_info") {
-    def custoBeConfig = [
-        enable_evict_file_cache_in_advance : false,
-        file_cache_enter_disk_resource_limit_mode_percent : 99
-    ]
-
-    setBeConfigTemporary(custoBeConfig) {
+import org.apache.doris.regression.util.Http
 
-    String[][] backends = sql """ show backends """
-    def backendSockets = []
-    def backendIdToBackendIP = [:]
-    def backendIdToBackendHttpPort = [:]
-    for (String[] backend in backends) {
-        if (backend[9].equals("true")) {
-            backendIdToBackendIP.put(backend[0], backend[1])
-            backendIdToBackendHttpPort.put(backend[0], backend[4])
+suite("test_file_cache_info", "nonConcurrent") {
+    def customBeConfig = [
+        enable_evict_file_cache_in_advance: false,
+        file_cache_enter_disk_resource_limit_mode_percent: 99
+    ]
+    setBeConfigTemporary(customBeConfig) {
+        String tableName = "test_file_cache_info_lifecycle"
+        def clusters = sql "SHOW CLUSTERS"
+        assertTrue(!clusters.isEmpty(), "No compute group found")
+        String clusterName = clusters[0][0].toString()
+        String dbName = sql("SELECT DATABASE()")[0][0].toString()
+        def backends = sql_return_maparray("SHOW BACKENDS").findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collectEntries { be -> [(be.BackendId as Long): be] }
+        assertTrue(!backends.isEmpty(), "No alive backends in ${clusterName}")
+        def backendIds = backends.keySet()
+        Long tabletId = null
+        def lastState = [:]
+        def clearResponses = [:]
+        def originalQueryCache = sql("select @@enable_sql_cache, @@enable_query_cache")[0].collect { value ->
+            String setting = value.toString().toLowerCase(Locale.ROOT)
+            assertTrue(setting in ["true", "false", "0", "1"], "Unexpected cache setting: ${value}")
+            setting
         }
-    }
-    assertTrue(backendIdToBackendIP.size() > 0, "No alive backends found")
-
-    backendIdToBackendIP.each { backendId, ip ->
-        def socket = ip + ":" + backendIdToBackendHttpPort.get(backendId)
-        backendSockets.add(socket)
-    }
-
-    sql "drop table IF EXISTS customer"
-
-    sql """
-        CREATE TABLE IF NOT EXISTS customer (
-            `c_custkey` int NULL,
-            `c_name` string NULL,
-            `c_address` string NULL,
-            `c_city` string NULL,
-            `c_nation` string NULL,
-            `c_region` string NULL,
-            `c_phone` string NULL,
-            `c_mktsegment` string NULL
-        )
-        DUPLICATE KEY(`c_custkey`)
-        DISTRIBUTED BY HASH(`c_custkey`) BUCKETS 1
-        PROPERTIES (
-            "file_cache_ttl_seconds" = "3600"
-        )
-    """
-
-    sql """
-        insert into customer values
-        (1, 'Customer#000000001', 'address1', 'city1', 'nation1', 'region1', 'phone1', 'segment1'),
-        (2, 'Customer#000000002', 'address2', 'city2', 'nation2', 'region2', 'phone2', 'segment2'),
-        (3, 'Customer#000000003', 'address3', 'city3', 'nation3', 'region3', 'phone3', 'segment3'),
-        (4, 'Customer#000000004', 'address4', 'city4', 'nation4', 'region4', 'phone4', 'segment4'),
-        (5, 'Customer#000000005', 'address5', 'city5', 'nation5', 'region5', 'phone5', 'segment5')
-    """
-    sql "sync"
-
-    sql "select count(*) from customer"
-
-    Thread.sleep(10000)
-
-    def get_tablet_id = { String tbl_name ->
-        def tablets = sql "show tablets from ${tbl_name}"
-        assertEquals(tablets.size(), 1, "Should have exactly one tablet with BUCKETS=1")
-        return tablets[0][0] as Long
-    }
-
-    def tablet_id = get_tablet_id("customer")
-    println "Tablet ID: ${tablet_id}"
-
-    def desc_cache_info = sql "desc information_schema.file_cache_info"
-    assertTrue(desc_cache_info.size() > 0, "desc information_schema.file_cache_info should not be empty")
-    assertEquals(desc_cache_info[0][0].toString().toUpperCase(), "HASH")
-    assertEquals(desc_cache_info[1][0].toString().toUpperCase(), "OFFSET")
-
-    def cache_info = sql "select * from information_schema.file_cache_info"
-
-    assertTrue(cache_info.size() > 0, "file_cache_info should not be empty for tablet_id ${tablet_id}")
-
-    println "First query - File cache info for tablet_id ${tablet_id}:"
-    cache_info.each { row ->
-        println "  ${row}"
-    }
-
-    def clearResults = []
-    backendSockets.each { socket ->
-        httpTest {
-            endpoint ""
-            uri socket + "/api/file_cache?op=clear&sync=true"
-            op "get"
-            check {respCode, body ->
-                assertEquals(respCode, 200, "clear local cache fail, maybe you can find something in respond: " + parseJson(body))
-                clearResults.add(true)
+        def expectedRows = (1..5).collect { i ->
+            [i as Long, String.format('Customer#%09d', i), "address${i}".toString(),
+             "city${i}".toString(), "nation${i}".toString(), "region${i}".toString(),
+             "phone${i}".toString(), "segment${i}".toString()]
+        }
+        def readTable = {
+            sql "use @${clusterName}"
+            // Read and validate every column; result caches are disabled for both reads.
+            def rows = sql "select * from ${tableName} order by c_custkey"
+            assertTrue(rows.every { it.size() == 8 && it.every { value -> value != null } },
+                    "Unexpected data result: ${rows}")
+            def normalized = rows.collect { row -> [row[0] as Long] + row.drop(1).collect { it.toString() } }
+            assertEquals(expectedRows, normalized)
+        }
+        def getCache = {
+            sql "use @${clusterName}"
+            def rows = sql """select be_id, cache_path, tablet_id, `hash`, `offset`, size, lower(type)
+                from information_schema.file_cache_info
+                where tablet_id = ${tabletId} and be_id in (${backendIds.join(',')})"""
+            def blocks = [:]
+            rows.each { row ->
+                def key = [row[0] as Long, row[1].toString(), row[2] as Long,
+                           row[3].toString(), row[4] as Long]
+                assertTrue(backendIds.contains(key[0]) && key[2] == tabletId, "Unexpected cache owner: ${row}")
+                assertTrue(key[4] >= 0L && (row[5] as Long) > 0L, "Invalid cache range: ${row}")
+                assertTrue(!blocks.containsKey(key), "Duplicate cache block: ${key}")
+                blocks[key] = [size: row[5] as Long, type: row[6]?.toString()]
+            }
+            blocks
+        }
+        def summarizeCache = { Map blocks ->
+            blocks.groupBy { key, value -> [key[0], key[2]] }.collectEntries { owner, entries ->
+                [(owner): [block_count: entries.size(), bytes: entries.values().sum(0L) { it.size },
+                           bytes_by_type: entries.values().groupBy { it.type }.collectEntries { type, values ->
+                               [(type): values.sum(0L) { it.size }]
+                           }]]
+            }
+        }
+        def waitForCache = { String phase, boolean expectPresent, long timeoutMs ->
+            long startedMs = System.currentTimeMillis()
+            long deadlineMs = startedMs + timeoutMs
+            long stableSince = 0L
+            long lastLogMs = 0L
+            def previous = null
+            while (System.currentTimeMillis() < deadlineMs) {
+                def blocks = getCache()
+                boolean matches = expectPresent ? !blocks.isEmpty() : blocks.isEmpty()
+                lastState = [phase: phase, elapsed_ms: System.currentTimeMillis() - startedMs,
+                             cache_by_be_tablet: summarizeCache(blocks), blocks: blocks]
+                if (blocks != previous || System.currentTimeMillis() - lastLogMs >= 30000L) {
+                    logger.info("file_cache_info lifecycle: cluster=${clusterName}, ${lastState}")
+                    lastLogMs = System.currentTimeMillis()
+                }
+                if (!matches || blocks != previous) {
+                    stableSince = System.currentTimeMillis()
+                }
+                previous = blocks
+                // A transient empty metadata snapshot is not a completed clear.
+                if (matches && System.currentTimeMillis() - stableSince >= 3000L) {
+                    logger.info("file_cache_info phase complete: ${lastState}")
+                    return blocks
+                }
+                sleep(1000)
+            }
+            assertTrue(false, "Timeout waiting for ${phase}: cluster=${clusterName}, tablet=${tabletId}, " +
+                    "BEs=${backendIds}, clear_responses=${clearResponses}, last_state=${lastState}")
+        }
+        def waitForReaders = {
+            long deadlineMs = System.currentTimeMillis() + 60000L
+            long quietSince = System.currentTimeMillis()
+            while (System.currentTimeMillis() < deadlineMs) {
+                def active = sql_return_maparray("SHOW PROCESSLIST").findAll { row ->
+                    row.Db?.toString() == dbName && row.Info?.toString()?.contains(tableName) &&
+                            !"${row.Command}".equalsIgnoreCase("Sleep")
+                }
+                lastState = [phase: "reader_drain", active_queries: active]
+                if (!active.isEmpty()) {
+                    quietSince = System.currentTimeMillis()
+                } else if (System.currentTimeMillis() - quietSince >= 3000L) {
+                    return
+                }
+                sleep(1000)
+            }
+            assertTrue(false, "Timeout waiting for table queries to finish: ${lastState}")
+        }
+        def clearCache = { long beId ->
+            def be = backends[beId]
+            String url = "http://${be.Host}:${be.HttpPort}/api/file_cache?op=clear&sync=true"
+            long startedMs = System.currentTimeMillis()
+            clearResponses[beId] = [url: url]
+            logger.info("file_cache_info clear start: BE=${beId}, url=${url}")
+            def connection = Http.openConnection(url)
+            connection.setConnectTimeout(5000)
+            connection.setReadTimeout(60000)
+            try {
+                int status = connection.getResponseCode()
+                String body = (status == 200 ? connection.getInputStream() : connection.getErrorStream())
+                        ?.withCloseable { it.getText("UTF-8") } ?: ""
+                clearResponses[beId] += [http_status: status, elapsed_ms: System.currentTimeMillis() - startedMs,
+                                         response: body]
+                logger.info("file_cache_info clear response: BE=${beId}, ${clearResponses[beId]}")
+                assertEquals(200, status, "Clear failed on BE ${beId}: ${body}")
+                def response = parseJson(body)
+                assertTrue("${response.status}".equalsIgnoreCase("OK"), "Clear failed on BE ${beId}: ${body}")
+                def counters = ["num_files_all", "num_cells_all", "num_cells_to_delete", "num_cells_wait_recycle"]
+                        .collectEntries { name ->
+                            def matcher = response.msg?.toString() =~ /\b${name}=(\d+)/
+                            def values = []
+                            while (matcher.find()) { values.add(matcher.group(1) as Long) }
+                            [(name): values.isEmpty() ? null : values.sum(0L)]
+                        }
+                clearResponses[beId].counters = counters
+                logger.info("file_cache_info clear counters: BE=${beId}, ${counters}")
+            } catch (Throwable t) {
+                clearResponses[beId].error = t.toString()
+                clearResponses[beId].elapsed_ms = System.currentTimeMillis() - startedMs
+                throw t
+            } finally {
+                connection.disconnect()
+            }
+        }
+        Throwable failure = null
+        try {
+            sql "use @${clusterName}"
+            sql "set enable_sql_cache = false"
+            sql "set enable_query_cache = false"
+            sql "drop table if exists ${tableName} force"
+            sql """CREATE TABLE ${tableName} (
+                c_custkey INT, c_name STRING, c_address STRING, c_city STRING,
+                c_nation STRING, c_region STRING, c_phone STRING, c_mktsegment STRING
+            ) DUPLICATE KEY(c_custkey)
+            DISTRIBUTED BY HASH(c_custkey) BUCKETS 1
+            PROPERTIES("file_cache_ttl_seconds"="3600", "disable_auto_compaction"="true")"""
+            def tablets = sql_return_maparray("show tablets from ${tableName}")
+                    .collect { it.TabletId as Long }.unique()
+            assertEquals(1, tablets.size(), "Expected one tablet: ${tablets}")
+            tabletId = tablets[0]
+            String values = expectedRows.collect { row ->
+                "(${row[0]}, " + row.drop(1).collect { "'${it}'" }.join(", ") + ")"
+            }.join(", ")
+            sql "insert into ${tableName} values ${values}"
+            readTable()
+            def baseline = waitForCache("populated", true, 120000L)
+            waitForReaders()
+            // SQL quiescence does not prove that every BE holder has been released. The scoped
+            // empty-state check below remains the barrier for clear and async Meta Store deletion.
+            def cacheOwners = baseline.keySet().collect { it[0] }.unique().sort()
+            logger.info("file_cache_info before clear: cluster=${clusterName}, tablet=${tabletId}, " +
+                    "cache_owners=${cacheOwners}, cache_by_be_tablet=${summarizeCache(baseline)}, blocks=${baseline}")
+            long clearStartedMs = System.currentTimeMillis()
+            cacheOwners.each { clearCache(it as Long) }
+            waitForCache("cleared", false, 600000L)
+            logger.info("file_cache_info clear verified: tablet=${tabletId}, " +
+                    "clear_to_empty_ms=${System.currentTimeMillis() - clearStartedMs}, responses=${clearResponses}")
+
+            // Only now read the business table again. No repeated clear or forced release can
+            // bypass a stuck holder; persistent remnants must fail with the block details above.
+            long reloadStartedMs = System.currentTimeMillis()
+            readTable()
+            def reloaded = waitForCache("reloaded", true, 120000L)
+            // Readers may split a file into different blocks than the INSERT writer did.
+            logger.info("file_cache_info reload verified: tablet=${tabletId}, " +
+                    "read_to_visible_ms=${System.currentTimeMillis() - reloadStartedMs}, " +
+                    "cache_by_be_tablet=${summarizeCache(reloaded)}, blocks=${reloaded}")
+        } catch (Throwable t) {
+            failure = t
+            logger.warn("file_cache_info lifecycle failed: cluster=${clusterName}, tablet=${tabletId}, " +
+                    "clear_responses=${clearResponses}, last_state=${lastState}, error=${t}")
+            throw t
+        } finally {
+            def cleanupErrors = []
+            try { sql "drop table if exists ${tableName} force" } catch (Throwable t) { cleanupErrors.add(t) }
+            [enable_sql_cache: originalQueryCache[0], enable_query_cache: originalQueryCache[1]].each { name, value ->
+                try { sql "set ${name} = ${value}" } catch (Throwable t) { cleanupErrors.add(t) }
+            }
+            if (!cleanupErrors.isEmpty()) {
+                if (failure != null) {
+                    cleanupErrors.each { failure.addSuppressed(it) }
+                } else {
+                    cleanupErrors.tail().each { cleanupErrors[0].addSuppressed(it) }
+                    throw cleanupErrors[0]
+                }
             }
         }
-    }
-    assertEquals(clearResults.size(), backendSockets.size(), "Failed to clear cache on some backends")
-
-    Thread.sleep(5000)
-
-    def cache_info_after_clear = sql "select * from information_schema.file_cache_info where tablet_id = ${tablet_id}"
-    assertEquals(cache_info_after_clear.size(), 0, "file_cache_info should be empty after clearing cache")
-
-    println "After clearing cache - File cache info is empty as expected"
-
-    sql "select * from customer"
-
-    Thread.sleep(10000)
-
-    def cache_info_reloaded = sql "select * from information_schema.file_cache_info where tablet_id = ${tablet_id}"
-    assertTrue(cache_info_reloaded.size() > 0, "file_cache_info should not be empty after reloading data")
-
-    println "After reloading data - File cache info for tablet_id ${tablet_id}:"
-    cache_info_reloaded.each { row ->
-        println "  ${row}"
-    }
-
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_2.groovy b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_2.groovy
index cb804ae3b62..e652f2de6c1 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_2.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_2.groovy
@@ -15,9 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.WarmupMetricsUtils
 
-suite("alter_ttl_2") {
+suite("alter_ttl_2", "nonConcurrent") {
+    def originalQueryCache = sql("select @@enable_sql_cache, @@enable_query_cache")[0]
+            .collect { it.toString().toLowerCase(Locale.ROOT) }
+    assertTrue(originalQueryCache.every { it in ["true", "false", "1", "0"] })
+    onFinish {
+        sql "set enable_sql_cache = ${originalQueryCache[0]}"
+        sql "set enable_query_cache = ${originalQueryCache[1]}"
+    }
+    sql "set enable_sql_cache = false"
+    sql "set enable_query_cache = false"
     def custoBeConfig = [
         enable_evict_file_cache_in_advance : false,
         file_cache_enter_disk_resource_limit_mode_percent : 99,
@@ -31,7 +40,9 @@ suite("alter_ttl_2") {
     assertTrue(!clusters.isEmpty())
     def validCluster = clusters[0][0]
     sql """use @${validCluster};""";
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="300") """
+    long initialTtlSeconds = 3600L
+    long expiredTtlSeconds = 1L
+    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="${initialTtlSeconds}") """
     String[][] backends = sql """ show backends """
     String backendId;
     def backendIdToBackendIP = [:]
@@ -47,26 +58,11 @@ suite("alter_ttl_2") {
     assertEquals(backendIdToBackendIP.size(), 1)
 
     backendId = backendIdToBackendIP.keySet()[0]
-    def url = backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendHttpPort.get(backendId) + """/api/file_cache?op=clear&sync=true"""
-    logger.info(url)
-    def clearFileCache = { check_func ->
-        httpTest {
-            endpoint ""
-            uri url
-            op "get"
-            body ""
-            check check_func
-        }
-    }
-
-    def getMetricsMethod = { check_func ->
-        httpTest {
-            endpoint backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendBrpcPort.get(backendId)
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
-        }
-    }
+    def cacheBackend = [
+        ip: backendIdToBackendIP.get(backendId),
+        httpPort: backendIdToBackendHttpPort.get(backendId),
+        brpcPort: backendIdToBackendBrpcPort.get(backendId)
+    ]
 
     def getTabletIds = { String tableName ->
         def tablets = sql "show tablets from ${tableName}"
@@ -74,31 +70,57 @@ suite("alter_ttl_2") {
         tablets.collect { it[0] as Long }
     }
 
-    def waitForFileCacheType = { List<Long> tabletIds, String expectedType, long timeoutMs = 180000L, long intervalMs = 2000L ->
-        long start = System.currentTimeMillis()
-        while (System.currentTimeMillis() - start < timeoutMs) {
-            boolean allMatch = true
-            for (Long tabletId in tabletIds) {
-                def rows = sql "select type from information_schema.file_cache_info where tablet_id = ${tabletId}"
-                if (rows.isEmpty()) {
-                    logger.warn("file_cache_info is empty for tablet ${tabletId} while waiting for ${expectedType}")
-                    allMatch = false
-                    break
-                }
-                def mismatch = rows.find { row -> !row[0]?.toString()?.equalsIgnoreCase(expectedType) }
-                if (mismatch) {
-                    logger.info("tablet ${tabletId} has cache types ${rows.collect { it[0] }} while waiting for ${expectedType}")
-                    allMatch = false
-                    break
-                }
-            }
-            if (allMatch) {
-                logger.info("All file cache entries for tablets ${tabletIds} are ${expectedType}")
-                return
+    def getCache = { List<Long> tabletIds ->
+        sql "use @${validCluster}"
+        def rows = sql """select be_id, cache_path, tablet_id, `hash`, `offset`, size, lower(type)
+            from information_schema.file_cache_info
+            where tablet_id in (${tabletIds.join(',')}) and be_id = ${backendId}"""
+        def blocks = [:]
+        rows.each { row ->
+            def key = [row[0] as Long, row[1].toString(), row[2] as Long,
+                       row[3].toString(), row[4] as Long]
+            assertTrue(!blocks.containsKey(key), "Duplicate cache block: ${key}")
+            assertTrue((row[5] as Long) > 0L, "Invalid block size: ${row}")
+            blocks[key] = [size: row[5] as Long, type: row[6].toString()]
+        }
+        blocks
+    }
+    def sizes = { Map blocks -> blocks.collectEntries { key, value -> [(key): value.size] } }
+    def waitForFileCacheType = { List<Long> tabletIds, String expectedType, Map expectedBlocks = null ->
+        long deadline = System.currentTimeMillis() + 600000L
+        long stableSince = 0L
+        def previous = null
+        def lastState = [:]
+        while (System.currentTimeMillis() < deadline) {
+            def blocks = getCache(tabletIds)
+            def missing = tabletIds.findAll { id -> !blocks.keySet().any { it[2] == id } }
+            boolean ready = missing.isEmpty() && blocks.values().every { it.type == expectedType } &&
+                    (expectedBlocks == null || sizes(blocks) == sizes(expectedBlocks))
+            lastState = [expected_type: expectedType, missing_tablets: missing,
+                         by_type: blocks.values().groupBy { it.type }.collectEntries { type, values ->
+                             [(type): [blocks: values.size(), bytes: values.sum(0L) { it.size }]]
+                         }, missing_blocks: expectedBlocks == null ? [] : (expectedBlocks.keySet() - blocks.keySet()).take(10)]
+            if (blocks != previous) { logger.info("Scoped TTL transition: ${lastState}") }
+            if (!ready || blocks != previous) { stableSince = System.currentTimeMillis() }
+            previous = blocks
+            if (ready && System.currentTimeMillis() - stableSince >= 3000L) {
+                logger.info("Verified scoped cache: tablets=${tabletIds}, state=${lastState}")
+                return blocks
             }
-            sleep(intervalMs)
+            sleep(1000)
+        }
+        assertTrue(false, "Timeout waiting for scoped cache conversion on ${validCluster}/BE ${backendId}: ${lastState}")
+    }
+    def getGlobalMetrics = {
+        ["ttl_cache_size", "normal_queue_cache_size"].collectEntries { name ->
+            [(name): WarmupMetricsUtils.getBrpcMetric(cacheBackend.ip.toString(), cacheBackend.brpcPort.toString(), name)]
         }
-        assertTrue(false, "Timeout waiting for file_cache_info type ${expectedType} for tablets ${tabletIds}")
+    }
+    def scanTable = {
+        // Force a BE column scan and check fixture data before and after the cache transition.
+        def result = sql "select count(*), sum(C_CUSTKEY) from customer_ttl"
+        assertEquals(200L, result[0][0] as Long)
+        assertEquals(2020100L, result[0][1] as Long)
     }
 
     sql new File("""${context.file.parent}/../ddl/customer_ttl_delete.sql""").text
@@ -130,101 +152,30 @@ suite("alter_ttl_2") {
         }
     }
 
-    clearFileCache.call() {
-        respCode, body -> {}
-    }
-    sleep(30000)
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    logger.info("ttl_cache_size line before assert zero: " + line)
-                    assertEquals(line.substring(i).toLong(), 0)
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
-    }
+    try {
+        def metricsBeforeClear = getGlobalMetrics()
+        WarmupMetricsUtils.clearFileCache(cacheBackend.ip.toString(), cacheBackend.httpPort.toString())
+        def metricsAfterClear = getGlobalMetrics()
+        // sync=true may leave held blocks from other tables. Global counters are diagnostic only.
+        logger.info("TTL clear metrics: before=${metricsBeforeClear}, after=${metricsAfterClear}")
 
-    load_customer_ttl_once("customer_ttl")
-    def tabletIds = getTabletIds.call("customer_ttl")
-    waitForFileCacheType.call(tabletIds, "ttl", 60000L)
-    sql """ select count(*) from customer_ttl """
-    sleep(30000)
-    long ttl_cache_size = 0
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    logger.info("ttl_cache_size line after load: " + line)
-                    ttl_cache_size = line.substring(i).toLong()
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
-    }
-    sql """ ALTER TABLE customer_ttl SET ("file_cache_ttl_seconds"="120") """
-    sleep(80000)
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    logger.info("ttl_cache_size line after ttl update: " + line)
-                    assertEquals(line.substring(i).toLong(), ttl_cache_size)
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
-    }
-    // the first load data ttl is 300,so need wait for 200s until the ttl timeout
-    sleep(200000)
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    assertEquals(line.substring(i).toLong(), 0)
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
-    }
+        load_customer_ttl_once("customer_ttl")
+        def tabletIds = getTabletIds.call("customer_ttl")
+        scanTable()
+        def baseline = waitForFileCacheType(tabletIds, "ttl")
+        def metricsBeforeAlter = getGlobalMetrics()
+        logger.info("TTL before shortening: tablets=${tabletIds}, blocks=${baseline}, metrics=${metricsBeforeAlter}")
 
-    waitForFileCacheType.call(tabletIds, "normal", 300000L)
+        // The stable TTL baseline above is already more than one second after tablet creation.
+        // Retain the positive -> positive transition; the original blocks must change type in place.
+        sql """ALTER TABLE customer_ttl SET ("file_cache_ttl_seconds"="${expiredTtlSeconds}")"""
+        waitForFileCacheType(tabletIds, "normal", baseline)
+        def metricsAfterAlter = getGlobalMetrics()
+        def delta = metricsAfterAlter.collectEntries { name, value -> [(name): value - metricsBeforeAlter[name]] }
+        logger.info("TTL after shortening: before=${metricsBeforeAlter}, after=${metricsAfterAlter}, delta=${delta}")
+        scanTable()
+    } finally {
+        sql "DROP TABLE IF EXISTS customer_ttl FORCE"
+    }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy
index 691994c36c5..c78a140ba47 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy
@@ -15,205 +15,324 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
-
-suite("alter_ttl_4") {
-    def custoBeConfig = [
-        enable_evict_file_cache_in_advance : false,
-        file_cache_enter_disk_resource_limit_mode_percent : 99
+suite("alter_ttl_4", "nonConcurrent") {
+    def customBeConfig = [
+        enable_evict_file_cache_in_advance: false,
+        file_cache_enter_disk_resource_limit_mode_percent: 99,
+        file_cache_background_ttl_gc_interval_ms: 1000,
+        file_cache_background_ttl_info_update_interval_ms: 1000,
+        file_cache_background_tablet_id_flush_interval_ms: 1000
     ]
-
-    setBeConfigTemporary(custoBeConfig) {
-    def clusters = sql " SHOW CLUSTERS; "
-    assertTrue(!clusters.isEmpty())
-    def validCluster = clusters[0][0]
-    sql """use @${validCluster};""";
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="900") """
-    String[][] backends = sql """ show backends """
-    String backendId;
-    def backendIdToBackendIP = [:]
-    def backendIdToBackendHttpPort = [:]
-    def backendIdToBackendBrpcPort = [:]
-    for (String[] backend in backends) {
-        if (backend[9].equals("true") && backend[19].contains("${validCluster}")) {
-            backendIdToBackendIP.put(backend[0], backend[1])
-            backendIdToBackendHttpPort.put(backend[0], backend[4])
-            backendIdToBackendBrpcPort.put(backend[0], backend[5])
+    setBeConfigTemporary(customBeConfig) {
+        String targetTable = "alter_ttl_4_target"
+        String pressureTable = "alter_ttl_4_pressure"
+        long initialTtl = 3600L
+        long rowsPerLoad = 15000000L // TPCH SF100 customer.tbl
+        long maxPressureRows = 64L * rowsPerLoad
+        def clusters = sql "SHOW CLUSTERS"
+        assertTrue(!clusters.isEmpty())
+        String clusterName = clusters[0][0].toString()
+        def backends = sql_return_maparray("SHOW BACKENDS").findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collectEntries { be -> [(be.BackendId as Long): be] }
+        assertTrue(!backends.isEmpty(), "No alive backends in ${clusterName}")
+        def backendIds = backends.keySet()
+        def tabletIds = []
+        def pendingLoads = [] as Set
+        def lastState = [:]
+        def cacheDifference = [:]
+        def originalQueryCache = sql("select @@enable_sql_cache, @@enable_query_cache")[0].collect { value ->
+            String setting = value.toString().toLowerCase(Locale.ROOT)
+            assertTrue(setting in ["true", "false", "0", "1"], "Unexpected cache setting: ${value}")
+            setting
         }
-    }
-    assertEquals(backendIdToBackendIP.size(), 1)
-
-    backendId = backendIdToBackendIP.keySet()[0]
-    def url = backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendHttpPort.get(backendId) + """/api/file_cache?op=clear&sync=true"""
-    logger.info(url)
-    def clearFileCache = { check_func ->
-        httpTest {
-            endpoint ""
-            uri url
-            op "get"
-            body ""
-            check check_func
+        def waitUntil = { String description, long timeoutMs, Closure ready ->
+            long deadline = System.currentTimeMillis() + timeoutMs
+            while (System.currentTimeMillis() < deadline) {
+                if (ready()) {
+                    return
+                }
+                sleep(1000)
+            }
+            assertTrue(false, "Timeout after ${timeoutMs}ms: ${description}; last_state=${lastState}")
         }
-    }
-
-    def getMetricsMethod = { check_func ->
-        httpTest {
-            endpoint backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendBrpcPort.get(backendId)
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
+        def scanTarget = {
+            sql "use @${clusterName}"
+            // Read every data column, including strings; neither COUNT metadata nor LIMIT can satisfy this scan.
+            def rows = sql """select count(*), sum(C_CUSTKEY), sum(length(C_NAME)), sum(length(C_ADDRESS)),
+                sum(C_NATIONKEY), sum(length(C_PHONE)), sum(C_ACCTBAL),
+                sum(length(C_MKTSEGMENT)), sum(length(C_COMMENT)) from ${targetTable}"""
+            assertTrue(rows.size() == 1 && rows[0].size() == 9 && rows[0].every { it != null },
+                    "Unexpected data result: ${rows}")
+            assertEquals(rowsPerLoad, rows[0][0] as Long)
+            rows[0].collect { new BigDecimal(it.toString()).stripTrailingZeros() }
         }
-    }
-
-    def s3BucketName = getS3BucketName()
-    def s3WithProperties = """WITH S3 (
-        |"AWS_ACCESS_KEY" = "${getS3AK()}",
-        |"AWS_SECRET_KEY" = "${getS3SK()}",
-        |"AWS_ENDPOINT" = "${getS3Endpoint()}",
-        |"AWS_REGION" = "${getS3Region()}",
-        |"provider" = "${getS3Provider()}")
-        |PROPERTIES(
-        |"exec_mem_limit" = "8589934592",
-        |"load_parallelism" = "3")""".stripMargin()
-
-
-    sql new File("""${context.file.parent}/../ddl/customer_ttl_delete.sql""").text
-    sql new File("""${context.file.parent}/../ddl/customer_delete.sql""").text
-    def load_customer_ttl_once =  { String table ->
-        def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
-        // def table = "customer"
-        // create table if not exists
-        sql (new File("""${context.file.parent}/../ddl/${table}.sql""").text + ttlProperties)
-        sql """ alter table ${table} set ("disable_auto_compaction" = "true") """ // no influence from compaction
-        def loadLabel = table + "_" + uniqueID
-        // load data from cos
-        def loadSql = new File("""${context.file.parent}/../ddl/${table}_load.sql""").text.replaceAll("\\\$\\{s3BucketName\\}", s3BucketName)
-        loadSql = loadSql.replaceAll("\\\$\\{loadLabel\\}", loadLabel) + s3WithProperties
-        sql loadSql
-
-        // check load state
-        while (true) {
-            def stateResult = sql "show load where Label = '${loadLabel}'"
-            def loadState = stateResult[stateResult.size() - 1][2].toString()
-            if ("CANCELLED".equalsIgnoreCase(loadState)) {
-                throw new IllegalStateException("load ${loadLabel} failed.")
-            } else if ("FINISHED".equalsIgnoreCase(loadState)) {
-                break
-            }
-            sleep(5000)
+        def getCache = {
+            sql "use @${clusterName}"
+            def rows = sql """select be_id, cache_path, tablet_id, `hash`, `offset`, size, lower(type)
+                from information_schema.file_cache_info
+                where tablet_id in (${tabletIds.join(',')}) and be_id in (${backendIds.join(',')})"""
+            def blocks = [:]
+            rows.each { row ->
+                def key = [row[0] as Long, row[1].toString(), row[2] as Long,
+                           row[3].toString(), row[4] as Long]
+                assertTrue(backendIds.contains(key[0]) && tabletIds.contains(key[2]), "Unexpected cache owner: ${row}")
+                assertTrue(key[4] >= 0L && (row[5] as Long) > 0L, "Invalid cache range: ${row}")
+                assertTrue(!blocks.containsKey(key), "Duplicate cache block: ${key}")
+                blocks[key] = [size: row[5] as Long, type: row[6]?.toString()]
+            }
+            blocks
         }
-    }
-
-    def load_customer_once =  { String table ->
-        def uniqueID = Math.abs(UUID.randomUUID().hashCode()).toString()
-        // def table = "customer"
-        // create table if not exists
-        sql new File("""${context.file.parent}/../ddl/${table}.sql""").text
-        def loadLabel = table + "_" + uniqueID
-        // load data from cos
-        def loadSql = new File("""${context.file.parent}/../ddl/${table}_load.sql""").text.replaceAll("\\\$\\{s3BucketName\\}", s3BucketName)
-        loadSql = loadSql.replaceAll("\\\$\\{loadLabel\\}", loadLabel) + s3WithProperties
-        sql loadSql
-
-        // check load state
-        while (true) {
-            def stateResult = sql "show load where Label = '${loadLabel}'"
-            def loadState = stateResult[stateResult.size() - 1][2].toString()
-            if ("CANCELLED".equalsIgnoreCase(loadState)) {
-                throw new IllegalStateException("load ${loadLabel} failed.")
-            } else if ("FINISHED".equalsIgnoreCase(loadState)) {
-                break
-            }
-            sleep(5000)
+        def blockSizes = { Map blocks -> blocks.collectEntries { key, value -> [(key): value.size] } }
+        def sameBlocks = { Map expected, Map actual ->
+            cacheDifference = [missing: (expected.keySet() - actual.keySet()).take(10),
+                               unexpected: (actual.keySet() - expected.keySet()).take(10),
+                               changed_sizes: actual.findAll { key, value -> value.size != expected[key]?.size }.take(10)]
+            blockSizes(expected) == blockSizes(actual)
         }
-    }
-
-    clearFileCache.call() {
-        respCode, body -> {}
-    }
-    sleep(30000)
+        def cacheSummary = { Map blocks ->
+            blocks.groupBy { key, value -> [key[0], key[2], value.type] }
+                    .collectEntries { key, entries -> [(key): entries.values().sum(0L) { it.size }] }
+        }
+        def coverage = { Map blocks ->
+            blocks.groupBy { key, value -> key[0..3] }.collectEntries { file, entries ->
+                def ranges = []
+                entries.collect { key, value -> [key[4], key[4] + value.size] }.sort { a, b -> a[0] <=> b[0] }
+                        .each { range ->
+                            assertTrue(ranges.isEmpty() || range[0] >= ranges.last()[1],
+                                    "Overlapping cache ranges: file=${file}, ranges=${ranges}, next=${range}")
+                            if (!ranges.isEmpty() && range[0] == ranges.last()[1]) {
+                                ranges.last()[1] = range[1]
+                            } else {
+                                ranges.add(range)
+                            }
+                        }
+                [(file): ranges]
+            }
+        }
+        def missingCoverage = { Map expected, Map actual ->
+            expected.findAll { file, ranges ->
+                !ranges.every { range ->
+                    actual[file]?.any { present -> present[0] <= range[0] && present[1] >= range[1] }
+                }
+            }
+        }
+        def waitForCache = { String stage, String type, Closure matches ->
+            def previous = null
+            def result = [:]
+            long stableSince = 0L
+            waitUntil(stage, 600000L) {
+                result = getCache()
+                def missingTablets = tabletIds.findAll { id -> !result.keySet().any { it[2] == id } }
+                boolean matching = matches(result)
+                boolean complete = missingTablets.isEmpty() &&
+                        result.values().every { it.type == type } && matching
+                lastState = [stage: stage, cache_by_be_tablet_type: cacheSummary(result),
+                             missing_tablets: missingTablets, differences: cacheDifference]
+                if (result != previous) {
+                    logger.info("${lastState}")
+                }
+                if (!complete || result != previous) {
+                    stableSince = System.currentTimeMillis()
+                }
+                previous = result
+                complete && System.currentTimeMillis() - stableSince >= 3000L
+            }
+            result
+        }
+        def metricNames = ["file_cache_ttl_cache_size", "file_cache_capacity", "file_cache_evict_by_self_lru_normal"]
+        def getMetrics = {
+            // Per-BE diagnostic snapshots; residual cache from other tables is not a failure.
+            backends.collectEntries { id, be ->
+                def values = metricNames.collectEntries { [(it): null] }
+                try {
+                    String text = new URL("http://${be.Host}:${be.BrpcPort}/brpc_metrics")
+                            .getText(connectTimeout: 5000, readTimeout: 5000)
+                    text.readLines().each { line ->
+                        def fields = line.trim().split(/\s+/)
+                        if (fields.size() == 2 && !fields[0].startsWith("#")) {
+                            metricNames.each { name ->
+                                if (fields[0] == name || fields[0].endsWith("_${name}")) {
+                                    values[name] = (values[name] ?: 0L) + (fields[1] as Long)
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.warn("Cannot read diagnostic metrics for BE ${id}: ${e.message}")
+                }
+                [(id): values]
+            }
+        }
+        def logMetricDelta = { String stage, Map before, Map after ->
+            def delta = backendIds.collectEntries { id ->
+                [(id): metricNames.collectEntries { name ->
+                    [(name): before[id][name] != null && after[id][name] != null ?
+                            after[id][name] - before[id][name] : null]
+                }]
+            }
+            logger.info("${stage} metrics: before=${before}, after=${after}, delta_by_be=${delta}")
+        }
+        def loadTable = { String table, long timeoutMs ->
+            long deadline = System.currentTimeMillis() + timeoutMs
+            String label = "${table}_${UUID.randomUUID().toString().replace('-', '')}"
+            pendingLoads.add(label)
+            String statement = new File("${context.file.parent}/../ddl/customer_ttl_load.sql").text
+                    .replace('${loadLabel}', label).replace('${s3BucketName}', getS3BucketName())
+                    .replace('INTO TABLE customer_ttl', "INTO TABLE ${table}")
+            sql(statement + """WITH S3 (
+                "AWS_ACCESS_KEY"="${getS3AK()}", "AWS_SECRET_KEY"="${getS3SK()}",
+                "AWS_ENDPOINT"="${getS3Endpoint()}", "AWS_REGION"="${getS3Region()}",
+                "provider"="${getS3Provider()}")
+                PROPERTIES("exec_mem_limit"="8589934592", "load_parallelism"="3")""")
+            waitUntil("load ${label}", Math.max(0L, deadline - System.currentTimeMillis())) {
+                def rows = sql "show load where Label = '${label}'"
+                lastState = [label: label, state: rows.isEmpty() ? "NOT_VISIBLE" : rows.last()[2]]
+                if (rows.isEmpty()) {
+                    return false
+                }
+                String state = rows.last()[2].toString()
+                assertTrue(!state.equalsIgnoreCase("CANCELLED"), "Load cancelled: ${label}, details=${rows.last()}")
+                if (state.equalsIgnoreCase("FINISHED")) {
+                    pendingLoads.remove(label)
+                    return true
+                }
+                false
+            }
+        }
+        Throwable failure = null
+        try {
+            sql "use @${clusterName}"
+            sql "set enable_sql_cache = false"
+            sql "set enable_query_cache = false"
+            [(targetTable): initialTtl, (pressureTable): 0L].each { table, ttl ->
+                sql "drop table if exists ${table} force"
+                String ddl = new File("${context.file.parent}/../ddl/customer_ttl.sql").text
+                        .replace('customer_ttl', table)
+                sql(ddl + """ PROPERTIES("file_cache_ttl_seconds"="${ttl}", "disable_auto_compaction"="true")""")
+            }
+            tabletIds = sql_return_maparray("show tablets from ${targetTable}")
+                    .collect { it.TabletId as Long }.unique().sort()
+            assertTrue(tabletIds.size() == 32, "Expected 32 target tablets: ${tabletIds}")
+            loadTable(targetTable, 300000L)
+            def dataBaseline = scanTarget()
 
-    load_customer_ttl_once("customer_ttl")
-    sql """ select count(*) from customer_ttl """
-    sleep(30000)
-    long custom_ttl_size = 0
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag2 = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+            def creationTimes = [:]
+            // The full scan above verifies data; cloud header JSON does not include rowsets.
+            waitUntil("target tablet TTL metadata", 600000L) {
+                creationTimes = [:]
+                sql_return_maparray("show tablets from ${targetTable}").each { tablet ->
+                    long id = tablet.TabletId as Long
+                    long primary = tablet.PrimaryBackendId as Long
+                    if (!(id in tabletIds) || !backends.containsKey(primary) || (tablet.BackendId as Long) != primary) {
+                        return
+                    }
+                    def be = backends[primary]
+                    try {
+                        httpTest {
+                            endpoint "${be.Host}:${be.HttpPort}"
+                            uri "/api/meta/header/${id}"
+                            op "get"
+                            check { code, body ->
+                                if ((code as int) == 200) {
+                                    def meta = parseJson(body.toString())
+                                    if ((meta.tablet_id as Long) == id && (meta.creation_time as Long) > 0L &&
+                                            (meta.ttl_seconds as Long) == initialTtl &&
+                                            meta.tablet_state == "PB_RUNNING" && meta.schema != null) {
+                                        creationTimes[id] = meta.creation_time as Long
+                                    }
+                                }
+                            }
+                        }
+                    } catch (Exception e) {
+                        logger.info("Waiting for metadata: tablet=${id}, BE=${primary}, ${e.message}")
                     }
-                    def i = line.indexOf(' ')
-                    custom_ttl_size = line.substring(i).toLong()
-                    flag2 = true
                 }
+                lastState = [missing_tablets: tabletIds - creationTimes.keySet()]
+                creationTimes.size() == tabletIds.size()
             }
-            assertTrue(flag2)
-    }
-    sleep(60000)
-    // one customer table would take about 1.3GB, the total cache size is 20GB
-    // the following would take 20G all
-    // evict customer_ttl
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
-    load_customer_once("customer")
+            def ttlBaseline = waitForCache("initial TTL cache", "ttl") { true }
+            def baselineCoverage = coverage(ttlBaseline)
+            waitUntil("tablet creation time plus one second to have passed", 60000L) {
+                long now = sql("select unix_timestamp()")[0][0] as Long
+                assertTrue(now < creationTimes.values().min() + initialTtl, "Initial TTL already expired")
+                now > creationTimes.values().max() + 1L
+            }
+            logger.info("TTL baseline: creation_times=${creationTimes}, metrics=${getMetrics()}")
 
-    // some datas in s3 and will download them
-    sql """ select C_CUSTKEY from customer_ttl order by C_CUSTKEY limit 1"""
-    sql """ select C_NAME from customer_ttl order by C_NAME limit 1"""
-    sql """ select C_ADDRESS from customer_ttl order by C_ADDRESS limit 1"""
-    sql """ select C_NATIONKEY from customer_ttl order by C_NATIONKEY limit 1"""
-    sql """ select C_PHONE from customer_ttl order by C_PHONE limit 1 """
-    sql """ select C_ACCTBAL from customer_ttl order by C_ACCTBAL limit 1"""
-    sql """ select C_MKTSEGMENT from customer_ttl order by C_MKTSEGMENT limit 1"""
-    sql """ select C_COMMENT from customer_ttl order by C_COMMENT limit 1"""
+            sql "alter table ${targetTable} set (\"file_cache_ttl_seconds\"=\"1\")"
+            waitForCache("expired TTL blocks to become NORMAL", "normal") { blocks ->
+                sameBlocks(ttlBaseline, blocks)
+            }
 
-    sql """ ALTER TABLE customer_ttl SET ("file_cache_ttl_seconds"="3600") """
-    // wait for fetching new tablet meta in BE
-    sleep(60000)
-    // wait for updating file cache metrics
-    sleep(30000)
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+            long pressureDeadline = System.currentTimeMillis() + 1200000L
+            long pressureRows = 0L
+            def metricsBeforePressure = getMetrics()
+            def evictedFiles = [:]
+            while (evictedFiles.isEmpty() && pressureRows < maxPressureRows &&
+                    System.currentTimeMillis() < pressureDeadline) {
+                loadTable(pressureTable, Math.min(300000L, pressureDeadline - System.currentTimeMillis()))
+                pressureRows += rowsPerLoad
+                // Poll cache metadata between loads without touching target data or clearing caches.
+                long checkDeadline = Math.min(pressureDeadline, System.currentTimeMillis() + 5000L)
+                while (evictedFiles.isEmpty() && System.currentTimeMillis() < checkDeadline) {
+                    def current = getCache()
+                    assertTrue(current.values().every { it.type == "normal" },
+                            "Expired target cache changed type during pressure: ${cacheSummary(current)}")
+                    evictedFiles = missingCoverage(baselineCoverage, coverage(current))
+                    if (evictedFiles.isEmpty()) {
+                        sleep(1000)
                     }
-                    def i = line.indexOf(' ')
-                    assertEquals(custom_ttl_size, line.substring(i).toLong())
-                    flag1 = true
                 }
+                logger.info("Cache pressure: imported_rows=${pressureRows}, missing_files=${evictedFiles.keySet()}")
             }
-            assertTrue(flag1)
-    }
+            def metricsAfterPressure = getMetrics()
+            logMetricDelta("Pressure", metricsBeforePressure, metricsAfterPressure)
+            assertTrue(!evictedFiles.isEmpty(), "No target cache eviction before pressure limit: " +
+                    "imported_rows=${pressureRows}/${maxPressureRows}, metrics=${metricsAfterPressure}")
+
+            assertEquals(dataBaseline, scanTarget())
+            // Re-reading an evicted file may split blocks differently. Compare merged byte ranges.
+            def reloaded = waitForCache("reloaded NORMAL cache coverage", "normal") { blocks ->
+                def current = coverage(blocks)
+                cacheDifference = [missing: missingCoverage(baselineCoverage, current).take(10),
+                                   extra: missingCoverage(current, baselineCoverage).take(10)]
+                current == baselineCoverage
+            }
+            def metricsBeforeExtension = getMetrics()
+            long now = sql("select unix_timestamp()")[0][0] as Long
+            long extendedTtl = Math.max(initialTtl, now - (creationTimes.values().min() as Long) + 1200L)
+            sql "alter table ${targetTable} set (\"file_cache_ttl_seconds\"=\"${extendedTtl}\")"
+            // Keep positive -> positive: routing through TTL=0 would hide expired-TTL renewal bugs.
+            // No target reads after ALTER until the background conversion has been verified.
+            waitForCache("renewed TTL on existing NORMAL blocks (1 -> ${extendedTtl})", "ttl") { blocks ->
+                sameBlocks(reloaded, blocks)
+            }
+            logMetricDelta("Extended TTL=${extendedTtl}", metricsBeforeExtension, getMetrics())
+            assertEquals(dataBaseline, scanTarget())
+        } catch (Throwable t) {
+            failure = t
+            throw t
+        } finally {
+            def cleanupErrors = []
+            pendingLoads.each { label ->
+                try { sql "cancel load where label = '${label}'" } catch (Throwable t) { cleanupErrors.add(t) }
+            }
+            [targetTable, pressureTable].each { table ->
+                try { sql "drop table if exists ${table} force" } catch (Throwable t) { cleanupErrors.add(t) }
+            }
+            [enable_sql_cache: originalQueryCache[0], enable_query_cache: originalQueryCache[1]].each { name, value ->
+                try { sql "set ${name} = ${value}" } catch (Throwable t) { cleanupErrors.add(t) }
+            }
+            if (!cleanupErrors.isEmpty()) {
+                if (failure != null) {
+                    cleanupErrors.each { failure.addSuppressed(it) }
+                } else {
+                    cleanupErrors.tail().each { cleanupErrors[0].addSuppressed(it) }
+                    throw cleanupErrors[0]
+                }
+            }
+        }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_random.groovy b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_random.groovy
index 3aa0a3a2c58..c875757807c 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_random.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_random.groovy
@@ -91,20 +91,27 @@ suite("test_ttl_random") {
 
     load_customer_once("customer_ttl")
 
-    Random random = new Random()
+    long maxFileCacheTtlSeconds = Long.MAX_VALUE / 2L
+    long randomSeed = 20260902L
+    Random random = new Random(randomSeed)
+    long lastAlterTtlSeconds = 180L
+    logger.info("Use deterministic random seed ${randomSeed} for TTL ALTER values")
     for (int j = 0; j < 40; j++) {
-        long number = random.nextLong() % 10l;
+        int number = j % 10
         if (number < 5) {
             sql """ select * from customer_ttl limit 10"""
         } else if (number >= 5 && number < 8) {
             load_customer_once("customer_ttl")
         } else {
-            def alterTtlSeconds = random.nextLong()
-            if (alterTtlSeconds < 0) {
-                alterTtlSeconds *= -1
-            }
-            sql """ ALTER TABLE customer_ttl SET ("file_cache_ttl_seconds"="${alterTtlSeconds}") """
+            lastAlterTtlSeconds = Math.floorMod(
+                    random.nextLong(), maxFileCacheTtlSeconds + 1L)
+            sql """ ALTER TABLE customer_ttl SET ("file_cache_ttl_seconds"="${lastAlterTtlSeconds}") """
             sleep(40000)
         }
     }
+
+    def showCreateTable = sql "show create table customer_ttl"
+    assertTrue(showCreateTable[0][1].toString().contains(
+            "\"file_cache_ttl_seconds\" = \"${lastAlterTtlSeconds}\""),
+            "file_cache_ttl_seconds should converge to the last ALTER value ${lastAlterTtlSeconds}")
 }
diff --git a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_seconds.groovy b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_seconds.groovy
deleted file mode 100644
index 491576e5601..00000000000
--- a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_seconds.groovy
+++ /dev/null
@@ -1,170 +0,0 @@
-// 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.codehaus.groovy.runtime.IOGroovyMethods
-
-suite("test_ttl_seconds") {
-    def custoBeConfig = [
-        enable_evict_file_cache_in_advance : false,
-        file_cache_enter_disk_resource_limit_mode_percent : 99,
-        file_cache_background_ttl_gc_interval_ms : 1000,
-        file_cache_background_ttl_info_update_interval_ms : 1000,
-        file_cache_background_tablet_id_flush_interval_ms : 1000
-    ]
-
-    setBeConfigTemporary(custoBeConfig) {
-    sql "set global enable_auto_analyze = false"
-    sql "set global enable_audit_plugin = false"
-    def clusters = sql " SHOW CLUSTERS; "
-    assertTrue(!clusters.isEmpty())
-    def validCluster = clusters[0][0]
-    sql """use @${validCluster};""";
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="5") """
-    String[][] backends = sql """ show backends """
-    String backendId;
-    def backendIdToBackendIP = [:]
-    def backendIdToBackendHttpPort = [:]
-    def backendIdToBackendBrpcPort = [:]
-    for (String[] backend in backends) {
-        if (backend[9].equals("true") && backend[19].contains("regression_cluster_name1")) {
-            backendIdToBackendIP.put(backend[0], backend[1])
-            backendIdToBackendHttpPort.put(backend[0], backend[4])
-            backendIdToBackendBrpcPort.put(backend[0], backend[5])
-        }
-    }
-    assertEquals(backendIdToBackendIP.size(), 1)
-
-    backendId = backendIdToBackendIP.keySet()[0]
-    def url = backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendHttpPort.get(backendId) + """/api/file_cache?op=clear&sync=true"""
-    logger.info(url)
-    def clearFileCache = { check_func ->
-        httpTest {
-            endpoint ""
-            uri url
-            op "get"
-            body ""
-            check check_func
-        }
-    }
-
-    sql new File("""${context.file.parent}/../ddl/customer_ttl_delete.sql""").text
-    def load_customer_once =  { String table ->
-        sql (new File("""${context.file.parent}/../ddl/${table}.sql""").text + ttlProperties)
-        sql """ alter table ${table} set ("disable_auto_compaction" = "true") """ // no influence from compaction
-        def totalRows = 200
-        def batchSize = 100
-        def commentSuffix = ' ' + ('X' * 50)
-        for (int offset = 0; offset < totalRows; offset += batchSize) {
-            def sb = new StringBuilder()
-            int batchEnd = Math.min(totalRows, offset + batchSize)
-            for (int idx = offset; idx < batchEnd; idx++) {
-                def customerId = 10001 + idx
-                def customerName = String.format('Customer#%09d', customerId)
-                sb.append("""INSERT INTO ${table} VALUES (
-                    ${customerId},
-                    '${customerName}',
-                    'Address Line 1',
-                    15,
-                    '123-456-7890',
-                    12345.67,
-                    'AUTOMOBILE',
-                    'This is a test comment for the customer.${commentSuffix}'
-                    );
-                    """)
-            }
-            sql sb.toString()
-        }
-    }
-
-    def getMetricsMethod = { check_func ->
-        httpTest {
-            endpoint backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendBrpcPort.get(backendId)
-            uri "/brpc_metrics"
-            op "get"
-            check check_func
-        }
-    }
-
-    def getTabletIds = { String tableName ->
-        def tablets = sql "show tablets from ${tableName}"
-        assertTrue(tablets.size() > 0, "No tablets found for table ${tableName}")
-        tablets.collect { it[0] as Long }
-    }
-
-    def waitForFileCacheType = { List<Long> tabletIds, String expectedType, long timeoutMs = 60000L, long intervalMs = 1000L ->
-        long start = System.currentTimeMillis()
-        while (System.currentTimeMillis() - start < timeoutMs) {
-            boolean allMatch = true
-            for (Long tabletId in tabletIds) {
-                def rows = sql "select type from information_schema.file_cache_info where tablet_id = ${tabletId}"
-                if (rows.isEmpty()) {
-                    logger.warn("file_cache_info is empty for tablet ${tabletId} while waiting for ${expectedType}")
-                    allMatch = false
-                    break
-                }
-                def mismatch = rows.find { row -> !row[0]?.toString()?.equalsIgnoreCase(expectedType) }
-                if (mismatch) {
-                    logger.info("tablet ${tabletId} has cache types ${rows.collect { it[0] }} while waiting for ${expectedType}")
-                    allMatch = false
-                    break
-                }
-            }
-            if (allMatch) {
-                logger.info("All file cache entries for tablets ${tabletIds} are ${expectedType}")
-                return
-            }
-            sleep(intervalMs)
-        }
-        assertTrue(false, "Timeout waiting for file_cache_info type ${expectedType} for tablets ${tabletIds}")
-    }
-
-    clearFileCache.call() {
-        respCode, body -> {}
-    }
-    sleep(30000)
-
-    load_customer_once("customer_ttl")
-    def tabletIds = getTabletIds.call("customer_ttl")
-    // No wait for the "ttl" type here. The TTL deadline is the tablet creation time plus
-    // file_cache_ttl_seconds, and with a 5s ttl the load itself outlives it, so most of the
-    // data is written straight into the normal queue and the table never has all of its
-    // blocks in the TTL queue at once.
-    sleep(30000) // 30s
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    logger.info("ttl_cache_size after load: " + line)
-                    assertEquals(line.substring(i).toLong(), 0)
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
-    }
-
-    waitForFileCacheType.call(tabletIds, "normal", 60000L)
-    }
-}
diff --git a/regression-test/suites/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.groovy b/regression-test/suites/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.groovy
index ffc864e0e53..824cb8d7dd0 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/st04_alter_ttl_n_to_0_runtime.groovy
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-suite("st04_alter_ttl_n_to_0_runtime") {
+suite("st04_alter_ttl_n_to_0_runtime", "nonConcurrent") {
     def customBeConfig = [
         enable_evict_file_cache_in_advance : false,
         file_cache_enter_disk_resource_limit_mode_percent : 99,
@@ -27,119 +27,160 @@ suite("st04_alter_ttl_n_to_0_runtime") {
     setBeConfigTemporary(customBeConfig) {
         def clusters = sql "SHOW CLUSTERS"
         assertTrue(!clusters.isEmpty())
-        def validCluster = clusters[0][0]
-        sql """use @${validCluster};"""
-
+        String clusterName = clusters[0][0].toString()
+        def backends = sql_return_maparray("SHOW BACKENDS").findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collectEntries { be -> [(be.BackendId as Long): be] }
+        assertTrue(!backends.isEmpty(), "No alive backends in ${clusterName}")
+        def backendIds = backends.keySet()
         String tableName = "st04_ttl_n_to_0_tpl"
-        def ddl = new File("""${context.file.parent}/../ddl/st04_alter_ttl_n_to_0_runtime.sql""").text
-                .replace("\${TABLE_NAME}", tableName)
-        sql ddl
-
-        String[][] backends = sql """show backends"""
-        def backendIdToBackendIP = [:]
-        def backendIdToBackendHttpPort = [:]
-        def backendIdToBackendBrpcPort = [:]
-        for (String[] backend in backends) {
-            if (backend[9].equals("true") && backend[19].contains("${validCluster}")) {
-                backendIdToBackendIP.put(backend[0], backend[1])
-                backendIdToBackendHttpPort.put(backend[0], backend[4])
-                backendIdToBackendBrpcPort.put(backend[0], backend[5])
-            }
+        String dataQuery = "select count(*), sum(k1), sum(length(v1)) from ${tableName}"
+        def tabletIds = []
+        def originalQueryCache = sql("select @@enable_sql_cache, @@enable_query_cache")[0].collect { value ->
+            String setting = value.toString().toLowerCase(Locale.ROOT)
+            assertTrue(setting in ["true", "false", "0", "1"], "Unexpected cache setting: ${value}")
+            setting
         }
-        assertEquals(backendIdToBackendIP.size(), 1)
 
-        def backendId = backendIdToBackendIP.keySet()[0]
-        def clearUrl = backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendHttpPort.get(backendId) + "/api/file_cache?op=clear&sync=true"
-        httpTest {
-            endpoint ""
-            uri clearUrl
-            op "get"
-            body ""
-            check { respCode, body ->
-                assertEquals("${respCode}".toString(), "200")
+        def getCache = {
+            sql "use @${clusterName}"
+            def rows = sql """select be_id, cache_path, tablet_id, `hash`, `offset`, size, lower(type)
+                from information_schema.file_cache_info
+                where tablet_id in (${tabletIds.join(',')}) and be_id in (${backendIds.join(',')})"""
+            def blocks = [:]
+            rows.each { row ->
+                def key = [row[0] as Long, row[1].toString(), row[2] as Long,
+                           row[3].toString(), row[4] as Long]
+                assertTrue(backendIds.contains(key[0]) && tabletIds.contains(key[2]),
+                        "Cache outside ${clusterName}/${tabletIds}: ${row}")
+                assertTrue(!blocks.containsKey(key), "Duplicate cache block: ${key}")
+                blocks[key] = [size: row[5] as Long, type: row[6]?.toString()]
             }
+            blocks
         }
-
-        def getTabletIds = { String tbl ->
-            def tablets = sql """show tablets from ${tbl}"""
-            assertTrue(tablets.size() > 0, "No tablets found for table ${tbl}")
-            tablets.collect { it[0] as Long }
+        def summarizeCache = { Map blocks ->
+            blocks.groupBy { key, value -> [key[0], key[2]] }.collectEntries { key, entries ->
+                [(key): [blocks: entries.size(),
+                         bytesByType: entries.values().groupBy { it.type }.collectEntries { type, values ->
+                             [(type): values.sum(0L) { it.size }]
+                         }]]
+            }
         }
-
-        def waitForFileCacheType = { List<Long> tabletIds, String expectedType, long timeoutMs = 600000L, long intervalMs = 2000L ->
-            long start = System.currentTimeMillis()
-            while (System.currentTimeMillis() - start < timeoutMs) {
-                boolean allMatch = true
-                for (Long tabletId in tabletIds) {
-                    def rows = sql """select type from information_schema.file_cache_info where tablet_id = ${tabletId}"""
-                    if (rows.isEmpty()) {
-                        allMatch = false
-                        break
-                    }
-                    def mismatch = rows.find { row -> !row[0]?.toString()?.equalsIgnoreCase(expectedType) }
-                    if (mismatch) {
-                        allMatch = false
-                        break
-                    }
+        def waitForCache = { String description, Closure ready ->
+            // Updating the interval cannot interrupt an already sleeping background thread.
+            long deadline = System.currentTimeMillis() + 600000L
+            def blocks = [:]
+            def previous = null
+            while (System.currentTimeMillis() < deadline) {
+                blocks = getCache()
+                if (blocks != previous) {
+                    logger.info("${description}: cluster=${clusterName}, " +
+                            "cache_by_be_tablet=${summarizeCache(blocks)}, blocks=${blocks}")
+                    previous = blocks
                 }
-                if (allMatch) {
-                    return
+                if (ready(blocks)) {
+                    return blocks
                 }
-                sleep(intervalMs)
+                sleep(2000)
             }
-            assertTrue(false, "Timeout waiting for ${expectedType}, tablets=${tabletIds}")
+            assertTrue(false, "Timeout after 600000ms waiting for ${description}: " +
+                    "cluster=${clusterName}, tablets=${tabletIds}, BEs=${backendIds}, blocks=${blocks}")
         }
-
-        def waitTtlCacheSizeZero = { long timeoutMs = 120000L, long intervalMs = 2000L ->
-            long start = System.currentTimeMillis()
-            while (System.currentTimeMillis() - start < timeoutMs) {
-                long ttlCacheSize = -1L
-                httpTest {
-                    endpoint backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendBrpcPort.get(backendId)
-                    uri "/brpc_metrics"
-                    op "get"
-                    check { respCode, body ->
-                        assertEquals("${respCode}".toString(), "200")
-                        String out = "${body}".toString()
-                        def lines = out.split('\n')
-                        for (String line in lines) {
-                            if (line.startsWith("#")) {
-                                continue
-                            }
-                            if (line.contains("ttl_cache_size")) {
-                                def idx = line.indexOf(' ')
-                                ttlCacheSize = line.substring(idx).trim().toLong()
-                                break
-                            }
+        def getGlobalTtlBytes = {
+            // Diagnostic only: other tables and busy blocks may keep the BE's TTL queue nonempty.
+            backends.collectEntries { id, be ->
+                Long bytes = null
+                try {
+                    String metrics = new URL("http://${be.Host}:${be.BrpcPort}/brpc_metrics")
+                            .getText(connectTimeout: 5000, readTimeout: 5000)
+                    def values = metrics.readLines().findResults { line ->
+                        def fields = line.trim().split(/\s+/)
+                        if (fields.size() == 2 && (fields[0] == "file_cache_ttl_cache_size" ||
+                                fields[0].endsWith("_file_cache_ttl_cache_size"))) {
+                            return fields[1] as Long
                         }
+                        null
                     }
+                    if (!values.isEmpty()) {
+                        bytes = values.sum(0L)
+                    }
+                } catch (Exception e) {
+                    logger.warn("Cannot read diagnostic TTL metric for BE ${id}: ${e.message}")
                 }
-                if (ttlCacheSize == 0L) {
-                    return
-                }
-                sleep(intervalMs)
+                [(id): bytes]
             }
-            assertTrue(false, "Timeout waiting ttl_cache_size = 0")
         }
 
-        def insertBatch = { int start, int end ->
-            def values = (start..<end).collect { i -> "(${i}, 'value_${i}')" }.join(",")
-            sql """insert into ${tableName} values ${values}"""
-        }
-        insertBatch(0, 200)
-        insertBatch(200, 400)
-
-        qt_sql """select count(*) from ${tableName} where v1 like 'value_%'"""
-        sleep(5000)
+        try {
+            sql "use @${clusterName}"
+            sql "set enable_sql_cache = false"
+            sql "set enable_query_cache = false"
+            def ddl = new File("${context.file.parent}/../ddl/st04_alter_ttl_n_to_0_runtime.sql").text
+                    .replace('${TABLE_NAME}', tableName)
+            sql ddl
+            tabletIds = sql_return_maparray("show tablets from ${tableName}")
+                    .collect { it.TabletId as Long }.unique().sort()
+            assertTrue(tabletIds.size() == 8, "Expected 8 tablets: ${tabletIds}")
 
-        def tabletIds = getTabletIds.call(tableName)
-        waitForFileCacheType.call(tabletIds, "ttl")
+            // Fresh tablet IDs isolate this case without clearing other tables' caches.
+            [0, 200].each { start ->
+                def values = (start..<(start + 200)).collect { i -> "(${i}, 'value_${i}')" }.join(",")
+                sql "insert into ${tableName} values ${values}"
+            }
+            qt_before_alter dataQuery
 
-        // ST-04 未覆盖点模板:运行期 ALTER N->0 后,缓存类型应从 ttl 转为 normal
-        sql """alter table ${tableName} set ("file_cache_ttl_seconds"="0")"""
-        waitForFileCacheType.call(tabletIds, "normal")
-        waitTtlCacheSizeZero.call()
+            def candidate = null
+            long stableSince = 0L
+            def baseline = waitForCache("stable TTL baseline") { blocks ->
+                boolean allTtl = tabletIds.every { id -> blocks.keySet().any { it[2] == id } } &&
+                        blocks.values().every { it.size > 0L && it.type == "ttl" }
+                if (!allTtl || blocks != candidate) {
+                    candidate = allTtl ? blocks : null
+                    stableSince = System.currentTimeMillis()
+                    return false
+                }
+                System.currentTimeMillis() - stableSince >= 3000L
+            }
+            def globalBefore = getGlobalTtlBytes()
+            logger.info("TTL before ALTER: global_by_be=${globalBefore}, " +
+                    "case_by_be_tablet=${summarizeCache(baseline)}")
+            sql "alter table ${tableName} set (\"file_cache_ttl_seconds\"=\"0\")"
 
-        sql """drop table if exists ${tableName}"""
+            // Do not read table data here: verify the background conversion of existing blocks.
+            def lastDifference = null
+            def converted = waitForCache("existing TTL blocks to become NORMAL") { blocks ->
+                def difference = [missing: baseline.keySet() - blocks.keySet(),
+                                  unexpected: blocks.keySet() - baseline.keySet(),
+                                  changed: blocks.findAll { key, value ->
+                                      value.type != "normal" || value.size != baseline[key]?.size
+                                  }]
+                if (difference != lastDifference) {
+                    logger.info("TTL conversion differences: ${difference}")
+                    lastDifference = difference
+                }
+                difference.values().every { it.isEmpty() }
+            }
+            // Exact block equality also ensures every tablet retains its nonzero baseline bytes.
+            def globalAfter = getGlobalTtlBytes()
+            def globalDelta = backendIds.collectEntries { id ->
+                [(id): globalBefore[id] != null && globalAfter[id] != null ?
+                        globalAfter[id] - globalBefore[id] : null]
+            }
+            logger.info("TTL after ALTER: global_by_be=${globalAfter}, global_delta_by_be=${globalDelta}, " +
+                    "case_by_be_tablet=${summarizeCache(converted)}")
+            qt_after_alter dataQuery
+        } finally {
+            try {
+                sql "use @${clusterName}"
+                sql "drop table if exists ${tableName} force"
+            } finally {
+                try {
+                    sql "set enable_sql_cache = ${originalQueryCache[0]}"
+                } finally {
+                    sql "set enable_query_cache = ${originalQueryCache[1]}"
+                }
+            }
+        }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.groovy b/regression-test/suites/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.groovy
index 0d8e55c9da5..fbc90987acc 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/st06_warmup_ttl_type_assert.groovy
@@ -15,7 +15,19 @@
 // specific language governing permissions and limitations
 // under the License.
 
-suite("st06_warmup_ttl_type_assert") {
+suite("st06_warmup_ttl_type_assert", "nonConcurrent") {
+    // This setting is static in FE; isolate it and restore it even when a cache assertion fails.
+    String originalSyncLoad = sql("select @@enable_multi_cluster_sync_load")[0][0].toString()
+    assertTrue(originalSyncLoad.toLowerCase() in ["true", "false", "1", "0"],
+            "Unexpected sync-load setting: ${originalSyncLoad}")
+    onFinish {
+        sql "set enable_multi_cluster_sync_load = ${originalSyncLoad}"
+        logger.info("Restored enable_multi_cluster_sync_load=${originalSyncLoad}")
+    }
+    // Only explicit warm-up should populate the target cache in this case.
+    sql "set enable_multi_cluster_sync_load = false"
+    logger.info("Explicit warmup: disabled automatic sync load, previous=${originalSyncLoad}")
+
     def customBeConfig = [
         enable_evict_file_cache_in_advance : false,
         file_cache_enter_disk_resource_limit_mode_percent : 99
@@ -32,95 +44,154 @@ suite("st06_warmup_ttl_type_assert") {
         def targetCluster = clusters[1][0]
         String tableName = "st06_warmup_ttl_tpl"
 
-        sql """use @${sourceCluster};"""
-        def ddl = new File("""${context.file.parent}/../ddl/st06_warmup_ttl_type_assert.sql""").text
-                .replace("\${TABLE_NAME}", tableName)
-        sql ddl
-
-        def values = (0..<200).collect { i -> "(${i}, 'warmup_tpl_${i}')" }.join(",")
-        sql """insert into ${tableName} values ${values}"""
-        qt_source_preheat """select count(*) from ${tableName}"""
-
-        def sourceTablets = sql """show tablets from ${tableName}"""
-        assertTrue(sourceTablets.size() > 0, "No tablets found for table ${tableName} in source cluster ${sourceCluster}")
-        def sourceTabletIds = sourceTablets.collect { it[0] as Long }
-
-        // ST-06 部分覆盖点模板:显式断言 warmup 后目标集群缓存类型为 ttl
-        def jobIdRows = sql """warm up cluster ${targetCluster} with table ${tableName};"""
-        assertTrue(!jobIdRows.isEmpty())
-        def jobId = jobIdRows[0][0]
-
-        def waitWarmUpJobFinished = { Object id, long timeoutMs = 600000L, long intervalMs = 5000L ->
-            long start = System.currentTimeMillis()
-            while (System.currentTimeMillis() - start < timeoutMs) {
-                def stateRows = sql """SHOW WARM UP JOB WHERE ID = ${id}"""
-                if (stateRows.isEmpty()) {
-                    sleep(intervalMs)
-                    continue
+        // Read both data columns on BE; count(*) may be evaluated entirely in FE.
+        sql "set enable_sql_cache = false"
+        sql "set enable_query_cache = false"
+        String dataQuery = "select sum(k1), sum(length(v1)) from ${tableName}"
+
+        def getClusterBackends = { String clusterName ->
+            sql_return_maparray("SHOW BACKENDS").findAll { be ->
+                "${be.Alive}".equalsIgnoreCase("true") &&
+                        parseJson(be.Tag.toString()).compute_group_name == clusterName
+            }.collectEntries { be -> [(be.BackendId as Long): be] }
+        }
+
+        def waitForTargetTablets = { List<Long> tabletIds ->
+            sql "use @${targetCluster}"
+            long deadline = System.currentTimeMillis() + 120000L
+            def missing = tabletIds
+            while (System.currentTimeMillis() < deadline) {
+                def backends = getClusterBackends.call(targetCluster)
+                def tablets = sql_return_maparray("show tablets from ${tableName}")
+                def readyIds = []
+                for (def tablet : tablets) {
+                    long tabletId = tablet.TabletId as Long
+                    long primaryBeId = tablet.PrimaryBackendId as Long
+                    // BackendId alone can be computed on demand before the primary route exists.
+                    if (!tabletIds.contains(tabletId) || !backends.containsKey(primaryBeId) ||
+                            (tablet.BackendId as Long) != primaryBeId) {
+                        continue
+                    }
+                    def be = backends[primaryBeId]
+                    boolean ready = false
+                    try {
+                        // Fetch only tablet metadata, leaving the target data cache cold for warm-up.
+                        httpTest {
+                            endpoint "${be.Host}:${be.HttpPort}"
+                            uri "/api/meta/header/${tabletId}"
+                            op "get"
+                            check { respCode, body ->
+                                if ((respCode as int) == 200) {
+                                    def meta = parseJson(body.toString())
+                                    // Cloud header JSON omits rs_metas. Check tablet identity/state here;
+                                    // the source scan and post-warm-up cache checks establish data readiness.
+                                    ready = (meta.tablet_id as Long) == tabletId &&
+                                            (meta.ttl_seconds as Long) == 3600L &&
+                                            meta.tablet_state == "PB_RUNNING" && meta.schema != null
+                                }
+                            }
+                        }
+                    } catch (Exception e) {
+                        logger.info("Waiting for tablet ${tabletId} on ${targetCluster}, " +
+                                "BE ${primaryBeId}: ${e.message}")
+                    }
+                    if (ready) {
+                        readyIds.add(tabletId)
+                    }
                 }
-                def state = stateRows[0][3].toString()
-                if ("FINISHED".equalsIgnoreCase(state)) {
+                missing = tabletIds - readyIds
+                if (missing.isEmpty()) {
+                    logger.info("Target tablet metadata is ready: cluster=${targetCluster}, tablets=${tabletIds}")
                     return
                 }
-                if ("CANCELLED".equalsIgnoreCase(state) || "FAILED".equalsIgnoreCase(state)) {
-                    assertTrue(false, "Warm up job failed, id=${id}, state=${state}")
-                }
-                sleep(intervalMs)
+                logger.info("Waiting for target tablet metadata: cluster=${targetCluster}, missing=${missing}")
+                sleep(1000)
             }
-            assertTrue(false, "Timeout waiting warm up job finished, id=${id}")
+            assertTrue(false, "Timeout waiting for target tablet metadata: cluster=${targetCluster}, missing=${missing}")
         }
-        waitWarmUpJobFinished.call(jobId)
-
-        sql """use @${targetCluster};"""
-        qt_target_query """select count(*) from ${tableName}"""
-        def targetTablets = sql """show tablets from ${tableName}"""
-        assertTrue(targetTablets.size() > 0, "No tablets found for table ${tableName} in target cluster ${targetCluster}")
-        def targetTabletIds = targetTablets.collect { it[0] as Long }
-        assertTrue(sourceTabletIds.size() == targetTabletIds.size(),
-                "Tablet size mismatch between source and target, source=${sourceTabletIds.size()}, target=${targetTabletIds.size()}")
-
-        def waitForFileCacheType = { List<Long> sourceIds, List<Long> targetIds, String expectedType, long timeoutMs = 600000L, long intervalMs = 2000L ->
-            logger.info("waitForFileCacheType, sourceIds=${sourceIds.toString()}, targetIds=${targetIds.toString()}, expectedType=${expectedType}")
-            assertTrue(sourceIds.size() == targetIds.size(),
-                    "Tablet size mismatch before waiting file cache type, source=${sourceIds.size()}, target=${targetIds.size()}")
-            long start = System.currentTimeMillis()
-            while (System.currentTimeMillis() - start < timeoutMs) {
-                int sourceMatched = 0
-                int targetMatched = 0
-                for (Long sourceTabletId in sourceIds) {
-                    def sourceTabletIdStr = sql """select * from information_schema.file_cache_info where tablet_id = ${sourceTabletId}"""
-                    logger.info("[source tablet] tablet_id=${sourceTabletId}, tablet_cache_info=${sourceTabletIdStr.toString()}")
-                    def rows = sql """select type from information_schema.file_cache_info where tablet_id = ${sourceTabletId}"""
-                    if (!rows.isEmpty()) {
-                        def mismatch = rows.find { row -> !row[0]?.toString()?.equalsIgnoreCase(expectedType) }
-                        if (!mismatch) {
-                            sourceMatched++
-                        }
-                    }
-                }
-                for (Long targetTabletId in targetIds) {
-                    def targetTabletIdStr = sql """select * from information_schema.file_cache_info where tablet_id = ${targetTabletId}"""
-                    logger.info("[target tablet] tablet_id=${targetTabletId}, tablet_cache_info=${targetTabletIdStr.toString()}")
-                    def rows = sql """select type from information_schema.file_cache_info where tablet_id = ${targetTabletId}"""
-                    if (!rows.isEmpty()) {
-                        def mismatch = rows.find { row -> !row[0]?.toString()?.equalsIgnoreCase(expectedType) }
-                        if (!mismatch) {
-                            targetMatched++
-                        }
-                    }
+
+        def waitForFileCacheType = { String clusterName, List<Long> tabletIds ->
+            sql "use @${clusterName}"
+            def backendIds = getClusterBackends.call(clusterName).keySet()
+            assertTrue(!backendIds.isEmpty(), "No alive backends in ${clusterName}")
+            long deadline = System.currentTimeMillis() + 600000L
+            def rows = []
+            def missing = tabletIds
+            while (System.currentTimeMillis() < deadline) {
+                rows = sql """select tablet_id, type, be_id from information_schema.file_cache_info
+                              where tablet_id in (${tabletIds.join(',')})"""
+                assertTrue(rows.every { backendIds.contains(it[2] as Long) },
+                        "Cache query ran outside ${clusterName}: expected BEs=${backendIds}, rows=${rows}")
+                missing = tabletIds.findAll { tabletId ->
+                    def tabletRows = rows.findAll { (it[0] as Long) == tabletId }
+                    tabletRows.isEmpty() || tabletRows.any { !it[1]?.toString()?.equalsIgnoreCase("ttl") }
                 }
-              
-                if (sourceMatched == sourceIds.size() && targetMatched == targetIds.size()) {
+                if (missing.isEmpty()) {
                     return
                 }
-                sleep(intervalMs)
+                logger.info("Waiting for TTL cache: cluster=${clusterName}, missing=${missing}, rows=${rows}")
+                sleep(2000)
             }
-            assertTrue(false, "Timeout waiting for ${expectedType}, sourceTablets=${sourceIds}, targetTablets=${targetIds}")
+            assertTrue(false, "Timeout waiting for TTL cache: cluster=${clusterName}, missing=${missing}, rows=${rows}")
         }
-        waitForFileCacheType.call(sourceTabletIds, targetTabletIds, "ttl")
 
-        // cleanup
-        sql """use @${sourceCluster};"""
-        sql """drop table if exists ${tableName}"""
+        try {
+            sql """use @${sourceCluster};"""
+            def ddl = new File("""${context.file.parent}/../ddl/st06_warmup_ttl_type_assert.sql""").text
+                    .replace("\${TABLE_NAME}", tableName)
+            sql ddl
+
+            def values = (0..<200).collect { i -> "(${i}, 'warmup_tpl_${i}')" }.join(",")
+            sql """insert into ${tableName} values ${values}"""
+            qt_source_preheat dataQuery
+
+            def sourceTablets = sql """show tablets from ${tableName}"""
+            assertTrue(sourceTablets.size() > 0, "No tablets found for table ${tableName} in source cluster ${sourceCluster}")
+            def sourceTabletIds = sourceTablets.collect { it[0] as Long }
+
+            waitForTargetTablets.call(sourceTabletIds)
+
+            // ST-06 部分覆盖点模板:显式断言 warmup 后目标集群缓存类型为 ttl
+            def jobIdRows = sql """warm up cluster ${targetCluster} with table ${tableName};"""
+            assertTrue(!jobIdRows.isEmpty())
+            def jobId = jobIdRows[0][0]
+
+            def waitWarmUpJobFinished = { Object id, long timeoutMs = 600000L, long intervalMs = 5000L ->
+                long start = System.currentTimeMillis()
+                while (System.currentTimeMillis() - start < timeoutMs) {
+                    def stateRows = sql """SHOW WARM UP JOB WHERE ID = ${id}"""
+                    if (stateRows.isEmpty()) {
+                        sleep(intervalMs)
+                        continue
+                    }
+                    def state = stateRows[0][3].toString()
+                    if ("FINISHED".equalsIgnoreCase(state)) {
+                        return
+                    }
+                    if ("CANCELLED".equalsIgnoreCase(state) || "FAILED".equalsIgnoreCase(state)) {
+                        assertTrue(false, "Warm up job failed, id=${id}, state=${state}")
+                    }
+                    sleep(intervalMs)
+                }
+                assertTrue(false, "Timeout waiting warm up job finished, id=${id}")
+            }
+            waitWarmUpJobFinished.call(jobId)
+
+            sql """use @${targetCluster};"""
+            def targetTablets = sql """show tablets from ${tableName}"""
+            assertTrue(targetTablets.size() > 0, "No tablets found for table ${tableName} in target cluster ${targetCluster}")
+            def targetTabletIds = targetTablets.collect { it[0] as Long }
+            assertTrue(sourceTabletIds.toSet() == targetTabletIds.toSet(),
+                    "Tablet IDs differ between source and target, source=${sourceTabletIds}, target=${targetTabletIds}")
+
+            // Check warm-up before reading target data, so a query cannot populate a missing cache.
+            waitForFileCacheType.call(sourceCluster, sourceTabletIds)
+            waitForFileCacheType.call(targetCluster, targetTabletIds)
+            sql """use @${targetCluster};"""
+            qt_target_query dataQuery
+        } finally {
+            sql """use @${sourceCluster};"""
+            sql """drop table if exists ${tableName}"""
+        }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/ttl/test_show_ttl.groovy b/regression-test/suites/cloud_p0/cache/ttl/test_show_ttl.groovy
new file mode 100644
index 00000000000..91ca261fe5b
--- /dev/null
+++ b/regression-test/suites/cloud_p0/cache/ttl/test_show_ttl.groovy
@@ -0,0 +1,57 @@
+// 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_show_ttl", "nonConcurrent") {
+    String tableName = "test_show_ttl_properties"
+    def checkTtlProperty = { long expectedTtl ->
+        def rows = sql "SHOW CREATE TABLE ${tableName}"
+        assertEquals(1, rows.size())
+        assertEquals(tableName, rows[0][0].toString())
+        String createTable = rows[0][1].toString()
+        def matcher = createTable =~ /"file_cache_ttl_seconds"\s*=\s*"(\d+)"/
+        assertTrue(matcher.find(), "Missing file_cache_ttl_seconds in SHOW CREATE TABLE: ${createTable}")
+        long actualTtl = matcher.group(1).toLong()
+        assertFalse(matcher.find(), "Duplicate file_cache_ttl_seconds in SHOW CREATE TABLE: ${createTable}")
+        logger.info("SHOW CREATE TABLE TTL property: table=${tableName}, expected=${expectedTtl}, actual=${actualTtl}")
+        assertEquals(expectedTtl, actualTtl)
+    }
+
+    try {
+        sql "DROP TABLE IF EXISTS ${tableName}"
+        sql """
+            CREATE TABLE ${tableName} (
+                C_CUSTKEY INTEGER NOT NULL,
+                C_NAME VARCHAR(25) NOT NULL,
+                C_ADDRESS VARCHAR(40) NOT NULL,
+                C_NATIONKEY INTEGER NOT NULL,
+                C_PHONE CHAR(15) NOT NULL,
+                C_ACCTBAL DECIMAL(15,2) NOT NULL,
+                C_MKTSEGMENT CHAR(10) NOT NULL,
+                C_COMMENT VARCHAR(117) NOT NULL
+            )
+            DUPLICATE KEY(C_CUSTKEY, C_NAME)
+            DISTRIBUTED BY HASH(C_CUSTKEY) BUCKETS 32
+            PROPERTIES("file_cache_ttl_seconds"="300")
+        """
+        // Other SHOW CREATE TABLE properties depend on cluster defaults.
+        checkTtlProperty(300L)
+        sql "ALTER TABLE ${tableName} SET (\"file_cache_ttl_seconds\"=\"0\")"
+        checkTtlProperty(0L)
+    } finally {
+        sql "DROP TABLE IF EXISTS ${tableName}"
+    }
+}
diff --git a/regression-test/suites/cloud_p0/cache/ttl/test_show_ttl.sql b/regression-test/suites/cloud_p0/cache/ttl/test_show_ttl.sql
deleted file mode 100644
index ce7c5c1986c..00000000000
--- a/regression-test/suites/cloud_p0/cache/ttl/test_show_ttl.sql
+++ /dev/null
@@ -1,40 +0,0 @@
--- 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.
-
-DROP TABLE IF EXISTS customer_ttl;
-
-CREATE TABLE IF NOT EXISTS customer_ttl (
-  C_CUSTKEY     INTEGER NOT NULL,
-  C_NAME        VARCHAR(25) NOT NULL,
-  C_ADDRESS     VARCHAR(40) NOT NULL,
-  C_NATIONKEY   INTEGER NOT NULL,
-  C_PHONE       CHAR(15) NOT NULL,
-  C_ACCTBAL     DECIMAL(15,2)   NOT NULL,
-  C_MKTSEGMENT  CHAR(10) NOT NULL,
-  C_COMMENT     VARCHAR(117) NOT NULL
-)
-DUPLICATE KEY(C_CUSTKEY, C_NAME)
-DISTRIBUTED BY HASH(C_CUSTKEY) BUCKETS 32
-PROPERTIES(
-    "file_cache_ttl_seconds"="300"
-);
-
-show create table customer_ttl;
-
-alter table customer_ttl set ("file_cache_ttl_seconds"="0");
-
-show create table customer_ttl;
diff --git a/regression-test/suites/cloud_p0/cache/ttl/test_ttl.groovy b/regression-test/suites/cloud_p0/cache/ttl/test_ttl.groovy
index 09ee37aef07..2deb8eac632 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/test_ttl.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/test_ttl.groovy
@@ -15,190 +15,295 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import org.codehaus.groovy.runtime.IOGroovyMethods
+import org.apache.doris.regression.util.Http
 
-suite("test_ttl") {
-    def custoBeConfig = [
-        enable_evict_file_cache_in_advance : false,
-        file_cache_enter_disk_resource_limit_mode_percent : 99,
-        file_cache_background_ttl_gc_interval_ms : 1000,
-        file_cache_background_ttl_info_update_interval_ms : 1000,
-        file_cache_background_tablet_id_flush_interval_ms : 1000
+suite("test_ttl", "nonConcurrent") {
+    def customBeConfig = [
+        enable_evict_file_cache_in_advance: false,
+        file_cache_enter_disk_resource_limit_mode_percent: 99,
+        file_cache_background_ttl_gc_interval_ms: 1000,
+        file_cache_background_ttl_info_update_interval_ms: 1000,
+        file_cache_background_tablet_id_flush_interval_ms: 1000
     ]
-
-    setBeConfigTemporary(custoBeConfig) {
-    def clusters = sql " SHOW CLUSTERS; "
-    assertTrue(!clusters.isEmpty())
-    def validCluster = clusters[0][0]
-    sql """use @${validCluster};""";
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="180") """
-    String[][] backends = sql """ show backends """
-    String backendId;
-    def backendIdToBackendIP = [:]
-    def backendIdToBackendHttpPort = [:]
-    def backendIdToBackendBrpcPort = [:]
-    for (String[] backend in backends) {
-        if (backend[9].equals("true") && backend[19].contains("${validCluster}")) {
-            backendIdToBackendIP.put(backend[0], backend[1])
-            backendIdToBackendHttpPort.put(backend[0], backend[4])
-            backendIdToBackendBrpcPort.put(backend[0], backend[5])
+    setBeConfigTemporary(customBeConfig) {
+        String tableName = "test_ttl_natural_expiration"
+        long ttlSeconds = 300L
+        int bucketCount = 4
+        def clusters = sql "SHOW CLUSTERS"
+        assertTrue(!clusters.isEmpty())
+        String clusterName = clusters[0][0].toString()
+        def backends = sql_return_maparray("SHOW BACKENDS").findAll { be ->
+            "${be.Alive}".equalsIgnoreCase("true") &&
+                    parseJson(be.Tag.toString()).compute_group_name == clusterName
+        }.collectEntries { be -> [(be.BackendId as Long): be] }
+        assertTrue(!backends.isEmpty(), "No alive backends in ${clusterName}")
+        def backendIds = backends.keySet()
+        def tabletIds = []
+        def metadata = [:]
+        def lastState = [:]
+        def metricsBaseline = null
+        def originalQueryCache = sql("select @@enable_sql_cache, @@enable_query_cache")[0].collect { value ->
+            String setting = value.toString().toLowerCase(Locale.ROOT)
+            assertTrue(setting in ["true", "false", "0", "1"], "Unexpected cache setting: ${value}")
+            setting
         }
-    }
-    assertEquals(backendIdToBackendIP.size(), 1)
-
-    backendId = backendIdToBackendIP.keySet()[0]
-    def url = backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendHttpPort.get(backendId) + """/api/file_cache?op=clear&sync=true"""
-    logger.info(url)
-    def clearFileCache = { check_func ->
-        try {
-            httpTest {
-                endpoint ""
-                uri url
-                op "get"
-                body ""
-                check check_func
+        def serverTime = { sql("select unix_timestamp()")[0][0] as Long }
+        def waitUntil = { String stage, long deadlineMs, Closure ready ->
+            while (System.currentTimeMillis() < deadlineMs) {
+                if (ready()) {
+                    return
+                }
+                sleep(1000)
             }
-        } catch (Exception e) {
-            logger.error("Failed to clear file cache: ${e.message}")
-            throw e
+            assertTrue(false, "Timeout waiting for ${stage}: cluster=${clusterName}, " +
+                    "tablets=${tabletIds}, BEs=${backendIds}, last_state=${lastState}")
         }
-    }
-
-    sql new File("""${context.file.parent}/../ddl/customer_ttl_delete.sql""").text
-    def load_customer_once =  { String table ->
-        try {
-            sql (new File("""${context.file.parent}/../ddl/${table}.sql""").text + ttlProperties)
-            sql """ alter table ${table} set ("disable_auto_compaction" = "true") """ // no influence from compaction
-            def totalRows = 200
-            def batchSize = 100
-            def commentSuffix = ' ' + ('X' * 50)
-            for (int offset = 0; offset < totalRows; offset += batchSize) {
-                def sb = new StringBuilder()
-                int batchEnd = Math.min(totalRows, offset + batchSize)
-                for (int idx = offset; idx < batchEnd; idx++) {
-                    def customerId = 10001 + idx
-                    def customerName = String.format('Customer#%09d', customerId)
-                    sb.append("""INSERT INTO ${table} VALUES (
-                        ${customerId},
-                        '${customerName}',
-                        'Address Line 1',
-                        15,
-                        '123-456-7890',
-                        12345.67,
-                        'AUTOMOBILE',
-                        'This is a test comment for the customer.${commentSuffix}'
-                        );
-                        """)
-                }
-                sql sb.toString()
+        def getCache = {
+            sql "use @${clusterName}"
+            def blocks = [:]
+            def rows = sql """select be_id, cache_path, tablet_id, `hash`, `offset`, size, lower(type)
+                from information_schema.file_cache_info
+                where tablet_id in (${tabletIds.join(',')}) and be_id in (${backendIds.join(',')})"""
+            rows.each { row ->
+                def key = [row[0] as Long, row[1].toString(), row[2] as Long,
+                           row[3].toString(), row[4] as Long]
+                assertTrue(backendIds.contains(key[0]) && tabletIds.contains(key[2]), "Unexpected cache owner: ${row}")
+                assertTrue(key[4] >= 0L && (row[5] as Long) > 0L, "Invalid cache range: ${row}")
+                assertTrue(!blocks.containsKey(key), "Duplicate cache block: ${key}")
+                blocks[key] = [size: row[5] as Long, type: row[6]?.toString()]
             }
-        } catch (Exception e) {
-            logger.error("Failed to load customer data: ${e.message}")
-            throw e
+            blocks
         }
-    }
-
-    def getMetricsMethod = { check_func ->
-        try {
-            httpTest {
-                endpoint backendIdToBackendIP.get(backendId) + ":" + backendIdToBackendBrpcPort.get(backendId)
-                uri "/brpc_metrics"
-                op "get"
-                check check_func
+        def blockSizes = { Map blocks -> blocks.collectEntries { key, value -> [(key): value.size] } }
+        def summarizeCache = { Map blocks ->
+            blocks.groupBy { key, value -> [key[0], key[2]] }.collectEntries { owner, entries ->
+                [(owner): [block_count: entries.size(), bytes: entries.values().sum(0L) { it.size },
+                           by_type: (["ttl", "normal"] + entries.values().collect { it.type }).unique().collectEntries { type ->
+                               def values = entries.values().findAll { it.type == type }
+                               [(type): [block_count: values.size(), bytes: values.sum(0L) { it.size }]]
+                           }]]
             }
-        } catch (Exception e) {
-            logger.error("Failed to get metrics: ${e.message}")
-            throw e
         }
-    }
-
-    def getTabletIds = { String tableName ->
-        def tablets = sql "show tablets from ${tableName}"
-        assertTrue(tablets.size() > 0, "No tablets found for table ${tableName}")
-        tablets.collect { it[0] as Long }
-    }
-
-    def waitForFileCacheType = { List<Long> tabletIds, String expectedType, long timeoutMs = 60000L, long intervalMs = 2000L ->
-        long start = System.currentTimeMillis()
-        while (System.currentTimeMillis() - start < timeoutMs) {
-            boolean allMatch = true
-            for (Long tabletId in tabletIds) {
-                def rows = sql "select type from information_schema.file_cache_info where tablet_id = ${tabletId}"
-                if (rows.isEmpty()) {
-                    logger.warn("file_cache_info is empty for tablet ${tabletId} while waiting for ${expectedType}")
-                    allMatch = false
-                    break
+        def metricNames = ["file_cache_ttl_cache_size", "file_cache_normal_queue_cache_size",
+                           "file_cache_ttl_cache_lru_queue_size", "file_cache_ttl_cache_lru_queue_element_count",
+                           "file_cache_normal_queue_element_count", "file_cache_ttl_cache_evict_size",
+                           "file_cache_normal_queue_evict_size", "file_cache_ttl_mgr_tablet_id_set_size"]
+        def getMetrics = {
+            // These include other tables and update asynchronously. Only scoped cache data is asserted.
+            backends.collectEntries { id, be ->
+                def values = metricNames.collectEntries { [(it): null] }
+                try {
+                    String text = new URL("http://${be.Host}:${be.BrpcPort}/brpc_metrics")
+                            .getText(connectTimeout: 5000, readTimeout: 5000)
+                    text.readLines().each { line ->
+                        def fields = line.trim().split(/\s+/)
+                        if (fields.size() == 2 && !fields[0].startsWith("#")) {
+                            metricNames.each { name ->
+                                if (fields[0] == name || fields[0].endsWith("_${name}")) {
+                                    values[name] = (values[name] ?: 0L) + (fields[1] as Long)
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.warn("Cannot read diagnostic metrics for BE ${id}: ${e.message}")
                 }
-                def mismatches = rows.findAll { row -> !row[0]?.toString()?.equalsIgnoreCase(expectedType) }
-                if (!mismatches.isEmpty()) {
-                    logger.info("tablet ${tabletId} has cache types ${rows.collect { it[0] }} while waiting for ${expectedType}")
-                    allMatch = false
-                    break
+                def missing = values.findAll { name, value -> value == null }.keySet()
+                if (!missing.isEmpty()) {
+                    logger.warn("Unavailable diagnostic metrics: BE=${id}, names=${missing}")
                 }
+                [(id): values]
             }
-            if (allMatch) {
-                logger.info("All file cache entries for tablets ${tabletIds} are ${expectedType}")
-                return
+        }
+        def logMetrics = { String stage ->
+            def current = getMetrics()
+            def delta = backendIds.collectEntries { id ->
+                [(id): metricNames.collectEntries { name ->
+                    [(name): metricsBaseline != null && metricsBaseline[id][name] != null && current[id][name] != null ?
+                            current[id][name] - metricsBaseline[id][name] : null]
+                }]
             }
-            sleep(intervalMs)
+            logger.info("TTL metrics: stage=${stage}, cluster=${clusterName}, by_be=${current}, " +
+                    "delta_from_ttl_baseline=${delta}")
+            current
         }
-        assertTrue(false, "Timeout waiting for file_cache_info type ${expectedType} for tablets ${tabletIds}")
-    }
-
-    clearFileCache.call() {
-        respCode, body -> {}
-    }
-    sleep(10000)
+        String address = "Address Line 1"
+        String phone = "123-456-7890"
+        String segment = "AUTOMOBILE"
+        String comment = "This is a test comment for the customer. " + ("X" * 50)
+        def customerIds = (10001..10200).toList()
+        def customerNames = customerIds.collect { String.format('Customer#%09d', it) }
+        def normalize = { List values -> values.collect { new BigDecimal(it.toString()).stripTrailingZeros() } }
+        def expectedData = normalize([customerIds.size(), customerIds.sum(0L), customerNames.sum(0L) { it.length() },
+                200L * address.length(), 200L * 15L, 200L * phone.length(),
+                200G * 12345.67G, 200L * segment.length(), 200L * comment.length()])
+        def scanTable = {
+            // Read every column; result caches and metadata-only COUNT cannot satisfy this query.
+            def rows = sql """select count(*), sum(C_CUSTKEY), sum(length(C_NAME)), sum(length(C_ADDRESS)),
+                sum(C_NATIONKEY), sum(length(rtrim(C_PHONE))), sum(C_ACCTBAL),
+                sum(length(rtrim(C_MKTSEGMENT))), sum(length(C_COMMENT)) from ${tableName}"""
+            assertTrue(rows.size() == 1 && rows[0].size() == 9 && rows[0].every { it != null },
+                    "Unexpected data result: ${rows}")
+            assertEquals(expectedData, normalize(rows[0]))
+        }
+        Throwable failure = null
+        try {
+            sql "use @${clusterName}"
+            sql "set enable_sql_cache = false"
+            sql "set enable_query_cache = false"
+            logMetrics("before_create")
+            sql "drop table if exists ${tableName} force"
+            long prepareStartedMs = System.currentTimeMillis()
+            // Leave a minute for observing TTL before expiry, even on ASAN and with delayed metadata flushes.
+            long prepareDeadlineMs = prepareStartedMs + (ttlSeconds - 60L) * 1000L
+            String ddl = new File("${context.file.parent}/../ddl/customer_ttl.sql").text
+                    .replace('customer_ttl', tableName).replace('BUCKETS 32', "BUCKETS ${bucketCount}")
+            sql(ddl + """ PROPERTIES("file_cache_ttl_seconds"="${ttlSeconds}", "disable_auto_compaction"="true")""")
+            tabletIds = sql_return_maparray("show tablets from ${tableName}")
+                    .collect { it.TabletId as Long }.unique().sort()
+            assertTrue(tabletIds.size() == bucketCount, "Expected ${bucketCount} tablets: ${tabletIds}")
+            String values = customerIds.withIndex().collect { id, i ->
+                "(${id}, '${customerNames[i]}', '${address}', 15, '${phone}', 12345.67, '${segment}', '${comment}')"
+            }.join(",")
+            long insertStartedMs = System.currentTimeMillis()
+            sql "insert into ${tableName} values ${values}"
+            logger.info("TTL fixture: rows=200, tablets=${tabletIds}, insert_elapsed_ms=${System.currentTimeMillis() - insertStartedMs}")
+            scanTable()
 
-    def tabletIds = []
-    load_customer_once("customer_ttl")
-    sleep(10000)
-    tabletIds = getTabletIds.call("customer_ttl")
-    waitForFileCacheType.call(tabletIds, "ttl")
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            long ttl_cache_size = 0;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
+            // The full scan above verifies data; cloud header JSON does not include rowsets.
+            waitUntil("tablet TTL metadata", prepareDeadlineMs) {
+                metadata = [:]
+                sql_return_maparray("show tablets from ${tableName}").each { tablet ->
+                    long id = tablet.TabletId as Long
+                    long primary = tablet.PrimaryBackendId as Long
+                    if (!(id in tabletIds) || !backends.containsKey(primary) || (tablet.BackendId as Long) != primary) {
+                        return
+                    }
+                    def be = backends[primary]
+                    try {
+                        def connection = Http.openConnection(
+                                "http://${be.Host}:${be.HttpPort}/api/meta/header/${id}")
+                        connection.connectTimeout = 5000
+                        connection.readTimeout = 5000
+                        try {
+                            def meta = connection.inputStream.withCloseable {
+                                parseJson(it.getText("UTF-8"))
+                            }
+                            if ((meta.tablet_id as Long) == id && (meta.creation_time as Long) > 0L &&
+                                    (meta.ttl_seconds as Long) == ttlSeconds &&
+                                    meta.tablet_state == "PB_RUNNING" && meta.schema != null) {
+                                metadata[id] = [be_id: primary, creation_time: meta.creation_time as Long,
+                                                expires_at: (meta.creation_time as Long) + ttlSeconds]
+                            }
+                        } finally {
+                            connection.disconnect()
+                        }
+                    } catch (Exception e) {
+                        logger.info("Waiting for loaded metadata: tablet=${id}, BE=${primary}, ${e.message}")
                     }
-                    def i = line.indexOf(' ')
-                    ttl_cache_size = line.substring(i).toLong()
-                    logger.info("ttl_cache_size (initial) line: " + line)
-                    flag1 = true
                 }
+                lastState = [missing_tablets: tabletIds - metadata.keySet(), metadata: metadata]
+                metadata.size() == tabletIds.size()
             }
-            assertTrue(flag1)
-            assertTrue(ttl_cache_size > 10737)
-    }
-    sleep(180000)
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    logger.info("ttl_cache_size line before assert zero: " + line)
-                    assertEquals(line.substring(i).toLong(), 0)
-                    flag1 = true
+            long now = serverTime()
+            assertTrue(metadata.values().every { it.creation_time <= now }, "Future tablet creation time: ${metadata}, now=${now}")
+            long earliestExpiry = metadata.values().min { it.expires_at }.expires_at
+            long latestExpiry = metadata.values().max { it.expires_at }.expires_at
+            logger.info("TTL deadlines: ttl_seconds=${ttlSeconds}, server_now=${now}, by_tablet=${metadata}")
+            def baseline = [:]
+            def candidate = null
+            long stableSince = 0L
+            waitUntil("stable TTL baseline before expiry", prepareDeadlineMs) {
+                baseline = getCache()
+                now = serverTime()
+                assertTrue(now + 30L < earliestExpiry, "Preparation exhausted TTL window: now=${now}, metadata=${metadata}")
+                def missing = tabletIds.findAll { id -> !baseline.keySet().any { it[2] == id } }
+                boolean allTtl = missing.isEmpty() && baseline.values().every { it.type == "ttl" }
+                lastState = [missing_tablets: missing, cache_by_be_tablet: summarizeCache(baseline), seconds_to_expiry: earliestExpiry - now]
+                if (baseline != candidate) {
+                    logger.info("TTL baseline progress: ${lastState}")
+                }
+                if (!allTtl || baseline != candidate) {
+                    stableSince = System.currentTimeMillis()
                 }
+                candidate = baseline
+                allTtl && System.currentTimeMillis() - stableSince >= 3000L
             }
-            assertTrue(flag1)
-    }
+            logger.info("TTL baseline: preparation_elapsed_ms=${System.currentTimeMillis() - prepareStartedMs}, " +
+                    "cache_by_be_tablet=${summarizeCache(baseline)}, blocks=${baseline}")
+            metricsBaseline = logMetrics("ttl_baseline")
 
-    waitForFileCacheType.call(tabletIds, "normal")
+            // Keep TTL unchanged: this case covers natural expiry, separately from ALTER TTL cases.
+            // A new interval does not wake a thread already sleeping for the old 180-second interval.
+            now = serverTime()
+            long conversionDeadlineMs = System.currentTimeMillis() + Math.max(0L, latestExpiry + 1L - now) * 1000L + 600000L
+            def firstNormalAt = [:]
+            def converted = [:]
+            def previous = null
+            long lastLogMs = 0L
+            stableSince = 0L
+            waitUntil("natural TTL expiration", conversionDeadlineMs) {
+                // Do not read business data until the original blocks have converted completely.
+                converted = getCache()
+                now = serverTime() // Read time after the snapshot to avoid a boundary crossing during the query.
+                def differences = [missing: (baseline.keySet() - converted.keySet()).take(10),
+                                   unexpected: (converted.keySet() - baseline.keySet()).take(10),
+                                   changed_sizes: converted.findAll { key, value -> value.size != baseline[key]?.size }.take(10)]
+                converted.each { key, value ->
+                    // FE sets creation_time; BE checks its own clock. Allow two seconds of clock skew.
+                    assertTrue(value.type != "normal" || now + 2L >= metadata[key[2]].expires_at,
+                            "Cache converted before expiry: block=${key}, now=${now}, metadata=${metadata[key[2]]}")
+                }
+                tabletIds.each { id ->
+                    def expected = baseline.findAll { key, value -> key[2] == id }
+                    def actual = converted.findAll { key, value -> key[2] == id }
+                    if (!firstNormalAt.containsKey(id) && blockSizes(expected) == blockSizes(actual) &&
+                            actual.values().every { it.type == "normal" }) {
+                        firstNormalAt[id] = now
+                    }
+                }
+                boolean complete = blockSizes(baseline) == blockSizes(converted) &&
+                        converted.values().every { it.type == "normal" } && now > latestExpiry
+                lastState = [server_now: now, seconds_to_last_expiry: latestExpiry - now,
+                             cache_by_be_tablet: summarizeCache(converted), differences: differences,
+                             tablets_not_yet_normal: tabletIds - firstNormalAt.keySet()]
+                if (converted != previous || System.currentTimeMillis() - lastLogMs >= 30000L) {
+                    logger.info("TTL expiration progress: ${lastState}")
+                    lastLogMs = System.currentTimeMillis()
+                }
+                if (!complete || converted != previous) {
+                    stableSince = System.currentTimeMillis()
+                }
+                previous = converted
+                complete && System.currentTimeMillis() - stableSince >= 3000L
+            }
+            def timing = metadata.collectEntries { id, meta ->
+                [(id): meta + [first_normal_observed_at: firstNormalAt[id],
+                               observed_conversion_lag_seconds: firstNormalAt[id] - meta.expires_at]]
+            }
+            logger.info("TTL natural expiration verified: by_tablet=${timing}, " +
+                    "before=${summarizeCache(baseline)}, after=${summarizeCache(converted)}")
+            logMetrics("normal_after_expiry")
+            scanTable()
+        } catch (Throwable t) {
+            failure = t
+            logger.warn("TTL case failed: cluster=${clusterName}, metadata=${metadata}, last_state=${lastState}")
+            try { logMetrics("failure") } catch (Throwable diagnosticError) { t.addSuppressed(diagnosticError) }
+            throw t
+        } finally {
+            def cleanupErrors = []
+            try { sql "drop table if exists ${tableName} force" } catch (Throwable t) { cleanupErrors.add(t) }
+            [enable_sql_cache: originalQueryCache[0], enable_query_cache: originalQueryCache[1]].each { name, value ->
+                try { sql "set ${name} = ${value}" } catch (Throwable t) { cleanupErrors.add(t) }
+            }
+            if (!cleanupErrors.isEmpty()) {
+                if (failure != null) {
+                    cleanupErrors.each { failure.addSuppressed(it) }
+                } else {
+                    cleanupErrors.tail().each { cleanupErrors[0].addSuppressed(it) }
+                    throw cleanupErrors[0]
+                }
+            }
+        }
     }
 }
diff --git a/regression-test/suites/cloud_p0/cache/warm_up/test_file_cache_warmup_read_metrics_docker.groovy b/regression-test/suites/cloud_p0/cache/warm_up/test_file_cache_warmup_read_metrics_docker.groovy
index e2fc6d6fe65..8ef6eed35d7 100644
--- a/regression-test/suites/cloud_p0/cache/warm_up/test_file_cache_warmup_read_metrics_docker.groovy
+++ b/regression-test/suites/cloud_p0/cache/warm_up/test_file_cache_warmup_read_metrics_docker.groovy
@@ -60,8 +60,8 @@ suite('test_file_cache_warmup_read_metrics_docker', 'docker') {
         'DORIS_CLOUD_AK=minioadmin',
         'DORIS_CLOUD_SK=minioadmin',
         "DORIS_CLOUD_BUCKET=${minioBucket}",
-        "DORIS_CLOUD_ENDPOINT=host.docker.internal:${minioPort}",
-        "DORIS_CLOUD_EXTERNAL_ENDPOINT=host.docker.internal:${minioPort}",
+        "DORIS_CLOUD_ENDPOINT=http://host.docker.internal:${minioPort}",
+        "DORIS_CLOUD_EXTERNAL_ENDPOINT=http://host.docker.internal:${minioPort}",
         'DORIS_CLOUD_REGION=us-east-1',
         'DORIS_CLOUD_PROVIDER=S3',
     ]
diff --git a/regression-test/suites/cloud_p0/cache/warm_up/test_warmup_table_docker.groovy b/regression-test/suites/cloud_p0/cache/warm_up/test_warmup_table_docker.groovy
index e93560b389e..14c02d48f8b 100644
--- a/regression-test/suites/cloud_p0/cache/warm_up/test_warmup_table_docker.groovy
+++ b/regression-test/suites/cloud_p0/cache/warm_up/test_warmup_table_docker.groovy
@@ -27,7 +27,7 @@ suite('test_warmup_table_docker', 'docker') {
     options.beConfigs += [
         'file_cache_enter_disk_resource_limit_mode_percent=99',
         'enable_evict_file_cache_in_advance=false',
-        'enable_only_warm_up_idx=true',
+        'file_cache_enable_only_warm_up_idx=true',
         'file_cache_background_gc_interval_ms=10',
         'file_cache_background_monitor_interval_ms=100',
         'file_cache_background_block_lru_update_interval_ms=100',
@@ -189,8 +189,8 @@ suite('test_warmup_table_docker', 'docker') {
         def cache_size_after_clear = getClusterFileCacheSize(clusterName)
         assertEquals(0, cache_size_after_clear)
 
-        // Set enable_only_warm_up_idx = true
-        updateBeConf(clusterName, "enable_only_warm_up_idx", "true")
+        // Set file_cache_enable_only_warm_up_idx = true
+        updateBeConf(clusterName, "file_cache_enable_only_warm_up_idx", "true")
 
         // Trigger warm up
         def jobId = sql "WARM UP CLUSTER ${clusterName} WITH TABLE ${testTable}"
diff --git a/regression-test/suites/cloud_p0/compaction/test_cloud_alter_disable_auto_compaction.groovy b/regression-test/suites/cloud_p0/compaction/test_cloud_alter_disable_auto_compaction.groovy
index 799cc05810b..65d78d48ed7 100644
--- a/regression-test/suites/cloud_p0/compaction/test_cloud_alter_disable_auto_compaction.groovy
+++ b/regression-test/suites/cloud_p0/compaction/test_cloud_alter_disable_auto_compaction.groovy
@@ -28,8 +28,10 @@ suite("test_cloud_alter_disable_auto_compaction", "p0,docker") {
         'cloud_cluster_check_interval_second=1',
     ]
     options.beConfigs += [
-        // Short sync interval for faster property propagation
-        'tablet_sync_interval_s=5',
+        // A tablet becomes eligible for meta sync after this interval.
+        'tablet_sync_interval_s=1',
+        // The background sync thread must also run frequently enough to propagate ALTER properties.
+        'schedule_sync_tablets_interval_s=1',
         // Trigger cumulative compaction after 5 rowsets
         'cumulative_compaction_min_deltas=5',
         // Enable file cache (required for sync_meta to work)
@@ -116,7 +118,7 @@ suite("test_cloud_alter_disable_auto_compaction", "p0,docker") {
         assertTrue(result[0][1].contains('"disable_auto_compaction" = "true"'))
 
         // Wait for sync_meta to propagate the change to BE
-        // tablet_sync_interval_s is set to 5 seconds, so wait a bit longer
+        // Both tablet staleness and background scheduling intervals are set to 1 second.
         logger.info("Waiting for sync_meta to propagate disable_auto_compaction=true to BE...")
         sleep(10000)
 
diff --git a/regression-test/suites/cloud_p0/multi_cluster/async_copy_into/async_load.groovy b/regression-test/suites/cloud_p0/multi_cluster/async_copy_into/async_load.groovy
index 0e346d0389a..89cb6124e1c 100644
--- a/regression-test/suites/cloud_p0/multi_cluster/async_copy_into/async_load.groovy
+++ b/regression-test/suites/cloud_p0/multi_cluster/async_copy_into/async_load.groovy
@@ -18,6 +18,7 @@
 import groovy.json.JsonOutput
 
 suite("async_load") {
+    withRestoredMultiClusterState(false) {
     def externalStageName = "regression_test_tpch"
     def prefix = "tpch/sf1"
 
@@ -51,7 +52,7 @@ suite("async_load") {
             }
         }
     }
-    wait_cluster_change()
+    sleep(20000)
 
     List<List<Object>> result  = sql "show clusters"
     assertTrue(result.size() == 0);
@@ -60,7 +61,7 @@ suite("async_load") {
                      "regression_cluster_name0", "regression_cluster_id0");
     add_cluster.call(beUniqueIdList[1], ipList[1], hbPortList[1],
                      "regression_cluster_name1", "regression_cluster_id1");
-    wait_cluster_change()
+    sleep(20000)
 
     result  = sql "show clusters"
     assertEquals(result.size(), 2);
@@ -97,7 +98,7 @@ suite("async_load") {
         'prefix' = 'regression' ,
         'ak' = '${getS3AK()}' ,
         'sk' = '${getS3SK()}' ,
-        'provider' = '${getProvider()}',
+        'provider' = '${getS3Provider()}',
         'access_type' = 'aksk',
         'default.file.column_separator' = "|" 
         );
@@ -109,7 +110,7 @@ suite("async_load") {
         assertTrue(result.size() == 1)
         assertTrue(result[0].size() == 8)
         //assertTrue(result[0][1].equals("FINISHED"))
-        id = result[0][0]
+        def id = result[0][0]
         while(true) {
             result = sql "show copy where id = \'${id}\'"
             logger.info("copy result: " + result)
@@ -272,4 +273,5 @@ suite("async_load") {
 
     assertTrue(before_cluster0_query_scan_rows == after_cluster0_query_scan_rows)
     assertTrue(before_cluster1_query_scan_rows < after_cluster1_query_scan_rows)
+    }
 }
diff --git a/regression-test/suites/cloud_p0/multi_cluster/copy_into/sync_laod.groovy b/regression-test/suites/cloud_p0/multi_cluster/copy_into/sync_laod.groovy
index 72c84017185..32c7344e838 100644
--- a/regression-test/suites/cloud_p0/multi_cluster/copy_into/sync_laod.groovy
+++ b/regression-test/suites/cloud_p0/multi_cluster/copy_into/sync_laod.groovy
@@ -16,8 +16,25 @@
 // under the License.
 
 import groovy.json.JsonOutput
+import org.apache.doris.regression.action.ProfileAction
 
 suite("sync_load") {
+    withRestoredMultiClusterState(false) {
+
+    def queryFrontend = sql_return_maparray('SHOW FRONTENDS').find {
+        it.CurrentConnected?.toString()?.equalsIgnoreCase('Yes')
+    }
+    assertNotNull(queryFrontend, 'Cannot locate the FE executing profile queries')
+    def profileAction = new ProfileAction(context,
+            new InetSocketAddress(queryFrontend.Host.toString(), queryFrontend.HttpPort.toString().toInteger()))
+    def checkQueryProfile = { query, pattern ->
+        // Match this execution, even when the profile list normalizes or truncates the SQL text.
+        def marker = "multi_cluster_${UUID.randomUUID().toString().replace('-', '')}"
+        sql 'sync'
+        sql "/* ${marker} */ ${query}"
+        profileAction.getProfileBySql(marker, [pattern.toString()], 60000L, 500L)
+    }
+
     def externalStageName = "regression_test_tpch"
     def prefix = "tpch/sf1"
 
@@ -25,7 +42,6 @@ suite("sync_load") {
     List<String> hbPortList = new ArrayList<>()
     List<String> httpPortList = new ArrayList<>()
     List<String> beUniqueIdList = new ArrayList<>()
-    List<String> bePortList = new ArrayList<>()
 
     String[] bes = context.config.multiClusterBes.split(',');
     println("the value is " + context.config.multiClusterBes);
@@ -51,7 +67,7 @@ suite("sync_load") {
             }
         }
     }
-    wait_cluster_change()
+    sleep(20000)
 
     List<List<Object>> result  = sql "show clusters"
     assertTrue(result.size() == 0);
@@ -60,7 +76,7 @@ suite("sync_load") {
                      "regression_cluster_name0", "regression_cluster_id0");
     add_cluster.call(beUniqueIdList[1], ipList[1], hbPortList[1],
                      "regression_cluster_name1", "regression_cluster_id1");
-    wait_cluster_change()
+    sleep(20000)
 
     result  = sql "show clusters"
     assertEquals(result.size(), 2);
@@ -98,7 +114,7 @@ suite("sync_load") {
         'prefix' = 'regression' ,
         'ak' = '${getS3AK()}' ,
         'sk' = '${getS3SK()}' ,
-        'provider' = '${getProvider()}',
+        'provider' = '${getS3Provider()}',
         'access_type' = 'aksk',
         'default.file.column_separator' = "|" 
         );
@@ -205,21 +221,18 @@ suite("sync_load") {
     //    sql new File("""${context.file.parent}/ddl/${sql}.sql""").text
     //}
 
-    // fill bePortList
-    for (int i = 0; i < ipList.size(); ++i) {
-        result = sql """show backends"""
-        for (row : result) {
-            println row
-            println row[2]
-            if (ipList[i] == row[1] && hbPortList[i] == row[2]) {
-                bePortList.add(row[3]);
-            }
+    // QeProcessorImpl attaches execution profiles using Backend.getHeartbeatAddress().
+    def profileBackends = sql_return_maparray('SHOW BACKENDS')
+    def profileHeartbeatPorts = (0..1).collect { index ->
+        def backend = profileBackends.find {
+            it.Host == ipList[index] && it.HeartbeatPort.toString() == hbPortList[index]
         }
+        assertNotNull(backend, "Missing profile backend ${ipList[index]}:${hbPortList[index]}")
+        backend.HeartbeatPort.toString()
     }
-    assertEquals(bePortList.size(), 2);
 
     // q01
-    sql """
+    def q01 = """
         SELECT
           l_returnflag,
           l_linestatus,
@@ -243,11 +256,10 @@ suite("sync_load") {
         l_linestatus
     """
 
-    def set = [ipList[0] + ":" +bePortList[0]] as Set
-    checkProfile.call(set)
+    checkQueryProfile(q01, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     // q02
-    sql """
+    def q02 = """
         SELECT
           s_acctbal,
           s_name,
@@ -290,10 +302,10 @@ suite("sync_load") {
           p_partkey
         LIMIT 100
     """
-    checkProfile.call(set)
+    checkQueryProfile(q02, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     // q03
-    sql """
+    def q03 = """
         SELECT
           l_orderkey,
           sum(l_extendedprice * (1 - l_discount)) AS revenue,
@@ -318,10 +330,10 @@ suite("sync_load") {
           o_orderdate
         LIMIT 10
     """
-    checkProfile.call(set)
+    checkQueryProfile(q03, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     // q04
-    sql """
+    def q04 = """
         SELECT
           o_orderpriority,
           count(*) AS order_count
@@ -341,10 +353,10 @@ suite("sync_load") {
         ORDER BY
         o_orderpriority
     """
-    checkProfile.call(set)
+    checkQueryProfile(q04, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     // q05
-    sql """
+    def q05 = """
         SELECT
           n_name,
           sum(l_extendedprice * (1 - l_discount)) AS revenue
@@ -370,10 +382,10 @@ suite("sync_load") {
         ORDER BY
         revenue DESC
     """
-    checkProfile.call(set)
+    checkQueryProfile(q05, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     // q06
-    sql """
+    def q06 = """
         SELECT sum(l_extendedprice * l_discount) AS revenue
         FROM
           lineitem
@@ -383,10 +395,10 @@ suite("sync_load") {
         AND l_discount BETWEEN 0.06 - 0.01 AND .06 + 0.01
         AND l_quantity < 24
     """
-    checkProfile.call(set)
+    checkQueryProfile(q06, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     // q07
-    sql """
+    def q07 = """
         SELECT
           supp_nation,
           cust_nation,
@@ -426,13 +438,12 @@ suite("sync_load") {
           cust_nation,
           l_year
     """
-    checkProfile.call(set)
+    checkQueryProfile(q07, "hostname:${ipList[0]}, port:${profileHeartbeatPorts[0]}")
 
     sql "use @regression_cluster_name1"
-    set = [ipList[1] + ":" +bePortList[1]] as Set
 
     // q08
-    sql """
+    def q08 = """
         SELECT
           o_year,
           sum(CASE
@@ -471,10 +482,10 @@ suite("sync_load") {
         ORDER BY
           o_year
     """
-    checkProfile.call(set)
+    checkQueryProfile(q08, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q09
-    sql """
+    def q09 = """
         SELECT
           nation,
           o_year,
@@ -507,10 +518,10 @@ suite("sync_load") {
           nation,
           o_year DESC
     """
-    checkProfile.call(set)
+    checkQueryProfile(q09, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q10
-    sql """
+    def q10 = """
         SELECT
           c_custkey,
           c_name,
@@ -544,10 +555,10 @@ suite("sync_load") {
           revenue DESC
         LIMIT 20
     """
-    checkProfile.call(set)
+    checkQueryProfile(q10, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q11
-    sql """
+    def q11 = """
         SELECT
           ps_partkey,
           sum(ps_supplycost * ps_availqty) AS value
@@ -576,10 +587,10 @@ suite("sync_load") {
         ORDER BY
           value DESC
     """
-    checkProfile.call(set)
+    checkQueryProfile(q11, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q12
-    sql """
+    def q12 = """
         SELECT
           l_shipmode,
           sum(CASE
@@ -609,10 +620,10 @@ suite("sync_load") {
         ORDER BY
           l_shipmode
     """
-    checkProfile.call(set)
+    checkQueryProfile(q12, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q13
-    sql """
+    def q13 = """
         SELECT
           c_count,
           count(*) AS custdist
@@ -634,10 +645,10 @@ suite("sync_load") {
           custdist DESC,
           c_count DESC
     """
-    checkProfile.call(set)
+    checkQueryProfile(q13, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q14
-    sql """
+    def q14 = """
         SELECT 100.00 * sum(CASE
                             WHEN p_type LIKE 'PROMO%'
                               THEN l_extendedprice * (1 - l_discount)
@@ -651,10 +662,10 @@ suite("sync_load") {
           AND l_shipdate >= DATE '1995-09-01'
           AND l_shipdate < DATE '1995-09-01' + INTERVAL '1' MONTH
     """
-    checkProfile.call(set)
+    checkQueryProfile(q14, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q15
-    sql """
+    def q15 = """
         SELECT
           s_suppkey,
           s_name,
@@ -674,10 +685,10 @@ suite("sync_load") {
         ORDER BY
           s_suppkey;
     """
-    checkProfile.call(set)
+    checkQueryProfile(q15, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q16
-    sql """
+    def q16 = """
         SELECT
           p_brand,
           p_type,
@@ -708,10 +719,10 @@ suite("sync_load") {
           p_type,
           p_size
     """
-    checkProfile.call(set)
+    checkQueryProfile(q16, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q17
-    sql """
+    def q17 = """
         SELECT sum(l_extendedprice) / 7.0 AS avg_yearly
         FROM
           lineitem,
@@ -728,10 +739,10 @@ suite("sync_load") {
               l_partkey = p_partkey
           )
     """
-    checkProfile.call(set)
+    checkQueryProfile(q17, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q18
-    sql """
+    def q18 = """
         SELECT
           c_name,
           c_custkey,
@@ -766,10 +777,10 @@ suite("sync_load") {
           o_orderdate
         LIMIT 100
     """
-    checkProfile.call(set)
+    checkQueryProfile(q18, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q19
-    sql """
+    def q19 = """
         SELECT sum(l_extendedprice * (1 - l_discount)) AS revenue
         FROM
           lineitem,
@@ -805,10 +816,10 @@ suite("sync_load") {
             AND l_shipinstruct = 'DELIVER IN PERSON'
           )
     """
-    checkProfile.call(set)
+    checkQueryProfile(q19, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
 
     // q20
-    sql """
+    def q20 = """
         SELECT
           s_name,
           s_address
@@ -842,5 +853,6 @@ suite("sync_load") {
         AND n_name = 'CANADA'
         ORDER BY s_name
     """
-    checkProfile.call(set)
+    checkQueryProfile(q20, "hostname:${ipList[1]}, port:${profileHeartbeatPorts[1]}")
+    }
 }
diff --git a/regression-test/suites/cloud_p0/multi_cluster/default_cluster.groovy b/regression-test/suites/cloud_p0/multi_cluster/default_cluster.groovy
index 7580ca13a8b..5d7f1368c4b 100644
--- a/regression-test/suites/cloud_p0/multi_cluster/default_cluster.groovy
+++ b/regression-test/suites/cloud_p0/multi_cluster/default_cluster.groovy
@@ -18,6 +18,7 @@
 import groovy.json.JsonOutput
 
 suite("default_cluster") {
+    withRestoredMultiClusterState(false) {
     def token = context.config.metaServiceToken
     def instance_id = context.config.multiClusterInstance
 
@@ -27,9 +28,9 @@ suite("default_cluster") {
     List<String> beUniqueIdList = new ArrayList<>()
 
     String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
+    logger.info("the value is " + context.config.multiClusterBes);
     for(String values : bes) {
-        println("the value is " + values);
+        logger.info("the value is " + values);
         String[] beInfo = values.split(':');
         ipList.add(beInfo[0]);
         hbPortList.add(beInfo[1]);
@@ -37,10 +38,10 @@ suite("default_cluster") {
         beUniqueIdList.add(beInfo[3]);
     }
 
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
+    logger.info("the ip is " + ipList);
+    logger.info("the heartbeat port is " + hbPortList);
+    logger.info("the http port is " + httpPortList);
+    logger.info("the be unique id is " + beUniqueIdList);
 
     for (unique_id : beUniqueIdList) {
         resp = get_cluster.call(unique_id);
@@ -50,7 +51,7 @@ suite("default_cluster") {
             }
         }
     }
-    wait_cluster_change()
+    sleep(20000)
 
     List<List<Object>> result  = sql "show clusters"
     assertTrue(result.size() == 0);
@@ -59,22 +60,22 @@ suite("default_cluster") {
                      "regression_cluster_name2", "regression_cluster_id2");
     add_cluster.call(beUniqueIdList[1], ipList[1], hbPortList[1],
                      "regression_cluster_name3", "regression_cluster_id3");
-    wait_cluster_change()
+    sleep(20000)
 
     result  = sql "show clusters"
     assertTrue(result.size() == 2);
     for (row : result) {
-        log.info("show cluster row: ${row}".toString())
+        logger.info("show cluster row: ${row}".toString())
     }
 
     sql "SET PROPERTY 'default_cloud_cluster' = 'regression_cluster_name3'"
 
     context.reconnectFe()
     def before_cluster0_flush = get_be_metric(ipList[0], httpPortList[0], "memtable_flush_total");
-    log.info("before_cluster0_flush : ${before_cluster0_flush}".toString())
+    logger.info("before_cluster0_flush : ${before_cluster0_flush}".toString())
 
     def before_cluster1_flush = get_be_metric(ipList[1], httpPortList[1], "memtable_flush_total");
-    log.info("before_cluster1_flush : ${before_cluster1_flush}".toString())
+    logger.info("before_cluster1_flush : ${before_cluster1_flush}".toString())
 
 
     sql """
@@ -92,17 +93,17 @@ suite("default_cluster") {
     """
 
     def after_cluster0_flush = get_be_metric(ipList[0], httpPortList[0], "memtable_flush_total");
-    log.info("after_cluster0_flush : ${after_cluster0_flush}".toString())
+    logger.info("after_cluster0_flush : ${after_cluster0_flush}".toString())
 
     def after_cluster1_flush = get_be_metric(ipList[1], httpPortList[1], "memtable_flush_total");
-    log.info("after_cluster1_flush : ${after_cluster1_flush}".toString())
+    logger.info("after_cluster1_flush : ${after_cluster1_flush}".toString())
 
     assertTrue(before_cluster0_flush == after_cluster0_flush)
     assertTrue(before_cluster1_flush < after_cluster1_flush)
 
     result  = sql "show clusters"
     for (row : result) {
-        println row
+        logger.info("row: ${row.toString()}")
         if(row[0] == "regression_cluster_name2") {
             assertTrue(row[1].toString().toLowerCase() == "false")
         }
@@ -115,7 +116,7 @@ suite("default_cluster") {
     sql "use @regression_cluster_name2"
     result  = sql "show clusters"
     for (row : result) {
-        println row
+        logger.info("row: ${row.toString()}")
         if(row[0] == "regression_cluster_name2") {
             assertTrue(row[1].toString().toLowerCase() == "true")
         }
@@ -128,7 +129,7 @@ suite("default_cluster") {
     context.reconnectFe()
     result  = sql "show clusters"
     for (row : result) {
-        println row
+        logger.info("row: ${row.toString()}")
         if(row[0] == "regression_cluster_name2") {
             assertTrue(row[1].toString().toLowerCase() == "true")
         }
@@ -140,7 +141,7 @@ suite("default_cluster") {
     sql "use @regression_cluster_name3"
     result  = sql "show clusters"
     for (row : result) {
-        println row
+        logger.info("row: ${row.toString()}")
         if(row[0] == "regression_cluster_name2") {
             assertTrue(row[1].toString().toLowerCase() == "false")
         }
@@ -160,7 +161,7 @@ suite("default_cluster") {
 
     result  = sql "show property"
     for (row : result) {
-        println row
+        logger.info("row: ${row.toString()}")
         if(row[0] == "default_cloud_cluster") {
             assertTrue(row[1].toString().isEmpty())
         }
@@ -169,4 +170,5 @@ suite("default_cluster") {
     sql """
         drop table if exists test_table
     """
+    }
 }
diff --git a/regression-test/suites/cloud_p0/multi_cluster/multi_cluster_s3_load/load.groovy b/regression-test/suites/cloud_p0/multi_cluster/multi_cluster_s3_load/load.groovy
index 20108615679..aeed4a1c95b 100644
--- a/regression-test/suites/cloud_p0/multi_cluster/multi_cluster_s3_load/load.groovy
+++ b/regression-test/suites/cloud_p0/multi_cluster/multi_cluster_s3_load/load.groovy
@@ -27,52 +27,33 @@
 // sed -nr 's/.*tables: (.*)$/\1/gp' /path/to/*.sql | sed -nr 's/,/\n/gp' | sort | uniq
 
 import groovy.json.JsonOutput
+import org.apache.doris.regression.util.MultiClusterStateGuard
 
 suite("load") {
-    List<String> ipList = new ArrayList<>()
-    List<String> hbPortList = new ArrayList<>()
-    List<String> httpPortList = new ArrayList<>()
-    List<String> beUniqueIdList = new ArrayList<>()
-
-    String[] bes = context.config.multiClusterBes.split(',');
-    println("the value is " + context.config.multiClusterBes);
-    for(String values : bes) {
-        println("the value is " + values);
-        String[] beInfo = values.split(':');
-        ipList.add(beInfo[0]);
-        hbPortList.add(beInfo[1]);
-        httpPortList.add(beInfo[2]);
-        beUniqueIdList.add(beInfo[3]);
-    }
-
-    println("the ip is " + ipList);
-    println("the heartbeat port is " + hbPortList);
-    println("the http port is " + httpPortList);
-    println("the be unique id is " + beUniqueIdList);
-
-    for (unique_id : beUniqueIdList) {
-        resp = get_cluster.call(unique_id);
-        for (cluster : resp) {
-            if (cluster.type == "COMPUTE") {
-                drop_cluster.call(cluster.cluster_name, cluster.cluster_id);
-            }
+    withRestoredMultiClusterState(true) {
+    def baseline = multiClusterBaseline()
+    def ipList = baseline.collect { it.nodes[0].ip }
+    def httpPortList = baseline.collect { it.nodes[0].http_port.toString() }
+
+    // Establish the shared baseline after a previous suite or run left different group names.
+    def existing = get_cluster.call(baseline[0].nodes[0].cloud_unique_id)
+            .findAll { it.type == 'COMPUTE' }
+    if (!MultiClusterStateGuard.frontendMatches(baseline,
+            sql_return_maparray('SHOW CLUSTERS'), sql_return_maparray('SHOW BACKENDS'))) {
+        existing.each { group -> drop_cluster.call(group.cluster_name, group.cluster_id) }
+        sleep(20000)
+        baseline.each { group ->
+            def node = group.nodes[0]
+            add_cluster.call(node.cloud_unique_id, node.ip, node.heartbeat_port.toString(),
+                    group.cluster_name, group.cluster_id)
         }
+        sleep(20000)
     }
-    wait_cluster_change()
-
-    List<List<Object>> result  = sql "show clusters"
-    assertTrue(result.size() == 0);
+    checkMultiClusterBaseline(baseline)
+    sql "SET PROPERTY 'default_cloud_cluster' = '${baseline[0].cluster_name}'"
+    List<List<Object>> result
 
-    add_cluster.call(beUniqueIdList[0], ipList[0], hbPortList[0],
-                     "regression_cluster_name0", "regression_cluster_id0");
-    add_cluster.call(beUniqueIdList[1], ipList[1], hbPortList[1],
-                     "regression_cluster_name1", "regression_cluster_id1");
-    wait_cluster_change()
-
-    result  = sql "show clusters"
-    assertEquals(result.size(), 2);
-
... 12426 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org