| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "paimon/common/sst/block_cache.h" |
| |
| #include <cstdint> |
| #include <cstring> |
| #include <memory> |
| #include <string> |
| |
| #include "gtest/gtest.h" |
| #include "paimon/common/io/cache/cache_manager.h" |
| #include "paimon/common/io/cache/lru_cache.h" |
| #include "paimon/memory/memory_pool.h" |
| #include "paimon/testing/utils/testharness.h" |
| namespace paimon::test { |
| |
| class BlockCacheTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| dir_ = UniqueTestDirectory::Create(); |
| fs_ = dir_->GetFileSystem(); |
| pool_ = GetDefaultPool(); |
| } |
| |
| void TearDown() override {} |
| |
| Status WriteTestFile(const std::string& path, int32_t num_blocks, int32_t block_size) const { |
| PAIMON_ASSIGN_OR_RAISE(auto out, fs_->Create(path, false)); |
| for (int32_t i = 0; i < num_blocks; i++) { |
| auto segment = MemorySegment::AllocateHeapMemory(block_size, pool_.get()); |
| std::memset(segment.MutableData(), i & 0xFF, block_size); |
| PAIMON_RETURN_NOT_OK(out->Write(segment.MutableData(), block_size)); |
| } |
| PAIMON_RETURN_NOT_OK(out->Flush()); |
| PAIMON_RETURN_NOT_OK(out->Close()); |
| return Status::OK(); |
| } |
| |
| Result<MemorySegment> GetBlock(int32_t block_id, int32_t block_size, BlockCache* block_cache, |
| bool is_index = false) const { |
| return block_cache->GetBlock(/*position=*/block_id * block_size, /*length=*/block_size, |
| is_index, |
| /*decompress_func=*/nullptr); |
| } |
| |
| bool ContainsBlock(int32_t block_id, int32_t block_size, BlockCache* block_cache, |
| bool is_index = false) const { |
| return block_cache->ContainsBlock(/*position=*/block_id * block_size, /*length=*/block_size, |
| is_index); |
| } |
| |
| private: |
| std::unique_ptr<UniqueTestDirectory> dir_; |
| std::shared_ptr<FileSystem> fs_; |
| std::shared_ptr<MemoryPool> pool_; |
| }; |
| |
| /// Verifies that the first GetBlock call reads from IO and subsequent calls return from the |
| /// local blocks_ cache without re-reading. |
| TEST_F(BlockCacheTest, TestBasicCacheHit) { |
| const int32_t block_size = 64; |
| const int32_t num_blocks = 4; |
| auto file_path = dir_->Str() + "/basic_hit.data"; |
| ASSERT_OK(WriteTestFile(file_path, num_blocks, block_size)); |
| |
| auto cache_manager = std::make_shared<CacheManager>(block_size * num_blocks * 2, 0.0); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(file_path)); |
| BlockCache block_cache(file_path, in, cache_manager, pool_); |
| |
| // Initially blocks_ is empty |
| ASSERT_EQ(block_cache.BlocksSize(), 0); |
| |
| // First access: populates both blocks_ and LRU |
| ASSERT_OK_AND_ASSIGN(auto seg1, GetBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(seg1.Size(), block_size); |
| ASSERT_EQ(seg1.Get(0), static_cast<char>(0)); |
| ASSERT_EQ(block_cache.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 1); |
| |
| // Second access: returns from blocks_, no new LRU entry |
| ASSERT_OK_AND_ASSIGN(auto seg2, GetBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(seg2.Size(), block_size); |
| ASSERT_EQ(block_cache.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 1); |
| |
| // Load a different block |
| ASSERT_OK_AND_ASSIGN(auto seg3, GetBlock(1, block_size, &block_cache)); |
| ASSERT_EQ(seg3.Get(0), static_cast<char>(1)); |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 2); |
| } |
| |
| /// Verifies that when LRU evicts an entry due to capacity pressure, the eviction callback |
| /// removes the corresponding entry from BlockCache's blocks_ map. |
| TEST_F(BlockCacheTest, TestLruEvictionSyncsWithBlocks) { |
| const int32_t block_size = 100; |
| auto file_path = dir_->Str() + "/eviction.data"; |
| ASSERT_OK(WriteTestFile(file_path, 5, block_size)); |
| |
| // Cache can hold at most 2 blocks (200 bytes) |
| auto cache_manager = std::make_shared<CacheManager>(block_size * 2, 0.0); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(file_path)); |
| BlockCache block_cache(file_path, in, cache_manager, pool_); |
| |
| // Load block 0 at position 0 |
| ASSERT_OK_AND_ASSIGN(auto seg0, GetBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(seg0.Get(0), static_cast<char>(0)); |
| ASSERT_EQ(block_cache.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| |
| // Load block 1 at position block_size |
| ASSERT_OK_AND_ASSIGN(auto seg1, GetBlock(1, block_size, &block_cache)); |
| ASSERT_EQ(seg1.Get(0), static_cast<char>(1)); |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache)); |
| |
| // Load block 2: evicts block 0 (LRU) from both LRU and blocks_ |
| ASSERT_OK_AND_ASSIGN(auto seg2, GetBlock(2, block_size, &block_cache)); |
| ASSERT_EQ(seg2.Get(0), static_cast<char>(2)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 2); |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| // block 0 should be evicted from blocks_ |
| ASSERT_FALSE(ContainsBlock(0, block_size, &block_cache)); |
| // block 1 and block 2 should remain |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(2, block_size, &block_cache)); |
| |
| // Re-access block 0: triggers fresh IO read, evicts block 1 (now LRU) |
| ASSERT_OK_AND_ASSIGN(auto seg0_reloaded, GetBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(seg0_reloaded.Get(0), static_cast<char>(0)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 2); |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| // block 1 should now be evicted |
| ASSERT_FALSE(ContainsBlock(1, block_size, &block_cache)); |
| // block 0 and block 2 should remain |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(2, block_size, &block_cache)); |
| } |
| |
| /// Verifies that Close() invalidates all entries from both blocks_ and the LRU cache. |
| TEST_F(BlockCacheTest, TestClose) { |
| const int32_t block_size = 64; |
| auto file_path = dir_->Str() + "/close.data"; |
| ASSERT_OK(WriteTestFile(file_path, 3, block_size)); |
| |
| auto cache_manager = std::make_shared<CacheManager>(block_size * 10, 0.0); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(file_path)); |
| BlockCache block_cache(file_path, in, cache_manager, pool_); |
| |
| // Load 3 blocks and verify blocks_ keys |
| for (int32_t i = 0; i < 3; i++) { |
| ASSERT_OK_AND_ASSIGN(auto seg, GetBlock(i, block_size, &block_cache)); |
| ASSERT_EQ(seg.Get(0), static_cast<char>(i)); |
| } |
| ASSERT_EQ(block_cache.BlocksSize(), 3); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(2, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 3); |
| |
| block_cache.Close(); |
| |
| // After Close, both blocks_ and LRU should be empty |
| ASSERT_EQ(block_cache.BlocksSize(), 0); |
| ASSERT_FALSE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_FALSE(ContainsBlock(1, block_size, &block_cache)); |
| ASSERT_FALSE(ContainsBlock(2, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 0); |
| } |
| |
| /// Verifies that two BlockCache instances sharing the same CacheManager have independent blocks_ |
| /// maps, but eviction in the shared LRU only affects the owning BlockCache's blocks_. |
| TEST_F(BlockCacheTest, TestSharedCacheManagerEvictionIsolation) { |
| const int32_t block_size = 100; |
| auto file_path_a = dir_->Str() + "/file_a.data"; |
| auto file_path_b = dir_->Str() + "/file_b.data"; |
| ASSERT_OK(WriteTestFile(file_path_a, 3, block_size)); |
| ASSERT_OK(WriteTestFile(file_path_b, 3, block_size)); |
| |
| // Shared cache can hold 3 blocks total |
| auto cache_manager = std::make_shared<CacheManager>(block_size * 3, 0.0); |
| |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in_a, fs_->Open(file_path_a)); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in_b, fs_->Open(file_path_b)); |
| BlockCache cache_a(file_path_a, in_a, cache_manager, pool_); |
| BlockCache cache_b(file_path_b, in_b, cache_manager, pool_); |
| |
| // Load 2 blocks from file_a |
| ASSERT_OK_AND_ASSIGN(auto seg_a0, GetBlock(0, block_size, &cache_a)); |
| ASSERT_OK_AND_ASSIGN(auto seg_a1, GetBlock(1, block_size, &cache_a)); |
| ASSERT_EQ(cache_a.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &cache_a)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &cache_a)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 2); |
| |
| // Load 1 block from file_b (total 3, at capacity) |
| ASSERT_OK_AND_ASSIGN(auto seg_b0, GetBlock(0, block_size, &cache_b)); |
| ASSERT_EQ(cache_b.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &cache_b)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 3); |
| |
| // Load another block from file_b: should evict file_a's block 0 (the LRU entry) |
| ASSERT_OK_AND_ASSIGN(auto seg_b1, GetBlock(1, block_size, &cache_b)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 3); |
| |
| // cache_b should have 2 entries |
| ASSERT_EQ(cache_b.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &cache_b)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &cache_b)); |
| |
| // cache_a's block 0 was evicted by LRU callback, only block 1 remains |
| ASSERT_EQ(cache_a.BlocksSize(), 1); |
| ASSERT_FALSE(ContainsBlock(0, block_size, &cache_a)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &cache_a)); |
| |
| // Re-access file_a's block 0: triggers fresh IO read, evicts file_a's block 1 (now LRU) |
| ASSERT_OK_AND_ASSIGN(auto seg_a0_reloaded, GetBlock(0, block_size, &cache_a)); |
| ASSERT_EQ(seg_a0_reloaded.Get(0), static_cast<char>(0)); |
| ASSERT_EQ(cache_a.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &cache_a)); |
| ASSERT_FALSE(ContainsBlock(1, block_size, &cache_a)); |
| |
| // cache_b should be unaffected |
| ASSERT_EQ(cache_b.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &cache_b)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &cache_b)); |
| |
| cache_a.Close(); |
| cache_b.Close(); |
| } |
| |
| /// Verifies the REFRESH_COUNT mechanism interacts correctly with LRU eviction ordering. |
| /// After refreshing a block (re-inserting into LRU front), it should not be the first to be |
| /// evicted when capacity pressure occurs. |
| TEST_F(BlockCacheTest, TestRefreshPreventsEviction) { |
| const int32_t block_size = 100; |
| auto file_path = dir_->Str() + "/refresh_eviction.data"; |
| ASSERT_OK(WriteTestFile(file_path, 4, block_size)); |
| |
| // Cache can hold 2 blocks |
| auto cache_manager = std::make_shared<CacheManager>(block_size * 2, 0.0); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(file_path)); |
| BlockCache block_cache(file_path, in, cache_manager, pool_); |
| |
| // Load block 0 and block 1 |
| ASSERT_OK_AND_ASSIGN(auto seg0, GetBlock(0, block_size, &block_cache)); |
| ASSERT_OK_AND_ASSIGN(auto seg1, GetBlock(1, block_size, &block_cache)); |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 2); |
| |
| // Access block 0 REFRESH_COUNT times to trigger a refresh (moves it to LRU front) |
| for (int32_t i = 1; i < CacheManager::REFRESH_COUNT; i++) { |
| ASSERT_OK_AND_ASSIGN(seg0, GetBlock(0, block_size, &block_cache)); |
| } |
| // This 11th access triggers refresh, moving block 0 to LRU front |
| ASSERT_OK_AND_ASSIGN(seg0, GetBlock(0, block_size, &block_cache)); |
| |
| // blocks_ should still have both entries after refresh |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache)); |
| |
| // Load block 2: should evict block 1 (not block 0, since block 0 was just refreshed) |
| ASSERT_OK_AND_ASSIGN(auto seg2, GetBlock(2, block_size, &block_cache)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 2); |
| ASSERT_EQ(block_cache.BlocksSize(), 2); |
| // block 0 should still be in blocks_ (was refreshed to LRU front) |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache)); |
| // block 1 should be evicted from blocks_ |
| ASSERT_FALSE(ContainsBlock(1, block_size, &block_cache)); |
| // block 2 should be in blocks_ |
| ASSERT_TRUE(ContainsBlock(2, block_size, &block_cache)); |
| |
| // Block 0 should still be accessible from blocks_ cache |
| ASSERT_OK_AND_ASSIGN(seg0, GetBlock(0, block_size, &block_cache)); |
| ASSERT_EQ(seg0.Get(0), static_cast<char>(0)); |
| |
| // Block 1 was evicted, re-accessing triggers IO read |
| ASSERT_OK_AND_ASSIGN(seg1, GetBlock(1, block_size, &block_cache)); |
| ASSERT_EQ(seg1.Get(0), static_cast<char>(1)); |
| } |
| |
| TEST_F(BlockCacheTest, TestIndexAndDataCache) { |
| const int32_t block_size = 100; |
| const int32_t num_blocks = 6; |
| auto file_path = dir_->Str() + "/index_and_data.data"; |
| ASSERT_OK(WriteTestFile(file_path, num_blocks, block_size)); |
| |
| // index cache max weight = 100 |
| // data cache max weight = 300 |
| auto cache_manager = std::make_shared<CacheManager>(block_size * 4, 0.25); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(file_path)); |
| BlockCache block_cache(file_path, in, cache_manager, pool_); |
| |
| // First access for seg0, index |
| ASSERT_OK_AND_ASSIGN(auto seg0, GetBlock(0, block_size, &block_cache, /*is_index=*/true)); |
| ASSERT_EQ(seg0.Get(0), static_cast<char>(0)); |
| ASSERT_EQ(block_cache.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(0, block_size, &block_cache, /*is_index=*/true)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 0); |
| ASSERT_EQ(cache_manager->IndexCache()->Size(), 1); |
| |
| // Second access for seg1, index, seg0 will be evicted |
| ASSERT_OK_AND_ASSIGN(auto seg1, GetBlock(1, block_size, &block_cache, /*is_index=*/true)); |
| ASSERT_EQ(seg1.Get(0), static_cast<char>(1)); |
| ASSERT_EQ(block_cache.BlocksSize(), 1); |
| ASSERT_TRUE(ContainsBlock(1, block_size, &block_cache, /*is_index=*/true)); |
| ASSERT_FALSE(ContainsBlock(0, block_size, &block_cache, /*is_index=*/true)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 0); |
| ASSERT_EQ(cache_manager->IndexCache()->Size(), 1); |
| |
| // Fills data cache |
| for (int32_t i = 2; i < 5; i++) { |
| ASSERT_OK_AND_ASSIGN(auto seg, GetBlock(i, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_EQ(seg.Get(0), static_cast<char>(i)); |
| ASSERT_EQ(block_cache.BlocksSize(), 1 + i - 1); |
| ASSERT_TRUE(ContainsBlock(i, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_EQ(cache_manager->IndexCache()->Size(), 1); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), i - 1); |
| } |
| |
| ASSERT_OK_AND_ASSIGN(auto seg5, GetBlock(5, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_EQ(seg5.Get(0), static_cast<char>(5)); |
| ASSERT_EQ(block_cache.BlocksSize(), 4); |
| ASSERT_TRUE(ContainsBlock(5, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_TRUE(ContainsBlock(4, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_TRUE(ContainsBlock(3, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_FALSE(ContainsBlock(2, block_size, &block_cache, /*is_index=*/false)); |
| ASSERT_EQ(cache_manager->DataCache()->Size(), 3); |
| ASSERT_EQ(cache_manager->IndexCache()->Size(), 1); |
| } |
| |
| } // namespace paimon::test |