blob: 453a76eba8c7dd9846de8831db1296a00a3e2b31 [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/utils/file_block_cache.h"
#include <cstring>
#include <mutex>
#include <shared_mutex>
#include "paimon/common/memory/bytes_utils.h"
namespace paimon {
FileBlockCache::FileBlockCache(const std::shared_ptr<InputStream>& stream, uint64_t file_size,
uint64_t block_size, uint64_t capacity,
const std::shared_ptr<MemoryPool>& memory_pool)
: stream_(stream),
file_size_(file_size),
block_size_(block_size),
capacity_(capacity),
memory_pool_(memory_pool) {}
FileBlockCache::~FileBlockCache() {
// The fetches write into the block buffers, so they must not outlive the
// stream they read from.
std::lock_guard<std::shared_mutex> lock(mutex_);
for (auto& block : blocks_) {
block.second->future.wait();
}
}
bool FileBlockCache::Read(const ByteRange& range, char* dest) {
if (!CanServe(range)) {
return false;
}
const uint64_t index = IndexOf(range.offset);
std::shared_ptr<Block> block;
bool dispatch = false;
// Hit fast path under the read lock: concurrent reads of already-cached blocks
// proceed in parallel instead of serializing on an exclusive lock.
{
std::shared_lock<std::shared_mutex> read_lock(mutex_);
auto it = blocks_.find(index);
if (it != blocks_.end()) {
block = it->second;
}
}
if (!block) {
// Miss: take the write lock and re-check, since another reader may have
// inserted the block while the read lock was dropped. Publishing the
// promise-backed block under the lock before its fetch is dispatched is what
// makes concurrent readers of the same block wait for that one fetch instead
// of issuing their own.
std::unique_lock<std::shared_mutex> write_lock(mutex_);
auto it = blocks_.find(index);
if (it != blocks_.end()) {
block = it->second;
} else {
const ByteRange block_range = RangeOf(index);
// Blocks are never evicted, so an exhausted capacity means this read
// goes back to the caller instead of replacing a cached block.
if (cached_bytes_ + block_range.length > capacity_) {
return false;
}
block = std::make_shared<Block>();
block->range = block_range;
// The buffer keeps the pool alive, for the fetch callbacks that a
// stream destroys later than it resolves them.
block->buffer = AllocateBytesKeepingPoolAlive(block_range.length, memory_pool_);
block->promise = std::make_shared<std::promise<Status>>();
block->future = block->promise->get_future().share();
blocks_.emplace(index, block);
cached_bytes_ += block_range.length;
dispatch = true;
}
}
if (dispatch) {
Fetch(block);
}
// Wait and copy OUTSIDE the lock, so that a reader waiting for a fetch does
// not keep the other readers out of the map.
if (!block->future.get().ok()) {
// A block fetch reads more than the caller asked for, so its failure must
// not fail the caller's read: the read goes back to the caller, which
// reports the real error itself if its own bytes cannot be read either.
// This is what keeps a block whose range the file does not have - the
// file metadata records a size larger than the physical file - from
// failing the reads of the region it covers.
//
// The block is left in place with its failed future, so the later reads
// of that block are declined without fetching it again.
return false;
}
std::memcpy(dest, block->buffer->data() + (range.offset - block->range.offset), range.length);
hit_metrics_.Record(range.length);
return true;
}
void FileBlockCache::Release() {
std::unordered_map<uint64_t, std::shared_ptr<Block>> to_release;
{
std::lock_guard<std::shared_mutex> lock(mutex_);
// Swap the blocks out under the lock instead of waiting on them in place: an
// in-flight fetch may take arbitrarily long, and holding the lock while
// waiting would stall concurrent readers.
to_release.swap(blocks_);
cached_bytes_ = 0;
}
// Wait OUTSIDE the lock. Blocks are never evicted, so this covers every
// dispatched fetch before the buffers they write into go away.
for (auto& block : to_release) {
block.second->future.wait();
}
}
void FileBlockCache::ResetCounters() {
hit_metrics_.Reset();
fetch_metrics_.Reset();
}
FileBlockCache::Counters FileBlockCache::GetCounters() const {
Counters counters;
counters.hits = hit_metrics_.Count();
counters.hit_bytes = hit_metrics_.Bytes();
counters.fetches = fetch_metrics_.Count();
counters.fetch_bytes = fetch_metrics_.Bytes();
return counters;
}
bool FileBlockCache::CanServe(const ByteRange& range) const {
if (capacity_ == 0 || block_size_ == 0 || file_size_ == 0) {
return false;
}
if (range.length == 0 || range.length > block_size_) {
return false;
}
// A read reaching past EOF is left to the caller: serving it would mean
// short-reading into the block buffer.
if (range.offset >= file_size_ || range.length > file_size_ - range.offset) {
return false;
}
// A read straddling two blocks would need both of them to be present; it is
// left to the caller instead, which keeps one block per served read.
return IndexOf(range.offset) == IndexOf(range.offset + range.length - 1);
}
uint64_t FileBlockCache::IndexOf(uint64_t offset) const {
// Counted from the end of the file, so that block 0 is the last block.
return (file_size_ - 1 - offset) / block_size_;
}
ByteRange FileBlockCache::RangeOf(uint64_t index) const {
const uint64_t end = file_size_ - index * block_size_;
const uint64_t offset = end > block_size_ ? end - block_size_ : 0;
return {offset, end - offset};
}
void FileBlockCache::Fetch(const std::shared_ptr<Block>& block) {
fetch_metrics_.Record(block->range.length);
auto promise = block->promise;
auto buffer = block->buffer;
// The buffer and the promise are captured, so the async read keeps its
// destination and the future it resolves alive. The buffer keeps the memory
// pool alive as well, so a callback outliving this cache still frees the
// buffer against a live pool.
stream_->ReadAsync(buffer->data(), static_cast<int64_t>(buffer->size()),
static_cast<int64_t>(block->range.offset),
[promise, buffer](Status status) { promise->set_value(status); });
}
} // namespace paimon