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]