-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[fix](be) Harden FileScannerV2 schema and reader edge cases #65548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -358,11 +358,16 @@ Status build_node_schema_with_mode(const ::parquet::SchemaDescriptor& schema, | |
| } | ||
| column_schema->type_descriptor = resolve_parquet_type(column_schema->descriptor); | ||
| column_schema->type = column_schema->type_descriptor.doris_type; | ||
| if (column_schema->type == nullptr && | ||
| !column_schema->type_descriptor.unsupported_reason.empty()) { | ||
| // Keep unsupported logical leaves in the file schema using their physical storage | ||
| // type. For example, a file `{id: INT32, clock: TIME_MILLIS}` remains readable for | ||
| // `SELECT id`: schema mapping sees `clock` as its physical INT32 but never creates its | ||
| // reader. `SELECT clock` still fails explicitly in ParquetColumnReaderFactory before | ||
| // any physical value is decoded, preserving the unsupported-type contract. | ||
| column_schema->type = column_schema->type_descriptor.physical_doris_type; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fallback keeps unsupported TIME leaves in the schema as their physical type, but the rejection now happens only if a |
||
| } | ||
| if (column_schema->type == nullptr) { | ||
| if (!column_schema->type_descriptor.unsupported_reason.empty()) { | ||
| return Status::NotSupported("Unsupported parquet column '{}': {}", node.name(), | ||
| column_schema->type_descriptor.unsupported_reason); | ||
| } | ||
| return Status::NotSupported("Unsupported parquet column type for column {}", | ||
| node.name()); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,6 @@ | |
| #include <limits> | ||
| #include <mutex> | ||
| #include <string_view> | ||
| #include <unordered_map> | ||
| #include <utility> | ||
|
|
||
| #include "common/check.h" | ||
|
|
@@ -45,6 +44,75 @@ namespace doris::format::parquet { | |
|
|
||
| namespace detail { | ||
|
|
||
| namespace { | ||
|
|
||
| bool page_cache_range_less(const ParquetPageCacheRange& lhs, const ParquetPageCacheRange& rhs) { | ||
| return lhs.offset < rhs.offset || (lhs.offset == rhs.offset && lhs.size < rhs.size); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| ParquetPageCacheRangeIndex::ParquetPageCacheRangeIndex(size_t max_ranges) | ||
| : _max_ranges(max_ranges) { | ||
| DORIS_CHECK(_max_ranges > 0); | ||
| } | ||
|
|
||
| void ParquetPageCacheRangeIndex::insert(ParquetPageCacheRange range) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The old global directory had a per-file range cap, but the replacement |
||
| std::lock_guard lock(_mutex); | ||
| auto it = std::lower_bound(_ranges.begin(), _ranges.end(), range, page_cache_range_less); | ||
| if (it != _ranges.end() && it->offset == range.offset && it->size == range.size) { | ||
| return; | ||
| } | ||
| if (_ranges.size() >= _max_ranges) { | ||
| _ranges.erase(_ranges.begin()); | ||
| it = std::lower_bound(_ranges.begin(), _ranges.end(), range, page_cache_range_less); | ||
| } | ||
| _ranges.insert(it, range); | ||
| } | ||
|
|
||
| void ParquetPageCacheRangeIndex::erase(ParquetPageCacheRange range) { | ||
| std::lock_guard lock(_mutex); | ||
| const auto it = std::lower_bound(_ranges.begin(), _ranges.end(), range, page_cache_range_less); | ||
| if (it != _ranges.end() && it->offset == range.offset && it->size == range.size) { | ||
| _ranges.erase(it); | ||
| } | ||
| } | ||
|
|
||
| std::vector<ParquetPageCacheRange> ParquetPageCacheRangeIndex::ranges() const { | ||
| std::lock_guard lock(_mutex); | ||
| return _ranges; | ||
| } | ||
|
|
||
| size_t ParquetPageCacheRangeIndex::size() const { | ||
| std::lock_guard lock(_mutex); | ||
| return _ranges.size(); | ||
| } | ||
|
|
||
| ParquetPageCacheRangeDirectory::ParquetPageCacheRangeDirectory(size_t max_files) | ||
| : _max_files(max_files) { | ||
| DORIS_CHECK(_max_files > 0); | ||
| } | ||
|
|
||
| std::shared_ptr<ParquetPageCacheRangeIndex> ParquetPageCacheRangeDirectory::get_or_create( | ||
| const std::string& file_key) { | ||
| DORIS_CHECK(!file_key.empty()); | ||
| std::lock_guard lock(_mutex); | ||
| if (const auto it = _indexes.find(file_key); it != _indexes.end()) { | ||
| return it->second; | ||
| } | ||
| if (_indexes.size() >= _max_files) { | ||
| _indexes.erase(_indexes.begin()); | ||
| } | ||
| auto index = std::make_shared<ParquetPageCacheRangeIndex>(); | ||
| _indexes.emplace(file_key, index); | ||
| return index; | ||
| } | ||
|
|
||
| size_t ParquetPageCacheRangeDirectory::size() const { | ||
| std::lock_guard lock(_mutex); | ||
| return _indexes.size(); | ||
| } | ||
|
|
||
| std::vector<ParquetPageCacheReadPlanEntry> plan_page_cache_range_read( | ||
| int64_t position, int64_t nbytes, const std::vector<ParquetPageCacheRange>& cached_ranges) { | ||
| if (position < 0 || nbytes <= 0) { | ||
|
|
@@ -133,59 +201,14 @@ bool should_use_merge_range_reader(const std::vector<ParquetPageCacheRange>& ran | |
|
|
||
| namespace { | ||
|
|
||
| // StoragePageCache only supports exact-key lookup. Keep lightweight range metadata here so later | ||
| // Arrow ReadAt requests can reuse cached bytes when their requested ranges are subsets of, or are | ||
| // fully covered by, previously cached ranges. Stale metadata is pruned on lookup. | ||
| std::mutex cached_page_range_index_mutex; | ||
| std::unordered_map<std::string, std::vector<ParquetPageCacheRange>> cached_page_range_index; | ||
| constexpr size_t MAX_CACHED_PAGE_RANGE_FILES = 4096; | ||
| constexpr size_t MAX_CACHED_PAGE_RANGES_PER_FILE = 65536; | ||
|
|
||
| void register_cached_page_range(const std::string& file_key, int64_t position, int64_t nbytes) { | ||
| DORIS_CHECK(nbytes > 0); | ||
| std::lock_guard lock(cached_page_range_index_mutex); | ||
| if (cached_page_range_index.find(file_key) == cached_page_range_index.end() && | ||
| cached_page_range_index.size() >= MAX_CACHED_PAGE_RANGE_FILES) { | ||
| cached_page_range_index.erase(cached_page_range_index.begin()); | ||
| } | ||
| auto& ranges = cached_page_range_index[file_key]; | ||
| auto it = std::find_if(ranges.begin(), ranges.end(), [&](const ParquetPageCacheRange& range) { | ||
| return range.offset == position && range.size == nbytes; | ||
| }); | ||
| if (it == ranges.end()) { | ||
| if (ranges.size() >= MAX_CACHED_PAGE_RANGES_PER_FILE) { | ||
| ranges.erase(ranges.begin()); | ||
| } | ||
| ranges.push_back(ParquetPageCacheRange {position, nbytes}); | ||
| } | ||
| } | ||
|
|
||
| void unregister_cached_page_range(const std::string& file_key, | ||
| const ParquetPageCacheRange& stale_range) { | ||
| std::lock_guard lock(cached_page_range_index_mutex); | ||
| auto it = cached_page_range_index.find(file_key); | ||
| if (it == cached_page_range_index.end()) { | ||
| return; | ||
| } | ||
| auto& ranges = it->second; | ||
| ranges.erase(std::remove_if(ranges.begin(), ranges.end(), | ||
| [&](const ParquetPageCacheRange& range) { | ||
| return range.offset == stale_range.offset && | ||
| range.size == stale_range.size; | ||
| }), | ||
| ranges.end()); | ||
| if (ranges.empty()) { | ||
| cached_page_range_index.erase(it); | ||
| } | ||
| } | ||
|
|
||
| std::vector<ParquetPageCacheRange> cached_page_ranges_for_file(const std::string& file_key) { | ||
| std::lock_guard lock(cached_page_range_index_mutex); | ||
| auto it = cached_page_range_index.find(file_key); | ||
| if (it == cached_page_range_index.end()) { | ||
| return {}; | ||
| } | ||
| return it->second; | ||
| detail::ParquetPageCacheRangeDirectory& cached_page_range_directory() { | ||
| // Directory lookup is paid once when a reader opens. ReadAt() then synchronizes only on the | ||
| // shared index for this file, so unrelated Parquet files no longer serialize on a process-wide | ||
| // hot-path mutex. Strong references deliberately keep range discovery alive after reader A | ||
| // closes: reader B can reuse cached [100, 200) for a request [120, 150). The directory and each | ||
| // per-file index are independently capped, bounding stale metadata left by page-cache eviction. | ||
| static detail::ParquetPageCacheRangeDirectory directory; | ||
| return directory; | ||
| } | ||
|
|
||
| std::string build_page_cache_file_key(const io::FileReader& file_reader, | ||
|
|
@@ -213,7 +236,11 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| _base_file_reader(_file_reader), | ||
| _io_ctx(io_ctx), | ||
| _enable_page_cache(enable_page_cache), | ||
| _page_cache_file_key(std::move(page_cache_file_key)) { | ||
| _page_cache_file_key(std::move(page_cache_file_key)), | ||
| _cached_page_range_index( | ||
| _enable_page_cache && !_page_cache_file_key.empty() | ||
| ? cached_page_range_directory().get_or_create(_page_cache_file_key) | ||
| : nullptr) { | ||
| DORIS_CHECK(_file_reader != nullptr); | ||
| if (auto tracing_reader = std::dynamic_pointer_cast<io::TracingFileReader>(_file_reader)) { | ||
| _file_reader_stats = tracing_reader->stats(); | ||
|
|
@@ -226,6 +253,10 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| arrow::Status Close() override { | ||
| if (!_closed) { | ||
| collect_active_merge_range_profile(); | ||
| std::lock_guard lock(_page_cache_mutex); | ||
| // Page payloads and their bounded per-file range index intentionally outlive this | ||
| // reader for warm scans. Only reader-specific projected ranges are released here. | ||
| std::vector<ParquetPageCacheRange>().swap(_page_cache_ranges); | ||
| _closed = true; | ||
| } | ||
| return arrow::Status::OK(); | ||
|
|
@@ -363,7 +394,8 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| private: | ||
| bool page_cache_enabled() const { | ||
| return _enable_page_cache && !config::disable_storage_page_cache && | ||
| StoragePageCache::instance() != nullptr && !_page_cache_file_key.empty(); | ||
| StoragePageCache::instance() != nullptr && !_page_cache_file_key.empty() && | ||
| _cached_page_range_index != nullptr; | ||
| } | ||
|
|
||
| bool range_in_page_cache_scope(int64_t position, int64_t nbytes) const { | ||
|
|
@@ -391,7 +423,7 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| if (!StoragePageCache::instance()->lookup( | ||
| page_cache_key(cached_range.offset, cached_range.size), &handle, | ||
| segment_v2::DATA_PAGE)) { | ||
| unregister_cached_page_range(_page_cache_file_key, cached_range); | ||
| _cached_page_range_index->erase(cached_range); | ||
| return false; | ||
| } | ||
| Slice cached = handle.data(); | ||
|
|
@@ -404,8 +436,8 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| } | ||
|
|
||
| bool try_read_from_cached_ranges(int64_t position, int64_t nbytes, void* out) { | ||
| auto plan = detail::plan_page_cache_range_read( | ||
| position, nbytes, cached_page_ranges_for_file(_page_cache_file_key)); | ||
| auto plan = detail::plan_page_cache_range_read(position, nbytes, | ||
| _cached_page_range_index->ranges()); | ||
| if (plan.empty()) { | ||
| return false; | ||
| } | ||
|
|
@@ -456,7 +488,8 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| PageCacheHandle handle; | ||
| StoragePageCache::instance()->insert(page_cache_key(position, nbytes), page, &handle, | ||
| segment_v2::DATA_PAGE); | ||
| register_cached_page_range(_page_cache_file_key, position, nbytes); | ||
| _cached_page_range_index->insert( | ||
| ParquetPageCacheRange {.offset = position, .size = nbytes}); | ||
| ++_page_cache_stats.write_count; | ||
| ++_page_cache_stats.compressed_write_count; | ||
| } | ||
|
|
@@ -514,6 +547,7 @@ class DorisRandomAccessFile final : public arrow::io::RandomAccessFile { | |
| std::string _page_cache_file_key; | ||
| mutable std::mutex _page_cache_mutex; | ||
| std::vector<ParquetPageCacheRange> _page_cache_ranges; | ||
| std::shared_ptr<detail::ParquetPageCacheRangeIndex> _cached_page_range_index; | ||
| ParquetPageCacheStats _page_cache_stats; | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still treats too many EOF statuses as valid empty splits. A Native file that has a valid header followed by an explicit zero-length first block is malformed:
NativeReader::_read_next_pblock()consumes the block length, setseofwhenblock_len == 0at file end, and_ensure_schema_loaded()turns the empty buffer intoStatus::EndOfFile. The existingRejectsZeroLengthBlockAndInvalidPBlockunit builds exactlyheader + uint64(0)and expectsget_schema()to fail. When TableReader lazily opens that reader fromget_block(), this branch would instead abort the split, incrementEmptyFileNum, and continue as if the file were a valid header-only empty file. Please make the Native reader return a hard error for an explicit zero-length block, or have the scanner skip only a dedicated empty-file status that cannot also represent malformed input.