| /* |
| * 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 |