blob: aa7e5cb613b92b63d86e994f52255b3de2620d10 [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.
*/
// Adapted from Apache ORC
// https://github.com/apache/orc/blob/main/c%2B%2B/src/io/Cache.cc
#include "paimon/utils/read_ahead_cache.h"
#include <algorithm>
#include <cassert>
#include <future>
#include <shared_mutex>
#include "paimon/common/utils/byte_range_combiner.h"
namespace paimon {
struct RangeCacheEntry {
ByteRange range;
std::shared_ptr<Bytes> buffer;
std::shared_future<Status> future; // use shared_future in case of multiple get calls
RangeCacheEntry() = default;
RangeCacheEntry(const ByteRange& range, std::shared_ptr<Bytes> buffer,
std::future<Status> future)
: range(range), buffer(std::move(buffer)), future(std::move(future).share()) {}
friend bool operator<(const RangeCacheEntry& left, const RangeCacheEntry& right) {
return left.range.offset < right.range.offset;
}
};
CacheConfig::CacheConfig(uint64_t buffer_size_limit, uint64_t range_size_limit,
uint64_t hole_size_limit, uint64_t pre_buffer_limit)
: buffer_size_limit_(buffer_size_limit),
range_size_limit_(range_size_limit),
hole_size_limit_(hole_size_limit),
pre_buffer_limit_(pre_buffer_limit) {}
CacheConfig::CacheConfig()
: CacheConfig(/*buffer_size_limit=*/512 * 1024 * 1024,
/*range_size_limit=*/16 * 1024 * 1024,
/*hole_size_limit=*/8 * 1024,
/*pre_buffer_limit=*/128 * 1024 * 1024) {}
class ReadAheadCache::Impl {
public:
Impl(const std::shared_ptr<InputStream>& stream, const CacheConfig& config,
const std::shared_ptr<MemoryPool>& memory_pool);
~Impl();
Status Init(std::vector<ByteRange>&& ranges);
Result<ByteSlice> Read(const ByteRange& range);
void Reset();
private:
std::vector<RangeCacheEntry> MakeCacheEntries(const std::vector<ByteRange>& ranges) const;
void PreBuffer(uint64_t offset);
/// Cache the given ranges in the background.
///
/// The caller must ensure that the ranges do not overlap with each other,
/// nor with previously cached ranges. Otherwise, behaviour will be undefined.
void Cache(std::vector<ByteRange> ranges);
std::shared_ptr<InputStream> stream_;
CacheConfig config_;
// Ordered by offset (so as to find a matching region by binary search)
std::vector<RangeCacheEntry> entries_;
std::shared_ptr<MemoryPool> memory_pool_;
std::shared_mutex rw_mutex_;
std::vector<std::atomic<bool>> is_cached_;
std::vector<ByteRange> pending_ranges_;
bool is_initialized_ = false;
};
void ReadAheadCache::Impl::Cache(std::vector<ByteRange> ranges) {
std::sort(ranges.begin(), ranges.end(),
[](const ByteRange& a, const ByteRange& b) { return a.offset < b.offset; });
std::vector<RangeCacheEntry> new_entries = MakeCacheEntries(ranges);
// Add new entries, themselves ordered by offset
std::unique_lock<std::shared_mutex> lock(rw_mutex_);
if (entries_.size() > 0) {
size_t new_entries_size = 0;
for (const auto& e : new_entries) {
new_entries_size += e.range.length;
}
size_t total_size = 0;
for (const auto& e : entries_) {
total_size += e.range.length;
}
size_t limit = config_.GetBufferSizeLimit();
while (!entries_.empty() && total_size + new_entries_size > limit) {
auto iter = entries_.begin();
total_size -= entries_.front().range.length;
entries_.erase(iter);
}
std::vector<RangeCacheEntry> merged(entries_.size() + new_entries.size());
std::merge(entries_.begin(), entries_.end(), new_entries.begin(), new_entries.end(),
merged.begin());
entries_ = std::move(merged);
} else {
entries_ = std::move(new_entries);
}
}
Status ReadAheadCache::Impl::Init(std::vector<ByteRange>&& ranges) {
if (is_initialized_) {
return Status::Invalid("Cache has already been initialized");
}
if (config_.GetRangeSizeLimit() > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
return Status::Invalid("CacheConfig range_size_limit exceeds uint32_t max");
}
PAIMON_ASSIGN_OR_RAISE(
std::vector<ByteRange> pending_ranges,
ByteRangeCombiner::CoalesceByteRanges(std::move(ranges), config_.GetHoleSizeLimit(),
config_.GetRangeSizeLimit()));
for (const auto& pending_range : pending_ranges) {
if (pending_range.length > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
return Status::Invalid("range length should not be larger than uint32_t max");
}
}
pending_ranges_ = pending_ranges;
is_cached_ = std::vector<std::atomic<bool>>(pending_ranges_.size());
for (auto& is_cached : is_cached_) {
is_cached.store(false);
}
is_initialized_ = true;
return Status::OK();
}
void ReadAheadCache::Impl::PreBuffer(uint64_t offset) {
auto it = std::lower_bound(pending_ranges_.begin(), pending_ranges_.end(), offset,
[](const ByteRange& range, uint64_t offset) {
return range.offset + range.length <= offset;
});
if (it == pending_ranges_.end() || it->offset > offset) {
return;
}
size_t start_idx = std::distance(pending_ranges_.begin(), it);
std::vector<ByteRange> ranges;
size_t total_bytes = 0;
for (size_t i = start_idx; i < pending_ranges_.size(); ++i) {
size_t range_size = pending_ranges_[i].length;
total_bytes += range_size;
if (total_bytes > config_.GetPreBufferLimit()) {
break;
}
if (is_cached_[i].exchange(true)) {
continue;
}
ranges.emplace_back(pending_ranges_[i]);
}
if (!ranges.empty()) {
Cache(std::move(ranges));
}
}
ReadAheadCache::Impl::Impl(const std::shared_ptr<InputStream>& stream, const CacheConfig& config,
const std::shared_ptr<MemoryPool>& memory_pool)
: stream_(stream), config_(config), memory_pool_(memory_pool) {}
ReadAheadCache::Impl::~Impl() {
std::unique_lock<std::shared_mutex> lock(rw_mutex_);
for (auto& entry : entries_) {
entry.future.wait();
}
}
void ReadAheadCache::Impl::Reset() {
std::unique_lock<std::shared_mutex> lock(rw_mutex_);
for (auto& entry : entries_) {
entry.future.wait();
}
entries_.clear();
is_cached_.clear();
pending_ranges_.clear();
is_initialized_ = false;
}
Result<ByteSlice> ReadAheadCache::Impl::Read(const ByteRange& range) {
if (range.length == 0) {
return ByteSlice{std::make_shared<Bytes>(0, memory_pool_.get()), 0, 0};
}
PreBuffer(range.offset);
ByteSlice result{};
{
std::shared_lock<std::shared_mutex> lock(rw_mutex_);
auto it = std::lower_bound(entries_.begin(), entries_.end(), range.offset,
[](const RangeCacheEntry& e, uint64_t offset) {
return e.range.offset + e.range.length <= offset;
});
if (it != entries_.end() && it->range.Contains(range)) {
PAIMON_RETURN_NOT_OK(it->future.get());
result = ByteSlice{it->buffer, range.offset - it->range.offset, range.length};
return result;
}
}
return result;
}
std::vector<RangeCacheEntry> ReadAheadCache::Impl::MakeCacheEntries(
const std::vector<ByteRange>& ranges) const {
std::vector<RangeCacheEntry> new_entries;
new_entries.reserve(ranges.size());
for (const auto& range : ranges) {
auto promise = std::make_shared<std::promise<Status>>();
auto future = promise->get_future();
auto buffer = std::make_shared<Bytes>(range.length, memory_pool_.get());
stream_->ReadAsync(
buffer->data(), static_cast<uint32_t>(buffer->size()), range.offset,
[promise, buffer](Status status) mutable { promise->set_value(status); });
new_entries.emplace_back(range, std::move(buffer), std::move(future));
}
return new_entries;
}
ReadAheadCache::ReadAheadCache(const std::shared_ptr<InputStream>& stream,
const CacheConfig& config,
const std::shared_ptr<MemoryPool>& memory_pool)
: impl_(std::make_unique<Impl>(stream, config, memory_pool)) {}
ReadAheadCache::~ReadAheadCache() = default;
Status ReadAheadCache::Init(std::vector<ByteRange>&& ranges) {
return impl_->Init(std::move(ranges));
}
Result<ByteSlice> ReadAheadCache::Read(const ByteRange& range) {
return impl_->Read(range);
}
void ReadAheadCache::Reset() {
return impl_->Reset();
}
} // namespace paimon