This is an automated email from the ASF dual-hosted git repository.
lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new df007728 perf(read): reduce metadata round trips during scan planning
(#338)
df007728 is described below
commit df007728e468b49805a314484ed8cd3b1a3c66ff
Author: Yonghao Fang <[email protected]>
AuthorDate: Tue Sep 15 13:16:42 2026 +0800
perf(read): reduce metadata round trips during scan planning (#338)
---
src/paimon/core/index/index_file_handler.cpp | 4 +-
.../core/manifest/index_manifest_file_handler.cpp | 4 +-
.../manifest/index_manifest_file_handler_test.cpp | 18 ++--
src/paimon/core/manifest/manifest_file.cpp | 5 +-
src/paimon/core/manifest/manifest_file.h | 6 ++
src/paimon/core/manifest/manifest_file_test.cpp | 89 +++++++++++++---
src/paimon/core/manifest/manifest_list.h | 9 +-
src/paimon/core/manifest/manifest_list_test.cpp | 115 ++++++++++++++++++++-
.../core/operation/commit/commit_scanner.cpp | 8 +-
src/paimon/core/operation/expire_snapshots.cpp | 13 ++-
.../core/operation/file_store_commit_impl.cpp | 6 +-
.../core/operation/file_store_commit_impl_test.cpp | 21 ++--
src/paimon/core/operation/file_store_scan.cpp | 39 +++++--
src/paimon/core/operation/manifest_file_merger.cpp | 4 +-
.../core/operation/manifest_file_merger_test.cpp | 4 +-
.../core/operation/orphan_files_cleaner_impl.cpp | 12 ++-
src/paimon/core/snapshot_file_scan.cpp | 6 +-
src/paimon/core/snapshot_file_scan_test.cpp | 67 ++++++++++++
.../core/table/system/global_system_tables.cpp | 5 +-
.../core/table/system/metadata_system_tables.cpp | 4 +-
src/paimon/core/utils/file_utils.cpp | 19 ++--
src/paimon/core/utils/objects_file.h | 57 +++++++---
src/paimon/fs/jindo/jindo_file_system.cpp | 15 ++-
src/paimon/fs/jindo/jindo_file_system_test.cpp | 16 +++
24 files changed, 452 insertions(+), 94 deletions(-)
diff --git a/src/paimon/core/index/index_file_handler.cpp
b/src/paimon/core/index/index_file_handler.cpp
index 9b45e748..d614d85e 100644
--- a/src/paimon/core/index/index_file_handler.cpp
+++ b/src/paimon/core/index/index_file_handler.cpp
@@ -55,8 +55,8 @@ Result<std::vector<IndexManifestEntry>>
IndexFileHandler::Scan(
return std::vector<IndexManifestEntry>();
}
std::vector<IndexManifestEntry> index_entries;
- PAIMON_RETURN_NOT_OK(
- index_manifest_file_->Read(index_manifest.value(), filter,
&index_entries));
+ PAIMON_RETURN_NOT_OK(index_manifest_file_->Read(index_manifest.value(),
filter,
+
/*file_size=*/std::nullopt, &index_entries));
return index_entries;
}
diff --git a/src/paimon/core/manifest/index_manifest_file_handler.cpp
b/src/paimon/core/manifest/index_manifest_file_handler.cpp
index 41543729..55261de9 100644
--- a/src/paimon/core/manifest/index_manifest_file_handler.cpp
+++ b/src/paimon/core/manifest/index_manifest_file_handler.cpp
@@ -18,6 +18,7 @@
#include "paimon/core/manifest/index_manifest_file_handler.h"
+#include <optional>
#include <set>
#include <string>
#include <unordered_map>
@@ -201,7 +202,8 @@ Result<std::string> IndexManifestFileHandler::Write(
std::vector<IndexManifestEntry> entries;
if (previous_index_manifest != std::nullopt) {
PAIMON_RETURN_NOT_OK(index_manifest_file->Read(previous_index_manifest.value(),
- /*filter=*/nullptr,
&entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &entries));
}
for (const auto& entry : entries) {
if (!(entry.kind == FileKind::Add())) {
diff --git a/src/paimon/core/manifest/index_manifest_file_handler_test.cpp
b/src/paimon/core/manifest/index_manifest_file_handler_test.cpp
index 9661697c..42048dc7 100644
--- a/src/paimon/core/manifest/index_manifest_file_handler_test.cpp
+++ b/src/paimon/core/manifest/index_manifest_file_handler_test.cpp
@@ -123,7 +123,8 @@ TEST_F(IndexManifestFileHandlerTest,
GlobalCombinerDeletesThenAddsByFileName) {
/*bucket_mode=*/4,
index_manifest_file.get()));
std::vector<IndexManifestEntry> written_entries;
- ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
&written_entries));
+ ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&written_entries));
ASSERT_EQ(written_entries.size(), 1);
ASSERT_EQ(written_entries[0].index_file->FileName(), "global-0");
ASSERT_EQ(written_entries[0].index_file->RowCount(), 2);
@@ -156,7 +157,8 @@ TEST_F(IndexManifestFileHandlerTest,
BucketedCombinerUsesPartitionBucketAndIndex
/*bucket_mode=*/2,
index_manifest_file.get()));
std::vector<IndexManifestEntry> written_entries;
- ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
&written_entries));
+ ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&written_entries));
ASSERT_EQ(written_entries.size(), 2);
bool found_bucket0 = false;
@@ -192,7 +194,8 @@ TEST_F(IndexManifestFileHandlerTest,
BucketedCombinerOverwritesDuplicateAddedEnt
/*bucket_mode=*/2, index_manifest_file.get()));
std::vector<IndexManifestEntry> written_entries;
- ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
&written_entries));
+ ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&written_entries));
ASSERT_EQ(written_entries.size(), 1);
ASSERT_EQ(written_entries[0].index_file->FileName(), "dv-0-new");
ASSERT_EQ(written_entries[0].index_file->RowCount(), 20);
@@ -213,7 +216,8 @@ TEST_F(IndexManifestFileHandlerTest,
GlobalCombinerOverwritesDuplicateAddedEntri
/*bucket_mode=*/4, index_manifest_file.get()));
std::vector<IndexManifestEntry> written_entries;
- ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
&written_entries));
+ ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&written_entries));
ASSERT_EQ(written_entries.size(), 1);
ASSERT_EQ(written_entries[0].index_file->FileName(), "global-0");
ASSERT_EQ(written_entries[0].index_file->RowCount(), 20);
@@ -257,7 +261,8 @@ TEST_F(IndexManifestFileHandlerTest,
GlobalDvCombinerReplacesIndexFileInBucketUn
/*bucket_mode=*/-1,
index_manifest_file.get()));
std::vector<IndexManifestEntry> written_entries;
- ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
&written_entries));
+ ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&written_entries));
ASSERT_EQ(written_entries.size(), 2);
std::set<std::string> written_file_names;
for (const auto& entry : written_entries) {
@@ -278,7 +283,8 @@ TEST_F(IndexManifestFileHandlerTest,
GlobalDvCombinerReplacesIndexFileInBucketUn
/*bucket_mode=*/-1,
index_manifest_file.get()));
written_entries.clear();
- ASSERT_OK(index_manifest_file->Read(reordered_manifest,
/*filter=*/nullptr, &written_entries));
+ ASSERT_OK(index_manifest_file->Read(reordered_manifest, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&written_entries));
ASSERT_EQ(written_entries.size(), 2);
written_file_names.clear();
for (const auto& entry : written_entries) {
diff --git a/src/paimon/core/manifest/manifest_file.cpp
b/src/paimon/core/manifest/manifest_file.cpp
index 0fdb78d7..ecbcb6bd 100644
--- a/src/paimon/core/manifest/manifest_file.cpp
+++ b/src/paimon/core/manifest/manifest_file.cpp
@@ -20,6 +20,7 @@
#include <cassert>
#include <limits>
+#include <optional>
#include <utility>
#include "arrow/c/abi.h"
@@ -90,6 +91,7 @@ Result<std::unique_ptr<ManifestFile>> ManifestFile::Create(
}
Status ManifestFile::ReadBucketEntries(const std::string& file_name, int32_t
bucket,
+ std::optional<int64_t> file_size,
std::vector<ManifestEntry>* entries)
const {
return ReadArrowBatches(
file_name,
@@ -106,7 +108,8 @@ Status ManifestFile::ReadBucketEntries(const std::string&
file_name, int32_t buc
entries->push_back(std::move(entry));
}
return Status::OK();
- });
+ },
+ file_size);
}
Result<std::vector<ManifestFileMeta>> ManifestFile::Write(
diff --git a/src/paimon/core/manifest/manifest_file.h
b/src/paimon/core/manifest/manifest_file.h
index 528bb7bf..2f3a60cb 100644
--- a/src/paimon/core/manifest/manifest_file.h
+++ b/src/paimon/core/manifest/manifest_file.h
@@ -20,6 +20,7 @@
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
#include <vector>
@@ -63,7 +64,12 @@ class ManifestFile : public ObjectsFile<ManifestEntry> {
Result<std::vector<ManifestFileMeta>> Write(const
std::vector<ManifestEntry>& entries);
/// Read a manifest file and deserialize only entries for the specified
bucket.
+ ///
+ /// @param file_size Length of the manifest when the caller already has it
from the manifest
+ /// list, which saves the read a metadata request on a
remote store. Pass
+ /// std::nullopt when the length is not known.
Status ReadBucketEntries(const std::string& file_name, int32_t bucket,
+ std::optional<int64_t> file_size,
std::vector<ManifestEntry>* entries) const;
private:
diff --git a/src/paimon/core/manifest/manifest_file_test.cpp
b/src/paimon/core/manifest/manifest_file_test.cpp
index 5cd264aa..0ce9cc72 100644
--- a/src/paimon/core/manifest/manifest_file_test.cpp
+++ b/src/paimon/core/manifest/manifest_file_test.cpp
@@ -55,6 +55,13 @@ class CountingFileSystem : public FileSystem {
return local_.Open(path);
}
+ /// Overridden because the base implementation forwards to `Open(path)`,
which would fold the
+ /// two ways of opening into one counter and hide whether a known length
reached the store.
+ Result<std::unique_ptr<InputStream>> Open(const FileStatus& file_status)
const override {
+ opened_lengths.push_back(file_status.GetLen());
+ return local_.Open(file_status.GetPath());
+ }
+
Result<std::unique_ptr<OutputStream>> Create(const std::string& path,
bool overwrite) const
override {
return local_.Create(path, overwrite);
@@ -93,6 +100,7 @@ class CountingFileSystem : public FileSystem {
mutable int open_count = 0;
mutable int get_file_status_count = 0;
+ mutable std::vector<int64_t> opened_lengths;
private:
LocalFileSystem local_;
@@ -124,10 +132,12 @@ class ManifestFileTest : public testing::Test {
/*target_file_size=*/1024, pool, options,
unused_schema));
std::vector<ManifestEntry> manifest_entries;
if (bucket) {
- EXPECT_OK(
- manifest_file->ReadBucketEntries(file_name, bucket.value(),
&manifest_entries));
+ EXPECT_OK(manifest_file->ReadBucketEntries(file_name,
bucket.value(),
+
/*file_size=*/std::nullopt,
+ &manifest_entries));
} else {
- EXPECT_OK(manifest_file->Read(file_name, /*filter=*/nullptr,
&manifest_entries));
+ EXPECT_OK(manifest_file->Read(file_name, /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&manifest_entries));
}
return manifest_entries;
@@ -263,7 +273,7 @@ TEST_F(ManifestFileTest,
TestManifestCacheIsDisabledWithoutInjectedCache) {
std::vector<ManifestEntry> first_read;
ASSERT_OK(manifest_file->Read("manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
- /*filter=*/nullptr, &first_read));
+ /*filter=*/nullptr,
/*file_size=*/std::nullopt, &first_read));
ASSERT_EQ(5, first_read.size());
ASSERT_EQ(1, counting_file_system->open_count);
ASSERT_EQ(0, counting_file_system->get_file_status_count);
@@ -272,7 +282,7 @@ TEST_F(ManifestFileTest,
TestManifestCacheIsDisabledWithoutInjectedCache) {
ASSERT_OK(manifest_file->Read(
"manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
[](const ManifestEntry& entry) -> Result<bool> { return entry.Kind()
== FileKind::Add(); },
- &filtered_read));
+ /*file_size=*/std::nullopt, &filtered_read));
ASSERT_EQ(1, filtered_read.size());
ASSERT_EQ(2, counting_file_system->open_count);
ASSERT_EQ(0, counting_file_system->get_file_status_count);
@@ -305,10 +315,10 @@ TEST_F(ManifestFileTest,
TestManifestCacheReusesCachedBytes) {
std::vector<ManifestEntry> first_read;
ASSERT_OK(manifest_file->Read("manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
- /*filter=*/nullptr, &first_read));
+ /*filter=*/nullptr,
/*file_size=*/std::nullopt, &first_read));
std::vector<ManifestEntry> second_read;
ASSERT_OK(manifest_file->Read("manifest-3ea5ee21-d399-4f1c-a749-2fc63dbf0852-1",
- /*filter=*/nullptr, &second_read));
+ /*filter=*/nullptr,
/*file_size=*/std::nullopt, &second_read));
ASSERT_EQ(first_read, second_read);
ASSERT_EQ(1, counting_file_system->open_count);
@@ -345,20 +355,24 @@ TEST_F(ManifestFileTest,
TestReadBucketEntriesMaterializesOnlySelectedBucket) {
const std::string manifest_name =
"manifest-3a44a0da-1008-463c-914e-28d271375e24-0";
std::vector<ManifestEntry> all_entries;
- ASSERT_OK(manifest_file->Read(manifest_name, /*filter=*/nullptr,
&all_entries));
+ ASSERT_OK(manifest_file->Read(manifest_name, /*filter=*/nullptr,
/*file_size=*/std::nullopt,
+ &all_entries));
ASSERT_EQ(2, all_entries.size());
std::vector<ManifestEntry> bucket_one_entries;
- ASSERT_OK(manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/1,
&bucket_one_entries));
+ ASSERT_OK(manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/1,
+ /*file_size=*/std::nullopt,
&bucket_one_entries));
ASSERT_EQ(std::vector<ManifestEntry>({all_entries[0]}),
bucket_one_entries);
std::vector<ManifestEntry> bucket_zero_entries;
- ASSERT_OK(manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/0,
&bucket_zero_entries));
+ ASSERT_OK(manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/0,
+ /*file_size=*/std::nullopt,
&bucket_zero_entries));
ASSERT_EQ(std::vector<ManifestEntry>({all_entries[1]}),
bucket_zero_entries);
std::vector<ManifestEntry> missing_bucket_entries;
- ASSERT_OK(
- manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/2,
&missing_bucket_entries));
+ ASSERT_OK(manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/2,
+ /*file_size=*/std::nullopt,
+ &missing_bucket_entries));
ASSERT_TRUE(missing_bucket_entries.empty());
ASSERT_EQ(1, counting_file_system->open_count);
@@ -366,6 +380,51 @@ TEST_F(ManifestFileTest,
TestReadBucketEntriesMaterializesOnlySelectedBucket) {
ASSERT_EQ(1, manifest_cache->SupplierCallCount());
}
+// A scan reads manifests whose length the manifest list already recorded.
Handing that length over
+// is what lets the store skip the metadata request a bare open issues, which
on a remote store is
+// a round trip paid before any of the file is read.
+TEST_F(ManifestFileTest, TestReadPassesKnownSizeToOpen) {
+ auto pool = GetDefaultPool();
+ auto counting_file_system = std::make_shared<CountingFileSystem>();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FileFormat> file_format,
+ FileFormatFactory::Get("orc", {}));
+ std::string root_path = paimon::test::GetDataDir() +
"/orc/append_09.db/append_09";
+ auto unused_schema = arrow::schema(arrow::FieldVector({arrow::field("f0",
arrow::utf8())}));
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<FileStorePathFactory> path_factory,
+ FileStorePathFactory::Create(root_path, unused_schema,
/*partition_keys=*/{},
+ /*default_part_value=*/"",
file_format->Identifier(),
+ /*data_file_prefix=*/"data-",
+ /*legacy_partition_name_enabled=*/true,
/*external_paths=*/{},
+
/*global_index_external_path=*/std::nullopt,
+ /*index_file_in_data_file_dir=*/false,
pool));
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<ManifestFile> manifest_file,
+ ManifestFile::Create(counting_file_system, file_format, "zstd",
path_factory,
+ /*target_file_size=*/1024, pool, options,
unused_schema));
+
+ const std::string manifest_name =
"manifest-3a44a0da-1008-463c-914e-28d271375e24-0";
+ // The length the checked-in manifest list records for this manifest, and
the length the file
+ // on disk actually has.
+ constexpr int64_t kRecordedSize = 2617;
+
+ std::vector<ManifestEntry> all_entries;
+ ASSERT_OK(manifest_file->Read(manifest_name, /*filter=*/nullptr,
kRecordedSize, &all_entries));
+ ASSERT_EQ(2, all_entries.size());
+ ASSERT_EQ(std::vector<int64_t>({kRecordedSize}),
counting_file_system->opened_lengths);
+ ASSERT_EQ(0, counting_file_system->open_count);
+
+ std::vector<ManifestEntry> bucket_one_entries;
+ ASSERT_OK(manifest_file->ReadBucketEntries(manifest_name, /*bucket=*/1,
kRecordedSize,
+ &bucket_one_entries));
+ ASSERT_EQ(std::vector<ManifestEntry>({all_entries[0]}),
bucket_one_entries);
+ ASSERT_EQ(std::vector<int64_t>({kRecordedSize, kRecordedSize}),
+ counting_file_system->opened_lengths);
+ ASSERT_EQ(0, counting_file_system->open_count);
+}
+
TEST_F(ManifestFileTest, TestReadBucketEntriesSkipsDeserializingOtherBuckets) {
auto pool = GetDefaultPool();
std::vector<ManifestEntry> source_entries =
@@ -406,11 +465,13 @@ TEST_F(ManifestFileTest,
TestReadBucketEntriesSkipsDeserializingOtherBuckets) {
manifest_file->WriteWithoutRolling({invalid_other_bucket,
valid_target_bucket}));
std::vector<ManifestEntry> all_entries;
- ASSERT_NOK_WITH_MSG(manifest_file->Read(written_file.first,
/*filter=*/nullptr, &all_entries),
+ ASSERT_NOK_WITH_MSG(manifest_file->Read(written_file.first,
/*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&all_entries),
"Unsupported byte value 2 for file kind.");
std::vector<ManifestEntry> bucket_entries;
- ASSERT_OK(manifest_file->ReadBucketEntries(written_file.first,
/*bucket=*/0, &bucket_entries));
+ ASSERT_OK(manifest_file->ReadBucketEntries(written_file.first,
/*bucket=*/0,
+ /*file_size=*/std::nullopt,
&bucket_entries));
ASSERT_EQ(std::vector<ManifestEntry>({valid_target_bucket}),
bucket_entries);
}
diff --git a/src/paimon/core/manifest/manifest_list.h
b/src/paimon/core/manifest/manifest_list.h
index 6bf64e8e..918d0491 100644
--- a/src/paimon/core/manifest/manifest_list.h
+++ b/src/paimon/core/manifest/manifest_list.h
@@ -90,7 +90,8 @@ class ManifestList : public ObjectsFile<ManifestFileMeta> {
/// @return Status indicating whether the operation was successful or not.
Status ReadBaseManifests(const Snapshot& snapshot,
std::vector<ManifestFileMeta>* manifests) const {
- return Read(snapshot.BaseManifestList(), /*filter=*/nullptr,
manifests);
+ return Read(snapshot.BaseManifestList(), /*filter=*/nullptr,
+ snapshot.BaseManifestListSize(), manifests);
}
/// Return a `ManifestFileMeta` for each delta manifest in this snapshot.
@@ -100,7 +101,8 @@ class ManifestList : public ObjectsFile<ManifestFileMeta> {
/// @return Status indicating whether the operation was successful or not.
Status ReadDeltaManifests(const Snapshot& snapshot,
std::vector<ManifestFileMeta>* manifests) const {
- return Read(snapshot.DeltaManifestList(), /*filter=*/nullptr,
manifests);
+ return Read(snapshot.DeltaManifestList(), /*filter=*/nullptr,
+ snapshot.DeltaManifestListSize(), manifests);
}
/// Return a `ManifestFileMeta` for each changelog manifest in this
snapshot.
@@ -113,7 +115,8 @@ class ManifestList : public ObjectsFile<ManifestFileMeta> {
const std::optional<std::string>& changelog_manifest_list =
snapshot.ChangelogManifestList();
if (changelog_manifest_list) {
- return Read(changelog_manifest_list.value(), /*filter=*/nullptr,
manifests);
+ return Read(changelog_manifest_list.value(), /*filter=*/nullptr,
+ snapshot.ChangelogManifestListSize(), manifests);
} else {
return Status::OK();
}
diff --git a/src/paimon/core/manifest/manifest_list_test.cpp
b/src/paimon/core/manifest/manifest_list_test.cpp
index 97b420cf..d8d25538 100644
--- a/src/paimon/core/manifest/manifest_list_test.cpp
+++ b/src/paimon/core/manifest/manifest_list_test.cpp
@@ -19,6 +19,7 @@
#include "paimon/core/manifest/manifest_list.h"
#include <map>
+#include <optional>
#include <variant>
#include "arrow/type.h"
@@ -36,12 +37,67 @@
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
+
+namespace {
+
+/// Records which `Open` overload a read took. A length that planning already
knows should reach
+/// the file system instead of being rediscovered on open, which on a remote
store costs a round
+/// trip of its own before a single byte is read. `Open(FileStatus)` has to be
overridden here:
+/// the base implementation forwards to `Open(path)`, which would leave the
two paths
+/// indistinguishable.
+class OpenRecordingFileSystem : public LocalFileSystem {
+ public:
+ Result<std::unique_ptr<InputStream>> Open(const std::string& path) const
override {
+ ++open_without_length_count;
+ return LocalFileSystem::Open(path);
+ }
+
+ Result<std::unique_ptr<InputStream>> Open(const FileStatus& file_status)
const override {
+ opened_lengths.push_back(file_status.GetLen());
+ return LocalFileSystem::Open(file_status.GetPath());
+ }
+
+ mutable int open_without_length_count = 0;
+ mutable std::vector<int64_t> opened_lengths;
+};
+
+/// Builds a snapshot carrying manifest list sizes the way a commit records
them, or leaving them
+/// unset the way a snapshot written before those fields existed would.
+Snapshot MakeSnapshot(const std::string& base_list, const
std::optional<int64_t>& base_size,
+ const std::string& delta_list, const
std::optional<int64_t>& delta_size) {
+ return Snapshot(
+ /*id=*/1, /*schema_id=*/0, /*base_manifest_list=*/base_list,
+ /*base_manifest_list_size=*/base_size,
/*delta_manifest_list=*/delta_list,
+ /*delta_manifest_list_size=*/delta_size,
/*changelog_manifest_list=*/std::nullopt,
+ /*changelog_manifest_list_size=*/std::nullopt,
/*index_manifest=*/std::nullopt,
+ /*commit_user=*/"user", /*commit_identifier=*/1,
Snapshot::CommitKind::Append(),
+ /*time_millis=*/0, /*total_record_count=*/1, /*delta_record_count=*/1,
+ /*changelog_record_count=*/1, /*watermark=*/std::nullopt,
/*statistics=*/std::nullopt,
+ /*properties=*/std::nullopt, /*next_row_id=*/std::nullopt);
+}
+
+ManifestFileMeta MakeMeta(const std::string& name, int64_t file_size, int64_t
num_added_files,
+ int64_t num_deleted_files) {
+ return ManifestFileMeta(name, file_size, num_added_files,
num_deleted_files,
+ SimpleStats::EmptyStats(), /*schema_id=*/0,
/*min_bucket=*/0,
+ /*max_bucket=*/0, /*min_level=*/0, /*max_level=*/0,
+ /*min_row_id=*/std::nullopt,
/*max_row_id=*/std::nullopt);
+}
+
+} // namespace
+
class ManifestListTest : public testing::Test {
public:
std::unique_ptr<ManifestList> CreateManifestList(
const std::string& file_format_str, const std::string& root_path,
const std::shared_ptr<MemoryPool>& pool) const {
- std::shared_ptr<FileSystem> file_system =
std::make_shared<LocalFileSystem>();
+ return CreateManifestList(std::make_shared<LocalFileSystem>(),
file_format_str, root_path,
+ pool);
+ }
+
+ std::unique_ptr<ManifestList> CreateManifestList(
+ const std::shared_ptr<FileSystem>& file_system, const std::string&
file_format_str,
+ const std::string& root_path, const std::shared_ptr<MemoryPool>& pool)
const {
EXPECT_OK_AND_ASSIGN(std::shared_ptr<FileFormat> file_format,
FileFormatFactory::Get(file_format_str, {}));
auto unused_schema =
arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));
@@ -65,7 +121,8 @@ class ManifestListTest : public testing::Test {
const std::string& file_name, const std::shared_ptr<MemoryPool>& pool)
const {
auto manifest_list = CreateManifestList(file_format_str, root_path,
pool);
std::vector<ManifestFileMeta> manifest_file_metas;
- EXPECT_OK(manifest_list->Read(file_name, /*filter=*/nullptr,
&manifest_file_metas));
+ EXPECT_OK(manifest_list->Read(file_name, /*filter=*/nullptr,
/*file_size=*/std::nullopt,
+ &manifest_file_metas));
return manifest_file_metas;
}
};
@@ -178,6 +235,60 @@ TEST_F(ManifestListTest, TestReadChangelogManifests) {
ASSERT_EQ(std::vector<ManifestFileMeta>({expected_meta}), actual_metas);
}
+TEST_F(ManifestListTest, TestReadDataManifestsOpensWithSizeFromSnapshot) {
+ auto pool = GetDefaultPool();
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto fs = std::make_shared<OpenRecordingFileSystem>();
+ auto manifest_list = CreateManifestList(fs, "avro", dir->Str(), pool);
+ ManifestFileMeta base_meta = MakeMeta("manifest-base", /*file_size=*/100,
/*num_added_files=*/1,
+ /*num_deleted_files=*/0);
+ ManifestFileMeta delta_meta =
+ MakeMeta("manifest-delta", /*file_size=*/200, /*num_added_files=*/2,
+ /*num_deleted_files=*/1);
+ ASSERT_OK_AND_ASSIGN(auto base_list, manifest_list->Write({base_meta}));
+ ASSERT_OK_AND_ASSIGN(auto delta_list, manifest_list->Write({base_meta,
delta_meta}));
+ // Writing only creates files, so everything recorded from here on comes
from the reads.
+ ASSERT_EQ(fs->open_without_length_count, 0);
+ ASSERT_TRUE(fs->opened_lengths.empty());
+
+ Snapshot snapshot =
+ MakeSnapshot(base_list.first, base_list.second, delta_list.first,
delta_list.second);
+ std::vector<ManifestFileMeta> actual_metas;
+ ASSERT_OK(manifest_list->ReadDataManifests(snapshot, &actual_metas));
+ ASSERT_EQ(std::vector<ManifestFileMeta>({base_meta, base_meta,
delta_meta}), actual_metas);
+ // Each list was opened with the length the snapshot carried for it, so
neither read had to ask
+ // the store how long the file is.
+ ASSERT_EQ(std::vector<int64_t>({base_list.second, delta_list.second}),
fs->opened_lengths);
+ ASSERT_EQ(fs->open_without_length_count, 0);
+}
+
+// The sizes are optional in the snapshot format, so a snapshot written before
they were recorded
+// has to keep reading. This is what makes handing the size over an
optimization rather than a new
+// requirement on the metadata.
+TEST_F(ManifestListTest, TestReadDataManifestsWithoutSizesStillReads) {
+ auto pool = GetDefaultPool();
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto fs = std::make_shared<OpenRecordingFileSystem>();
+ auto manifest_list = CreateManifestList(fs, "avro", dir->Str(), pool);
+ ManifestFileMeta base_meta = MakeMeta("manifest-base", /*file_size=*/100,
/*num_added_files=*/1,
+ /*num_deleted_files=*/0);
+ ManifestFileMeta delta_meta =
+ MakeMeta("manifest-delta", /*file_size=*/200, /*num_added_files=*/2,
+ /*num_deleted_files=*/1);
+ ASSERT_OK_AND_ASSIGN(auto base_list, manifest_list->Write({base_meta}));
+ ASSERT_OK_AND_ASSIGN(auto delta_list, manifest_list->Write({base_meta,
delta_meta}));
+
+ Snapshot snapshot = MakeSnapshot(base_list.first,
/*base_size=*/std::nullopt, delta_list.first,
+ /*delta_size=*/std::nullopt);
+ std::vector<ManifestFileMeta> actual_metas;
+ ASSERT_OK(manifest_list->ReadDataManifests(snapshot, &actual_metas));
+ ASSERT_EQ(std::vector<ManifestFileMeta>({base_meta, base_meta,
delta_meta}), actual_metas);
+ ASSERT_TRUE(fs->opened_lengths.empty());
+ ASSERT_EQ(fs->open_without_length_count, 2);
+}
+
TEST_F(ManifestListTest, TestManifestListCompatibleWithJavaPaimon09) {
auto pool = GetDefaultPool();
auto manifest_file_metas = ReadManifestFileMeta("avro",
paimon::test::GetDataDir() + "/avro",
diff --git a/src/paimon/core/operation/commit/commit_scanner.cpp
b/src/paimon/core/operation/commit/commit_scanner.cpp
index 448bd7e4..1a14ac2c 100644
--- a/src/paimon/core/operation/commit/commit_scanner.cpp
+++ b/src/paimon/core/operation/commit/commit_scanner.cpp
@@ -118,8 +118,8 @@ Result<std::vector<ManifestEntry>>
CommitScanner::ReadIncrementalEntries(
std::vector<ManifestEntry> incremental_entries;
for (const ManifestFileMeta& manifest_meta : delta_manifests) {
std::vector<ManifestEntry> manifest_entries;
- PAIMON_RETURN_NOT_OK(
- manifest_file_->Read(manifest_meta.FileName(), /*filter=*/nullptr,
&manifest_entries));
+ PAIMON_RETURN_NOT_OK(manifest_file_->Read(manifest_meta.FileName(),
/*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&manifest_entries));
for (const ManifestEntry& entry : manifest_entries) {
if (changed_partition_set.find(entry.Partition()) !=
changed_partition_set.end()) {
const bool drop_stats =
core_options_.ManifestDeleteFileDropStats() &&
@@ -194,8 +194,8 @@ Result<std::vector<IndexManifestEntry>>
CommitScanner::ReadAllIndexEntriesFromPa
return false;
};
- PAIMON_RETURN_NOT_OK(
- index_manifest_file_->Read(snapshot.IndexManifest().value(), filter,
&index_entries));
+
PAIMON_RETURN_NOT_OK(index_manifest_file_->Read(snapshot.IndexManifest().value(),
filter,
+
/*file_size=*/std::nullopt, &index_entries));
return index_entries;
}
diff --git a/src/paimon/core/operation/expire_snapshots.cpp
b/src/paimon/core/operation/expire_snapshots.cpp
index a7103e65..9915efd4 100644
--- a/src/paimon/core/operation/expire_snapshots.cpp
+++ b/src/paimon/core/operation/expire_snapshots.cpp
@@ -306,7 +306,8 @@ bool ExpireSnapshots::TryDeleteEmptyDirectory(const
std::string& path) const {
Status ExpireSnapshots::CleanUnusedManifests(const std::string&
manifest_list_name,
const std::set<std::string>&
skipping_sets) {
std::vector<ManifestFileMeta> manifest_file_metas;
- auto status = manifest_list_->Read(manifest_list_name, nullptr,
&manifest_file_metas);
+ auto status = manifest_list_->Read(manifest_list_name, nullptr,
/*file_size=*/std::nullopt,
+ &manifest_file_metas);
if (status.ok()) {
std::vector<std::string> to_delete_manifests;
// TODO(jinli.zjw): optimize for async
@@ -326,13 +327,14 @@ Status ExpireSnapshots::CleanUnusedDataFiles(
const std::string& manifest_list_name, const std::set<std::string>&
skipping_data_files,
DataFilePathFactoryCache* data_file_path_factory_cache) {
std::vector<ManifestFileMeta> manifest_file_metas;
- auto status = manifest_list_->Read(manifest_list_name, nullptr,
&manifest_file_metas);
+ auto status = manifest_list_->Read(manifest_list_name, nullptr,
/*file_size=*/std::nullopt,
+ &manifest_file_metas);
if (status.ok()) {
std::map<std::string, ManifestEntry> data_files_to_delete;
for (const auto& manifest_file_meta : manifest_file_metas) {
std::vector<ManifestEntry> manifest_entries;
- auto status =
- manifest_file_->Read(manifest_file_meta.FileName(), nullptr,
&manifest_entries);
+ auto status = manifest_file_->Read(manifest_file_meta.FileName(),
nullptr,
+ /*file_size=*/std::nullopt,
&manifest_entries);
if (!status.ok()) {
// cancel deletion if any exception occurs
PAIMON_LOG_WARN(logger_, "Failed to read some manifest files.
Cancel deletion. %s",
@@ -408,7 +410,8 @@ Status ExpireSnapshots::GetDataFileSkippingSet(
LinkedHashMap<FileEntry::Identifier, ManifestEntry> boundary_entries;
for (const ManifestFileMeta& manifest : manifests) {
std::vector<ManifestEntry> entries;
- PAIMON_RETURN_NOT_OK(manifest_file_->Read(manifest.FileName(),
nullptr, &entries));
+ PAIMON_RETURN_NOT_OK(manifest_file_->Read(manifest.FileName(),
nullptr,
+
/*file_size=*/std::nullopt, &entries));
if (i == 0) {
PAIMON_RETURN_NOT_OK(FileEntry::MergeEntries(entries,
&boundary_entries));
} else {
diff --git a/src/paimon/core/operation/file_store_commit_impl.cpp
b/src/paimon/core/operation/file_store_commit_impl.cpp
index 3fa60e98..06a1eba4 100644
--- a/src/paimon/core/operation/file_store_commit_impl.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl.cpp
@@ -25,6 +25,7 @@
#include <future>
#include <limits>
#include <list>
+#include <optional>
#include <set>
#include <unordered_map>
#include <unordered_set>
@@ -253,7 +254,8 @@ Result<std::vector<ManifestEntry>>
FileStoreCommitImpl::ReadAddManifestEntries(
for (const auto& meta : data_manifests) {
std::vector<ManifestEntry> entries;
PAIMON_RETURN_NOT_OK(manifest_file_->Read(
- meta.FileName(), [](const ManifestEntry&) -> Result<bool> { return
true; }, &entries));
+ meta.FileName(), [](const ManifestEntry&) -> Result<bool> { return
true; },
+ /*file_size=*/std::nullopt, &entries));
unmerged_entries.insert(unmerged_entries.end(),
std::make_move_iterator(entries.begin()),
std::make_move_iterator(entries.end()));
}
@@ -1133,7 +1135,7 @@ Result<std::optional<int64_t>>
FileStoreCommitImpl::MaxSequenceNumber(
std::vector<ManifestEntry> entries;
PAIMON_RETURN_NOT_OK(manifest_file_->Read(
manifest.FileName(), [](const ManifestEntry&) -> Result<bool> {
return true; },
- &entries));
+ /*file_size=*/std::nullopt, &entries));
std::optional<int64_t> current_max =
SequenceSnapshotProperties::MaxSequenceNumberFromFiles(entries);
if (current_max) {
diff --git a/src/paimon/core/operation/file_store_commit_impl_test.cpp
b/src/paimon/core/operation/file_store_commit_impl_test.cpp
index dfada944..b6710ba8 100644
--- a/src/paimon/core/operation/file_store_commit_impl_test.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl_test.cpp
@@ -23,6 +23,7 @@
#include <cstddef>
#include <filesystem>
#include <iostream>
+#include <optional>
#include <set>
#include <tuple>
#include <utility>
@@ -3372,7 +3373,8 @@ TEST_F(FileStoreCommitImplTest, TestCommitWithIndexFiles)
{
std::vector<IndexManifestEntry> index_entries;
ASSERT_OK(commit_impl->index_manifest_file_->Read(snapshot.IndexManifest().value(),
- /*filter=*/nullptr,
&index_entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &index_entries));
ASSERT_EQ(1u, index_entries.size());
ASSERT_EQ("bitmap-index-commit-1",
index_entries[0].index_file->FileName());
}
@@ -3436,7 +3438,8 @@ TEST_F(FileStoreCommitImplTest,
TestCommitWithCompactIndexFiles) {
std::vector<IndexManifestEntry> index_entries;
ASSERT_OK(commit_impl->index_manifest_file_->Read(snapshot.IndexManifest().value(),
- /*filter=*/nullptr,
&index_entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &index_entries));
ASSERT_EQ(1u, index_entries.size());
ASSERT_EQ("bitmap-index-commit-compact-1",
index_entries[0].index_file->FileName());
}
@@ -3474,7 +3477,8 @@ TEST_F(FileStoreCommitImplTest,
TestCommitWithDeletedIndexFiles) {
std::vector<IndexManifestEntry> index_entries;
ASSERT_OK(commit_impl->index_manifest_file_->Read(snapshot.IndexManifest().value(),
- /*filter=*/nullptr,
&index_entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &index_entries));
ASSERT_TRUE(index_entries.empty());
}
@@ -3513,7 +3517,8 @@ TEST_F(FileStoreCommitImplTest,
TestCommitWithCompactDeletedIndexFiles) {
std::vector<IndexManifestEntry> index_entries;
ASSERT_OK(commit_impl->index_manifest_file_->Read(snapshot.IndexManifest().value(),
- /*filter=*/nullptr,
&index_entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &index_entries));
ASSERT_TRUE(index_entries.empty());
}
@@ -3545,7 +3550,8 @@ TEST_F(FileStoreCommitImplTest,
TestOverwriteWithCompactIndexFiles) {
std::vector<IndexManifestEntry> index_entries;
ASSERT_OK(commit_impl->index_manifest_file_->Read(compact_snapshot.IndexManifest().value(),
- /*filter=*/nullptr,
&index_entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &index_entries));
ASSERT_EQ(1u, index_entries.size());
ASSERT_EQ("bitmap-index-compact-1",
index_entries[0].index_file->FileName());
}
@@ -3638,7 +3644,8 @@ TEST_F(FileStoreCommitImplTest,
TestFilterAndOverwriteWithCompactIndexFiles) {
std::vector<IndexManifestEntry> index_entries;
ASSERT_OK(commit_impl->index_manifest_file_->Read(compact_snapshot.IndexManifest().value(),
- /*filter=*/nullptr,
&index_entries));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &index_entries));
ASSERT_EQ(1u, index_entries.size());
ASSERT_EQ("bitmap-index-filter-compact-1",
index_entries[0].index_file->FileName());
}
@@ -3946,7 +3953,7 @@ TEST_F(FileStoreCommitImplTest,
TestOverwriteDropsDeleteFileStats) {
std::vector<ManifestEntry> delta_entries;
for (const ManifestFileMeta& manifest : delta_manifests) {
ASSERT_OK(commit_impl->manifest_file_->Read(manifest.FileName(),
/*filter=*/nullptr,
- &delta_entries));
+
/*file_size=*/std::nullopt, &delta_entries));
}
int32_t add_count = 0;
diff --git a/src/paimon/core/operation/file_store_scan.cpp
b/src/paimon/core/operation/file_store_scan.cpp
index 2b182107..89c137f3 100644
--- a/src/paimon/core/operation/file_store_scan.cpp
+++ b/src/paimon/core/operation/file_store_scan.cpp
@@ -276,8 +276,33 @@ Status
FileStoreScan::ReadManifests(std::optional<Snapshot>* snapshot_ptr,
Status FileStoreScan::ReadManifestsWithSnapshot(const Snapshot& snapshot,
std::vector<ManifestFileMeta>*
manifests) const {
switch (scan_mode_) {
- case ScanMode::ALL:
- return manifest_list_->ReadDataManifests(snapshot, manifests);
+ case ScanMode::ALL: {
+ // The base and the delta manifest list are two independent files
and neither read
+ // depends on the other, so issue both together instead of paying
the two metadata
+ // round trips one after the other. The result keeps the
base-then-delta order that
+ // ReadDataManifests produced.
+ auto read_list = [this, &snapshot](bool base) ->
Result<std::vector<ManifestFileMeta>> {
+ std::vector<ManifestFileMeta> metas;
+ PAIMON_RETURN_NOT_OK(base ?
manifest_list_->ReadBaseManifests(snapshot, &metas)
+ :
manifest_list_->ReadDeltaManifests(snapshot, &metas));
+ return metas;
+ };
+ std::vector<std::future<Result<std::vector<ManifestFileMeta>>>>
futures;
+ futures.reserve(2);
+ futures.push_back(
+ Via(executor_.get(), [read_list]() { return
read_list(/*base=*/true); }));
+ futures.push_back(
+ Via(executor_.get(), [read_list]() { return
read_list(/*base=*/false); }));
+ for (auto& metas : CollectAll(futures)) {
+ if (!metas.ok()) {
+ return metas.status();
+ }
+ for (auto& meta : metas.value()) {
+ manifests->emplace_back(std::move(meta));
+ }
+ }
+ return Status::OK();
+ }
case ScanMode::DELTA:
return manifest_list_->ReadDeltaManifests(snapshot, manifests);
case ScanMode::CHANGELOG:
@@ -299,8 +324,8 @@ Status FileStoreScan::ReadFileEntries(const
std::vector<ManifestFileMeta>& manif
if (apply_scan_filter) {
PAIMON_RETURN_NOT_OK(ReadManifestFileMeta(meta, &tmp_entries));
} else {
- PAIMON_RETURN_NOT_OK(
- manifest_file_->Read(meta.FileName(), /*filter=*/nullptr,
&tmp_entries));
+ PAIMON_RETURN_NOT_OK(manifest_file_->Read(meta.FileName(),
/*filter=*/nullptr,
+ meta.FileSize(),
&tmp_entries));
}
return tmp_entries;
};
@@ -429,8 +454,8 @@ Status FileStoreScan::ReadAndMergeBucketFileEntries(
for (const auto& meta : manifest_metas) {
auto read_meta_task = [this, meta, bucket]() ->
Result<std::vector<ManifestEntry>> {
std::vector<ManifestEntry> bucket_entries;
- PAIMON_RETURN_NOT_OK(
- manifest_file_->ReadBucketEntries(meta.FileName(), bucket,
&bucket_entries));
+ PAIMON_RETURN_NOT_OK(manifest_file_->ReadBucketEntries(
+ meta.FileName(), bucket, meta.FileSize(),
&bucket_entries));
return bucket_entries;
};
futures.push_back(Via(executor_.get(), read_meta_task));
@@ -551,7 +576,7 @@ Status FileStoreScan::ReadManifestFileMeta(const
ManifestFileMeta& manifest,
PAIMON_RETURN_NOT_OK(manifest_file_->Read(
manifest.FileName(),
[this](const ManifestEntry& entry) -> Result<bool> { return
FilterManifestEntry(entry); },
- &unfiltered_entries));
+ manifest.FileSize(), &unfiltered_entries));
entries->reserve(entries->size() + unfiltered_entries.size());
for (auto& entry : unfiltered_entries) {
entries->emplace_back(std::move(entry));
diff --git a/src/paimon/core/operation/manifest_file_merger.cpp
b/src/paimon/core/operation/manifest_file_merger.cpp
index aa308e09..f268e5cd 100644
--- a/src/paimon/core/operation/manifest_file_merger.cpp
+++ b/src/paimon/core/operation/manifest_file_merger.cpp
@@ -22,6 +22,7 @@
#include <cstddef>
#include <functional>
#include <list>
+#include <optional>
#include <string>
#include <utility>
@@ -175,7 +176,8 @@ Result<std::vector<ManifestFileMeta>>
ManifestFileMerger::MergeEntries(
}
std::vector<ManifestEntry> entries;
for (const auto& meta : metas) {
- PAIMON_RETURN_NOT_OK(manifest_file->Read(meta.FileName(),
/*filter=*/nullptr, &entries));
+ PAIMON_RETURN_NOT_OK(manifest_file->Read(meta.FileName(),
/*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&entries));
}
std::vector<ManifestEntry> result;
PAIMON_RETURN_NOT_OK(FileEntry::MergeEntries<ManifestEntry>(entries,
&result));
diff --git a/src/paimon/core/operation/manifest_file_merger_test.cpp
b/src/paimon/core/operation/manifest_file_merger_test.cpp
index 89a78a5a..af0830aa 100644
--- a/src/paimon/core/operation/manifest_file_merger_test.cpp
+++ b/src/paimon/core/operation/manifest_file_merger_test.cpp
@@ -23,6 +23,7 @@
#include <functional>
#include <limits>
#include <map>
+#include <optional>
#include <set>
#include <string>
#include <utility>
@@ -158,7 +159,8 @@ class ManifestFileMergerTest : public testing::Test {
const std::vector<std::pair<std::string, FileKind>>&
entry_file_name_expected) {
std::vector<ManifestEntry> entries;
for (const auto& meta : metas) {
- ASSERT_OK(manifest_file_->Read(meta.FileName(),
/*filter=*/nullptr, &entries));
+ ASSERT_OK(manifest_file_->Read(meta.FileName(), /*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&entries));
}
std::vector<std::pair<std::string, FileKind>> entry_file_name_actual;
for (const auto& entry : entries) {
diff --git a/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
b/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
index f42708c8..0af493ae 100644
--- a/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
+++ b/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
@@ -318,15 +318,19 @@ Result<std::set<std::string>>
OrphanFilesCleanerImpl::GetUsedFilesBySnapshot(
}
std::vector<ManifestFileMeta> manifests;
PAIMON_RETURN_NOT_OK(manifest_list_->ReadIfFileExist(snapshot.BaseManifestList(),
- /*filter=*/nullptr,
&manifests));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &manifests));
PAIMON_RETURN_NOT_OK(manifest_list_->ReadIfFileExist(snapshot.DeltaManifestList(),
- /*filter=*/nullptr,
&manifests));
+ /*filter=*/nullptr,
+
/*file_size=*/std::nullopt, &manifests));
for (const auto& manifest : manifests) {
used_files.insert(manifest.FileName());
std::vector<ManifestEntry> manifest_entries;
- PAIMON_RETURN_NOT_OK(manifest_file_->ReadIfFileExist(
- manifest.FileName(), /*filter=*/nullptr, &manifest_entries));
+
PAIMON_RETURN_NOT_OK(manifest_file_->ReadIfFileExist(manifest.FileName(),
+
/*filter=*/nullptr,
+
/*file_size=*/std::nullopt,
+
&manifest_entries));
for (const auto& manifest_entry : manifest_entries) {
used_files.insert(manifest_entry.FileName());
for (const std::optional<std::string>& extra_file :
diff --git a/src/paimon/core/snapshot_file_scan.cpp
b/src/paimon/core/snapshot_file_scan.cpp
index 700f2028..9d80ec86 100644
--- a/src/paimon/core/snapshot_file_scan.cpp
+++ b/src/paimon/core/snapshot_file_scan.cpp
@@ -184,10 +184,10 @@ class SnapshotFileCollector {
std::vector<ManifestEntry> entries;
if (bucket_id_) {
PAIMON_RETURN_NOT_OK(manifest_file_->ReadBucketEntries(
- meta.FileName(), bucket_id_.value(), &entries));
+ meta.FileName(), bucket_id_.value(), meta.FileSize(),
&entries));
} else {
- PAIMON_RETURN_NOT_OK(
- manifest_file_->Read(meta.FileName(),
/*filter=*/nullptr, &entries));
+ PAIMON_RETURN_NOT_OK(manifest_file_->Read(meta.FileName(),
/*filter=*/nullptr,
+ meta.FileSize(),
&entries));
}
PAIMON_RETURN_NOT_OK(ApplyPartitionFilter(&entries));
return ManifestReadResult{meta, std::move(entries)};
diff --git a/src/paimon/core/snapshot_file_scan_test.cpp
b/src/paimon/core/snapshot_file_scan_test.cpp
index 751b39aa..e6629fbd 100644
--- a/src/paimon/core/snapshot_file_scan_test.cpp
+++ b/src/paimon/core/snapshot_file_scan_test.cpp
@@ -23,14 +23,18 @@
#include <initializer_list>
#include <map>
#include <memory>
+#include <mutex>
#include <optional>
#include <set>
#include <string>
+#include <utility>
#include <vector>
#include "gtest/gtest.h"
+#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/string_utils.h"
#include "paimon/defs.h"
+#include "paimon/fs/local/local_file_system.h"
#include "paimon/predicate/predicate_builder.h"
#include "paimon/scan_context.h"
#include "paimon/testing/utils/testharness.h"
@@ -63,6 +67,34 @@ std::set<std::string> ExpectedFiles(const std::string&
table_path,
return paths;
}
+/// Records how each file was opened, so a scan can be shown to hand over the
lengths that planning
+/// already has instead of leaving the store to answer them again.
`Open(FileStatus)` has to be
+/// overridden here: the base implementation forwards to `Open(path)`, which
would erase the
+/// difference between the two.
+class OpenRecordingFileSystem : public LocalFileSystem {
+ public:
+ Result<std::unique_ptr<InputStream>> Open(const std::string& path) const
override {
+ {
+ std::lock_guard<std::mutex> lock(opens_mutex);
+ opens.emplace_back(path, std::nullopt);
+ }
+ return LocalFileSystem::Open(path);
+ }
+
+ Result<std::unique_ptr<InputStream>> Open(const FileStatus& file_status)
const override {
+ {
+ std::lock_guard<std::mutex> lock(opens_mutex);
+ opens.emplace_back(file_status.GetPath(), file_status.GetLen());
+ }
+ return LocalFileSystem::Open(file_status.GetPath());
+ }
+
+ // A scan reads manifests concurrently through the executor, so recording
the opens has to be
+ // synchronized; the reads of `opens` happen only after the scan has
collected every task.
+ mutable std::mutex opens_mutex;
+ mutable std::vector<std::pair<std::string, std::optional<int64_t>>> opens;
+};
+
} // namespace
TEST(SnapshotFileScanTest, TestLatestSnapshotAndBucketFilter) {
@@ -314,4 +346,39 @@ TEST(SnapshotFileScanTest, TestInvalidArguments) {
"snapshot file scan does not support predicate
filter");
}
+// The metadata that leads a scan to a manifest already records how long that
manifest is, in the
+// manifest list for a manifest and in the snapshot for a manifest list.
Handing those lengths over
+// is what saves a metadata round trip per file on a remote store, and it is
the whole point of the
+// size fields being carried through planning.
+TEST(SnapshotFileScanTest, TestListFilesOpensManifestsWithKnownLength) {
+ std::string table_path = GetDataDir() + "/orc/append_09.db/append_09";
+ auto file_system = std::make_shared<OpenRecordingFileSystem>();
+
+ ASSERT_OK_AND_ASSIGN(
+ std::set<std::string> all_files,
+ SnapshotFileScan::ListFiles(table_path, /*branch=*/"main",
/*snapshot_id=*/std::nullopt,
+ /*scan_filter=*/nullptr, /*options=*/{},
file_system,
+ /*executor=*/nullptr,
/*memory_pool=*/nullptr));
+ ASSERT_FALSE(all_files.empty());
+
+ // Every manifest the scan read has to be opened with the length the
manifest list recorded for
+ // it. The two manifest lists are not held to that here: this snapshot was
written before the
+ // size fields existed, so they fall back to discovering their own length,
and
+ // ManifestListTest.TestReadDataManifests* covers both sides of that
choice.
+ int manifest_opens = 0;
+ for (const auto& [path, length] : file_system->opens) {
+ const std::string name = PathUtil::GetName(path);
+ if (name.rfind("manifest-list-", 0) == 0) {
+ continue;
+ }
+ if (path.find("/manifest/") == std::string::npos) {
+ continue;
+ }
+ ++manifest_opens;
+ ASSERT_TRUE(length.has_value()) << "opened without a known length: "
<< path;
+ }
+ // The latest snapshot of this table references five manifests.
+ ASSERT_EQ(5, manifest_opens);
+}
+
} // namespace paimon::test
diff --git a/src/paimon/core/table/system/global_system_tables.cpp
b/src/paimon/core/table/system/global_system_tables.cpp
index 68ec9ac0..bd484edc 100644
--- a/src/paimon/core/table/system/global_system_tables.cpp
+++ b/src/paimon/core/table/system/global_system_tables.cpp
@@ -22,6 +22,7 @@
#include <functional>
#include <map>
#include <memory>
+#include <optional>
#include <set>
#include <string>
#include <utility>
@@ -230,8 +231,8 @@ Result<AggregatedFileStats> AggregateFileStats(const
std::shared_ptr<FileSystem>
std::vector<ManifestEntry> entries;
for (const auto& manifest : manifests) {
- PAIMON_RETURN_NOT_OK(
- manifest_file->Read(manifest.FileName(), /*filter=*/nullptr,
&entries));
+ PAIMON_RETURN_NOT_OK(manifest_file->Read(manifest.FileName(),
/*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&entries));
}
std::vector<ManifestEntry> merged_entries;
diff --git a/src/paimon/core/table/system/metadata_system_tables.cpp
b/src/paimon/core/table/system/metadata_system_tables.cpp
index 09399431..5279e687 100644
--- a/src/paimon/core/table/system/metadata_system_tables.cpp
+++ b/src/paimon/core/table/system/metadata_system_tables.cpp
@@ -270,8 +270,8 @@ Result<std::vector<ManifestEntry>>
ReadLatestManifestEntries(
CreateManifestFile(context, path_factory,
core_options, pool));
std::vector<ManifestEntry> entries;
for (const auto& manifest : manifests) {
- PAIMON_RETURN_NOT_OK(
- manifest_file->Read(manifest.FileName(), /*filter=*/nullptr,
&entries));
+ PAIMON_RETURN_NOT_OK(manifest_file->Read(manifest.FileName(),
/*filter=*/nullptr,
+ /*file_size=*/std::nullopt,
&entries));
}
return entries;
}
diff --git a/src/paimon/core/utils/file_utils.cpp
b/src/paimon/core/utils/file_utils.cpp
index 6b087730..012bb44d 100644
--- a/src/paimon/core/utils/file_utils.cpp
+++ b/src/paimon/core/utils/file_utils.cpp
@@ -60,15 +60,16 @@ Status FileUtils::ListOriginalVersionedFiles(const
std::shared_ptr<FileSystem>&
Status FileUtils::ListVersionedFileStatus(const std::shared_ptr<FileSystem>&
fs,
const std::string& dir, const
std::string& prefix,
std::vector<BasicFileStatus>*
file_status_list) {
- PAIMON_ASSIGN_OR_RAISE(bool exist, fs->Exists(dir));
- if (exist) {
- std::vector<BasicFileStatus> file_statuses;
- PAIMON_RETURN_NOT_OK(fs->ListDir(dir, &file_statuses));
- for (auto& file_status : file_statuses) {
- std::string file_name = PathUtil::GetName(file_status.GetPath());
- if (StringUtils::StartsWith(file_name, prefix)) {
- file_status_list->emplace_back(std::move(file_status));
- }
+ // No Exists() probe in front of the listing. Every FileSystem lists a
directory that is not
+ // there as an empty result rather than an error, so the probe only
decided whether to make a
+ // call that answers the same question. On a remote store it costs a round
trip of its own,
+ // which is what a schema or snapshot lookup pays before it can read
anything.
+ std::vector<BasicFileStatus> file_statuses;
+ PAIMON_RETURN_NOT_OK(fs->ListDir(dir, &file_statuses));
+ for (auto& file_status : file_statuses) {
+ std::string file_name = PathUtil::GetName(file_status.GetPath());
+ if (StringUtils::StartsWith(file_name, prefix)) {
+ file_status_list->emplace_back(std::move(file_status));
}
}
return Status::OK();
diff --git a/src/paimon/core/utils/objects_file.h
b/src/paimon/core/utils/objects_file.h
index 56ba6e31..845efd41 100644
--- a/src/paimon/core/utils/objects_file.h
+++ b/src/paimon/core/utils/objects_file.h
@@ -20,6 +20,7 @@
#include <functional>
#include <memory>
+#include <optional>
#include <string>
#include <utility>
#include <vector>
@@ -63,11 +64,15 @@ class ObjectsFile {
virtual ~ObjectsFile() = default;
+ /// @param file_size Length of the file when planning already knows it,
which lets the read
+ /// skip the metadata request a bare `Open` issues on a
remote store. Pass
+ /// std::nullopt when the length is not known; the read
then discovers it
+ /// itself.
Status Read(const std::string& file_name, const
std::function<Result<bool>(const T&)>& filter,
- std::vector<T>* result) const;
+ std::optional<int64_t> file_size, std::vector<T>* result)
const;
Status ReadIfFileExist(const std::string& file_name,
const std::function<Result<bool>(const T&)>& filter,
- std::vector<T>* result) const;
+ std::optional<int64_t> file_size, std::vector<T>*
result) const;
void DeleteQuietly(const std::string& file_name) {
std::string path = path_factory_->ToPath(file_name);
@@ -89,7 +94,8 @@ class ObjectsFile {
Status ReadArrowBatches(
const std::string& file_name,
- const std::function<Status(const
std::shared_ptr<arrow::StructArray>&)>& consumer) const;
+ const std::function<Status(const
std::shared_ptr<arrow::StructArray>&)>& consumer,
+ std::optional<int64_t> file_size) const;
std::shared_ptr<PathFactory> path_factory_;
std::shared_ptr<MemoryPool> pool_;
@@ -105,7 +111,12 @@ class ObjectsFile {
std::string compression_;
std::shared_ptr<Cache> cache_;
- Result<MemorySegment> ReadFileSegment(const std::string& file_path) const;
+ Result<MemorySegment> ReadFileSegment(const std::string& file_path,
+ const std::optional<int64_t>&
file_size) const;
+
+ /// Opens the file for reading, handing over the length when the caller
already has it.
+ Result<std::unique_ptr<InputStream>> OpenForRead(const std::string&
file_path,
+ const
std::optional<int64_t>& file_size) const;
};
template <typename T>
@@ -132,11 +143,12 @@ ObjectsFile<T>::ObjectsFile(const
std::shared_ptr<FileSystem>& file_system,
template <typename T>
Status ObjectsFile<T>::ReadIfFileExist(const std::string& file_name,
const std::function<Result<bool>(const
T&)>& filter,
+ std::optional<int64_t> file_size,
std::vector<T>* result) const {
std::string file_path = path_factory_->ToPath(file_name);
PAIMON_ASSIGN_OR_RAISE(bool path_exist, file_system_->Exists(file_path));
if (path_exist) {
- return Read(file_name, filter, result);
+ return Read(file_name, filter, file_size, result);
}
return Status::OK();
}
@@ -144,7 +156,7 @@ Status ObjectsFile<T>::ReadIfFileExist(const std::string&
file_name,
template <typename T>
Status ObjectsFile<T>::Read(const std::string& file_name,
const std::function<Result<bool>(const T&)>&
filter,
- std::vector<T>* result) const {
+ std::optional<int64_t> file_size, std::vector<T>*
result) const {
return ReadArrowBatches(
file_name,
[this, &filter, result](const std::shared_ptr<arrow::StructArray>&
struct_array) -> Status {
@@ -164,13 +176,15 @@ Status ObjectsFile<T>::Read(const std::string& file_name,
}
}
return Status::OK();
- });
+ },
+ file_size);
}
template <typename T>
Status ObjectsFile<T>::ReadArrowBatches(
const std::string& file_name,
- const std::function<Status(const std::shared_ptr<arrow::StructArray>&)>&
consumer) const {
+ const std::function<Status(const std::shared_ptr<arrow::StructArray>&)>&
consumer,
+ std::optional<int64_t> file_size) const {
std::string file_path = path_factory_->ToPath(file_name);
std::shared_ptr<InputStream> file_input_stream;
std::shared_ptr<Bytes> cached_bytes;
@@ -180,9 +194,9 @@ Status ObjectsFile<T>::ReadArrowBatches(
auto cache_key =
CacheKey::ForKind(file_path, /*position=*/0, /*length=*/-1,
CacheKind::MANIFEST);
auto supplier =
- [this,
- &file_path](const std::shared_ptr<CacheKey>&) ->
Result<std::shared_ptr<CacheValue>> {
- PAIMON_ASSIGN_OR_RAISE(MemorySegment segment,
ReadFileSegment(file_path));
+ [this, &file_path,
+ &file_size](const std::shared_ptr<CacheKey>&) ->
Result<std::shared_ptr<CacheValue>> {
+ PAIMON_ASSIGN_OR_RAISE(MemorySegment segment,
ReadFileSegment(file_path, file_size));
return std::make_shared<CacheValue>(segment, CacheCallback());
};
Result<std::shared_ptr<CacheValue>> cache_result =
cache_->Get(cache_key, supplier);
@@ -195,7 +209,7 @@ Status ObjectsFile<T>::ReadArrowBatches(
}
if (!file_input_stream) {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream>
unique_file_input_stream,
- file_system_->Open(file_path));
+ OpenForRead(file_path, file_size));
file_input_stream =
std::shared_ptr<InputStream>(std::move(unique_file_input_stream));
}
@@ -223,9 +237,24 @@ Status ObjectsFile<T>::ReadArrowBatches(
}
template <typename T>
-Result<MemorySegment> ObjectsFile<T>::ReadFileSegment(const std::string&
file_path) const {
+Result<std::unique_ptr<InputStream>> ObjectsFile<T>::OpenForRead(
+ const std::string& file_path, const std::optional<int64_t>& file_size)
const {
+ if (file_size.has_value()) {
+ // Planning already read this length out of the manifest metadata, and
`Open(FileStatus)` is
+ // documented to let the file system skip the metadata request a bare
open issues. That
+ // request is a round trip of its own on a remote store, paid before a
single byte of the
+ // file is read. The files here are written once and never rewritten,
so a length recorded
+ // at planning time cannot go stale underneath the read.
+ return file_system_->Open(FileStatus(file_path, file_size.value()));
+ }
+ return file_system_->Open(file_path);
+}
+
+template <typename T>
+Result<MemorySegment> ObjectsFile<T>::ReadFileSegment(
+ const std::string& file_path, const std::optional<int64_t>& file_size)
const {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream> input_stream,
- file_system_->Open(file_path));
+ OpenForRead(file_path, file_size));
PAIMON_ASSIGN_OR_RAISE(int64_t input_length, input_stream->Length());
PAIMON_RETURN_NOT_OK(input_stream->Seek(0, FS_SEEK_SET));
diff --git a/src/paimon/fs/jindo/jindo_file_system.cpp
b/src/paimon/fs/jindo/jindo_file_system.cpp
index 793f2b2f..866cbade 100644
--- a/src/paimon/fs/jindo/jindo_file_system.cpp
+++ b/src/paimon/fs/jindo/jindo_file_system.cpp
@@ -173,11 +173,18 @@ Result<FileStatus> JindoFileSystem::GetFileStatus(const
std::string& path) const
Status JindoFileSystem::ListDir(const std::string& directory,
std::vector<BasicFileStatus>*
file_status_list) const {
- PAIMON_ASSIGN_OR_RAISE(bool exist, Exists(directory));
- if (!exist) {
- return Status::OK();
+ // One status call answers what Exists() followed by GetFileStatus() asked
the store twice:
+ // whether the path is there at all, and whether it is a directory.
PAIMON_RETURN_NOT_OK_FROM_
+ // JINDO maps the SDK's not-found to Status::NotExist, which is what tells
a missing directory
+ // (listed as empty, as the other file systems do) from a call that
genuinely failed.
+ Result<FileStatus> dir_status = GetFileStatus(directory);
+ if (!dir_status.ok()) {
+ if (dir_status.status().IsNotExist()) {
+ return Status::OK();
+ }
+ return dir_status.status();
}
- PAIMON_ASSIGN_OR_RAISE(FileStatus file_status, GetFileStatus(directory));
+ const FileStatus& file_status = dir_status.value();
if (!file_status.IsDir()) {
return Status::Invalid(
fmt::format("file {} already exists and is not a directory",
file_status.GetPath()));
diff --git a/src/paimon/fs/jindo/jindo_file_system_test.cpp
b/src/paimon/fs/jindo/jindo_file_system_test.cpp
index e8b26dc6..714d1415 100644
--- a/src/paimon/fs/jindo/jindo_file_system_test.cpp
+++ b/src/paimon/fs/jindo/jindo_file_system_test.cpp
@@ -83,6 +83,22 @@ TEST_F(JindoFileSystemTest, TestRename) {
ASSERT_TRUE(is_exist);
}
+// Pins the contract the planning optimizations lean on: the SDK answers a
target that is not
+// there with its file-not-found error, the mapping surfaces that as Paimon's
NotExist (so callers
+// can tell "absent" apart from "the call failed"), and listing a missing
directory yields an empty
+// result instead of an error.
+TEST_F(JindoFileSystemTest,
TestMissingPathReportsNotExistAndMissingDirListsEmpty) {
+ const std::string missing_file = test_dir_ +
"no_such_dir/no_such_file.data";
+ Result<FileStatus> missing_status = fs_->GetFileStatus(missing_file);
+ ASSERT_FALSE(missing_status.ok());
+ ASSERT_TRUE(missing_status.status().IsNotExist()) <<
missing_status.status().ToString();
+
+ const std::string missing_dir = test_dir_ + "no_such_dir/";
+ std::vector<BasicFileStatus> file_statuses;
+ ASSERT_OK(fs_->ListDir(missing_dir, &file_statuses));
+ ASSERT_TRUE(file_statuses.empty());
+}
+
TEST_F(JindoFileSystemTest, TestSeek) {
std::string content = "abcdefghijk";
std::string file_path = test_dir_ + "file.data";