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

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new db72b96e524 [fix](be) Harden JNI reader split lifecycle (#65503)
db72b96e524 is described below

commit db72b96e5248666fb56a32beaddaf901fe367366
Author: Gabriel <[email protected]>
AuthorDate: Mon Jul 13 16:29:35 2026 +0800

    [fix](be) Harden JNI reader split lifecycle (#65503)
    
    ### What problem does this PR solve?
    
    JNI-backed file scans had several lifecycle, compatibility, and
    adaptive-batching gaps:
    
    - The scan-level FileScannerV2 selector cannot inspect split-level JNI
    metadata. It could therefore route legacy Paimon JNI splits—especially
    old-FE splits without `reader_type`—to a scanner that cannot dispatch
    them.
    - A cancelled query could continue requesting Java batches when every
    row in the current batches was filtered out.
    - JNI open or close failures could discard cleanup state or mark the
    outer scanner closed too early, preventing retained Java resources from
    being cleaned up on a retry.
    - Adaptive batch-size changes did not consistently reach already-open
    JNI scanners and Paimon/Hudi hybrid readers. Paimon's physical reader
    cannot be resized after open, so accepting a later logical update could
    make the reported batch size diverge from the actual reader.
    - End-of-split state could be lost after the JNI scanner closed, making
    repeated reads after EOF non-idempotent.
    - Paimon options and Hadoop configuration supplied at split level were
    not retained as mixed-version fallback values.
    
    This PR hardens those paths by:
    
    - conservatively keeping scan-level JNI compatibility shapes on the
    legacy scanner and rejecting unsupported Paimon C++ ranges in
    FileScannerV2;
    - checking cancellation before fetching another Java batch;
    - preserving JNI cleanup state after failures, retaining each Paimon
    Java resource until its cleanup succeeds, and allowing scanner-level
    close to retry table-reader cleanup;
    - seeding the adaptive probe before eager JNI open, forwarding supported
    batch-size updates to open JNI and hybrid readers, and preserving
    Paimon's initial physical batch size after open;
    - preserving EOF state across scanner cleanup; and
    - applying explicit precedence between current scan-level Paimon
    settings and split-level rolling-upgrade fallbacks.
    
    ### Release note
    
    Harden JNI table-reader lifecycle handling, adaptive batching, and
    rolling-upgrade compatibility for Paimon and Hudi scans.
    
    ### Check List
    
    - [x] Added comments for scanner-selection, lifecycle, adaptive-batch,
    and compatibility invariants.
    - [x] Added focused unit tests covering cancellation, EOF idempotence,
    cleanup failure and retry, adaptive batching, scanner selection, and
    Paimon fallback precedence.
    - [x] Passed 30 focused ASAN BE unit tests from `FileScannerV2Test.*`,
    `JniTableReaderTest.*`, and `PaimonJniReaderTest.*` after addressing the
    latest review.
    - [x] Passed all 10 `PaimonJniScannerTest` Java tests; checkstyle
    reported 0 violations.
    - [x] Passed `build-support/check-format.sh` and `git diff --check`.
    - [x] Ran changed-file clang-tidy; analysis is blocked by the existing
    toolchain missing `stddef.h` and pre-existing repository diagnostics.
---
 be/src/exec/operator/file_scan_operator.cpp        |   6 +-
 be/src/exec/scan/file_scanner_v2.cpp               |  20 ++-
 be/src/exec/scan/file_scanner_v2.h                 |   2 +
 be/src/format_v2/jni/jni_table_reader.cpp          | 149 ++++++++++++----
 be/src/format_v2/jni/jni_table_reader.h            |  26 ++-
 be/src/format_v2/jni/paimon_jni_reader.cpp         |  12 +-
 be/src/format_v2/jni/paimon_jni_reader.h           |   1 +
 be/src/format_v2/table/hudi_reader.cpp             |  20 ++-
 be/src/format_v2/table/hudi_reader.h               |  12 ++
 be/src/format_v2/table/paimon_reader.cpp           |  20 ++-
 be/src/format_v2/table/paimon_reader.h             |  10 ++
 be/src/format_v2/table_reader.h                    |   6 +-
 be/test/exec/scan/file_scanner_v2_test.cpp         |  73 ++++++++
 be/test/format_v2/jni/jni_table_reader_test.cpp    | 191 +++++++++++++++++++++
 be/test/format_v2/jni/paimon_jni_reader_test.cpp   |  48 ++++++
 be/test/format_v2/table/hudi_reader_test.cpp       |   9 +
 be/test/format_v2/table/paimon_reader_test.cpp     |   9 +
 .../org/apache/doris/paimon/PaimonJniScanner.java  |  13 +-
 .../apache/doris/paimon/PaimonJniScannerTest.java  |  80 +++++++++
 19 files changed, 651 insertions(+), 56 deletions(-)

diff --git a/be/src/exec/operator/file_scan_operator.cpp 
b/be/src/exec/operator/file_scan_operator.cpp
index 58aadfc87a6..8121590ebf0 100644
--- a/be/src/exec/operator/file_scan_operator.cpp
+++ b/be/src/exec/operator/file_scan_operator.cpp
@@ -116,10 +116,14 @@ bool 
FileScanLocalState::_should_use_file_scanner_v2(const TQueryOptions& query_
     const bool is_transactional_hive =
             scan_params.__isset.table_format_params &&
             scan_params.table_format_params.table_format_type == 
"transactional_hive";
+    // JNI reader selection is stored per split, but this scan-level selector 
cannot inspect the
+    // split yet. Older FEs may omit both the scan-level Paimon marker and 
split-level reader_type,
+    // so keep JNI scans on V1 until scanner selection can distinguish every 
compatibility shape.
     return query_options.__isset.enable_file_scanner_v2 && 
query_options.enable_file_scanner_v2 &&
            !is_load && scan_params.format_type != TFileFormatType::FORMAT_WAL 
&&
            scan_params.format_type != TFileFormatType::FORMAT_ES_HTTP &&
-           scan_params.format_type != TFileFormatType::FORMAT_LANCE && 
!is_transactional_hive;
+           scan_params.format_type != TFileFormatType::FORMAT_LANCE &&
+           scan_params.format_type != TFileFormatType::FORMAT_JNI && 
!is_transactional_hive;
 }
 
 Status FileScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
diff --git a/be/src/exec/scan/file_scanner_v2.cpp 
b/be/src/exec/scan/file_scanner_v2.cpp
index 519237ea85c..6700ed71276 100644
--- a/be/src/exec/scan/file_scanner_v2.cpp
+++ b/be/src/exec/scan/file_scanner_v2.cpp
@@ -212,6 +212,10 @@ Status rewrite_slot_refs_to_global_index(
 } // namespace
 
 #ifdef BE_TEST
+FileScannerV2::FileScannerV2(RuntimeState* state, RuntimeProfile* profile,
+                             std::unique_ptr<format::TableReader> table_reader)
+        : Scanner(state, profile), _table_reader(std::move(table_reader)) {}
+
 Status FileScannerV2::TEST_validate_scan_range(const TFileScanRangeParams& 
params,
                                                const TFileRangeDesc& range) {
     return _validate_scan_range(params, range);
@@ -407,6 +411,13 @@ Status FileScannerV2::_prepare_next_split(bool* eos) {
         DORIS_CHECK(_table_reader != nullptr);
         _current_range_path = _current_range.path;
 
+        const auto format_type = get_range_format_type(*_params, 
_current_range);
+        _init_adaptive_batch_size_state(format_type);
+        if (_should_run_adaptive_batch_size()) {
+            // JNI readers open eagerly in prepare_split(). Seed the probe 
size first so readers
+            // such as Paimon also use it for their first physical read batch.
+            _table_reader->set_batch_size(_predict_reader_batch_rows());
+        }
         std::map<std::string, Field> partition_values;
         RETURN_IF_ERROR(_generate_partition_values(_current_range, 
&partition_values));
         const auto status =
@@ -422,7 +433,6 @@ Status FileScannerV2::_prepare_next_split(bool* eos) {
             _state->update_num_finished_scan_range(1);
             continue;
         }
-        _init_adaptive_batch_size_state(get_range_format_type(*_params, 
_current_range));
         COUNTER_UPDATE(_file_counter, 1);
         _has_prepared_split = true;
         *eos = false;
@@ -824,7 +834,13 @@ Status FileScannerV2::close(RuntimeState* state) {
         return Status::OK();
     }
     if (_table_reader != nullptr) {
-        RETURN_IF_ERROR(_table_reader->close());
+        const auto close_status = _table_reader->close();
+        if (!close_status.ok()) {
+            // Reserve the close attempt with _try_close(), but commit the 
scanner-level closed
+            // state only after the retained table reader has completed its 
retryable cleanup.
+            _is_closed.store(false);
+            return close_status;
+        }
         _report_condition_cache_profile();
         _table_reader.reset();
     }
diff --git a/be/src/exec/scan/file_scanner_v2.h 
b/be/src/exec/scan/file_scanner_v2.h
index 66242b610f4..2bddc5d5e69 100644
--- a/be/src/exec/scan/file_scanner_v2.h
+++ b/be/src/exec/scan/file_scanner_v2.h
@@ -65,6 +65,8 @@ public:
 
     static bool is_supported(const TFileScanRangeParams& params, const 
TFileRangeDesc& range);
 #ifdef BE_TEST
+    FileScannerV2(RuntimeState* state, RuntimeProfile* profile,
+                  std::unique_ptr<format::TableReader> table_reader);
     static Status TEST_validate_scan_range(const TFileScanRangeParams& params,
                                            const TFileRangeDesc& range);
     static Status TEST_to_file_format(TFileFormatType::type format_type,
diff --git a/be/src/format_v2/jni/jni_table_reader.cpp 
b/be/src/format_v2/jni/jni_table_reader.cpp
index 9416659b8f5..3cb105193b5 100644
--- a/be/src/format_v2/jni/jni_table_reader.cpp
+++ b/be/src/format_v2/jni/jni_table_reader.cpp
@@ -36,6 +36,9 @@ Status JniTableReader::init(TableReadOptions&& options) {
 }
 
 Status JniTableReader::prepare_split(const SplitReadOptions& options) {
+    // EOF belongs to the previous split. Keep it set after closing that split 
so repeated reads
+    // are idempotent, and clear it only when a new split is explicitly 
prepared.
+    _eof = false;
     _current_range = options.current_range;
     RETURN_IF_ERROR(validate_scan_range(options.current_range));
     RETURN_IF_ERROR(TableReader::prepare_split(options));
@@ -68,13 +71,21 @@ Status JniTableReader::get_block(Block* output_block, bool* 
eos) {
         return _read_table_level_count(output_block, eos);
     }
 
-    DORIS_CHECK(_scanner_opened);
     if (_eof) {
         *eos = true;
         return Status::OK();
     }
+    DORIS_CHECK(_scanner_opened);
 
     while (true) {
+        // JNI readers can loop internally when conjuncts filter every Java 
batch. Mirror the base
+        // TableReader cancellation contract so a cancelled query does not 
drain the whole split.
+        if (_io_ctx != nullptr && _io_ctx->should_stop) {
+            _eof = true;
+            RETURN_IF_ERROR(_close_jni_scanner());
+            *eos = true;
+            return Status::OK();
+        }
         size_t current_rows = 0;
         bool current_eof = false;
         // get next block data from Java scanner, and fill the data to 
_jni_block_template
@@ -282,41 +293,43 @@ int64_t JniTableReader::self_split_weight() const {
     return _current_range.__isset.self_split_weight ? 
_current_range.self_split_weight : -1;
 }
 
-Status JniTableReader::close() {
-    if (_closed) {
-        return Status::OK();
+bool JniTableReader::_reserve_split_profile_publication() {
+    if (_split_profile_published) {
+        return false;
     }
-    _closed = true;
-    RETURN_IF_ERROR(_close_jni_scanner());
-    return TableReader::close();
+    _split_profile_published = true;
+    return true;
 }
 
-Status JniTableReader::_close_jni_scanner() {
-    if (!_scanner_opened) {
-        JNIEnv* env = nullptr;
-        if (!_jni_scanner_obj.uninitialized()) {
-            RETURN_IF_ERROR(Jni::Env::Get(&env));
-        }
-        _reset_split_state(env);
-        return Status::OK();
+void JniTableReader::_publish_split_profile(JNIEnv* env) {
+    // Cleanup can fail while the Java scanner and split watchers must remain 
available for a
+    // retry. Reserve profile publication separately so a retry only repeats 
resource cleanup.
+    if (!_reserve_split_profile_publication()) {
+        return;
     }
 
-    JNIEnv* env = nullptr;
-    RETURN_IF_ERROR(Jni::Env::Get(&env));
     if (_scanner_profile != nullptr) {
         COUNTER_UPDATE(_open_scanner_time, _jni_scanner_open_watcher);
         COUNTER_UPDATE(_fill_block_time, _fill_block_watcher);
     }
 
-    RETURN_ERROR_IF_EXC(env);
     jlong append_data_time = 0;
-    RETURN_IF_ERROR(_jni_scanner_obj.call_long_method(env, 
_jni_scanner_get_append_data_time)
-                            .call(&append_data_time));
+    const auto append_time_status =
+            _jni_scanner_obj.call_long_method(env, 
_jni_scanner_get_append_data_time)
+                    .call(&append_data_time);
     jlong create_vector_table_time = 0;
-    RETURN_IF_ERROR(
+    const auto create_table_time_status =
             _jni_scanner_obj.call_long_method(env, 
_jni_scanner_get_create_vector_table_time)
-                    .call(&create_vector_table_time));
-    if (_scanner_profile != nullptr) {
+                    .call(&create_vector_table_time);
+    if (!append_time_status.ok()) {
+        LOG(WARNING) << "failed to collect JNI append-data time during close: "
+                     << append_time_status;
+    }
+    if (!create_table_time_status.ok()) {
+        LOG(WARNING) << "failed to collect JNI vector-table time during close: 
"
+                     << create_table_time_status;
+    }
+    if (_scanner_profile != nullptr && append_time_status.ok() && 
create_table_time_status.ok()) {
         COUNTER_UPDATE(_java_append_data_time, append_data_time);
         COUNTER_UPDATE(_java_create_vector_table_time, 
create_vector_table_time);
         COUNTER_UPDATE(_java_scan_time,
@@ -326,13 +339,50 @@ Status JniTableReader::_close_jni_scanner() {
                 self_split_weight());
     }
     _collect_jni_scanner_profile(env);
+}
+
+Status JniTableReader::close() {
+    if (_closed) {
+        return Status::OK();
+    }
+    auto close_status = _close_jni_scanner();
+    auto table_status = TableReader::close();
+    if (close_status.ok() && !table_status.ok()) {
+        close_status = std::move(table_status);
+    }
+    if (close_status.ok()) {
+        _closed = true;
+    }
+    return close_status;
+}
+
+Status JniTableReader::_close_jni_scanner() {
+    if (!_scanner_opened) {
+        JNIEnv* env = nullptr;
+        if (!_jni_scanner_obj.uninitialized()) {
+            RETURN_IF_ERROR(Jni::Env::Get(&env));
+        }
+        _reset_split_state(env);
+        return Status::OK();
+    }
+
+    JNIEnv* env = nullptr;
+    RETURN_IF_ERROR(Jni::Env::Get(&env));
+    _publish_split_profile(env);
 
     // _fill_jni_block may fail before releasing the current Java table. 
JniScanner::releaseTable()
-    // is idempotent, so closing the split always releases it.
-    RETURN_IF_ERROR(_jni_scanner_obj.call_void_method(env, 
_jni_scanner_release_table).call());
-    RETURN_IF_ERROR(_jni_scanner_obj.call_void_method(env, 
_jni_scanner_close).call());
-    _reset_split_state(env);
-    return Status::OK();
+    // is idempotent, so closing the split always releases it. Java close must 
still run if that
+    // release fails; otherwise connector resources such as JDBC connections 
can leak.
+    auto cleanup_status = _jni_scanner_obj.call_void_method(env, 
_jni_scanner_release_table).call();
+    auto java_close_status = _jni_scanner_obj.call_void_method(env, 
_jni_scanner_close).call();
+    if (cleanup_status.ok() && !java_close_status.ok()) {
+        cleanup_status = std::move(java_close_status);
+    }
+    if (cleanup_status.ok()) {
+        // Keep the Java object and opened state on failure so close() can 
retry the cleanup.
+        _reset_split_state(env);
+    }
+    return cleanup_status;
 }
 
 void JniTableReader::_reset_split_state(JNIEnv* env) {
@@ -341,13 +391,13 @@ void JniTableReader::_reset_split_state(JNIEnv* env) {
         _jni_scanner_obj.reset(env);
     }
     _scanner_opened = false;
-    _eof = false;
     _scanner_params.clear();
     _jni_columns.clear();
     _jni_block_template.clear();
     _jni_scanner_open_watcher = 0;
     _java_scan_watcher = 0;
     _fill_block_watcher = 0;
+    _split_profile_published = false;
 }
 
 Status JniTableReader::_open_jni_scanner() {
@@ -370,14 +420,47 @@ Status JniTableReader::_open_jni_scanner() {
     SCOPED_RAW_TIMER(&_jni_scanner_open_watcher);
     RETURN_IF_ERROR(_register_jni_class_functions_once(env));
     RETURN_IF_ERROR(_create_jni_scanner_object(env, 
cast_set<int>(_batch_size)));
-    // call open() method in JAVA side.
-    RETURN_IF_ERROR(_jni_scanner_obj.call_void_method(env, 
_jni_scanner_open).call());
-    RETURN_ERROR_IF_EXC(env);
-
+    // Once the Java object exists, close it even if open() fails partway 
through initialization.
+    // Connector implementations may already own streams, off-heap tables, or 
JDBC connections.
     _scanner_opened = true;
+    // call open() method in JAVA side.
+    const auto open_status = _jni_scanner_obj.call_void_method(env, 
_jni_scanner_open).call();
+    if (!open_status.ok()) {
+        const auto close_status = _close_jni_scanner();
+        if (!close_status.ok()) {
+            LOG(WARNING) << "failed to clean up JNI scanner after open 
failure: " << close_status;
+        }
+        return open_status;
+    }
     return Status::OK();
 }
 
+void JniTableReader::set_batch_size(size_t batch_size) {
+    if (_scanner_opened && !supports_batch_size_update_after_open()) {
+        // Some connectors bake the constructor batch size into an 
already-open physical reader.
+        // Keep C++ and Java on that initial size instead of pretending a 
later resize took effect.
+        return;
+    }
+    TableReader::set_batch_size(batch_size);
+    if (!_scanner_opened) {
+        return;
+    }
+    const auto status = _set_open_scanner_batch_size(_batch_size);
+    if (!status.ok()) {
+        // Adaptive batch sizing is an optimization. Keep the scanner usable 
with its previous
+        // size if Java rejects a mid-split update, but surface the failure 
for diagnosis.
+        LOG(WARNING) << "failed to update JNI scanner batch size: " << status;
+    }
+}
+
+Status JniTableReader::_set_open_scanner_batch_size(size_t batch_size) {
+    JNIEnv* env = nullptr;
+    RETURN_IF_ERROR(Jni::Env::Get(&env));
+    return _jni_scanner_obj.call_void_method(env, _jni_scanner_set_batch_size)
+            .with_arg(cast_set<int>(batch_size))
+            .call();
+}
+
 void JniTableReader::_prepare_jni_scanner_schema() {
     std::vector<std::string> required_fields;
     std::vector<std::string> column_types;
diff --git a/be/src/format_v2/jni/jni_table_reader.h 
b/be/src/format_v2/jni/jni_table_reader.h
index f9c854e1971..24f84ca8d62 100644
--- a/be/src/format_v2/jni/jni_table_reader.h
+++ b/be/src/format_v2/jni/jni_table_reader.h
@@ -51,6 +51,20 @@ public:
     Status get_block(Block* block, bool* eos) override;
     Status abort_split() override;
     Status close() override;
+    void set_batch_size(size_t batch_size) override;
+
+#ifdef BE_TEST
+    void TEST_set_split_state(bool scanner_opened, bool eof) {
+        _scanner_opened = scanner_opened;
+        _eof = eof;
+        if (!scanner_opened) {
+            _split_profile_published = false;
+        }
+    }
+    bool TEST_scanner_opened() const { return _scanner_opened; }
+    bool TEST_eof() const { return _eof; }
+    bool TEST_closed() const { return _closed; }
+#endif
 
 protected:
     // Subclasses should implement these methods to specify the Java scanner 
class
@@ -63,6 +77,12 @@ protected:
     virtual Status finalize_jni_block(Block* jni_block, Block* output_block, 
size_t* rows);
     // used for profile
     virtual int64_t self_split_weight() const;
+    virtual Status _get_next_jni_block(size_t* rows, bool* eof);
+    virtual Status _close_jni_scanner();
+    virtual Status _set_open_scanner_batch_size(size_t batch_size);
+    virtual bool supports_batch_size_update_after_open() const { return true; }
+    virtual Status _open_jni_scanner();
+    bool _reserve_split_profile_publication();
     const std::vector<JniColumn>& jni_columns() const { return _jni_columns; }
     TFileRangeDesc _current_range;
 
@@ -71,18 +91,15 @@ private:
     void _init_profile();
     std::string _connector_name() const;
     // open
-    Status _open_jni_scanner();
     void _reset_split_state(JNIEnv* env);
     void _prepare_jni_scanner_schema();
     Status _register_jni_class_functions_once(JNIEnv* env);
     Status _create_jni_scanner_object(JNIEnv* env, int batch_size);
     // get_next
-    Status _get_next_jni_block(size_t* rows, bool* eof);
     Status _fill_jni_block(JniDataBridge::TableMetaAddress& table_meta, size_t 
num_rows);
     Status _get_statistics(JNIEnv* env, std::map<std::string, std::string>* 
result);
     void _collect_jni_scanner_profile(JNIEnv* env);
-
-    Status _close_jni_scanner();
+    void _publish_split_profile(JNIEnv* env);
 
     std::map<std::string, std::string> _scanner_params;
     std::vector<JniColumn> _jni_columns;
@@ -91,6 +108,7 @@ private:
     bool _closed = false;
     bool _scanner_opened = false;
     bool _eof = false;
+    bool _split_profile_published = false;
 
     RuntimeProfile::Counter* _open_scanner_time = nullptr;
     RuntimeProfile::Counter* _java_scan_time = nullptr;
diff --git a/be/src/format_v2/jni/paimon_jni_reader.cpp 
b/be/src/format_v2/jni/paimon_jni_reader.cpp
index ca1e602cdfe..47c0ef6c7bc 100644
--- a/be/src/format_v2/jni/paimon_jni_reader.cpp
+++ b/be/src/format_v2/jni/paimon_jni_reader.cpp
@@ -99,6 +99,11 @@ Status 
PaimonJniReader::build_scanner_params(std::map<std::string, std::string>*
         for (const auto& kv : _scan_params->paimon_options) {
             (*params)[std::string(PAIMON_OPTION_PREFIX) + kv.first] = 
kv.second;
         }
+    } else if (paimon_params.__isset.paimon_options) {
+        // Rolling upgrades can pair this BE with an older FE that only sends 
options per split.
+        for (const auto& kv : paimon_params.paimon_options) {
+            (*params)[std::string(PAIMON_OPTION_PREFIX) + kv.first] = 
kv.second;
+        }
     }
     const std::string enable_io_manager_key =
             std::string(PAIMON_OPTION_PREFIX) + 
std::string(DORIS_ENABLE_JNI_IO_MANAGER);
@@ -118,10 +123,13 @@ Status 
PaimonJniReader::build_scanner_params(std::map<std::string, std::string>*
         for (const auto& kv : _scan_params->properties) {
             (*params)[std::string(HADOOP_OPTION_PREFIX) + kv.first] = 
kv.second;
         }
+    } else if (paimon_params.__isset.hadoop_conf) {
+        for (const auto& kv : paimon_params.hadoop_conf) {
+            (*params)[std::string(HADOOP_OPTION_PREFIX) + kv.first] = 
kv.second;
+        }
     }
     // TODO: Remove legacy split-level paimon_predicate, paimon_options and 
hadoop_conf from thrift
-    // after all readers stop using them. Predicate keeps the split-level 
fallback for rolling
-    // upgrade compatibility with old FE paths that did not send scan-level 
paimon_predicate.
+    // after the minimum supported FE always sends their scan-level 
replacements.
     return Status::OK();
 }
 
diff --git a/be/src/format_v2/jni/paimon_jni_reader.h 
b/be/src/format_v2/jni/paimon_jni_reader.h
index 30788081565..26767458b42 100644
--- a/be/src/format_v2/jni/paimon_jni_reader.h
+++ b/be/src/format_v2/jni/paimon_jni_reader.h
@@ -50,6 +50,7 @@ protected:
     std::string connector_class() const override;
     Status validate_scan_range(const TFileRangeDesc& range) const override;
     Status build_scanner_params(std::map<std::string, std::string>* params) 
const override;
+    bool supports_batch_size_update_after_open() const override { return 
false; }
 
 private:
     static std::string build_default_io_manager_tmp_dirs(const 
std::vector<StorePath>& store_paths);
diff --git a/be/src/format_v2/table/hudi_reader.cpp 
b/be/src/format_v2/table/hudi_reader.cpp
index 3600b0ff706..f37d24bd888 100644
--- a/be/src/format_v2/table/hudi_reader.cpp
+++ b/be/src/format_v2/table/hudi_reader.cpp
@@ -91,6 +91,16 @@ Status HudiHybridReader::close() {
     return close_status;
 }
 
+void HudiHybridReader::set_batch_size(size_t batch_size) {
+    format::TableReader::set_batch_size(batch_size);
+    if (_native_reader != nullptr) {
+        _native_reader->set_batch_size(_batch_size);
+    }
+    if (_jni_reader != nullptr) {
+        _jni_reader->set_batch_size(_batch_size);
+    }
+}
+
 Status HudiHybridReader::_ensure_current_split_reader(const 
format::SplitReadOptions& options) {
     DORIS_CHECK(_scan_params != nullptr);
     if (_is_jni_split(*_scan_params, options.current_range)) {
@@ -116,7 +126,7 @@ Status 
HudiHybridReader::_init_child_reader(format::TableReader* reader,
     DORIS_CHECK(reader != nullptr);
     VExprContextSPtrs conjuncts;
     RETURN_IF_ERROR(_clone_conjuncts(&conjuncts));
-    return reader->init({
+    RETURN_IF_ERROR(reader->init({
             .projected_columns = _projected_columns,
             .conjuncts = std::move(conjuncts),
             .format = file_format,
@@ -126,7 +136,13 @@ Status 
HudiHybridReader::_init_child_reader(format::TableReader* reader,
             .scanner_profile = _scanner_profile,
             .push_down_agg_type = _push_down_agg_type,
             .condition_cache_digest = _condition_cache_digest,
-    });
+    }));
+    // Zero means no adaptive prediction has been produced yet. Preserve the 
child's normal
+    // runtime default until FileScannerV2 supplies the first positive 
prediction.
+    if (_batch_size > 0) {
+        reader->set_batch_size(_batch_size);
+    }
+    return Status::OK();
 }
 
 Status HudiHybridReader::_clone_conjuncts(VExprContextSPtrs* conjuncts) const {
diff --git a/be/src/format_v2/table/hudi_reader.h 
b/be/src/format_v2/table/hudi_reader.h
index 58f222c67ab..e22c6bd866f 100644
--- a/be/src/format_v2/table/hudi_reader.h
+++ b/be/src/format_v2/table/hudi_reader.h
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <memory>
+#include <utility>
 #include <vector>
 
 #include "format_v2/table_reader.h"
@@ -61,6 +62,17 @@ public:
     bool current_split_pruned() const override;
     Status abort_split() override;
     Status close() override;
+    void set_batch_size(size_t batch_size) override;
+
+#ifdef BE_TEST
+    void TEST_install_batch_size_children() {
+        _native_reader = std::make_unique<format::TableReader>();
+        _jni_reader = std::make_unique<format::TableReader>();
+    }
+    std::pair<size_t, size_t> TEST_child_batch_sizes() const {
+        return {_native_reader->TEST_batch_size(), 
_jni_reader->TEST_batch_size()};
+    }
+#endif
 
 private:
     Status _ensure_current_split_reader(const format::SplitReadOptions& 
options);
diff --git a/be/src/format_v2/table/paimon_reader.cpp 
b/be/src/format_v2/table/paimon_reader.cpp
index c2114968946..236daac118a 100644
--- a/be/src/format_v2/table/paimon_reader.cpp
+++ b/be/src/format_v2/table/paimon_reader.cpp
@@ -116,6 +116,16 @@ Status PaimonHybridReader::close() {
     return close_status;
 }
 
+void PaimonHybridReader::set_batch_size(size_t batch_size) {
+    format::TableReader::set_batch_size(batch_size);
+    if (_native_reader != nullptr) {
+        _native_reader->set_batch_size(_batch_size);
+    }
+    if (_jni_reader != nullptr) {
+        _jni_reader->set_batch_size(_batch_size);
+    }
+}
+
 Status PaimonHybridReader::_ensure_current_split_reader(const 
format::SplitReadOptions& options) {
     if (_is_jni_split(options.current_range)) {
         DCHECK(options.current_split_format == format::FileFormat::JNI);
@@ -144,7 +154,7 @@ Status 
PaimonHybridReader::_init_child_reader(format::TableReader* reader,
     DORIS_CHECK(reader != nullptr);
     VExprContextSPtrs conjuncts;
     RETURN_IF_ERROR(_clone_conjuncts(&conjuncts));
-    return reader->init({
+    RETURN_IF_ERROR(reader->init({
             .projected_columns = _projected_columns,
             .conjuncts = std::move(conjuncts),
             .format = file_format,
@@ -154,7 +164,13 @@ Status 
PaimonHybridReader::_init_child_reader(format::TableReader* reader,
             .scanner_profile = _scanner_profile,
             .push_down_agg_type = _push_down_agg_type,
             .condition_cache_digest = _condition_cache_digest,
-    });
+    }));
+    // Zero means no adaptive prediction has been produced yet. Preserve the 
child's normal
+    // runtime default until FileScannerV2 supplies the first positive 
prediction.
+    if (_batch_size > 0) {
+        reader->set_batch_size(_batch_size);
+    }
+    return Status::OK();
 }
 
 Status PaimonHybridReader::_clone_conjuncts(VExprContextSPtrs* conjuncts) 
const {
diff --git a/be/src/format_v2/table/paimon_reader.h 
b/be/src/format_v2/table/paimon_reader.h
index ae179e6135a..fc94777b6da 100644
--- a/be/src/format_v2/table/paimon_reader.h
+++ b/be/src/format_v2/table/paimon_reader.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+#include <utility>
+
 #include "format_v2/table_reader.h"
 
 namespace doris {
@@ -66,6 +68,7 @@ public:
     bool current_split_pruned() const override;
     Status abort_split() override;
     Status close() override;
+    void set_batch_size(size_t batch_size) override;
 
 #ifdef BE_TEST
     static bool TEST_is_jni_split(const TFileRangeDesc& range) { return 
_is_jni_split(range); }
@@ -73,6 +76,13 @@ public:
                                       format::FileFormat* file_format) {
         return _to_file_format(range, file_format);
     }
+    void TEST_install_batch_size_children() {
+        _native_reader = std::make_unique<format::TableReader>();
+        _jni_reader = std::make_unique<format::TableReader>();
+    }
+    std::pair<size_t, size_t> TEST_child_batch_sizes() const {
+        return {_native_reader->TEST_batch_size(), 
_jni_reader->TEST_batch_size()};
+    }
 #endif
 
 private:
diff --git a/be/src/format_v2/table_reader.h b/be/src/format_v2/table_reader.h
index 643fafc1fe4..b8457ee9b4a 100644
--- a/be/src/format_v2/table_reader.h
+++ b/be/src/format_v2/table_reader.h
@@ -176,13 +176,17 @@ public:
     // FileScannerV2 adjusts this before each get_block() using an adaptive 
bytes-per-row estimate.
     // Store it here as well as forwarding to the current reader so newly 
opened split readers start
     // with the latest predicted batch size.
-    void set_batch_size(size_t batch_size) {
+    virtual void set_batch_size(size_t batch_size) {
         _batch_size = std::max<size_t>(1, batch_size);
         if (_data_reader.reader != nullptr) {
             _data_reader.reader->set_batch_size(_batch_size);
         }
     }
 
+#ifdef BE_TEST
+    size_t TEST_batch_size() const { return _batch_size; }
+#endif
+
     // Prepare for reading a new split/task.
     // 1. Pass a new split/task to reader, which will be used in subsequent 
open_reader() to initialize the underlying file reader.
     // 2. Parse delete predicates from split/task information, which will be 
used for later dynamic filtering and delete handling.
diff --git a/be/test/exec/scan/file_scanner_v2_test.cpp 
b/be/test/exec/scan/file_scanner_v2_test.cpp
index 2ab86072327..abd3d1e76d7 100644
--- a/be/test/exec/scan/file_scanner_v2_test.cpp
+++ b/be/test/exec/scan/file_scanner_v2_test.cpp
@@ -63,6 +63,44 @@ TFileRangeDesc hudi_range_with_delta_logs() {
     return range;
 }
 
+TFileRangeDesc paimon_cpp_jni_range() {
+    auto range = range_with_format("paimon", TFileFormatType::FORMAT_JNI);
+    TPaimonFileDesc paimon_params;
+    paimon_params.__set_reader_type(TPaimonReaderType::PAIMON_CPP);
+    range.table_format_params.__set_paimon_params(std::move(paimon_params));
+    return range;
+}
+
+TFileRangeDesc legacy_paimon_jni_range_without_reader_type() {
+    auto range = range_with_format("paimon", TFileFormatType::FORMAT_JNI);
+    TPaimonFileDesc paimon_params;
+    paimon_params.__set_paimon_split("legacy-split");
+    paimon_params.__set_paimon_predicate("legacy-predicate");
+    range.table_format_params.__set_paimon_params(std::move(paimon_params));
+    return range;
+}
+
+struct RetryableCloseState {
+    int close_calls = 0;
+};
+
+class RetryableCloseTableReader final : public format::TableReader {
+public:
+    explicit RetryableCloseTableReader(std::shared_ptr<RetryableCloseState> 
state)
+            : _state(std::move(state)) {}
+
+    Status close() override {
+        ++_state->close_calls;
+        if (_state->close_calls == 1) {
+            return Status::InternalError("injected table reader close 
failure");
+        }
+        return Status::OK();
+    }
+
+private:
+    std::shared_ptr<RetryableCloseState> _state;
+};
+
 VExprSPtr slot_ref(int slot_id, int column_id, DataTypePtr type, const 
std::string& name) {
     return VSlotRef::create_shared(slot_id, column_id, -1, std::move(type), 
name);
 }
@@ -216,6 +254,41 @@ TEST(FileScannerV2Test, 
FileScanLocalStateSelectsV2ForSupportedQueriesOnly) {
     
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options, 
false, params));
 }
 
+TEST(FileScannerV2Test, JniCompatibilityShapesForceLegacyScanner) {
+    TQueryOptions query_options;
+    query_options.__set_enable_file_scanner_v2(true);
+    query_options.__set_enable_paimon_cpp_reader(true);
+
+    TFileScanRangeParams params;
+    params.__set_format_type(TFileFormatType::FORMAT_JNI);
+    // Rolling upgrades may carry the only Paimon marker and reader type on 
each split. Since the
+    // scan-level selector cannot inspect that split yet, JNI scans 
conservatively stay on V1.
+    
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options, 
false, params));
+    EXPECT_FALSE(FileScannerV2::is_supported(params, paimon_cpp_jni_range()));
+
+    // Older FEs can omit reader_type. The legacy scanner interprets this as 
Paimon JNI when the C++
+    // reader is disabled, so the scan-level choice must still stay on V1.
+    query_options.__set_enable_paimon_cpp_reader(false);
+    
EXPECT_FALSE(FileScanLocalState::TEST_should_use_file_scanner_v2(query_options, 
false, params));
+    EXPECT_FALSE(
+            FileScannerV2::is_supported(params, 
legacy_paimon_jni_range_without_reader_type()));
+}
+
+TEST(FileScannerV2Test, FailedTableReaderCloseCanBeRetriedThroughScanner) {
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    RuntimeProfile profile("file_scanner_v2_close_retry");
+    auto close_state = std::make_shared<RetryableCloseState>();
+    FileScannerV2 scanner(&state, &profile,
+                          
std::make_unique<RetryableCloseTableReader>(close_state));
+
+    EXPECT_FALSE(scanner.close(&state).ok());
+    EXPECT_EQ(close_state->close_calls, 1);
+    EXPECT_TRUE(scanner.close(&state).ok());
+    EXPECT_EQ(close_state->close_calls, 2);
+    EXPECT_TRUE(scanner.close(&state).ok());
+    EXPECT_EQ(close_state->close_calls, 2);
+}
+
 // Scenario: Once FileScannerV2 is selected, an unsupported range must fail 
instead of falling back
 // to FileScanner.
 TEST(FileScannerV2Test, ValidateScanRangeRejectsUnsupportedRange) {
diff --git a/be/test/format_v2/jni/jni_table_reader_test.cpp 
b/be/test/format_v2/jni/jni_table_reader_test.cpp
new file mode 100644
index 00000000000..1c5788b1df7
--- /dev/null
+++ b/be/test/format_v2/jni/jni_table_reader_test.cpp
@@ -0,0 +1,191 @@
+// 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.
+
+#include "format_v2/jni/jni_table_reader.h"
+
+#include <gtest/gtest.h>
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "io/io_common.h"
+
+namespace doris::format {
+namespace {
+
+class FakeJniTableReader final : public JniTableReader {
+public:
+    int get_next_calls = 0;
+    int close_calls = 0;
+    int64_t close_profile_delta = 0;
+    RuntimeProfile::Counter* close_profile_counter = nullptr;
+    std::vector<size_t> open_batch_sizes;
+    std::vector<size_t> propagated_batch_sizes;
+    std::vector<Status> close_results;
+    bool next_eof = false;
+
+protected:
+    std::string connector_class() const override { return 
"test/FakeJniScanner"; }
+
+    Status build_scanner_params(std::map<std::string, std::string>* params) 
const override {
+        params->clear();
+        return Status::OK();
+    }
+
+    Status _get_next_jni_block(size_t* rows, bool* eof) override {
+        ++get_next_calls;
+        *rows = 0;
+        *eof = next_eof;
+        return Status::OK();
+    }
+
+    Status _close_jni_scanner() override {
+        if (!TEST_scanner_opened()) {
+            return Status::OK();
+        }
+        ++close_calls;
+        if (_reserve_split_profile_publication() && close_profile_counter != 
nullptr) {
+            COUNTER_UPDATE(close_profile_counter, close_profile_delta);
+        }
+        Status status = Status::OK();
+        if (static_cast<size_t>(close_calls) <= close_results.size()) {
+            status = close_results[close_calls - 1];
+        }
+        if (status.ok()) {
+            TEST_set_split_state(false, TEST_eof());
+        }
+        return status;
+    }
+
+    Status _set_open_scanner_batch_size(size_t batch_size) override {
+        propagated_batch_sizes.push_back(batch_size);
+        return Status::OK();
+    }
+
+    Status _open_jni_scanner() override {
+        open_batch_sizes.push_back(TEST_batch_size());
+        TEST_set_split_state(true, false);
+        return Status::OK();
+    }
+};
+
+Status init_reader(FakeJniTableReader* reader, const 
std::shared_ptr<io::IOContext>& io_ctx,
+                   RuntimeProfile* scanner_profile = nullptr) {
+    return reader->init({
+            .projected_columns = {},
+            .conjuncts = {},
+            .format = FileFormat::JNI,
+            .scan_params = nullptr,
+            .io_ctx = io_ctx,
+            .runtime_state = nullptr,
+            .scanner_profile = scanner_profile,
+    });
+}
+
+TEST(JniTableReaderTest, CancellationStopsBeforeFetchingAnotherJavaBatch) {
+    auto io_ctx = std::make_shared<io::IOContext>();
+    FakeJniTableReader reader;
+    ASSERT_TRUE(init_reader(&reader, io_ctx).ok());
+    reader.TEST_set_split_state(true, false);
+    io_ctx->should_stop = true;
+
+    Block block;
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    EXPECT_TRUE(eos);
+    EXPECT_EQ(reader.get_next_calls, 0);
+    EXPECT_EQ(reader.close_calls, 1);
+}
+
+TEST(JniTableReaderTest, EndOfSplitRemainsIdempotentAfterScannerClose) {
+    FakeJniTableReader reader;
+    ASSERT_TRUE(init_reader(&reader, nullptr).ok());
+    reader.TEST_set_split_state(true, false);
+    reader.next_eof = true;
+
+    Block block;
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    EXPECT_TRUE(eos);
+    EXPECT_TRUE(reader.TEST_eof());
+    EXPECT_FALSE(reader.TEST_scanner_opened());
+
+    eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    EXPECT_TRUE(eos);
+    EXPECT_EQ(reader.get_next_calls, 1);
+    EXPECT_EQ(reader.close_calls, 1);
+}
+
+TEST(JniTableReaderTest, FailedCloseCanBeRetried) {
+    RuntimeProfile profile("FailedCloseCanBeRetried");
+    FakeJniTableReader reader;
+    reader.close_profile_counter = 
profile.add_counter("PublishedCloseProfile", TUnit::UNIT);
+    reader.close_profile_delta = 17;
+    ASSERT_TRUE(init_reader(&reader, nullptr, &profile).ok());
+    reader.TEST_set_split_state(true, false);
+    reader.close_results = {Status::InternalError("injected close failure"), 
Status::OK()};
+
+    EXPECT_FALSE(reader.close().ok());
+    EXPECT_FALSE(reader.TEST_closed());
+    EXPECT_TRUE(reader.TEST_scanner_opened());
+    EXPECT_EQ(reader.close_calls, 1);
+    EXPECT_EQ(reader.close_profile_counter->value(), 17);
+
+    EXPECT_TRUE(reader.close().ok());
+    EXPECT_TRUE(reader.TEST_closed());
+    EXPECT_EQ(reader.close_calls, 2);
+    EXPECT_EQ(reader.close_profile_counter->value(), 17);
+}
+
+TEST(JniTableReaderTest, AdaptiveBatchSizeUpdatesAnOpenJavaScanner) {
+    FakeJniTableReader reader;
+    ASSERT_TRUE(init_reader(&reader, nullptr).ok());
+
+    reader.set_batch_size(17);
+    EXPECT_EQ(reader.TEST_batch_size(), 17);
+    EXPECT_TRUE(reader.propagated_batch_sizes.empty());
+
+    reader.TEST_set_split_state(true, false);
+    reader.set_batch_size(33);
+    EXPECT_EQ(reader.TEST_batch_size(), 33);
+    EXPECT_EQ(reader.propagated_batch_sizes, std::vector<size_t>({33}));
+}
+
+TEST(JniTableReaderTest, AdaptiveProbeSetBeforePrepareControlsFirstJniOpen) {
+    FakeJniTableReader reader;
+    ASSERT_TRUE(init_reader(&reader, nullptr).ok());
+
+    reader.set_batch_size(32);
+    ASSERT_TRUE(reader.prepare_split({
+                                             .partition_values = {},
+                                             .partition_prune_conjuncts = {},
+                                             .cache = nullptr,
+                                             .current_range = {},
+                                             .current_split_format = 
FileFormat::JNI,
+                                             .global_rowid_context = 
std::nullopt,
+                                     })
+                        .ok());
+
+    EXPECT_EQ(reader.open_batch_sizes, std::vector<size_t>({32}));
+    EXPECT_TRUE(reader.TEST_scanner_opened());
+}
+
+} // namespace
+} // namespace doris::format
diff --git a/be/test/format_v2/jni/paimon_jni_reader_test.cpp 
b/be/test/format_v2/jni/paimon_jni_reader_test.cpp
index 08fd333abd3..570977a5f7e 100644
--- a/be/test/format_v2/jni/paimon_jni_reader_test.cpp
+++ b/be/test/format_v2/jni/paimon_jni_reader_test.cpp
@@ -123,5 +123,53 @@ TEST(PaimonJniReaderTest, 
RejectsMissingPredicateFromBothProtocolLocations) {
     EXPECT_NE(status.to_string().find("missing paimon_predicate"), 
std::string::npos);
 }
 
+TEST(PaimonJniReaderTest, FallsBackToLegacySplitOptionsAndHadoopConf) {
+    auto range = make_paimon_jni_range();
+    auto& paimon_params = range.table_format_params.paimon_params;
+    paimon_params.__set_paimon_predicate("legacy-predicate");
+    paimon_params.__set_paimon_options({{"legacy-option", "legacy-value"}});
+    paimon_params.__set_hadoop_conf({{"fs.defaultFS", "hdfs://legacy"}});
+
+    auto scan_params = make_scan_params();
+    PaimonJniReader reader;
+    ASSERT_TRUE(init_reader(&reader, &scan_params).ok());
+
+    std::map<std::string, std::string> params;
+    ASSERT_TRUE(build_params(&reader, range, &params).ok());
+    EXPECT_EQ(params["paimon.legacy-option"], "legacy-value");
+    EXPECT_EQ(params["hadoop.fs.defaultFS"], "hdfs://legacy");
+}
+
+TEST(PaimonJniReaderTest, ScanLevelOptionsOverrideLegacySplitFallbacks) {
+    auto range = make_paimon_jni_range();
+    auto& paimon_params = range.table_format_params.paimon_params;
+    paimon_params.__set_paimon_predicate("legacy-predicate");
+    paimon_params.__set_paimon_options({{"source", "legacy"}});
+    paimon_params.__set_hadoop_conf({{"source", "legacy"}});
+
+    auto scan_params = make_scan_params();
+    scan_params.__set_paimon_options({{"source", "scan"}});
+    scan_params.__set_properties({{"source", "scan"}});
+    PaimonJniReader reader;
+    ASSERT_TRUE(init_reader(&reader, &scan_params).ok());
+
+    std::map<std::string, std::string> params;
+    ASSERT_TRUE(build_params(&reader, range, &params).ok());
+    EXPECT_EQ(params["paimon.source"], "scan");
+    EXPECT_EQ(params["hadoop.source"], "scan");
+}
+
+TEST(PaimonJniReaderTest, KeepsInitialPhysicalBatchSizeAfterOpen) {
+    PaimonJniReader reader;
+    reader.set_batch_size(32);
+    EXPECT_EQ(reader.TEST_batch_size(), 32);
+
+    // Paimon copies the constructor size into the RecordReader during Java 
open. A later predictor
+    // result cannot resize that physical reader, so keep the initial probe 
size for the split.
+    reader.TEST_set_split_state(true, false);
+    reader.set_batch_size(1);
+    EXPECT_EQ(reader.TEST_batch_size(), 32);
+}
+
 } // namespace
 } // namespace doris::format::paimon
diff --git a/be/test/format_v2/table/hudi_reader_test.cpp 
b/be/test/format_v2/table/hudi_reader_test.cpp
index 125183cd7a6..77cd75bc948 100644
--- a/be/test/format_v2/table/hudi_reader_test.cpp
+++ b/be/test/format_v2/table/hudi_reader_test.cpp
@@ -178,5 +178,14 @@ TEST(HudiReaderTest, 
ResetsSplitSchemaIdBeforePreparingNextSplit) {
     EXPECT_EQ(reader.TEST_mapping_mode(), TableColumnMappingMode::BY_NAME);
 }
 
+TEST(HudiHybridReaderTest, AdaptiveBatchSizeReachesBothChildReaders) {
+    hudi::HudiHybridReader reader;
+    reader.TEST_install_batch_size_children();
+    reader.set_batch_size(123);
+    const auto child_batch_sizes = reader.TEST_child_batch_sizes();
+    EXPECT_EQ(child_batch_sizes.first, 123);
+    EXPECT_EQ(child_batch_sizes.second, 123);
+}
+
 } // namespace
 } // namespace doris::format
diff --git a/be/test/format_v2/table/paimon_reader_test.cpp 
b/be/test/format_v2/table/paimon_reader_test.cpp
index f1c9196e733..a56ebcc5e87 100644
--- a/be/test/format_v2/table/paimon_reader_test.cpp
+++ b/be/test/format_v2/table/paimon_reader_test.cpp
@@ -637,6 +637,15 @@ TEST(PaimonHybridReaderTest, 
ConvertsNativeSplitFileFormat) {
     EXPECT_NE(std::string::npos, status.to_string().find("Unsupported native 
Paimon file format"));
 }
 
+TEST(PaimonHybridReaderTest, AdaptiveBatchSizeReachesBothChildReaders) {
+    paimon::PaimonHybridReader reader;
+    reader.TEST_install_batch_size_children();
+    reader.set_batch_size(321);
+    const auto child_batch_sizes = reader.TEST_child_batch_sizes();
+    EXPECT_EQ(child_batch_sizes.first, 321);
+    EXPECT_EQ(child_batch_sizes.second, 321);
+}
+
 TEST(PaimonHybridReaderTest, DispatchesNativeThenJniSplitToMatchingReader) {
     RuntimeProfile profile("test_profile");
     RuntimeState state {TQueryOptions(), TQueryGlobals()};
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
index 0cc3e814082..46caf45aaee 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
@@ -314,19 +314,19 @@ public class PaimonJniScanner extends JniScanner {
             if (reader != null) {
                 try {
                     reader.close();
+                    reader = null;
                 } catch (IOException e) {
                     if (exception == null) {
                         exception = e;
                     } else {
                         exception.addSuppressed(e);
                     }
-                } finally {
-                    reader = null;
                 }
             }
             if (ioManager != null) {
                 try {
                     ioManager.close();
+                    ioManager = null;
                 } catch (Exception e) {
                     LOG.warn("Failed to close Paimon JNI IOManager, temp dirs: 
{}", ioManagerTempDirs, e);
                     if (exception == null) {
@@ -334,8 +334,6 @@ public class PaimonJniScanner extends JniScanner {
                     } else {
                         exception.addSuppressed(e);
                     }
-                } finally {
-                    ioManager = null;
                 }
             }
         } finally {
@@ -348,11 +346,8 @@ public class PaimonJniScanner extends JniScanner {
 
     private void releaseRecordIterator() {
         if (recordIterator != null) {
-            try {
-                recordIterator.releaseBatch();
-            } finally {
-                recordIterator = null;
-            }
+            recordIterator.releaseBatch();
+            recordIterator = null;
         }
     }
 
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
index 6f893b38b1c..d43f251f58d 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
@@ -31,6 +31,7 @@ import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
 import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Field;
 import java.lang.reflect.Proxy;
 import java.util.Arrays;
@@ -40,6 +41,7 @@ import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class PaimonJniScannerTest {
     @Rule
@@ -190,6 +192,69 @@ public class PaimonJniScannerTest {
         Assert.assertNull(recordIteratorField.get(scanner));
     }
 
+    @Test
+    public void testFailedCloseRetainsResourcesForRetry() throws Exception {
+        PaimonJniScanner scanner = new PaimonJniScanner(128, 
createBaseParams());
+        AtomicInteger iteratorCloseCalls = new AtomicInteger();
+        RecordReader.RecordIterator<InternalRow> recordIterator =
+                new RecordReader.RecordIterator<InternalRow>() {
+                    @Override
+                    public InternalRow next() {
+                        return null;
+                    }
+
+                    @Override
+                    public void releaseBatch() {
+                        if (iteratorCloseCalls.incrementAndGet() == 1) {
+                            throw new RuntimeException("injected iterator 
close failure");
+                        }
+                    }
+                };
+        AtomicInteger readerCloseCalls = new AtomicInteger();
+        RecordReader<InternalRow> reader = new RecordReader<InternalRow>() {
+            @Override
+            public RecordIterator<InternalRow> readBatch() {
+                return null;
+            }
+
+            @Override
+            public void close() throws IOException {
+                if (readerCloseCalls.incrementAndGet() == 1) {
+                    throw new IOException("injected reader close failure");
+                }
+            }
+        };
+        RetryableIOManager ioManager = new RetryableIOManager();
+
+        Field recordIteratorField = 
PaimonJniScanner.class.getDeclaredField("recordIterator");
+        recordIteratorField.setAccessible(true);
+        recordIteratorField.set(scanner, recordIterator);
+        Field readerField = PaimonJniScanner.class.getDeclaredField("reader");
+        readerField.setAccessible(true);
+        readerField.set(scanner, reader);
+        Field ioManagerField = 
PaimonJniScanner.class.getDeclaredField("ioManager");
+        ioManagerField.setAccessible(true);
+        ioManagerField.set(scanner, ioManager);
+
+        try {
+            scanner.close();
+            Assert.fail("expected the first close to fail");
+        } catch (IOException expected) {
+            Assert.assertEquals("Failed to release Paimon record iterator", 
expected.getMessage());
+        }
+        Assert.assertSame(recordIterator, recordIteratorField.get(scanner));
+        Assert.assertSame(reader, readerField.get(scanner));
+        Assert.assertSame(ioManager, ioManagerField.get(scanner));
+
+        scanner.close();
+        Assert.assertNull(recordIteratorField.get(scanner));
+        Assert.assertNull(readerField.get(scanner));
+        Assert.assertNull(ioManagerField.get(scanner));
+        Assert.assertEquals(2, iteratorCloseCalls.get());
+        Assert.assertEquals(2, readerCloseCalls.get());
+        Assert.assertEquals(2, ioManager.closeCalls.get());
+    }
+
     private Map<String, String> createBaseParams() {
         Map<String, String> params = new HashMap<>();
         params.put("required_fields", "");
@@ -257,6 +322,21 @@ public class PaimonJniScannerTest {
         }
     }
 
+    public static class RetryableIOManager extends TestIOManager {
+        private final AtomicInteger closeCalls = new AtomicInteger();
+
+        public RetryableIOManager() {
+            super(new String[0]);
+        }
+
+        @Override
+        public void close() {
+            if (closeCalls.incrementAndGet() == 1) {
+                throw new RuntimeException("injected IO manager close 
failure");
+            }
+        }
+    }
+
     @Test
     public void testGetFieldIndexMatchesMixedCaseColumns() {
         Assert.assertEquals(1, 
PaimonJniScanner.getFieldIndex(Arrays.asList("data", "mIxEd_COL", "PART"),


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

Reply via email to