github-actions[bot] commented on code in PR #65931:
URL: https://github.com/apache/doris/pull/65931#discussion_r3635726315
##########
be/src/format_v2/parquet/native_schema_node.cpp:
##########
@@ -68,24 +68,39 @@ Status build_native_schema_node(const DataTypePtr&
projected_type,
return Status::Corruption("Parquet column {} is not a STRUCT",
file_schema.name);
}
const auto* struct_type = assert_cast<const
DataTypeStruct*>(type.get());
- std::map<std::string, const ParquetColumnSchema*> file_children;
- for (const auto& child : file_schema.children) {
- file_children.emplace(to_lower(child->name), child.get());
- }
auto node = std::make_shared<NativeStructSchemaNode>();
for (size_t i = 0; i < struct_type->get_elements().size(); ++i) {
const auto& table_name = struct_type->get_element_name(i);
- // Native metadata keeps writer casing. Match normalized names
while preserving the
- // original file name used to address the physical child reader.
- const auto child_it = file_children.find(to_lower(table_name));
- if (child_it == file_children.end()) {
+ const ParquetColumnSchema* file_child = nullptr;
+ for (const auto& child : file_schema.children) {
Review Comment:
[P2] Keep STRUCT child resolution indexed
This exact-name scan restarts from the first physical child for every
projected field, so a full N-field STRUCT now performs roughly N(N+1)/2
comparisons; fallback scans again and recomputes lowercase strings inside the
loop. `NativeColumnReader::create()` rebuilds this mapping for predicate/output
readers in every selected Row Group, making wide nested schemas pay the
quadratic cost repeatedly. This is distinct from the existing case-collision
correctness thread. Build exact-name and normalized unique/ambiguous indices
once per physical STRUCT, then preserve the current exact-first semantics
through indexed lookups.
##########
be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp:
##########
@@ -86,6 +86,82 @@ Status validate_uncompressed_page_sizes(const
tparquet::PageHeader& header,
return Status::OK();
}
+Status validate_fixed_width_page_size(const tparquet::PageHeader& header,
int32_t type_length,
+ level_t max_rep_level, level_t
max_def_level,
+ bool schema_is_required) {
+ if (type_length <= 0 || max_rep_level != 0) {
Review Comment:
[P1] Close the remaining compressed data-page preallocation paths
This still returns before validation for repeated V2 leaves, although V2
provides `num_values - num_nulls` and exact repetition/definition byte lengths.
The `type_length <= 0` and V1 optional fallbacks likewise reach
`_reserve_decompress_buf(uncompressed_size)` before Snappy reads the frame's
true decoded length. A tiny compressed `ARRAY<INT32>` V2 page or one-value
`BYTE_ARRAY`/optional V1 page can therefore advertise `INT32_MAX` and make
Doris reserve nearly 2 GiB before rejecting it. The existing thread covered
flat optional V2; these are distinct remaining layouts. Apply the exact V2
formula regardless of repetition, preflight codec output before reserving for
layouts without a statically known extent, and add compressed regressions for
both paths.
##########
be/test/format_v2/table/remote_doris_reader_test.cpp:
##########
@@ -467,4 +600,140 @@ TEST(RemoteDorisV2ReaderTest,
RejectsInvalidRemoteDorisRange) {
EXPECT_FALSE(reader->init(&state).ok());
}
+TEST(RemoteDorisV2ReaderTest, RuntimeCancellationInterruptsBlockedFlightDoGet)
{
+ BlockingFlightServer server(BlockingFlightServer::Mode::DO_GET);
+ const auto server_status = server.start();
+ ASSERT_TRUE(server_status.ok()) << server_status;
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = remote_slots(&pool, &desc_tbl);
+ RuntimeState state;
+ RuntimeProfile profile("remote_doris_v2_blocked_doget_test");
+ auto io_ctx = std::make_shared<io::IOContext>();
+ auto reader = create_flight_reader(&profile, remote_doris_range(server),
slots, io_ctx);
+ ASSERT_TRUE(reader->init(&state).ok());
+ auto request = std::make_shared<FileScanRequest>();
+ FileScanRequestBuilder builder(request.get());
+ ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
+
+ auto open_result =
+ std::async(std::launch::async, [&] { return
reader->open(std::move(request)); });
+ const bool entered = server.wait_until_entered(std::chrono::seconds(2));
+ if (!entered &&
+ open_result.wait_for(std::chrono::milliseconds(0)) ==
std::future_status::ready) {
+ FAIL() << "Flight DoGet failed before reaching the server: " <<
open_result.get();
+ }
+ ASSERT_TRUE(entered);
+ state.cancel(Status::Cancelled("cancel blocked Flight DoGet"));
+ const bool interrupted =
+ open_result.wait_for(std::chrono::milliseconds(750)) ==
std::future_status::ready;
+ if (!interrupted) {
+ server.release();
+ }
+ EXPECT_TRUE(interrupted);
+ EXPECT_FALSE(open_result.get().ok());
+}
+
+TEST(RemoteDorisV2ReaderTest,
BlockedDoGetRetainsQueryResourcesUntilWorkerExits) {
+ BlockingFlightServer server(BlockingFlightServer::Mode::DO_GET);
+ const auto server_status = server.start();
+ ASSERT_TRUE(server_status.ok()) << server_status;
+ ObjectPool pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ const auto slots = remote_slots(&pool, &desc_tbl);
+ auto state = std::make_unique<MockRuntimeState>();
+ std::weak_ptr<ResourceContext> resource_ctx =
state->get_query_ctx()->resource_ctx();
+ RuntimeProfile profile("remote_doris_v2_doget_resource_context_test");
+ auto reader = create_flight_reader(&profile, remote_doris_range(server),
slots,
+ std::make_shared<io::IOContext>());
+ ASSERT_TRUE(reader->init(state.get()).ok());
+ auto request = std::make_shared<FileScanRequest>();
+ FileScanRequestBuilder builder(request.get());
+ ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
+
+ auto open_result =
+ std::async(std::launch::async, [&] { return
reader->open(std::move(request)); });
+ ASSERT_TRUE(server.wait_until_entered(std::chrono::seconds(2)));
+ state->cancel(Status::Cancelled("cancel blocked Flight DoGet"));
+ ASSERT_EQ(open_result.wait_for(std::chrono::milliseconds(750)),
std::future_status::ready);
Review Comment:
[P2] Release the server before a fatal async assertion can return
If this 750 ms assertion fails, GoogleTest returns from the test and
destroys `open_result` before `server` (reverse declaration order). A
`std::future` from `std::async(std::launch::async, ...)` waits for its
unfinished task, while the server destructor that would release the blocked
`DoGet` cannot run; the only remaining bound is the default 3600-second
query/Flight deadline. The fatal assertions at lines 626 and 698 have the same
cleanup ordering. Install a scope guard that releases the server before the
future is destroyed, or explicitly release/join before reporting each failure.
##########
be/src/format_v2/parquet/reader/native/column_reader.cpp:
##########
@@ -1347,11 +1354,16 @@ Status ScalarColumnReader<IN_COLLECTION,
OFFSET_INDEX>::read_column_data(
}
const DataTypePtr serde_type = is_dict_filter ? file_type :
materialization_type;
- if (_serde_type != serde_type.get() || _dictionary_index_only !=
is_dict_filter) {
+ const bool serde_type_changed = _serde_type != serde_type.get();
Review Comment:
[P2] Compare logical SerDe types before dropping the retained dictionary
With TIMESTAMPTZ mapping enabled, native schema parsing already creates a
`DataTypeTimeStampTz`; `build_parquet_column_schema()` shares that pointer, but
`apply_timestamp_tz_mapping()` replaces it with a newly allocated, semantically
equal type. The pruning probe publishes the projected pointer, then every
dictionary-ID batch selects the native field pointer here and resets
`_materialization_state`; matched-value materialization switches back and
rebuilds the full typed dictionary, repeating each batch. The direct test
passes one shared type pointer and misses this production path. Compare
semantic type/scale (or avoid remapping an already equal type) and add a
multi-batch TIMESTAMPTZ dictionary-filter regression.
##########
be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp:
##########
@@ -86,6 +86,82 @@ Status validate_uncompressed_page_sizes(const
tparquet::PageHeader& header,
return Status::OK();
}
+Status validate_fixed_width_page_size(const tparquet::PageHeader& header,
int32_t type_length,
+ level_t max_rep_level, level_t
max_def_level,
+ bool schema_is_required) {
+ if (type_length <= 0 || max_rep_level != 0) {
+ return Status::OK();
+ }
+ const bool is_v2 = header.__isset.data_page_header_v2;
+ if (!is_v2 && !header.__isset.data_page_header) {
+ return Status::OK();
+ }
+ const auto encoding =
+ is_v2 ? header.data_page_header_v2.encoding :
header.data_page_header.encoding;
+ if (encoding != tparquet::Encoding::PLAIN &&
+ encoding != tparquet::Encoding::BYTE_STREAM_SPLIT) {
+ return Status::OK();
+ }
+ int32_t num_physical_values = 0;
+ int64_t level_bytes = 0;
+ if (is_v2) {
+ const auto& page = header.data_page_header_v2;
+ if (UNLIKELY(page.num_values < 0 || page.num_nulls < 0 ||
+ page.num_nulls > page.num_values ||
page.repetition_levels_byte_length < 0 ||
+ page.definition_levels_byte_length < 0)) {
+ return Status::Corruption("Parquet data page v2 has invalid value
or level counts");
+ }
+ num_physical_values = page.num_values - page.num_nulls;
+ level_bytes = static_cast<int64_t>(page.repetition_levels_byte_length)
+
+ page.definition_levels_byte_length;
+ } else {
+ if (max_def_level != 0 || !schema_is_required) {
+ return Status::OK();
+ }
+ num_physical_values = header.data_page_header.num_values;
+ }
+ if (level_bytes > std::numeric_limits<int32_t>::max() ||
num_physical_values < 0 ||
+ static_cast<uint64_t>(num_physical_values) >
+ (static_cast<uint64_t>(std::numeric_limits<int32_t>::max()) -
level_bytes) /
+ static_cast<uint32_t>(type_length)) {
+ return Status::Corruption("Parquet fixed-width PLAIN page byte size
overflows");
+ }
+ const int64_t expected = level_bytes +
static_cast<int64_t>(num_physical_values) * type_length;
+ if (UNLIKELY(header.uncompressed_page_size != expected)) {
+ // V2 exposes null and level extents separately, so fixed-width
payload size is known before
+ // decompression even for optional columns and must gate
attacker-controlled allocation.
+ return Status::Corruption("Parquet fixed-width page has {}
uncompressed bytes, expected {}",
+ header.uncompressed_page_size, expected);
+ }
+ return Status::OK();
+}
+
+Status validate_dictionary_page_size(const tparquet::PageHeader& header,
int32_t type_length) {
+ DORIS_CHECK(header.__isset.dictionary_page_header);
+ const int32_t num_values = header.dictionary_page_header.num_values;
+ if (UNLIKELY(num_values < 0 || (num_values == 0 &&
header.uncompressed_page_size != 0))) {
+ // An empty dictionary owns no payload; validate before allocating
from its untrusted size.
+ return Status::Corruption("Parquet dictionary has {} values and {}
uncompressed bytes",
+ num_values, header.uncompressed_page_size);
+ }
+ if (type_length > 0) {
Review Comment:
[P1] Validate variable-width dictionary output before allocating it
`BYTE_ARRAY` keeps `type_length == -1`, so this helper accepts any positive
`uncompressed_page_size`; `_decode_dict_page()` then allocates that advertised
size before reading either the cached or file payload. A one-entry Snappy
dictionary that really expands to a short length-prefixed value can therefore
advertise `INT32_MAX` and force a near-2-GiB allocation before
`GetUncompressedLength` exposes the mismatch. This is distinct from the
existing fixed-width dictionary thread. Preflight the compressed frame's
decoded length before allocation (and require it to match the page header),
with a compressed BYTE_ARRAY regression.
##########
be/src/format_v2/table/remote_doris_reader.cpp:
##########
@@ -99,14 +207,65 @@ class FlightRemoteDorisStream final : public
RemoteDorisStream {
}
private:
+ bool _is_cancelled() const {
+ return (_runtime_state != nullptr && _runtime_state->is_cancelled()) ||
+ (_io_ctx != nullptr && _io_ctx->should_stop);
Review Comment:
[P1] Synchronize the scanner-stop signal read by this watcher
`ScannerContext::stop_scanners()` can call `FileScannerV2::try_stop()` on
the operator thread, which writes `IOContext::should_stop`, while this new
`std::jthread` reads the same field here. `should_stop` is a plain `bool` and
no common lock protects it, so the LIMIT cancellation path has a C++ data race;
the watcher may miss the stop and leave `Next()` blocked until the Flight
deadline. The new blocked-Next test performs the same unsynchronized write.
This is distinct from the earlier missing-propagation thread: make the
lifecycle signal atomic/acquire-release or use a dedicated synchronized stop
token, and exercise that production mechanism in the test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]