airborne12 commented on code in PR #67977:
URL: https://github.com/apache/doris/pull/67977#discussion_r4214410185


##########
be/src/format/table/index_disk_usage_reader.cpp:
##########
@@ -0,0 +1,381 @@
+// 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/table/index_disk_usage_reader.h"
+
+#include <boost/algorithm/string/case_conv.hpp>
+#include <shared_mutex>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <variant>
+
+#include "cloud/cloud_tablet.h"
+#include "common/cast_set.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "runtime/exec_env.h"
+#include "runtime/runtime_state.h"
+#include "storage/index/inverted/inverted_index_desc.h"
+#include "storage/rowset/rowset.h"
+#include "storage/tablet/base_tablet.h"
+#include "storage/tablet/tablet_schema.h"
+
+namespace doris {
+
+using segment_v2::IndexDiskUsageCollector;
+using segment_v2::IndexDiskUsageLevel;
+using segment_v2::IndexDiskUsageRecord;
+using segment_v2::IndexDiskUsageRow;
+using segment_v2::IndexDiskUsageStructure;
+
+namespace {
+
+const std::vector<std::pair<std::string_view, IndexDiskUsageReader::Column>>& 
column_names() {
+    using C = IndexDiskUsageReader::Column;
+    static const std::vector<std::pair<std::string_view, C>> names = {
+            {"PARTITION_NAME", C::kPartitionName}, {"TABLET_ID", C::kTabletId},
+            {"BACKEND_ID", C::kBackendId},         {"ROWSET_ID", C::kRowsetId},
+            {"SEGMENT_ID", C::kSegmentId},         {"INDEX_ID", C::kIndexId},
+            {"INDEX_NAME", C::kIndexName},         {"INDEX_TYPE", 
C::kIndexType},
+            {"COLUMN_NAME", C::kColumnName},       {"INDEX_SUFFIX", 
C::kIndexSuffix},
+            {"STRUCTURE", C::kStructure},          {"STORAGE_FORMAT", 
C::kStorageFormat},
+            {"SEGMENT_COUNT", C::kSegmentCount},   {"ROW_COUNT", C::kRowCount},
+            {"TOTAL_BYTES", C::kTotalBytes},       {"DICT_BYTES", 
C::kDictBytes},
+            {"POSTING_BYTES", C::kPostingBytes},   {"POSITION_BYTES", 
C::kPositionBytes},
+            {"STATS_BYTES", C::kStatsBytes},       {"OTHER_BYTES", 
C::kOtherBytes},
+            {"STATS_SOURCE", C::kStatsSource},
+    };
+    return names;
+}
+
+Result<IndexDiskUsageReader::Column> column_of(const std::string& slot_name) {
+    const std::string upper = boost::to_upper_copy(slot_name);
+    for (const auto& [name, column] : column_names()) {
+        if (upper == name) {
+            return column;
+        }
+    }
+    return ResultError(Status::InternalError("unknown index_disk_usage column 
{}", slot_name));
+}
+
+Result<IndexDiskUsageLevel> parse_level(const std::string& level) {
+    if (level == "tablet") {
+        return IndexDiskUsageLevel::kTablet;
+    }
+    if (level == "rowset") {
+        return IndexDiskUsageLevel::kRowset;
+    }
+    if (level == "segment") {
+        return IndexDiskUsageLevel::kSegment;
+    }
+    return ResultError(Status::InvalidArgument("unsupported index_disk_usage 
level {}", level));
+}
+
+std::string_view structure_name(IndexDiskUsageStructure structure) {
+    switch (structure) {
+    case IndexDiskUsageStructure::kTerm:
+        return "TERM";
+    case IndexDiskUsageStructure::kBkd:
+        return "BKD";
+    case IndexDiskUsageStructure::kAnn:
+        return "ANN";
+    case IndexDiskUsageStructure::kContainer:
+        return "CONTAINER";
+    }
+    return "UNKNOWN";
+}
+
+const TabletIndex* find_index(const TabletSchema& schema, const 
IndexDiskUsageRecord& record) {
+    for (const TabletIndex* index : schema.inverted_and_ann_indexes()) {
+        if (index->index_id() == record.index_id &&
+            index->get_index_suffix() == record.index_suffix) {
+            return index;
+        }
+    }
+    return nullptr;
+}
+
+void insert_null(IColumn* column) {
+    auto& nullable = reinterpret_cast<ColumnNullable&>(*column);
+    nullable.get_nested_column().insert_default();
+    nullable.get_null_map_data().push_back(1);
+}
+
+IColumn* non_null_nested(IColumn* column) {
+    auto& nullable = reinterpret_cast<ColumnNullable&>(*column);
+    nullable.get_null_map_data().push_back(0);
+    return nullable.get_nested_column_ptr().get();
+}
+
+void insert_int64(IColumn* column, int64_t value) {
+    assert_cast<ColumnInt64*>(non_null_nested(column))->insert_value(value);
+}
+
+void insert_int32(IColumn* column, int32_t value) {
+    assert_cast<ColumnInt32*>(non_null_nested(column))->insert_value(value);
+}
+
+void insert_string(IColumn* column, std::string_view value) {
+    
assert_cast<ColumnString*>(non_null_nested(column))->insert_data(value.data(), 
value.size());
+}
+
+} // namespace
+
+IndexDiskUsageReader::IndexDiskUsageReader(std::vector<SlotDescriptor*> slots, 
RuntimeState* state,
+                                           RuntimeProfile* /*profile*/, 
TMetaScanRange scan_range)
+        : _state(state), _slots(std::move(slots)), 
_scan_range(std::move(scan_range)) {}
+
+Status IndexDiskUsageReader::init_reader() {
+    if (!_scan_range.__isset.index_disk_usage_params) {
+        return Status::InvalidArgument("index_disk_usage scan range has no 
parameters");
+    }
+    const TIndexDiskUsageMetadataParams& params = 
_scan_range.index_disk_usage_params;
+    _level = DORIS_TRY(parse_level(params.level));
+    _options.position_detail = params.position_detail;
+    _options.index_ids.insert(params.index_ids.begin(), 
params.index_ids.end());
+    _slot_columns.clear();
+    for (const SlotDescriptor* slot : _slots) {
+        const Column column = DORIS_TRY(column_of(slot->col_name()));
+        _slot_columns.push_back(column);
+    }
+    return Status::OK();
+}
+
+Status IndexDiskUsageReader::_do_get_next_block(Block* block, size_t* 
read_rows, bool* eof) {
+    const auto& tablets = _scan_range.index_disk_usage_params.tablets;
+    *read_rows = 0;
+    while (_next_tablet < tablets.size()) {
+        RETURN_IF_CANCELLED(_state);
+        const TIndexDiskUsageTablet& target = tablets[_next_tablet++];
+        std::vector<IndexDiskUsageRow> rows;
+        TabletSchemaSPtr current_schema;
+        RETURN_IF_ERROR(_collect_tablet(target, &rows, &current_schema));
+        rows = segment_v2::aggregate_index_disk_usage(std::move(rows), _level);
+        if (rows.empty()) {
+            continue;
+        }
+        RETURN_IF_ERROR(_fill_block(block, target, *current_schema, rows));
+        *read_rows = rows.size();
+        *eof = false;
+        return Status::OK();
+    }
+    *eof = true;
+    return Status::OK();
+}
+
+Status IndexDiskUsageReader::_collect_tablet(const TIndexDiskUsageTablet& 
target,
+                                             std::vector<IndexDiskUsageRow>* 
rows,
+                                             TabletSchemaSPtr* current_schema) 
const {
+    BaseTabletSPtr tablet = DORIS_TRY(ExecEnv::get_tablet(target.tablet_id));
+    if (auto cloud_tablet = std::dynamic_pointer_cast<CloudTablet>(tablet)) {
+        SyncOptions options;
+        options.query_version = target.version;
+        RETURN_IF_ERROR(cloud_tablet->sync_rowsets(options));
+    }
+    std::vector<RowsetSharedPtr> rowsets;
+    {
+        std::shared_lock rdlock(tablet->get_header_lock());
+        auto captured = DORIS_TRY(tablet->capture_consistent_rowsets_unlocked(
+                Version(0, target.version), CaptureRowsetOps {}));
+        rowsets = std::move(captured.rowsets);
+    }
+    *current_schema = tablet->tablet_schema();
+
+    for (const RowsetSharedPtr& rowset : rowsets) {
+        const TabletSchemaSPtr schema = rowset->tablet_schema();
+        if (!schema->has_inverted_or_ann_index()) {
+            continue;
+        }
+        const InvertedIndexStorageFormatPB format = 
schema->get_inverted_index_storage_format();
+        const std::string rowset_id = rowset->rowset_id().to_string();
+        for (auto segment : rowset->segments()) {
+            RETURN_IF_CANCELLED(_state);
+            const std::string segment_path = DORIS_TRY(segment.path());
+            IndexDiskUsageCollector collector(
+                    rowset->rowset_meta()->fs(),
+                    
std::string(InvertedIndexDescriptor::get_index_file_path_prefix(segment_path)),
+                    schema, format, target.tablet_id);
+            std::vector<IndexDiskUsageRecord> records;
+            RETURN_IF_ERROR(collector.collect(_options, &records));

Review Comment:
   Fixed in 0b579993e1a. A file that the rowset meta records with a size was 
written, so a missing one is no longer treated as an index the segment skipped. 
The collector now returns the error for a missing V2/V3/SNII container when 
`InvertedIndexFileInfo.index_size` is set, and for a missing V1 file listed 
from `index_info`. A file that is only listed from the schema may still be 
absent. This keeps the legacy `skip_write_index_on_load` case working, because 
that writer writes no index file and records no size for it.
   
   `CollectMissingRecordedContainerFails` (V2, V3, SNII) and 
`CollectMissingRecordedV1IndexFileFails` returned OK with no rows before the 
change and fail the query now. `CollectMissingContainerReportsNothing`, 
`CollectEmptyContainerReportsNothing` and `CollectMissingV1IndexFileSkipsIndex` 
still cover the unrecorded case.
   
   One consequence to be aware of: a recorded V1 file can still be missing on a 
tablet after a cooldown or a disk migration. Both copy the rowset meta 
unchanged but copy V1 files through `_schema->inverted_indexs(*column)` 
(`BetaRowset::upload_to` and `copy_files_to`), which does not name V1 ANN 
files, so those are not copied. Before this change such a tablet silently 
reported fewer bytes; now the query fails with the path of the missing file.
   



##########
be/src/storage/index/index_disk_usage.cpp:
##########
@@ -0,0 +1,304 @@
+// 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 "storage/index/index_disk_usage.h"
+
+#include <algorithm>
+#include <map>
+#include <tuple>
+#include <utility>
+
+#include "common/cast_set.h"
+#include "common/check.h"
+#include "io/io_common.h"
+#include "storage/index/index_file_reader.h"
+#include "storage/index/inverted/inverted_index_desc.h"
+#include "storage/index/snii/format/dict_entry.h"
+#include "storage/index/snii/format/format_constants.h"
+#include "storage/index/snii/reader/logical_index_reader.h"
+
+namespace doris::segment_v2 {
+
+namespace {
+
+bool is_bkd_file(std::string_view name) {
+    return name == 
InvertedIndexDescriptor::get_temporary_bkd_index_data_file_name() ||
+           name == 
InvertedIndexDescriptor::get_temporary_bkd_index_meta_file_name() ||
+           name == 
InvertedIndexDescriptor::get_temporary_bkd_index_file_name();
+}
+
+bool is_wanted(const IndexDiskUsageOptions& options, int64_t index_id) {
+    return options.index_ids.empty() || options.index_ids.contains(index_id);
+}
+
+// Classifies every sub-file of a CLucene directory into `record` and adds 
their total length to
+// `files_bytes`.
+Status add_directory_files(const lucene::store::Directory& dir, 
IndexDiskUsageRecord* record,
+                           int64_t* files_bytes) {
+    try {
+        std::vector<std::string> names;
+        if (!dir.list(&names)) {
+            return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
+                    "failed to list inverted index sub-files");
+        }
+        for (const auto& name : names) {
+            const int64_t length = dir.fileLength(name.c_str());
+            classify_clucene_file(name, length, record);
+            *files_bytes += length;
+        }
+    } catch (CLuceneError& e) {
+        return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
+                "failed to read inverted index sub-files: {}", e.what());
+    }
+    return Status::OK();
+}
+
+// Sums the position bytes of the dictionary entries whose postings live in 
the posting region.
+// Inline postings stay in the dictionary region and are not counted.
+Status sum_snii_position_bytes(const IndexFileReader& reader, uint64_t 
index_id,
+                               std::string_view suffix, int64_t* 
position_bytes) {
+    // A full dictionary scan should not evict blocks that queries keep in the 
file cache.
+    io::IOContext io_ctx;
+    io_ctx.is_disposable = true;
+    io_ctx.is_inverted_index = true;
+    auto logical = DORIS_TRY(reader.open_snii_logical_index(
+            index_id, suffix, &io_ctx, 
snii::reader::LogicalIndexOpenMode::kCompaction));
+    snii_doris::DorisSniiFileReader::ScopedIOContext io_context_scope(&io_ctx);
+    std::vector<snii::format::DictEntry> entries;
+    for (uint32_t block = 0; block < logical->n_dict_blocks(); ++block) {
+        uint64_t frq_base = 0;
+        uint64_t prx_base = 0;
+        RETURN_IF_ERROR(logical->decode_dict_block(block, &entries, &frq_base, 
&prx_base));
+        for (const auto& entry : entries) {
+            if (entry.kind == snii::format::DictEntryKind::kPodRef) {
+                *position_bytes += cast_set<int64_t>(entry.prx_len);
+            }
+        }
+    }
+    return Status::OK();
+}
+
+int64_t merge_component(int64_t lhs, int64_t rhs) {
+    return lhs < 0 || rhs < 0 ? -1 : lhs + rhs;
+}
+
+} // namespace
+
+std::vector<IndexDiskUsageRow> 
aggregate_index_disk_usage(std::vector<IndexDiskUsageRow> rows,
+                                                          IndexDiskUsageLevel 
level) {
+    if (level == IndexDiskUsageLevel::kSegment) {
+        return rows;
+    }
+    using Key = std::tuple<std::string, int64_t, std::string, int, int>;
+    std::map<Key, size_t> positions;
+    std::vector<IndexDiskUsageRow> merged;
+    for (auto& row : rows) {
+        row.segment_id = -1;
+        if (level == IndexDiskUsageLevel::kTablet) {
+            row.rowset_id.clear();
+        }
+        Key key {row.rowset_id, row.record.index_id, row.record.index_suffix,
+                 static_cast<int>(row.format), 
static_cast<int>(row.record.structure)};
+        auto [it, inserted] = positions.emplace(std::move(key), merged.size());
+        if (inserted) {
+            merged.push_back(std::move(row));
+            continue;
+        }
+        IndexDiskUsageRow& target = merged[it->second];
+        target.segment_count += row.segment_count;
+        target.row_count += row.row_count;
+        IndexDiskUsageRecord& dst = target.record;
+        const IndexDiskUsageRecord& src = row.record;
+        dst.total_bytes += src.total_bytes;
+        dst.dict_bytes = merge_component(dst.dict_bytes, src.dict_bytes);
+        dst.posting_bytes = merge_component(dst.posting_bytes, 
src.posting_bytes);
+        dst.position_bytes = merge_component(dst.position_bytes, 
src.position_bytes);
+        dst.stats_bytes = merge_component(dst.stats_bytes, src.stats_bytes);
+        dst.other_bytes = merge_component(dst.other_bytes, src.other_bytes);
+    }
+    return merged;
+}
+
+void classify_clucene_file(std::string_view name, int64_t length, 
IndexDiskUsageRecord* record) {
+    record->total_bytes += length;
+    if (is_bkd_file(name)) {
+        record->structure = IndexDiskUsageStructure::kBkd;
+        return;
+    }
+    const size_t dot = name.rfind('.');
+    const std::string_view extension =
+            dot == std::string_view::npos ? std::string_view() : 
name.substr(dot + 1);
+    if (extension == "tis" || extension == "tii") {
+        record->dict_bytes += length;
+    } else if (extension == "frq") {
+        record->posting_bytes += length;
+    } else if (extension == "prx") {
+        record->position_bytes += length;
+    } else if (extension == "nrm") {
+        record->stats_bytes += length;
+    } else {
+        record->other_bytes += length;
+    }
+}
+
+IndexDiskUsageCollector::IndexDiskUsageCollector(io::FileSystemSPtr fs,
+                                                 std::string index_path_prefix,
+                                                 TabletSchemaSPtr schema,
+                                                 InvertedIndexStorageFormatPB 
format,
+                                                 int64_t tablet_id)
+        : _fs(std::move(fs)),
+          _index_path_prefix(std::move(index_path_prefix)),
+          _schema(std::move(schema)),
+          _format(format),
+          _tablet_id(tablet_id) {}
+
+Status IndexDiskUsageCollector::collect(const IndexDiskUsageOptions& options,
+                                        std::vector<IndexDiskUsageRecord>* 
out) {
+    DORIS_CHECK(out != nullptr);
+    switch (_format) {
+    case InvertedIndexStorageFormatPB::V1:
+        return _collect_v1(options, out);
+    case InvertedIndexStorageFormatPB::V2:
+    case InvertedIndexStorageFormatPB::V3:
+        return _collect_compound(options, out);
+    case InvertedIndexStorageFormatPB::SNII:
+        return _collect_snii(options, out);
+    default:
+        return Status::NotSupported("index disk usage does not support 
inverted index format {}",
+                                    
InvertedIndexStorageFormatPB_Name(_format));
+    }
+}
+
+Status IndexDiskUsageCollector::_collect_v1(const IndexDiskUsageOptions& 
options,
+                                            std::vector<IndexDiskUsageRecord>* 
out) {
+    IndexFileReader reader(_fs, _index_path_prefix, _format, 
InvertedIndexFileInfo(), _tablet_id);
+    RETURN_IF_ERROR(reader.init());
+    for (const TabletIndex* index : _schema->inverted_indexes()) {

Review Comment:
   Fixed in 0b579993e1a. The fallback for a V1 rowset without `index_info` now 
lists `inverted_and_ann_indexes()` of the rowset schema, so an ANN-only segment 
reports its ANN rows and a mixed segment reports both. When `index_info` exists 
it already lists every written file, and the ANN structure is recognized from 
the `ann.faiss` / `ann.ivfdata` sub-file names.
   
   `CollectV1AnnIndexWithoutFileInfo` covers the ANN-only and the mixed layout; 
it found no ANN record before the change and passes now.
   



-- 
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]

Reply via email to