| /* |
| * 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/io/cache/cache_manager.h" |
| |
| #include <cstdint> |
| #include <cstring> |
| #include <limits> |
| #include <memory> |
| #include <vector> |
| |
| #include "gtest/gtest.h" |
| #include "paimon/cache/cache.h" |
| #include "paimon/common/io/cache/cache_key.h" |
| #include "paimon/common/io/cache/lru_cache.h" |
| #include "paimon/memory/memory_pool.h" |
| #include "paimon/memory/memory_segment.h" |
| #include "paimon/testing/utils/testharness.h" |
| |
| namespace paimon::test { |
| |
| class CacheManagerTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| pool_ = GetDefaultPool(); |
| } |
| |
| std::shared_ptr<CacheKey> MakeKey(int64_t position, bool is_index = false) const { |
| return CacheKey::ForPosition("test_file", position, 64, is_index); |
| } |
| |
| MemorySegment MakeSegment(int32_t size, char fill_byte) const { |
| auto segment = MemorySegment::AllocateHeapMemory(size, pool_.get()); |
| std::memset(segment.MutableData(), fill_byte, size); |
| return segment; |
| } |
| |
| std::shared_ptr<LruCache> DataLru(const CacheManager& manager) const { |
| return std::dynamic_pointer_cast<LruCache>(manager.DataCache()); |
| } |
| |
| std::shared_ptr<LruCache> IndexLru(const CacheManager& manager) const { |
| return std::dynamic_pointer_cast<LruCache>(manager.IndexCache()); |
| } |
| |
| private: |
| std::shared_ptr<MemoryPool> pool_; |
| }; |
| |
| /// Regression test for the double->int64_t conversions in the CacheManager constructor: |
| /// (double)INT64_MAX rounds to 2^63, which is not representable as int64_t, so casting the |
| /// product back is undefined behavior (x86 cvttsd2si yields INT64_MIN, aarch64 fcvtzs |
| /// saturates to INT64_MAX). The conversion must saturate, keeping the capacity non-negative. |
| TEST_F(CacheManagerTest, TestCapacitySaturatesAtInt64Max) { |
| CacheManager manager(std::numeric_limits<int64_t>::max(), /*high_priority_pool_ratio=*/0.0); |
| |
| std::shared_ptr<LruCache> data_lru = DataLru(manager); |
| ASSERT_NE(data_lru, nullptr); |
| ASSERT_GE(data_lru->GetMaxWeight(), 0); |
| ASSERT_EQ(data_lru->GetMaxWeight(), std::numeric_limits<int64_t>::max()); |
| |
| // A ratio of 0.0 means index and data share the same cache. |
| ASSERT_EQ(manager.DataCache(), manager.IndexCache()); |
| |
| // The saturated capacity accepts entries instead of rejecting every insert. |
| std::shared_ptr<CacheKey> key = MakeKey(0); |
| auto reader = [&](const std::shared_ptr<CacheKey>&) -> Result<MemorySegment> { |
| return MakeSegment(64, 'A'); |
| }; |
| ASSERT_OK_AND_ASSIGN(MemorySegment segment, manager.GetPage(key, reader, {})); |
| ASSERT_EQ(segment.Size(), 64); |
| ASSERT_EQ(segment.Get(0), 'A'); |
| } |
| |
| /// Verifies the exact capacity split between the data and index caches for a normal |
| /// configuration, plus a Get/Invalidate smoke path through CacheManager::GetPage. |
| TEST_F(CacheManagerTest, TestNormalSplitAndSmokePath) { |
| CacheManager manager(/*max_memory_bytes=*/1024, /*high_priority_pool_ratio=*/0.5); |
| |
| std::shared_ptr<LruCache> data_lru = DataLru(manager); |
| std::shared_ptr<LruCache> index_lru = IndexLru(manager); |
| ASSERT_NE(data_lru, nullptr); |
| ASSERT_NE(index_lru, nullptr); |
| ASSERT_EQ(data_lru->GetMaxWeight(), 512); |
| ASSERT_EQ(index_lru->GetMaxWeight(), 512); |
| |
| std::shared_ptr<CacheKey> key = MakeKey(0); |
| int32_t reader_calls = 0; |
| auto reader = [&](const std::shared_ptr<CacheKey>&) -> Result<MemorySegment> { |
| reader_calls++; |
| return MakeSegment(128, 'B'); |
| }; |
| |
| // The first GetPage is a miss and invokes the reader; the second is a cache hit. |
| ASSERT_OK_AND_ASSIGN(MemorySegment first, manager.GetPage(key, reader, {})); |
| ASSERT_EQ(first.Get(0), 'B'); |
| ASSERT_EQ(reader_calls, 1); |
| ASSERT_OK_AND_ASSIGN(MemorySegment second, manager.GetPage(key, reader, {})); |
| ASSERT_EQ(second.Get(0), 'B'); |
| ASSERT_EQ(reader_calls, 1); |
| |
| // After InvalidPage the reader is invoked again. |
| manager.InvalidPage(key); |
| ASSERT_OK_AND_ASSIGN(MemorySegment third, manager.GetPage(key, reader, {})); |
| ASSERT_EQ(third.Get(0), 'B'); |
| ASSERT_EQ(reader_calls, 2); |
| } |
| |
| /// Verifies weight-based eviction through GetPage: inserting beyond the data cache capacity |
| /// evicts the least recently used page and runs its eviction callback. |
| TEST_F(CacheManagerTest, TestGetPageEviction) { |
| // The data cache capacity is 512 * (1.0 - 0.5) = 256 bytes. |
| CacheManager manager(/*max_memory_bytes=*/512, /*high_priority_pool_ratio=*/0.5); |
| |
| std::vector<int64_t> evicted; |
| auto callback_for = [&evicted](int64_t position) -> CacheCallback { |
| return |
| [&evicted, position](const std::shared_ptr<CacheKey>&) { evicted.push_back(position); }; |
| }; |
| auto reader = [&](const std::shared_ptr<CacheKey>&) -> Result<MemorySegment> { |
| return MakeSegment(128, 'C'); |
| }; |
| |
| std::shared_ptr<CacheKey> key0 = MakeKey(0); |
| std::shared_ptr<CacheKey> key1 = MakeKey(1); |
| std::shared_ptr<CacheKey> key2 = MakeKey(2); |
| ASSERT_OK_AND_ASSIGN(MemorySegment segment0, manager.GetPage(key0, reader, callback_for(0))); |
| ASSERT_EQ(segment0.Get(0), 'C'); |
| ASSERT_OK_AND_ASSIGN(MemorySegment segment1, manager.GetPage(key1, reader, callback_for(1))); |
| ASSERT_EQ(segment1.Get(0), 'C'); |
| ASSERT_TRUE(evicted.empty()); |
| |
| // 128 + 128 + 128 > 256: inserting key2 evicts key0, the least recently used page. |
| ASSERT_OK_AND_ASSIGN(MemorySegment segment2, manager.GetPage(key2, reader, callback_for(2))); |
| ASSERT_EQ(segment2.Get(0), 'C'); |
| ASSERT_EQ(evicted, std::vector<int64_t>({0})); |
| ASSERT_EQ(manager.DataCache()->Size(), 2); |
| } |
| |
| } // namespace paimon::test |