Skip to content

Commit ffbda4f

Browse files
authored
feat(manifest): support snapshot live manifest cache (#386)
1 parent 786d6ef commit ffbda4f

22 files changed

Lines changed: 1146 additions & 240 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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 bucket, partition, and statistics pruning. The snapshot live
23+
manifest entry cache reduces repeated manifest decoding cost for successive full
24+
scans that target the same bucket.
25+
26+
The cache stores decoded and merged live manifest entries by table path, branch,
27+
and bucket for ``ScanMode::ALL``. Each cache value can retain several snapshot
28+
results for that bucket. Exact snapshot hits are served from the cache; cache
29+
misses rebuild the target snapshot bucket from the target snapshot's data
30+
manifests and store the rebuilt live entries.
31+
32+
Request-specific filters are not stored in the cache. Partition, level, and
33+
predicate filters are still evaluated for each scan, so cached entries can be
34+
reused safely across different scan predicates for the same bucket.
35+
36+
Configuration
37+
-------------
38+
39+
Manifest entry caching reuses the cache instance provided by
40+
``ScanContextBuilder::WithCache()`` and stores bucket-scoped snapshot entries
41+
under
42+
``CacheKind::SNAPSHOT_LIVE_MANIFEST``:
43+
44+
.. code-block:: cpp
45+
46+
auto cache = std::make_shared<LruCache>(128 * 1024 * 1024);
47+
ScanContextBuilder context_builder(table_path);
48+
PAIMON_ASSIGN_OR_RAISE(
49+
std::unique_ptr<ScanContext> scan_context,
50+
context_builder
51+
.WithCache(cache)
52+
.AddOption(Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS, "3")
53+
.Finish());
54+
55+
Cache entries are scoped by table path, branch, and bucket, so they can be
56+
reused across newly created ``TableScan`` and ``FileStoreScan`` instances as
57+
long as they share the same cache object and scan the same bucket.
58+
59+
``Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS`` controls how many snapshot
60+
results are retained in each table/branch/bucket cache value. Older snapshots in
61+
the same bucket are evicted first. The default value is ``0``, which disables
62+
the cache path. Set it to a positive value to enable the cache when
63+
``ScanContextBuilder::WithCache()`` is also configured. Physical cache eviction
64+
is still controlled by the configured ``Cache`` implementation, for example the
65+
capacity of ``LruCache``.
66+
67+
If no cache is provided through ``ScanContextBuilder::WithCache()``, this
68+
optimization is skipped. The snapshot manifest entry cache shares the same
69+
``Cache`` interface with raw manifest and data-file footer caches, but it uses a
70+
dedicated ``CacheKind`` and a table/branch/bucket key instead of file byte
71+
ranges.
72+
73+
Limitations
74+
-----------
75+
76+
The cache is currently used only for ``ScanMode::ALL`` scans that can determine
77+
a single target bucket. It is skipped for scans without a bucket filter because
78+
reading or deserializing all buckets would be too expensive for selective
79+
queries. It is also skipped for row-range scans because row-range pruning is
80+
applied at manifest-meta level.
81+
82+
Metrics
83+
-------
84+
85+
The scan metrics expose existing counters for the last scan:
86+
87+
- ``lastScannedManifests``: how many manifest files were loaded during this
88+
scan before manifest entry decoding.

include/paimon/cache/cache.h

Lines changed: 4 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,9 @@ 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,
47+
int32_t bucket);
4448

4549
public:
4650
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 live manifest entry
170+
/// results retained per table, branch, and bucket. Setting it to 0 disables manifest entry
171+
/// cache. 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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,49 @@
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+
int32_t bucket)
26+
: CacheKey(CacheKind::SNAPSHOT_LIVE_MANIFEST),
27+
table_path_(table_path),
28+
branch_(branch),
29+
bucket_(bucket) {}
30+
31+
bool IsIndex() const override {
32+
return false;
33+
}
34+
35+
bool Equals(const CacheKey& other) const override {
36+
const auto* rhs = dynamic_cast<const SnapshotLiveManifestEntriesCacheKey*>(&other);
37+
if (!rhs) {
38+
return false;
39+
}
40+
return table_path_ == rhs->table_path_ && branch_ == rhs->branch_ &&
41+
bucket_ == rhs->bucket_ && GetKind() == rhs->GetKind();
42+
}
43+
44+
size_t HashCode() const override {
45+
size_t seed = 0;
46+
seed ^= std::hash<std::string>{}(table_path_) + HASH_CONSTANT + (seed << 6) + (seed >> 2);
47+
seed ^= std::hash<std::string>{}(branch_) + HASH_CONSTANT + (seed << 6) + (seed >> 2);
48+
seed ^= std::hash<int32_t>{}(bucket_) + HASH_CONSTANT + (seed << 6) + (seed >> 2);
49+
seed ^= std::hash<int32_t>{}(static_cast<int32_t>(GetKind())) + HASH_CONSTANT +
50+
(seed << 6) + (seed >> 2);
51+
return seed;
52+
}
53+
54+
private:
55+
static constexpr uint64_t HASH_CONSTANT = 0x9e3779b97f4a7c15ULL;
56+
57+
const std::string table_path_;
58+
const std::string branch_;
59+
const int32_t bucket_;
60+
};
61+
62+
} // namespace
2063

