Skip to content

Commit 8337c3b

Browse files
committed
feat(manifest): support snapshot mainifest cache
1 parent b631683 commit 8337c3b

18 files changed

Lines changed: 723 additions & 36 deletions

docs/source/user_guide.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ User Guide
2525
user_guide/snapshot
2626
user_guide/manifest
2727
user_guide/manifest_cache
28+
user_guide/manifest_entry_cache
2829
user_guide/parquet_metadata_cache
2930
user_guide/data_types
3031
user_guide/primary_key_table
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
.. Copyright 2026-present Alibaba Inc.
2+
3+
.. Licensed under the Apache License, Version 2.0 (the "License");
4+
.. you may not use this file except in compliance with the License.
5+
.. You may obtain a copy of the License at
6+
7+
.. http://www.apache.org/licenses/LICENSE-2.0
8+
9+
.. Unless required by applicable law or agreed to in writing, software
10+
.. distributed under the License is distributed on an "AS IS" BASIS,
11+
.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
.. See the License for the specific language governing permissions and
13+
.. limitations under the License.
14+
15+
Manifest Entry Cache
16+
====================
17+
18+
Overview
19+
--------
20+
21+
Large tables may contain many manifest entries, while a scan may only need a
22+
small subset after snapshot, partition, bucket, and statistics pruning. The
23+
snapshot-level manifest entry cache reduces repeated manifest decoding cost for
24+
successive full scans.
25+
26+
The cache stores decoded and merged live manifest entries by snapshot for
27+
``ScanMode::ALL``. When a newer snapshot is scanned, paimon-cpp tries to build
28+
the target snapshot incrementally from the latest cached snapshot by reading
29+
only intermediate delta manifests.
30+
31+
Request-specific filters are not stored in the cache. Partition, bucket, level,
32+
and predicate filters are still evaluated for each scan, so cached entries can
33+
be reused safely across different scan predicates.
34+
35+
Configuration
36+
-------------
37+
38+
Manifest entry caching reuses the cache instance provided by
39+
``ScanContextBuilder::WithCache()`` and stores the snapshot bundle under
40+
``CacheKind::SNAPSHOT_LIVE_MANIFEST``:
41+
42+
.. code-block:: cpp
43+
44+
auto cache = std::make_shared<LruCache>(128 * 1024 * 1024);
45+
ScanContextBuilder context_builder(table_path);
46+
PAIMON_ASSIGN_OR_RAISE(
47+
std::unique_ptr<ScanContext> scan_context,
48+
context_builder
49+
.WithCache(cache)
50+
.AddOption(Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS, "3")
51+
.Finish());
52+
53+
Cache entries are scoped by table path and branch, so they can be reused across
54+
newly created ``TableScan`` and ``FileStoreScan`` instances as long as they
55+
share the same cache object.
56+
57+
``Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS`` controls how many snapshot
58+
results are retained per table and branch. Older snapshot entries are evicted
59+
first. The default value is ``0``, which disables the cache path. Set it to a
60+
positive value to enable the cache when ``ScanContextBuilder::WithCache()`` is
61+
also configured.
62+
63+
If no cache is provided through ``ScanContextBuilder::WithCache()``, this
64+
optimization is skipped. The snapshot manifest entry cache shares the same
65+
``Cache`` interface with raw manifest and data-file footer caches, but it uses a
66+
dedicated ``CacheKind`` and a table/branch key instead of file byte ranges.
67+
68+
Limitations
69+
-----------
70+
71+
The cache is currently used only for ``ScanMode::ALL``. It is skipped for
72+
row-range scans because row-range pruning is applied at manifest-meta level.
73+
74+
Metrics
75+
-------
76+
77+
The scan metrics expose counters for the last scan:
78+
79+
- ``lastManifestEntryCacheHit``: whether the target snapshot was served
80+
directly from the cache.
81+
- ``lastManifestEntryCacheIncrementalSnapshots``: how many intermediate
82+
snapshots were applied during incremental construction.
83+
- ``lastManifestEntryCacheLoadedManifests``: how many manifest files were
84+
loaded for the cache path.

include/paimon/cache/cache.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ enum class CacheKind {
3333
DEFAULT,
3434
MANIFEST,
3535
DATA_FILE_FOOTER,
36+
SNAPSHOT_LIVE_MANIFEST,
3637
};
3738

