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 122a7db07cd [fix](be) Catch ORC exceptions during RowID seeks (#69465)
122a7db07cd is described below

commit 122a7db07cd7528132fc9a11317e6b461a30179d
Author: Socrates <[email protected]>
AuthorDate: Tue Oct 27 16:42:33 2026 +0800

    [fix](be) Catch ORC exceptions during RowID seeks (#69465)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: #67141
    
    The legacy ORC reader performs RowID seekToRow before the exception
    handlers around nextBatch. A seek can throw orc::ParseError while
    reading a missing or unreadable file, and that std::exception can escape
    the external row-fetch worker and terminate the BE.
    
    Catch seek exceptions before lazy or non-lazy reads and return an error
    with the file and cause. Preserve Doris exception propagation and error
    classification. Keep clean EOF only for a stopped reader throwing
    "stop"; report unrelated I/O errors even when cancellation races with
    them. Return zero rows when no requested RowIDs remain.
    
    The injected-error regression lives in its own
    `p0,external,hive,nonConcurrent` suite with a separate catalog and the
    preloaded Hive fixture. It pins the legacy-reader and TopN settings,
    asserts phase-two materialization, checks the expected seek failure, and
    verifies a successful five-row query after removing the debug point.
    Debug-point cleanup and session restoration run in finally blocks. The
    original TopN suite retains its concurrent correctness coverage.
    
    The branch has been rebased onto master `6e5a8929fba`, including the
    unrelated BE UT compile fix #69487.
    
    ### Release note
    
    Fix BE termination when an ORC RowID seek fails during external TopN
    late materialization.
    
    ### Check List (For Author)
    
    - Test:
    - Added real-ORC seek unit cases for lazy/non-lazy errors, cancellation,
    Doris exception propagation, empty RowIDs and normal seek progression.
    Runtime verification remains for CI.
    - Groovy 4.0.19 CONVERSION-phase compilation passed for both regression
    suites.
    - Local mock execution passed serial-group validation, independent
    catalog setup, recovery-query execution, and debug-point/session cleanup
    after success, unexpected query failure and partial enable failure.
      - git diff --check passed.
    - ./run-regression-test.sh --compile remains blocked by pre-existing
    duplicate Gson declarations rejected by the local Maven installation.
    Mocks do not replace live cluster regression.
    - BE UT and External Regression reruns requested for the updated head.
    The external runner enables -runNonConcurrent true.
    - Behavior changed: Yes. Seek exceptions return errors; cancelled stop
    reads retain EOF behavior.
    - Does this need documentation: No.
    
    ### Self-review
    
    - Goal and scope: repair the seek exception boundary and move only its
    global injection into an independently runnable serial suite.
    - Parallel paths: lazy and non-lazy seeks share the exception boundary;
    Doris errors retain their worker-side handling.
    - Concurrency and lifecycle: the all-BE injection is serial, scoped to
    one debug point, and removed before recovery. Session settings are
    restored on every exit.
    - Coverage and observability: the plan assertion ensures phase-two
    materialization; expected error matching and recovery remain enforced.
    The returned error includes the file and underlying cause.
    - Compatibility: no new configuration, FE/BE protocol, persistence,
    transaction or storage-format changes.
---
 be/src/format/orc/vorc_reader.cpp                  |  44 +++++-
 be/src/format/orc/vorc_reader.h                    |  11 +-
 be/test/format/orc/orc_reader_test.cpp             | 154 +++++++++++++++++++++
 .../hive/test_hive_orc_rowid_seek_exception.groovy |  69 +++++++++
 4 files changed, 265 insertions(+), 13 deletions(-)

diff --git a/be/src/format/orc/vorc_reader.cpp 
b/be/src/format/orc/vorc_reader.cpp
index 79d16190203..704566d978b 100644
--- a/be/src/format/orc/vorc_reader.cpp
+++ b/be/src/format/orc/vorc_reader.cpp
@@ -100,6 +100,7 @@
 #include "storage/id_manager.h"
 #include "storage/segment/column_reader.h"
 #include "storage/utils.h"
+#include "util/debug_points.h"
 #include "util/slice.h"
 #include "util/timezone_utils.h"
 #include "util/unaligned.h"
@@ -2300,6 +2301,25 @@ void OrcReader::_filter_rows_by_condition_cache(size_t* 
read_rows, bool* eof) {
     }
 }
 
+bool OrcReader::_seek_to_read_one_line() {
+    if (_read_by_rows) {
+        if (_row_ids.empty()) {
+            return false;
+        }
+        DBUG_EXECUTE_IF("OrcReader::rowid_seek.inject_error", {
+            // Unit tests inject a specific seek exception through the 
callback. The SQL
+            // regression uses the point without a callback and keeps the 
default failure.
+            if (dp->callback.has_value()) {
+                DBUG_RUN_CALLBACK();
+            }
+            throw orc::ParseError("injected ORC RowID seek failure");
+        });
+        _row_reader->seekToRow(_row_ids.front());
+        _row_ids.pop_front();
+    }
+    return true;
+}
+
 Status OrcReader::_get_next_block_impl(Block* block, size_t* read_rows, bool* 
eof) {
     if (_io_ctx && _io_ctx->should_stop) {
         *eof = true;
@@ -2325,9 +2345,27 @@ Status OrcReader::_get_next_block_impl(Block* block, 
size_t* read_rows, bool* eo
         }
     }
 
-    if (!_seek_to_read_one_line()) {
-        *eof = true;
-        return Status::OK();
+    // RowID seeks can perform I/O before either nextBatch exception boundary 
below.
+    // Translate their ORC exceptions here so an external row-fetch worker 
returns an error.
+    try {
+        if (!_seek_to_read_one_line()) {
+            *eof = true;
+            *read_rows = 0;
+            return Status::OK();
+        }
+    } catch (const Exception&) {
+        // Keep Doris errors (including memory limits) on the existing worker 
exception path.
+        throw;
+    } catch (const std::exception& e) {
+        const std::string err_msg = e.what();
+        if (_io_ctx && _io_ctx->should_stop && err_msg == "stop") {
+            block->clear_column_data();
+            *eof = true;
+            *read_rows = 0;
+            return Status::OK();
+        }
+        return Status::InternalError("Orc row reader seek failed. file = {}, 
reason = {}",
+                                     _file_description.path, err_msg);
     }
 
     if (_lazy_read_ctx.can_lazy_read) {
diff --git a/be/src/format/orc/vorc_reader.h b/be/src/format/orc/vorc_reader.h
index 64ea2466ade..6541e3c4da5 100644
--- a/be/src/format/orc/vorc_reader.h
+++ b/be/src/format/orc/vorc_reader.h
@@ -718,16 +718,7 @@ private:
         return true;
     }
 
-    bool _seek_to_read_one_line() {
-        if (_read_by_rows) {
-            if (_row_ids.empty()) {
-                return false;
-            }
-            _row_reader->seekToRow(_row_ids.front());
-            _row_ids.pop_front();
-        }
-        return true;
-    }
+    bool _seek_to_read_one_line();
 
     Status _set_read_one_line_impl() override {
         _batch_size = 1;
diff --git a/be/test/format/orc/orc_reader_test.cpp 
b/be/test/format/orc/orc_reader_test.cpp
index c8099b286f0..62f94000e68 100644
--- a/be/test/format/orc/orc_reader_test.cpp
+++ b/be/test/format/orc/orc_reader_test.cpp
@@ -18,6 +18,7 @@
 #include <glog/logging.h>
 #include <gtest/gtest.h>
 
+#include <functional>
 #include <list>
 #include <memory>
 #include <string>
@@ -25,6 +26,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include "common/exception.h"
 #include "core/assert_cast.h"
 #include "core/block/block.h"
 #include "core/column/column_nullable.h"
@@ -39,11 +41,13 @@
 #include "format/orc/orc_memory_pool.h"
 #include "format/orc/vorc_reader.h"
 #include "io/fs/file_meta_cache.h"
+#include "orc/Exceptions.hh"
 #include "orc/sargs/SearchArgument.hh"
 #include "runtime/exec_env.h"
 #include "runtime/runtime_profile.h"
 #include "runtime/runtime_state.h"
 #include "testutil/desc_tbl_builder.h"
+#include "util/debug_points.h"
 namespace doris {
 class OrcReaderTest : public testing::Test {
 public:
@@ -437,4 +441,154 @@ TEST_F(OrcReaderTest, 
deletion_vector_filters_rows_without_query_conjuncts) {
     EXPECT_EQ(filtered_order_keys, expected);
 }
 
+namespace {
+
+class OrcRowIdSeekTest : public testing::TestWithParam<bool> {
+protected:
+    void SetUp() override {
+        debug_points_enabled = config::enable_debug_points;
+        range.path = "./be/test/exec/test_data/orc_scanner/orders.orc";
+        reader = std::make_unique<OrcReader>(&profile, nullptr, params, range, 
1, "UTC", &io_ctx,
+                                             nullptr, GetParam());
+        reader->_reader = orc::createReader(orc::readLocalFile(range.path), 
orc::ReaderOptions());
+        reader->_row_reader = 
reader->_reader->createRowReader(orc::RowReaderOptions());
+        reader->_batch = reader->_row_reader->createRowBatch(1);
+        reader->_read_by_rows = true;
+        reader->_row_ids = {1, 2};
+        reader->_lazy_read_ctx.can_lazy_read = GetParam();
+    }
+
+    void TearDown() override {
+        DebugPoints::instance()->remove("OrcReader::rowid_seek.inject_error");
+        config::enable_debug_points = debug_points_enabled;
+    }
+
+    void fail_seek(std::function<void()> failure) {
+        config::enable_debug_points = true;
+        
DebugPoints::instance()->add_with_callback("OrcReader::rowid_seek.inject_error",
+                                                   
std::function<void()>([this, failure] {
+                                                       ++failed_seeks;
+                                                       failure();
+                                                   }));
+    }
+
+    TFileScanRangeParams params;
+    TFileRangeDesc range;
+    io::IOContext io_ctx;
+    RuntimeProfile profile {"OrcRowIdSeekTest"};
+    bool debug_points_enabled = false;
+    size_t failed_seeks = 0;
+    std::unique_ptr<OrcReader> reader;
+};
+
+TEST_P(OrcRowIdSeekTest, SeekIoFailureReturnsError) {
+    const std::string message = "[NOT_FOUND] missing ORC file";
+    fail_seek([message] { throw orc::ParseError(message); });
+    Block block;
+    size_t read_rows = 0;
+    bool eof = false;
+    Status status;
+    ASSERT_NO_THROW(status = reader->get_next_block(&block, &read_rows, &eof));
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find("Orc row reader seek failed"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find(range.path), std::string::npos);
+    EXPECT_NE(status.to_string().find(message), std::string::npos);
+    EXPECT_FALSE(eof);
+    EXPECT_EQ(reader->_row_ids, (std::list<int64_t> {1, 2}));
+    EXPECT_EQ(failed_seeks, 1);
+}
+
+TEST_P(OrcRowIdSeekTest, CancellationDuringSeekReturnsEof) {
+    fail_seek([this] {
+        io_ctx.should_stop = true;
+        throw orc::ParseError("stop");
+    });
+    Block block;
+    size_t read_rows = 99;
+    bool eof = false;
+    Status status;
+    ASSERT_NO_THROW(status = reader->get_next_block(&block, &read_rows, &eof));
+    EXPECT_TRUE(status.ok()) << status;
+    EXPECT_TRUE(io_ctx.should_stop);
+    EXPECT_TRUE(eof);
+    EXPECT_EQ(read_rows, 0);
+    EXPECT_EQ(failed_seeks, 1);
+}
+
+TEST_P(OrcRowIdSeekTest, DorisExceptionsKeepTheirErrorCode) {
+    fail_seek([] { throw Exception(ErrorCode::MEM_ALLOC_FAILED, "injected 
memory limit"); });
+    Block block;
+    size_t read_rows = 0;
+    bool eof = false;
+    try {
+        const auto status = reader->get_next_block(&block, &read_rows, &eof);
+        FAIL() << "Expected a Doris exception, got " << status;
+    } catch (const Exception& e) {
+        EXPECT_EQ(e.code(), ErrorCode::MEM_ALLOC_FAILED);
+    }
+    EXPECT_EQ(failed_seeks, 1);
+}
+
+TEST_P(OrcRowIdSeekTest, StopWithoutCancellationReturnsError) {
+    fail_seek([] { throw orc::ParseError("stop"); });
+    Block block;
+    size_t read_rows = 0;
+    bool eof = false;
+    const auto status = reader->get_next_block(&block, &read_rows, &eof);
+    EXPECT_FALSE(status.ok());
+    EXPECT_FALSE(eof);
+    EXPECT_EQ(failed_seeks, 1);
+}
+
+TEST_P(OrcRowIdSeekTest, CancellationDoesNotHideAnIoFailure) {
+    fail_seek([this] {
+        io_ctx.should_stop = true;
+        throw orc::ParseError("ORC read I/O failure");
+    });
+    Block block;
+    size_t read_rows = 0;
+    bool eof = false;
+    const auto status = reader->get_next_block(&block, &read_rows, &eof);
+    EXPECT_FALSE(status.ok());
+    EXPECT_FALSE(eof);
+    EXPECT_EQ(failed_seeks, 1);
+}
+
+TEST_P(OrcRowIdSeekTest, EmptyRowIdsReturnZeroRowsAndEof) {
+    reader->_row_ids.clear();
+    fail_seek([] { throw orc::ParseError("must not seek"); });
+    Block block;
+    size_t read_rows = 99;
+    bool eof = false;
+    const auto status = reader->get_next_block(&block, &read_rows, &eof);
+    EXPECT_TRUE(status.ok()) << status;
+    EXPECT_TRUE(eof);
+    EXPECT_EQ(read_rows, 0);
+    EXPECT_EQ(failed_seeks, 0);
+}
+
+TEST_P(OrcRowIdSeekTest, NormalSeekConsumesOnlyTheRequestedRowId) {
+    EXPECT_TRUE(reader->_seek_to_read_one_line());
+    EXPECT_EQ(reader->_row_ids, (std::list<int64_t> {2}));
+    EXPECT_TRUE(reader->_seek_to_read_one_line());
+    EXPECT_TRUE(reader->_row_ids.empty());
+    EXPECT_FALSE(reader->_seek_to_read_one_line());
+}
+
+TEST_P(OrcRowIdSeekTest, AlreadyCancelledDoesNotSeek) {
+    io_ctx.should_stop = true;
+    fail_seek([] { throw orc::ParseError("must not seek"); });
+    Block block;
+    size_t read_rows = 99;
+    bool eof = false;
+    const auto status = reader->get_next_block(&block, &read_rows, &eof);
+    EXPECT_TRUE(status.ok()) << status;
+    EXPECT_TRUE(eof);
+    EXPECT_EQ(read_rows, 0);
+    EXPECT_EQ(failed_seeks, 0);
+}
+
+INSTANTIATE_TEST_SUITE_P(LazyAndNonLazy, OrcRowIdSeekTest, testing::Bool());
+
+} // namespace
 } // namespace doris
diff --git 
a/regression-test/suites/external_table_p0/hive/test_hive_orc_rowid_seek_exception.groovy
 
b/regression-test/suites/external_table_p0/hive/test_hive_orc_rowid_seek_exception.groovy
new file mode 100644
index 00000000000..45ce58255b6
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/hive/test_hive_orc_rowid_seek_exception.groovy
@@ -0,0 +1,69 @@
+// 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_hive_orc_rowid_seek_exception", "p0,external,hive,nonConcurrent") {
+    String enabled = context.config.otherConfigs.get("enableHiveTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        logger.info("disable Hive test")
+        return
+    }
+
+    // Use a separate catalog and the preloaded Hive fixture, independent of 
the TopN suite.
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String hmsPort = context.config.otherConfigs.get("hive2HmsPort")
+    sql """drop catalog if exists test_hive_orc_rowid_seek_exception"""
+    sql """create catalog test_hive_orc_rowid_seek_exception properties (
+        "type" = "hms",
+        "hive.metastore.uris" = "thrift://${externalEnvIp}:${hmsPort}"
+    )"""
+    sql """use test_hive_orc_rowid_seek_exception.global_lazy_mat_db"""
+
+    def debugPoint = GetDebugPoint()
+    def previousSettings = sql """SELECT @@enable_file_scanner_v2, 
@@enable_sql_cache,
+                                       @@topn_lazy_materialization_threshold"""
+    def rowIdSeekQuery = """SELECT name, value, score FROM 
orc_topn_lazy_mat_table
+                           ORDER BY id LIMIT 5"""
+    try {
+        // Phase two uses the legacy ORC reader only when V2 is disabled.
+        sql "SET enable_file_scanner_v2=false"
+        sql "SET enable_sql_cache=false"
+        sql "SET topn_lazy_materialization_threshold=1024"
+        explain {
+            sql rowIdSeekQuery
+            contains("VMaterializeNode")
+        }
+        try {
+            // Inject after RowIDs exist, rather than during first-phase file 
initialization.
+            
debugPoint.enableDebugPointForAllBEs("OrcReader::rowid_seek.inject_error")
+            test {
+                sql rowIdSeekQuery
+                exception "injected ORC RowID seek failure"
+            }
+        } finally {
+            
debugPoint.disableDebugPointForAllBEs("OrcReader::rowid_seek.inject_error")
+        }
+        // The materializing query must still return rows after the 
asynchronous failure.
+        test {
+            sql rowIdSeekQuery
+            rowNum 5
+        }
+    } finally {
+        sql "SET enable_file_scanner_v2=${previousSettings[0][0]}"
+        sql "SET enable_sql_cache=${previousSettings[0][1]}"
+        sql "SET topn_lazy_materialization_threshold=${previousSettings[0][2]}"
+    }
+}


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

Reply via email to