Skip to content

Commit 88752d5

Browse files
manuzhangcodex
andcommitted
fix: bound snapshot manifest cache
Co-authored-by: Codex <codex@openai.com>
1 parent 433113f commit 88752d5

4 files changed

Lines changed: 212 additions & 33 deletions

File tree

‎src/iceberg/file_io.cc‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ Status FileIO::ConfigureMetadataCache(
253253
std::lock_guard lock(metadata_cache_state_->mutex);
254254
if (metadata_cache_state_->cache == nullptr) {
255255
metadata_cache_state_->cache = std::move(cache);
256+
metadata_cache_state_->snapshot_cache =
257+
internal::MakeSnapshotCacheData(metadata_cache_state_->cache);
256258
return {};
257259
}
258260
if (metadata_cache_state_->cache->options() == options) {
@@ -288,7 +290,8 @@ std::shared_ptr<MetadataCache> FileIO::GetMetadataCache() const {
288290
std::shared_ptr<internal::SnapshotCacheData> FileIO::GetSnapshotCacheData() const {
289291
std::lock_guard lock(metadata_cache_state_->mutex);
290292
if (metadata_cache_state_->snapshot_cache == nullptr) {
291-
metadata_cache_state_->snapshot_cache = internal::MakeSnapshotCacheData();
293+
metadata_cache_state_->snapshot_cache =
294+
internal::MakeSnapshotCacheData(metadata_cache_state_->cache);
292295
}
293296
return metadata_cache_state_->snapshot_cache;
294297
}

‎src/iceberg/snapshot.cc‎

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "iceberg/manifest/manifest_entry.h"
3131
#include "iceberg/manifest/manifest_list.h"
3232
#include "iceberg/manifest/manifest_reader.h"
33+
#include "iceberg/metadata_cache.h"
3334
#include "iceberg/util/macros.h"
3435
#include "iceberg/util/string_util.h"
3536

@@ -39,59 +40,107 @@ namespace internal {
3940

4041
class SnapshotCacheData {
4142
public:
42-
Result<std::reference_wrapper<SnapshotCache::ManifestsCache>> Get(
43+
explicit SnapshotCacheData(std::shared_ptr<MetadataCache> metadata_cache)
44+
: metadata_cache_(std::move(metadata_cache)) {}
45+
46+
Result<std::shared_ptr<const SnapshotCache::ManifestsCache>> Get(
4347
const Snapshot* snapshot, std::shared_ptr<FileIO> file_io) {
48+
if (metadata_cache_ == nullptr || !metadata_cache_->options().enabled) {
49+
ICEBERG_ASSIGN_OR_RAISE(
50+
auto loaded, SnapshotCache::LoadManifestsCache(snapshot, std::move(file_io)));
51+
return std::make_shared<const SnapshotCache::ManifestsCache>(std::move(loaded));
52+
}
53+
54+
const auto& location = snapshot->manifest_list;
4455
std::shared_ptr<Entry> entry;
45-
{
56+
while (true) {
57+
// Checking the content cache also applies its expiration policy and refreshes its
58+
// LRU position. Parsed entries are reusable only while the matching bytes remain
59+
// cached.
60+
auto content = metadata_cache_->GetIfPresent(location);
4661
std::unique_lock lock(mutex_);
47-
while (true) {
48-
auto [it, inserted] = entries_.try_emplace(snapshot->manifest_list);
49-
if (inserted) {
50-
it->second = std::make_shared<Entry>();
62+
PruneExpired();
63+
auto [it, inserted] = entries_.try_emplace(location);
64+
if (inserted) {
65+
it->second = std::make_shared<Entry>();
66+
}
67+
entry = it->second;
68+
if (entry->value != nullptr) {
69+
if (content != nullptr && entry->content.lock() == content) {
70+
return entry->value;
5171
}
52-
entry = it->second;
53-
if (entry->value.has_value()) {
54-
return std::ref(*entry->value);
72+
entries_.erase(it);
73+
continue;
74+
}
75+
if (entry->loading) {
76+
entry->loaded.wait(lock, [&entry] { return !entry->loading; });
77+
if (entry->error.has_value()) {
78+
return std::unexpected<Error>(*entry->error);
5579
}
56-
if (!entry->loading) {
57-
entry->loading = true;
58-
break;
80+
if (entry->value != nullptr) {
81+
return entry->value;
5982
}
60-
entry->loaded.wait(lock, [&entry] { return !entry->loading; });
83+
continue;
6184
}
85+
entry->loading = true;
86+
break;
6287
}
6388

64-
auto loaded = SnapshotCache::InitManifestsCache(snapshot, std::move(file_io));
89+
auto loaded = SnapshotCache::LoadManifestsCache(snapshot, std::move(file_io));
90+
auto content = metadata_cache_->GetIfPresent(location);
6591
std::lock_guard lock(mutex_);
66-
auto it = entries_.find(snapshot->manifest_list);
92+
auto it = entries_.find(location);
6793
if (!loaded.has_value()) {
6894
if (it != entries_.end() && it->second == entry) {
6995
entries_.erase(it);
7096
}
97+
entry->error = loaded.error();
7198
entry->loading = false;
7299
entry->loaded.notify_all();
73100
return std::unexpected<Error>(std::move(loaded).error());
74101
}
75102

76-
entry->value = std::move(loaded).value();
103+
entry->value =
104+
std::make_shared<const SnapshotCache::ManifestsCache>(std::move(loaded).value());
105+
if (content != nullptr) {
106+
entry->content = content;
107+
} else if (it != entries_.end() && it->second == entry) {
108+
// Oversized content is not retained by MetadataCache, so do not retain its parsed
109+
// representation in the cross-query cache either.
110+
entries_.erase(it);
111+
}
77112
entry->loading = false;
78113
entry->loaded.notify_all();
79-
return std::ref(*entry->value);
114+
return entry->value;
80115
}
81116

82117
private:
83118
struct Entry {
84119
bool loading = false;
85-
std::optional<SnapshotCache::ManifestsCache> value;
120+
std::shared_ptr<const SnapshotCache::ManifestsCache> value;
121+
std::weak_ptr<const std::string> content;
122+
std::optional<Error> error;
86123
std::condition_variable loaded;
87124
};
88125

126+
void PruneExpired() {
127+
for (auto it = entries_.begin(); it != entries_.end();) {
128+
if (!it->second->loading && it->second->content.expired()) {
129+
it = entries_.erase(it);
130+
} else {
131+
++it;
132+
}
133+
}
134+
}
135+
136+
std::shared_ptr<MetadataCache> metadata_cache_;
89137
std::mutex mutex_;
90138
std::unordered_map<std::string, std::shared_ptr<Entry>> entries_;
91139
};
92140

93-
std::shared_ptr<SnapshotCacheData> MakeSnapshotCacheData() {
94-
return std::make_shared<SnapshotCacheData>();
141+
std::shared_ptr<SnapshotCacheData> MakeSnapshotCacheData(
142+
std::shared_ptr<MetadataCache> metadata_cache) {
143+
return std::make_shared<SnapshotCacheData>(std::move(metadata_cache));
95144
}
96145

97146
} // namespace internal
@@ -267,7 +316,14 @@ Result<std::unique_ptr<Snapshot>> Snapshot::Make(
267316
});
268317
}
269318

270-
Result<SnapshotCache::ManifestsCache> SnapshotCache::InitManifestsCache(
319+
Result<std::shared_ptr<const SnapshotCache::ManifestsCache>>
320+
SnapshotCache::InitManifestsCache(const Snapshot* snapshot,
321+
std::shared_ptr<FileIO> file_io) {
322+
auto cache_data = file_io->GetSnapshotCacheData();
323+
return cache_data->Get(snapshot, std::move(file_io));
324+
}
325+
326+
Result<SnapshotCache::ManifestsCache> SnapshotCache::LoadManifestsCache(
271327
const Snapshot* snapshot, std::shared_ptr<FileIO> file_io) {
272328
if (file_io == nullptr) {
273329
return InvalidArgument("Cannot cache manifests: FileIO is null");
@@ -304,29 +360,29 @@ Result<std::span<const ManifestFile>> SnapshotCache::Manifests(
304360
std::shared_ptr<FileIO> file_io) const {
305361
ICEBERG_PRECHECK(snapshot_ != nullptr, "Cannot cache manifests for a null snapshot");
306362
ICEBERG_PRECHECK(file_io != nullptr, "Cannot cache manifests: FileIO is null");
307-
auto cache_data = file_io->GetSnapshotCacheData();
308-
ICEBERG_ASSIGN_OR_RAISE(auto cache_ref, cache_data->Get(snapshot_, std::move(file_io)));
309-
auto& cache = cache_ref.get();
363+
ICEBERG_ASSIGN_OR_RAISE(auto cache_ref,
364+
manifests_cache_.Get(snapshot_, std::move(file_io)));
365+
const auto& cache = *cache_ref.get();
310366
return std::span<const ManifestFile>(cache.first.data(), cache.first.size());
311367
}
312368

313369
Result<std::span<const ManifestFile>> SnapshotCache::DataManifests(
314370
std::shared_ptr<FileIO> file_io) const {
315371
ICEBERG_PRECHECK(snapshot_ != nullptr, "Cannot cache manifests for a null snapshot");
316372
ICEBERG_PRECHECK(file_io != nullptr, "Cannot cache manifests: FileIO is null");
317-
auto cache_data = file_io->GetSnapshotCacheData();
318-
ICEBERG_ASSIGN_OR_RAISE(auto cache_ref, cache_data->Get(snapshot_, std::move(file_io)));
319-
auto& cache = cache_ref.get();
373+
ICEBERG_ASSIGN_OR_RAISE(auto cache_ref,
374+
manifests_cache_.Get(snapshot_, std::move(file_io)));
375+
const auto& cache = *cache_ref.get();
320376
return std::span<const ManifestFile>(cache.first.data(), cache.second);
321377
}
322378

323379
Result<std::span<const ManifestFile>> SnapshotCache::DeleteManifests(
324380
std::shared_ptr<FileIO> file_io) const {
325381
ICEBERG_PRECHECK(snapshot_ != nullptr, "Cannot cache manifests for a null snapshot");
326382
ICEBERG_PRECHECK(file_io != nullptr, "Cannot cache manifests: FileIO is null");
327-
auto cache_data = file_io->GetSnapshotCacheData();
328-
ICEBERG_ASSIGN_OR_RAISE(auto cache_ref, cache_data->Get(snapshot_, std::move(file_io)));
329-
auto& cache = cache_ref.get();
383+
ICEBERG_ASSIGN_OR_RAISE(auto cache_ref,
384+
manifests_cache_.Get(snapshot_, std::move(file_io)));
385+
const auto& cache = *cache_ref.get();
330386
const size_t delete_start = cache.second;
331387
const size_t delete_count = cache.first.size() - delete_start;
332388
return std::span<const ManifestFile>(cache.first.data() + delete_start, delete_count);

‎src/iceberg/snapshot.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@
3434
#include "iceberg/manifest/manifest_list.h"
3535
#include "iceberg/result.h"
3636
#include "iceberg/type_fwd.h"
37+
#include "iceberg/util/lazy.h"
3738
#include "iceberg/util/timepoint.h"
3839

3940
namespace iceberg {
4041

42+
class MetadataCache;
43+
4144
namespace internal {
4245
class SnapshotCacheData;
43-
ICEBERG_EXPORT std::shared_ptr<SnapshotCacheData> MakeSnapshotCacheData();
46+
ICEBERG_EXPORT std::shared_ptr<SnapshotCacheData> MakeSnapshotCacheData(
47+
std::shared_ptr<MetadataCache> metadata_cache);
4448
} // namespace internal
4549

4650
/// \brief The type of snapshot reference
@@ -510,12 +514,18 @@ class ICEBERG_EXPORT SnapshotCache {
510514
/// \param snapshot The snapshot to initialize the manifests cache for
511515
/// \param file_io The FileIO instance to use for reading the manifest list
512516
/// \return A result containing the manifests cache
513-
static Result<ManifestsCache> InitManifestsCache(const Snapshot* snapshot,
517+
static Result<std::shared_ptr<const ManifestsCache>> InitManifestsCache(
518+
const Snapshot* snapshot, std::shared_ptr<FileIO> file_io);
519+
520+
static Result<ManifestsCache> LoadManifestsCache(const Snapshot* snapshot,
514521
std::shared_ptr<FileIO> file_io);
515522

516523
/// The underlying snapshot data
517524
const Snapshot* snapshot_;
518525

526+
/// Keep the selected shared cache entry alive while spans from this object are used.
527+
Lazy<InitManifestsCache> manifests_cache_;
528+
519529
friend class internal::SnapshotCacheData;
520530
};
521531

‎src/iceberg/test/manifest_writer_versions_test.cc‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919

20+
#include <algorithm>
2021
#include <cstdint>
2122
#include <memory>
2223
#include <optional>
@@ -33,13 +34,16 @@
3334
#include "iceberg/manifest/manifest_list.h"
3435
#include "iceberg/manifest/manifest_reader.h"
3536
#include "iceberg/manifest/manifest_writer.h"
37+
#include "iceberg/metadata_cache.h"
3638
#include "iceberg/metrics.h"
3739
#include "iceberg/partition_spec.h"
3840
#include "iceberg/row/partition_values.h"
3941
#include "iceberg/schema.h"
4042
#include "iceberg/schema_field.h"
43+
#include "iceberg/snapshot.h"
4144
#include "iceberg/table_metadata.h"
4245
#include "iceberg/test/matchers.h"
46+
#include "iceberg/test/mock_io.h"
4347
#include "iceberg/transform.h"
4448
#include "iceberg/type.h"
4549

@@ -127,6 +131,36 @@ std::unique_ptr<DataFile> CreateDeleteFile() {
127131
return delete_file;
128132
}
129133

134+
class CountingInputFile : public InputFile {
135+
public:
136+
CountingInputFile(std::unique_ptr<InputFile> input_file, int* open_count)
137+
: input_file_(std::move(input_file)), open_count_(open_count) {}
138+
139+
std::string_view location() const override { return input_file_->location(); }
140+
141+
Result<int64_t> Size() const override { return input_file_->Size(); }
142+
143+
Result<std::unique_ptr<SeekableInputStream>> Open() override {
144+
++*open_count_;
145+
return input_file_->Open();
146+
}
147+
148+
private:
149+
std::unique_ptr<InputFile> input_file_;
150+
int* open_count_;
151+
};
152+
153+
class CountingMockFileIO : public MockFileIO {
154+
public:
155+
Result<std::unique_ptr<InputFile>> NewInputFile(std::string file_location) override {
156+
ICEBERG_ASSIGN_OR_RAISE(auto input_file,
157+
MockFileIO::NewInputFile(std::move(file_location)));
158+
return std::make_unique<CountingInputFile>(std::move(input_file), &open_count);
159+
}
160+
161+
int open_count = 0;
162+
};
163+
130164
} // namespace
131165

132166
class ManifestWriterVersionsTest : public ::testing::Test {
@@ -410,6 +444,82 @@ class ManifestWriterVersionsTest : public ::testing::Test {
410444
std::shared_ptr<FileIO> file_io_{nullptr};
411445
};
412446

447+
TEST_F(ManifestWriterVersionsTest, SnapshotEntriesFollowContentCacheEviction) {
448+
auto file_io = std::make_shared<CountingMockFileIO>();
449+
auto write_manifest_list = [&](const std::string& location, int64_t snapshot_id) {
450+
auto writer_result = ManifestListWriter::MakeWriter(
451+
/*format_version=*/2, snapshot_id, /*parent_snapshot_id=*/std::nullopt, location,
452+
file_io, /*sequence_number=*/1);
453+
ASSERT_THAT(writer_result, IsOk());
454+
auto writer = std::move(writer_result).value();
455+
ManifestFile manifest{
456+
.manifest_path = location + ".manifest",
457+
.manifest_length = 10,
458+
.partition_spec_id = PartitionSpec::kInitialSpecId,
459+
.content = ManifestContent::kData,
460+
.sequence_number = 1,
461+
.min_sequence_number = 1,
462+
.added_snapshot_id = snapshot_id,
463+
.added_files_count = 1,
464+
.existing_files_count = 0,
465+
.deleted_files_count = 0,
466+
.added_rows_count = 1,
467+
.existing_rows_count = 0,
468+
.deleted_rows_count = 0,
469+
};
470+
ASSERT_THAT(writer->AddAll({manifest}), IsOk());
471+
ASSERT_THAT(writer->Close(), IsOk());
472+
};
473+
474+
const std::string first_location = "first-manifest-list.avro";
475+
const std::string second_location = "second-manifest-list.avro";
476+
write_manifest_list(first_location, /*snapshot_id=*/1);
477+
write_manifest_list(second_location, /*snapshot_id=*/2);
478+
const auto max_list_size = std::max(file_io->FileData(first_location).size(),
479+
file_io->FileData(second_location).size());
480+
ASSERT_THAT(file_io->ConfigureMetadataCache(
481+
{{std::string(MetadataCacheOptions::kEnabled), "true"},
482+
{std::string(MetadataCacheOptions::kMaxTotalBytes),
483+
std::to_string(max_list_size)},
484+
{std::string(MetadataCacheOptions::kMaxContentLength),
485+
std::to_string(max_list_size)}}),
486+
IsOk());
487+
488+
Snapshot first_snapshot{
489+
.snapshot_id = 1,
490+
.parent_snapshot_id = std::nullopt,
491+
.sequence_number = 1,
492+
.timestamp_ms = TimePointMs{},
493+
.manifest_list = first_location,
494+
};
495+
Snapshot second_snapshot{
496+
.snapshot_id = 2,
497+
.parent_snapshot_id = 1,
498+
.sequence_number = 2,
499+
.timestamp_ms = TimePointMs{},
500+
.manifest_list = second_location,
501+
};
502+
503+
SnapshotCache first_cache(&first_snapshot);
504+
ICEBERG_UNWRAP_OR_FAIL(auto first_manifests, first_cache.Manifests(file_io));
505+
ASSERT_EQ(first_manifests.size(), 1);
506+
EXPECT_EQ(file_io->open_count, 1);
507+
508+
SnapshotCache shared_first_cache(&first_snapshot);
509+
EXPECT_THAT(shared_first_cache.Manifests(file_io), IsOk());
510+
EXPECT_EQ(file_io->open_count, 1);
511+
512+
SnapshotCache second_cache(&second_snapshot);
513+
EXPECT_THAT(second_cache.Manifests(file_io), IsOk());
514+
EXPECT_EQ(file_io->open_count, 2);
515+
// Evicting the shared entry must not invalidate spans owned by an existing wrapper.
516+
EXPECT_EQ(first_manifests.front().manifest_path, first_location + ".manifest");
517+
518+
SnapshotCache reloaded_first_cache(&first_snapshot);
519+
EXPECT_THAT(reloaded_first_cache.Manifests(file_io), IsOk());
520+
EXPECT_EQ(file_io->open_count, 3);
521+
}
522+
413523
TEST_F(ManifestWriterVersionsTest, TestV1Write) {
414524
auto manifest = WriteManifest(/*format_version=*/1, {data_file_});
415525
CheckManifest(manifest, kInvalidSequenceNumber, kInvalidSequenceNumber);

0 commit comments

Comments
 (0)