3839
class PAIMON_EXPORT CacheKey {
@@ -41,6 +42,8 @@ class PAIMON_EXPORT CacheKey {
4142
int32_t length, bool is_index);
4243
static std::shared_ptr<CacheKey> ForKind(const std::string& file_path, int64_t position,
4344
int32_t length, CacheKind kind);
45+
static std::shared_ptr<CacheKey> ForSnapshotLiveManifestEntries(const std::string& table_path,
46+
const std::string& branch);
4447

4548
public:
4649
virtual ~CacheKey() = default;

include/paimon/defs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ struct PAIMON_EXPORT Options {
166166
/// "latest-full", "latest", "from-snapshot", "from-snapshot-full". Default value is "default".
167167
static const char SCAN_MODE[];
168168

169+
/// "scan.manifest-entry-cache.max-snapshots" - Maximum number of snapshot manifest entry
170+
/// results retained per table and branch. Setting it to 0 disables manifest entry cache.
171+
/// Default value is 0.
172+
static const char SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS[];
173+
169174
/// "read.batch-size" - Read batch size for any file format if it supports.
170175
/// The default value is 1024.
171176
static const char READ_BATCH_SIZE[];

src/paimon/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ set(PAIMON_CORE_SRCS
304304
core/operation/raw_file_split_read.cpp
305305
core/operation/read_context.cpp
306306
core/operation/scan_context.cpp
307+
core/manifest/snapshot_live_manifest_entries.cpp
307308
core/operation/write_context.cpp
308309
core/operation/write_restore.cpp
309310
core/postpone/postpone_bucket_writer.cpp

src/paimon/common/defs.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ const char Options::SOURCE_SPLIT_TARGET_SIZE[] = "source.split.target-size";
4747
const char Options::SOURCE_SPLIT_OPEN_FILE_COST[] = "source.split.open-file-cost";
4848
const char Options::SCAN_SNAPSHOT_ID[] = "scan.snapshot-id";
4949
const char Options::SCAN_MODE[] = "scan.mode";
50+
const char Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS[] =
51+
"scan.manifest-entry-cache.max-snapshots";
5052
const char Options::READ_BATCH_SIZE[] = "read.batch-size";
5153
const char Options::WRITE_BATCH_SIZE[] = "write.batch-size";
5254
const char Options::WRITE_BUFFER_SIZE[] = "write-buffer-size";

src/paimon/common/io/cache/cache_key.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,43 @@
1717
#include "paimon/common/io/cache/cache_key.h"
1818

1919
namespace paimon {
20+
namespace {
21+
22+
class SnapshotLiveManifestEntriesCacheKey : public CacheKey {
23+
public:
24+
SnapshotLiveManifestEntriesCacheKey(const std::string& table_path, const std::string& branch)
25+
: CacheKey(CacheKind::SNAPSHOT_LIVE_MANIFEST), table_path_(table_path), branch_(branch) {}
26+
27+
bool IsIndex() const override {
28+
return false;
29+
}
30+
31+
bool Equals(const CacheKey& other) const override {
32+
const auto* rhs = dynamic_cast<const SnapshotLiveManifestEntriesCacheKey*>(&other);
33+
if (!rhs) {
34+
return false;
35+
}
36+
return table_path_ == rhs->table_path_ && branch_ == rhs->branch_ &&
37+
GetKind() == rhs->GetKind();
38+
}
39+
40+
size_t HashCode() const override {
41+
size_t seed = 0;
42+
seed ^= std::hash<std::string>{}(table_path_) + HASH_CONSTANT + (seed << 6) + (seed >> 2);
43+
seed ^= std::hash<std::string>{}(branch_) + HASH_CONSTANT + (seed << 6) + (seed >> 2);
44+
seed ^= std::hash<int32_t>{}(static_cast<int32_t>(GetKind())) + HASH_CONSTANT +
45+
(seed << 6) + (seed >> 2);
46+
return seed;
47+
}
48+
49+
private:
50+
static constexpr uint64_t HASH_CONSTANT = 0x9e3779b97f4a7c15ULL;
51+
52+
const std::string table_path_;
53+
const std::string branch_;
54+
};
55+
56+
} // namespace
2057

2158
std::shared_ptr<CacheKey> CacheKey::ForPosition(const std::string& file_path, int64_t position,
2259
int32_t length, bool is_index) {
@@ -31,6 +68,11 @@ std::shared_ptr<CacheKey> CacheKey::ForKind(const std::string& file_path, int64_
3168
return key;
3269
}
3370

71+
std::shared_ptr<CacheKey> CacheKey::ForSnapshotLiveManifestEntries(const std::string& table_path,
72+
const std::string& branch) {
73+
return std::make_shared<SnapshotLiveManifestEntriesCacheKey>(table_path, branch);
74+
}
75+
3476
bool PositionCacheKey::IsIndex() const {
3577
return is_index_;
3678
}

src/paimon/common/io/cache/lru_cache_test.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,21 @@ TEST_F(LruCacheTest, TestForKindSetsKeyKind) {
381381
ASSERT_EQ(CacheKind::MANIFEST, put_key->GetKind());
382382
}
383383

384+
TEST_F(LruCacheTest, TestForSnapshotLiveManifestEntries) {
385+
auto main_key = CacheKey::ForSnapshotLiveManifestEntries("table_path", "main");
386+
auto same_key = CacheKey::ForSnapshotLiveManifestEntries("table_path", "main");
387+
auto branch_key = CacheKey::ForSnapshotLiveManifestEntries("table_path", "dev");
388+
auto table_key = CacheKey::ForSnapshotLiveManifestEntries("other_table_path", "main");
389+
auto hash_in_path_key = CacheKey::ForSnapshotLiveManifestEntries("table#path", "main");
390+
auto hash_in_branch_key = CacheKey::ForSnapshotLiveManifestEntries("table", "path#main");
391+
392+
ASSERT_EQ(CacheKind::SNAPSHOT_LIVE_MANIFEST, main_key->GetKind());
393+
ASSERT_TRUE(CacheKeyEqual()(main_key, same_key));
394+
ASSERT_FALSE(CacheKeyEqual()(main_key, branch_key));
395+
ASSERT_FALSE(CacheKeyEqual()(main_key, table_key));
396+
ASSERT_FALSE(CacheKeyEqual()(hash_in_path_key, hash_in_branch_key));
397+
}
398+
384399
/// Verifies that multiple evictions happen when a single large entry is inserted.
385400
TEST_F(LruCacheTest, TestMultipleEvictions) {
386401
LruCache cache(300);

src/paimon/core/core_options.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ struct CoreOptions::Impl {
403403
int32_t bucket = -1;
404404

405405
int32_t manifest_merge_min_count = 30;
406+
int32_t scan_manifest_entry_cache_max_snapshots = 0;
406407
int32_t read_batch_size = 1024;
407408
int32_t write_batch_size = 1024;
408409
int32_t local_sort_max_num_file_handles = 128;
@@ -717,6 +718,13 @@ struct CoreOptions::Impl {
717718
}
718719
// Parse scan.mode - scanning behavior of the source, default "default"
719720
PAIMON_RETURN_NOT_OK(parser.ParseStartupMode(&startup_mode));
721+
// Parse scan.manifest-entry-cache.max-snapshots - cache size by snapshot count
722+
PAIMON_RETURN_NOT_OK(parser.Parse(Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS,
723+
&scan_manifest_entry_cache_max_snapshots));
724+
if (scan_manifest_entry_cache_max_snapshots < 0) {
725+
return Status::Invalid(fmt::format("{} must be non-negative",
726+
Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS));
727+
}
720728
// Parse scan.fallback-branch - fallback branch when partition not found
721729
PAIMON_RETURN_NOT_OK(parser.Parse(Options::SCAN_FALLBACK_BRANCH, &scan_fallback_branch));
722730
// Parse branch - branch name, default "main"
@@ -968,6 +976,11 @@ std::optional<int64_t> CoreOptions::GetScanSnapshotId() const {
968976
std::optional<int64_t> CoreOptions::GetScanTimestampMillis() const {
969977
return impl_->scan_timestamp_millis;
970978
}
979+
980+
int32_t CoreOptions::GetScanManifestEntryCacheMaxSnapshots() const {
981+
return impl_->scan_manifest_entry_cache_max_snapshots;
982+
}
983+
971984
int64_t CoreOptions::GetManifestTargetFileSize() const {
972985
return impl_->manifest_target_file_size;
973986
}

src/paimon/core/core_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class PAIMON_EXPORT CoreOptions {
7878
int64_t GetSourceSplitOpenFileCost() const;
7979
std::optional<int64_t> GetScanSnapshotId() const;
8080
std::optional<int64_t> GetScanTimestampMillis() const;
81+
int32_t GetScanManifestEntryCacheMaxSnapshots() const;
8182

8283
int64_t GetManifestTargetFileSize() const;
8384
std::shared_ptr<Cache> GetCache() const;

0 commit comments

Comments
 (0)