blob: a74b352020c740021274e2a5baf3480eb57b2282 [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/common/utils/read_ahead_cache.h"
#include <algorithm>
#include <atomic>
#include <cassert>
#include <cstring>
#include <future>
#include <shared_mutex>
#include "paimon/common/utils/byte_range_combiner.h"
#include "paimon/common/utils/math.h"
#include "paimon/memory/bytes.h"
#include "paimon/metrics.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;
}
};
// Everything needed to dispatch the prefetch IO of an entry AFTER the entry
// has been published into entries_: the promise resolves the entry's future
// and the buffer capture keeps the destination alive for the async IO.
struct PendingFetch {
ByteRange range;
std::shared_ptr<Bytes> buffer;
std::shared_ptr<std::promise<Status>> promise;
};
namespace {
// Copy the requested window out of the covering entries into dest. The
// entries must fully cover the range and their futures must be resolved.
void CopyRangeFromEntries(const std::vector<RangeCacheEntry>& covering, const ByteRange& range,
char* dest) {
size_t pos = 0;
for (const auto& entry : covering) {
const uint64_t entry_end = entry.range.offset + entry.range.length;
const uint64_t copy_begin = std::max(range.offset, entry.range.offset);
const uint64_t copy_end = std::min(range.offset + range.length, entry_end);
const auto copy_len = static_cast<size_t>(copy_end - copy_begin);
std::memcpy(dest + pos, entry.buffer->data() + (copy_begin - entry.range.offset), copy_len);
pos += copy_len;
}
}
} // namespace
CacheConfig::CacheConfig(uint64_t range_size_limit, uint64_t hole_size_limit,
uint64_t pre_buffer_limit)
: range_size_limit_(range_size_limit),
hole_size_limit_(hole_size_limit),
pre_buffer_limit_(pre_buffer_limit) {}
CacheConfig::CacheConfig()
// Aligned with the reader's request granularity and with realistic data
// file sizes:
// - range_size_limit matches the parquet reader's 32 MiB request blocks
// (Arrow ReadRangeCache's own range limit); a smaller limit cuts entries
// below the request size, so a request can never be served from one piece.
// - pre_buffer_limit must exceed the LARGEST single read a reader issues
// (coalesced column-chunk reads of ~128 MiB were observed): fetches are
// only dispatched up to this window, so a request reaching past it can
// never be served and falls back to a second fetch of the same bytes.
: CacheConfig(/*range_size_limit=*/32 * 1024 * 1024,
/*hole_size_limit=*/8 * 1024,
/*pre_buffer_limit=*/256 * 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<bool> Read(const ByteRange& range, char* dest);
void Reset();
void ReleaseBuffers();
void Warmup();
void CollectMetrics(std::shared_ptr<Metrics>* metrics) const;
private:
/// Dispatch the prefetch IOs for entries that have already been published
/// into entries_.
void DispatchFetches(const std::vector<PendingFetch>& fetches);
/// Find the entries fully covering the given range under the read lock.
/// Returns an empty vector on miss. Entries are copied (shared buffers)
/// so the caller may use them after releasing the lock.
std::vector<RangeCacheEntry> FindCoveringEntries(const ByteRange& range);
void PreBuffer(uint64_t offset);
void CountHit(uint64_t size) {
hits_.fetch_add(1, std::memory_order_relaxed);
hit_bytes_.fetch_add(size, std::memory_order_relaxed);
}
void CountMiss(uint64_t size) {
misses_.fetch_add(1, std::memory_order_relaxed);
miss_bytes_.fetch_add(size, std::memory_order_relaxed);
}
/// Mark, publish and fetch the pending ranges at the given indices.
///
/// Marking is_cached_ and publishing the promise-backed entries happen
/// atomically under the write lock, before any IO is dispatched, so a
/// reader racing the prefetch waits on the in-flight entries instead of
/// re-fetching the same bytes.
void Cache(std::vector<size_t> pending_indices);
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;
// Statistics of the Read() requests issued to the cache, aggregated over
// all streams sharing this cache.
std::atomic<uint64_t> read_count_{0};
std::atomic<uint64_t> read_bytes_{0};
std::atomic<uint64_t> hits_{0};
std::atomic<uint64_t> hit_bytes_{0};
std::atomic<uint64_t> misses_{0};
std::atomic<uint64_t> miss_bytes_{0};
// Prefetch IO statistics: how many requests and bytes were actually issued
// to the underlying stream.
std::atomic<uint64_t> io_count_{0};
std::atomic<uint64_t> io_bytes_{0};
};
void ReadAheadCache::Impl::Cache(std::vector<size_t> pending_indices) {
std::vector<RangeCacheEntry> new_entries;
std::vector<PendingFetch> fetches;
// Mark is_cached_, publish the promise-backed entries and only then
// dispatch the IOs. The mark and the publication happen atomically under
// the write lock: a reader racing the prefetch observes is_cached_=true
// only once the covering entries are already visible, so it waits on
// their futures instead of issuing a duplicate underlying read.
{
std::unique_lock<std::shared_mutex> lock(rw_mutex_);
for (size_t idx : pending_indices) {
if (is_cached_[idx].exchange(true)) {
continue;
}
const ByteRange& range = pending_ranges_[idx];
auto promise = std::make_shared<std::promise<Status>>();
auto future = promise->get_future();
auto buffer = std::make_shared<Bytes>(range.length, memory_pool_.get());
fetches.push_back({range, buffer, promise});
new_entries.emplace_back(range, std::move(buffer), std::move(future));
}
if (!new_entries.empty()) {
// Entries are never evicted: the cache holds every published
// range until ReleaseBuffers()/Reset(), so an in-flight fetch
// always keeps its entry and thus its future reachable.
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);
}
}
DispatchFetches(fetches);
}
Status ReadAheadCache::Impl::Init(std::vector<ByteRange>&& ranges) {
if (is_initialized_) {
return Status::Invalid("Cache has already been initialized");
}
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) {
PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(pending_range.offset, "range offset"));
PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(pending_range.length, "range length"));
}
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<size_t> pending_indices;
size_t total_bytes = 0;
for (size_t i = start_idx; i < pending_ranges_.size(); ++i) {
total_bytes += pending_ranges_[i].length;
if (total_bytes > config_.GetPreBufferLimit()) {
break;
}
pending_indices.push_back(i);
}
if (!pending_indices.empty()) {
Cache(std::move(pending_indices));
}
}
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() {
ReleaseBuffers();
read_count_.store(0, std::memory_order_relaxed);
read_bytes_.store(0, std::memory_order_relaxed);
hits_.store(0, std::memory_order_relaxed);
hit_bytes_.store(0, std::memory_order_relaxed);
misses_.store(0, std::memory_order_relaxed);
miss_bytes_.store(0, std::memory_order_relaxed);
io_count_.store(0, std::memory_order_relaxed);
io_bytes_.store(0, std::memory_order_relaxed);
}
void ReadAheadCache::Impl::ReleaseBuffers() {
std::unique_lock<std::shared_mutex> lock(rw_mutex_);
// Entries are never evicted, so waiting on entries_ covers every
// dispatched fetch: no async callback can outlive the stream or the
// memory pool its buffer belongs to.
for (auto& entry : entries_) {
entry.future.wait();
}
entries_.clear();
is_cached_.clear();
pending_ranges_.clear();
is_initialized_ = false;
// The read/io counters are deliberately kept: a reader closed at EOF must
// still be able to report them through CollectMetrics().
}
void ReadAheadCache::Impl::CollectMetrics(std::shared_ptr<Metrics>* metrics) const {
if (metrics == nullptr || !*metrics) {
return;
}
auto& m = *metrics;
m->SetCounter(ReadAheadCacheMetrics::READ_COUNT, read_count_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::READ_BYTES, read_bytes_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::READ_HITS, hits_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::READ_HIT_BYTES,
hit_bytes_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::READ_MISSES, misses_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::READ_MISS_BYTES,
miss_bytes_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::IO_COUNT, io_count_.load(std::memory_order_relaxed));
m->SetCounter(ReadAheadCacheMetrics::IO_BYTES, io_bytes_.load(std::memory_order_relaxed));
}
void ReadAheadCache::Impl::Warmup() {
// Init() only registers the pending ranges; without this the first fetch
// starts when the first Read() arrives, racing the reader's own miss fetch.
if (!pending_ranges_.empty()) {
PreBuffer(pending_ranges_.front().offset);
}
}
std::vector<RangeCacheEntry> ReadAheadCache::Impl::FindCoveringEntries(const ByteRange& range) {
std::vector<RangeCacheEntry> covering;
std::shared_lock<std::shared_mutex> lock(rw_mutex_);
// Find the entry holding the start of the range: the first entry whose
// end is beyond range.offset (entries are disjoint and sorted by offset).
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.offset > range.offset) {
return covering;
}
if (it->range.Contains(range)) {
covering.push_back(*it);
return covering;
}
// The request spans several adjacent entries (a column chunk larger than
// one coalesced range): collect the contiguous run and check it covers
// the whole request. Entries are published before their fetch is
// dispatched, so a reader racing the prefetch waits for the in-flight
// fetch instead of issuing a second one for the same bytes.
uint64_t covered_end = it->range.offset + it->range.length;
covering.push_back(*it);
auto next = std::next(it);
while (covered_end < range.offset + range.length && next != entries_.end() &&
next->range.offset == covered_end) {
covered_end = next->range.offset + next->range.length;
covering.push_back(*next);
++next;
}
if (covered_end < range.offset + range.length) {
covering.clear();
}
return covering;
}
Result<bool> ReadAheadCache::Impl::Read(const ByteRange& range, char* dest) {
if (range.length == 0) {
return true;
}
read_count_.fetch_add(1, std::memory_order_relaxed);
read_bytes_.fetch_add(range.length, std::memory_order_relaxed);
PreBuffer(range.offset);
std::vector<RangeCacheEntry> covering = FindCoveringEntries(range);
if (covering.empty()) {
CountMiss(range.length);
return false;
}
// Wait OUTSIDE the lock: the futures resolve when the prefetch stream's
// async reads complete, and holding rw_mutex_ would block Cache().
for (const auto& entry : covering) {
PAIMON_RETURN_NOT_OK(entry.future.get());
}
// The data copy runs OUTSIDE the lock for the same reason.
CopyRangeFromEntries(covering, range, dest);
CountHit(range.length);
return true;
}
void ReadAheadCache::Impl::DispatchFetches(const std::vector<PendingFetch>& fetches) {
for (const auto& fetch : fetches) {
auto promise = fetch.promise;
auto buffer = fetch.buffer;
auto read_size = static_cast<int64_t>(buffer->size());
auto read_offset = static_cast<int64_t>(fetch.range.offset);
stream_->ReadAsync(
buffer->data(), read_size, read_offset,
[promise, buffer](Status status) mutable { promise->set_value(status); });
io_count_.fetch_add(1, std::memory_order_relaxed);
io_bytes_.fetch_add(fetch.range.length, std::memory_order_relaxed);
}
}
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<bool> ReadAheadCache::Read(const ByteRange& range, char* dest) {
return impl_->Read(range, dest);
}
void ReadAheadCache::Reset() {
return impl_->Reset();
}
void ReadAheadCache::ReleaseBuffers() {
return impl_->ReleaseBuffers();
}
void ReadAheadCache::Warmup() {
impl_->Warmup();
}
void ReadAheadCache::CollectMetrics(std::shared_ptr<Metrics>* metrics) const {
impl_->CollectMetrics(metrics);
}
} // namespace paimon