2164
std::shared_ptr<CacheKey> CacheKey::ForPosition(const std::string& file_path, int64_t position,
2265
int32_t length, bool is_index) {
@@ -31,6 +74,12 @@ std::shared_ptr<CacheKey> CacheKey::ForKind(const std::string& file_path, int64_
3174
return key;
3275
}
3376

77+
std::shared_ptr<CacheKey> CacheKey::ForSnapshotLiveManifestEntries(const std::string& table_path,
78+
const std::string& branch,
79+
int32_t bucket) {
80+
return std::make_shared<SnapshotLiveManifestEntriesCacheKey>(table_path, branch, bucket);
81+
}
82+
3483
bool PositionCacheKey::IsIndex() const {
3584
return is_index_;
3685
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,23 @@ 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", 0);
386+
auto same_key = CacheKey::ForSnapshotLiveManifestEntries("table_path", "main", 0);
387+
auto branch_key = CacheKey::ForSnapshotLiveManifestEntries("table_path", "dev", 0);
388+
auto table_key = CacheKey::ForSnapshotLiveManifestEntries("other_table_path", "main", 0);
389+
auto bucket_key = CacheKey::ForSnapshotLiveManifestEntries("table_path", "main", 1);
390+
auto hash_in_path_key = CacheKey::ForSnapshotLiveManifestEntries("table#path", "main", 0);
391+
auto hash_in_branch_key = CacheKey::ForSnapshotLiveManifestEntries("table", "path#main", 0);
392+
393+
ASSERT_EQ(CacheKind::SNAPSHOT_LIVE_MANIFEST, main_key->GetKind());
394+
ASSERT_TRUE(CacheKeyEqual()(main_key, same_key));
395+
ASSERT_FALSE(CacheKeyEqual()(main_key, branch_key));
396+
ASSERT_FALSE(CacheKeyEqual()(main_key, table_key));
397+
ASSERT_FALSE(CacheKeyEqual()(main_key, bucket_key));
398+
ASSERT_FALSE(CacheKeyEqual()(hash_in_path_key, hash_in_branch_key));
399+
}
400+
384401
/// Verifies that multiple evictions happen when a single large entry is inserted.
385402
TEST_F(LruCacheTest, TestMultipleEvictions) {
386403
LruCache cache(300);

src/paimon/core/core_options.cpp

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

403403
int32_t manifest_merge_min_count = 30;
404+
int32_t scan_manifest_entry_cache_max_snapshots = 0;
404405
int32_t read_batch_size = 1024;
405406
int32_t write_batch_size = 1024;
406407
int32_t local_sort_max_num_file_handles = 128;
@@ -708,6 +709,13 @@ struct CoreOptions::Impl {
708709
}
709710
// Parse scan.mode - scanning behavior of the source, default "default"
710711
PAIMON_RETURN_NOT_OK(parser.ParseStartupMode(&startup_mode));
712+
// Parse scan.manifest-entry-cache.max-snapshots - cached snapshots per bucket.
713+
PAIMON_RETURN_NOT_OK(parser.Parse(Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS,
714+
&scan_manifest_entry_cache_max_snapshots));
715+
if (scan_manifest_entry_cache_max_snapshots < 0) {
716+
return Status::Invalid(fmt::format("{} must be non-negative",
717+
Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS));
718+
}
711719
// Parse scan.fallback-branch - fallback branch when partition not found
712720
PAIMON_RETURN_NOT_OK(parser.Parse(Options::SCAN_FALLBACK_BRANCH, &scan_fallback_branch));
713721
// Parse branch - branch name, default "main"
@@ -959,6 +967,11 @@ std::optional<int64_t> CoreOptions::GetScanSnapshotId() const {
959967
std::optional<int64_t> CoreOptions::GetScanTimestampMillis() const {
960968
return impl_->scan_timestamp_millis;
961969
}
970+
971+
int32_t CoreOptions::GetScanManifestEntryCacheMaxSnapshots() const {
972+
return impl_->scan_manifest_entry_cache_max_snapshots;
973+
}
974+
962975
int64_t CoreOptions::GetManifestTargetFileSize() const {
963976
return impl_->manifest_target_file_size;
964977
}

src/paimon/core/core_options.h

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

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

0 commit comments

Comments
 (0)