blob: 2c2e36cff41ebdb71eb6d4522767fa21aa936cb8 [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_input_stream.h"
#include <fstream>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "paimon/common/factories/io_hook.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/fs/file_system.h"
#include "paimon/fs/file_system_factory.h"
#include "paimon/io/byte_array_input_stream.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/testing/utils/io_exception_helper.h"
#include "paimon/testing/utils/testharness.h"
#include "paimon/utils/read_ahead_cache.h"
namespace paimon::test {
class CacheInputStreamTest : public ::testing::Test {
public:
void SetUp() override {
pool_ = GetDefaultPool();
test_dir_ = UniqueTestDirectory::Create();
ASSERT_TRUE(test_dir_);
content_ = "abcdefghijklmnopqrstuvwxyz0123456789";
file_path_ = test_dir_->Str() + "/test_data";
std::ofstream file(file_path_, std::ios::binary);
ASSERT_TRUE(file.is_open());
file.write(content_.data(), content_.size());
file.close();
}
std::unique_ptr<InputStream> OpenFile() const {
EXPECT_OK_AND_ASSIGN(std::shared_ptr<FileSystem> fs,
FileSystemFactory::Get("local", file_path_, {}));
EXPECT_OK_AND_ASSIGN(std::unique_ptr<InputStream> in, fs->Open(file_path_));
return in;
}
std::shared_ptr<ReadAheadCache> CreateCache(std::vector<ByteRange> ranges) {
auto stream = OpenFile();
CacheConfig config(/*buffer_size_limit=*/1024 * 1024, /*range_size_limit=*/1024,
/*hole_size_limit=*/0, /*pre_buffer_limit=*/1024 * 1024);
auto cache = std::make_shared<ReadAheadCache>(std::move(stream), config, pool_);
EXPECT_OK(cache->Init(std::move(ranges)));
return cache;
}
protected:
std::shared_ptr<MemoryPool> pool_;
std::unique_ptr<UniqueTestDirectory> test_dir_;
std::string content_;
std::string file_path_;
};
// Test proxy methods: Seek, GetPos, Read (sequential), Close, GetUri, Length
TEST_F(CacheInputStreamTest, TestProxyMethods) {
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), /*cache=*/nullptr);
// Length
ASSERT_OK_AND_ASSIGN(uint64_t length, stream.Length());
ASSERT_EQ(length, content_.size());
// GetUri
ASSERT_OK_AND_ASSIGN(std::string uri, stream.GetUri());
ASSERT_FALSE(uri.empty());
// Seek + GetPos
ASSERT_OK(stream.Seek(5, SeekOrigin::FS_SEEK_SET));
ASSERT_OK_AND_ASSIGN(int64_t pos, stream.GetPos());
ASSERT_EQ(pos, 5);
// Read (sequential, no offset)
std::string buffer(3, '\0');
ASSERT_OK_AND_ASSIGN(int32_t bytes_read, stream.Read(buffer.data(), 3));
ASSERT_EQ(bytes_read, 3);
ASSERT_EQ(buffer, "fgh");
// Close
ASSERT_OK(stream.Close());
}
// Test Read(offset) with cache == nullptr → direct fallback to input_stream
TEST_F(CacheInputStreamTest, TestReadWithOffsetNullCache) {
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), /*cache=*/nullptr);
std::string buffer(5, '\0');
ASSERT_OK_AND_ASSIGN(int32_t bytes_read, stream.Read(buffer.data(), 5, /*offset=*/2));
ASSERT_EQ(bytes_read, 5);
ASSERT_EQ(buffer, "cdefg");
}
// Test Read(offset) with cache hit → memcpy from cache
TEST_F(CacheInputStreamTest, TestReadWithOffsetCacheHit) {
// Cache range [2, 5) i.e. offset=2, length=5
auto cache = CreateCache({{2, 5}});
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), cache);
std::string buffer(5, '\0');
ASSERT_OK_AND_ASSIGN(int32_t bytes_read, stream.Read(buffer.data(), 5, /*offset=*/2));
ASSERT_EQ(bytes_read, 5);
ASSERT_EQ(buffer, "cdefg");
}
// Test Read(offset) with cache miss → fallback to input_stream
TEST_F(CacheInputStreamTest, TestReadWithOffsetCacheMiss) {
// Cache range [2, 5) but read from offset 10 which is not cached
auto cache = CreateCache({{2, 5}});
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), cache);
std::string buffer(3, '\0');
ASSERT_OK_AND_ASSIGN(int32_t bytes_read, stream.Read(buffer.data(), 3, /*offset=*/10));
ASSERT_EQ(bytes_read, 3);
ASSERT_EQ(buffer, "klm");
}
// Test ReadAsync with cache == nullptr → direct fallback to input_stream->ReadAsync
TEST_F(CacheInputStreamTest, TestReadAsyncNullCache) {
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), /*cache=*/nullptr);
std::string buffer(4, '\0');
bool callback_called = false;
Status callback_status = Status::Invalid("not called");
stream.ReadAsync(buffer.data(), 4, /*offset=*/0, [&](Status status) {
callback_called = true;
callback_status = status;
});
ASSERT_TRUE(callback_called);
ASSERT_OK(callback_status);
ASSERT_EQ(buffer, "abcd");
}
// Test ReadAsync with cache hit → memcpy + callback(OK)
TEST_F(CacheInputStreamTest, TestReadAsyncCacheHit) {
auto cache = CreateCache({{0, 10}});
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), cache);
std::string buffer(4, '\0');
bool callback_called = false;
Status callback_status = Status::Invalid("not called");
stream.ReadAsync(buffer.data(), 4, /*offset=*/3, [&](Status status) {
callback_called = true;
callback_status = status;
});
ASSERT_TRUE(callback_called);
ASSERT_OK(callback_status);
ASSERT_EQ(buffer, "defg");
}
// Test ReadAsync with cache miss → fallback to input_stream->ReadAsync
TEST_F(CacheInputStreamTest, TestReadAsyncCacheMiss) {
// Cache range [0, 5) but read from offset 20 which is not cached
auto cache = CreateCache({{0, 5}});
auto underlying = OpenFile();
CacheInputStream stream(std::move(underlying), cache);
std::string buffer(4, '\0');
bool callback_called = false;
Status callback_status = Status::Invalid("not called");
stream.ReadAsync(buffer.data(), 4, /*offset=*/20, [&](Status status) {
callback_called = true;
callback_status = status;
});
ASSERT_TRUE(callback_called);
ASSERT_OK(callback_status);
ASSERT_EQ(buffer, "uvwx");
}
// Test ReadAsync when cache_->Read() returns error status.
// Uses IOHook to inject IO error during ReadAheadCache's prefetch, causing cache_->Read() to fail.
TEST_F(CacheInputStreamTest, TestReadAsyncCacheReadError) {
auto io_hook = paimon::IOHook::GetInstance();
bool error_triggered = false;
for (size_t i = 0; i < 20; i++) {
// Open the cache stream and underlying stream BEFORE activating IOHook
ASSERT_OK_AND_ASSIGN(auto fs, FileSystemFactory::Get("local", file_path_, {}));
ASSERT_OK_AND_ASSIGN(auto cache_stream, fs->Open(file_path_));
ASSERT_OK_AND_ASSIGN(auto underlying, fs->Open(file_path_));
CacheConfig config(/*buffer_size_limit=*/1024 * 1024, /*range_size_limit=*/1024,
/*hole_size_limit=*/0, /*pre_buffer_limit=*/1024 * 1024);
auto cache = std::make_shared<ReadAheadCache>(std::move(cache_stream), config, pool_);
ASSERT_OK(cache->Init(std::vector<ByteRange>{{0, 10}}));
// Now activate IOHook so that the prefetch IO (triggered by cache_->Read -> PreBuffer)
// will fail at the i-th IO operation
paimon::ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
io_hook->Reset(i, paimon::IOHook::Mode::RETURN_ERROR);
CacheInputStream stream(std::move(underlying), cache);
std::string buffer(5, '\0');
bool callback_called = false;
Status callback_status = Status::OK();
stream.ReadAsync(buffer.data(), 5, /*offset=*/0, [&](Status status) {
callback_called = true;
callback_status = status;
});
ASSERT_TRUE(callback_called);
CHECK_HOOK_STATUS(callback_status, i);
ASSERT_EQ(buffer, "abcde");
error_triggered = true;
break;
}
ASSERT_TRUE(error_triggered);
}
} // namespace paimon::test