blob: 4d7c180a3fb67f077161ed987d134a11464b84ef [file]
/*
* 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