blob: 7ae2f631548090d0ffd7f3df0304e73a83bfc585 [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.
*/
#pragma once
#include <chrono>
#include <cstdint>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <utility>
#include "fmt/format.h"
#include "paimon/result.h"
#include "paimon/traits.h"
namespace paimon {
/// A generic LRU cache with support for weight-based eviction, time-based expiration,
/// and removal callbacks.
///
/// Uses std::list + unordered_map for O(1) get/put/evict:
/// - list stores entries in LRU order (most recently used at front)
/// - map stores key -> list::iterator for O(1) lookup
///
/// @tparam K Key type
/// @tparam V Value type
/// @tparam Hash Hash function for K (default: std::hash<K>)
/// @tparam KeyEqual Equality function for K (default: std::equal_to<K>)
///
/// @note Thread-safe: all public methods are protected by a read-write lock.
template <typename K, typename V, typename Hash = std::hash<K>,
typename KeyEqual = std::equal_to<K>>
class GenericLruCache {
public:
/// Cause of a cache entry removal, passed to the removal callback.
enum class RemovalCause {
EXPLICIT, // Removed by Invalidate() or InvalidateAll()
SIZE, // Evicted because total weight exceeded max_weight
EXPIRED, // Evicted because the entry expired (expireAfterAccess)
REPLACED // Replaced by a new value for the same key via Put()
};
using WeighFunc = std::function<int64_t(const K&, const V&)>;
using RemovalCallback = std::function<void(const K&, const V&, RemovalCause)>;
/// Configuration options for the cache.
struct Options {
/// Maximum total weight of all entries. Entries are evicted (LRU) when exceeded.
int64_t max_weight = INT64_MAX;
/// Time in milliseconds after last access before an entry expires.
/// A value < 0 disables expiration.
int64_t expire_after_access_ms = -1;
/// Function to compute the weight of an entry. If nullptr, each entry has weight 1.
WeighFunc weigh_func = nullptr;
/// Callback invoked when an entry is removed (evicted, invalidated, or replaced).
/// If nullptr, no callback is invoked.
RemovalCallback removal_callback = nullptr;
};
explicit GenericLruCache(Options options) : options_(std::move(options)) {}
/// Look up a key in the cache. On hit, promotes the entry to the front (most recently
/// used) and updates its access time. Returns std::nullopt on miss or if the entry
/// has expired.
std::optional<V> GetIfPresent(const K& key) {
std::unique_lock<std::shared_mutex> lock(mutex_);
return FindPromoteOrExpire(key);
}
/// Look up a key. On miss, load via the supplier, insert into cache, and return.
/// If the supplier returns an error, the error is propagated and nothing is cached.
Result<V> Get(const K& key, std::function<Result<V>(const K&)> supplier) {
{
std::unique_lock<std::shared_mutex> lock(mutex_);
auto cached = FindPromoteOrExpire(key);
if (cached.has_value()) {
return std::move(cached.value());
}
}
// Cache miss: load via supplier outside the lock
PAIMON_ASSIGN_OR_RAISE(V value, supplier(key));
int64_t weight = ComputeWeight(key, value);
if (weight > options_.max_weight) {
return value;
}
std::unique_lock<std::shared_mutex> lock(mutex_);
// Double-check: another thread may have inserted while we were loading
auto cached = FindPromoteOrExpire(key);
if (cached.has_value()) {
return std::move(cached.value());
}
InsertEntry(key, value, weight);
EvictIfNeeded();
return value;
}
/// Insert or update an entry. If the key already exists, the old value is replaced
/// and the REPLACED callback is invoked. Triggers eviction if needed.
/// @return Status::Invalid if the entry's weight exceeds max_weight, Status::OK otherwise.
Status Put(const K& key, V value) {
int64_t weight = ComputeWeight(key, value);
if (weight > options_.max_weight) {
return Status::Invalid(
fmt::format("Entry weight {} exceeds cache max weight {}, entry will not be cached",
weight, options_.max_weight));
}
std::unique_lock<std::shared_mutex> lock(mutex_);
auto it = lru_map_.find(key);
if (it != lru_map_.end()) {
if (ValuesEqual(it->second->value, value)) {
Promote(it->second);
return Status::OK();
}
ReplaceEntry(it->second, std::move(value), weight);
} else {
InsertEntry(key, std::move(value), weight);
}
EvictIfNeeded();
return Status::OK();
}
/// Remove a specific entry. Invokes the EXPLICIT removal callback if the key exists.
void Invalidate(const K& key) {
std::unique_lock<std::shared_mutex> lock(mutex_);
auto it = lru_map_.find(key);
if (it != lru_map_.end()) {
RemoveEntry(it->second, RemovalCause::EXPLICIT);
}
}
/// Remove all entries. Each entry's EXPLICIT removal callback is invoked.
void InvalidateAll() {
std::unique_lock<std::shared_mutex> lock(mutex_);
while (!lru_list_.empty()) {
RemoveEntry(std::prev(lru_list_.end()), RemovalCause::EXPLICIT);
}
current_weight_ = 0;
}
/// @return The number of entries currently in the cache.
size_t Size() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return lru_map_.size();
}
/// @return The current total weight of all entries.
int64_t GetCurrentWeight() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return current_weight_;
}
/// @return The maximum weight configured for this cache.
int64_t GetMaxWeight() const {
return options_.max_weight;
}
private:
struct CacheEntry {
K key;
V value;
int64_t weight;
std::chrono::steady_clock::time_point last_access_time;
};
using EntryList = std::list<CacheEntry>;
using EntryMap = std::unordered_map<K, typename EntryList::iterator, Hash, KeyEqual>;
/// Look up a key, promote if found and not expired, or remove if expired.
/// Must be called with mutex_ held.
/// @return The value if found and valid, std::nullopt otherwise.
std::optional<V> FindPromoteOrExpire(const K& key) {
auto it = lru_map_.find(key);
if (it == lru_map_.end()) {
return std::nullopt;
}
auto list_it = it->second;
if (IsExpired(list_it->last_access_time)) {
RemoveEntry(list_it, RemovalCause::EXPIRED);
return std::nullopt;
}
Promote(list_it);
return list_it->value;
}
/// Move an entry to the front of the LRU list and update its access time.
void Promote(typename EntryList::iterator list_it) {
list_it->last_access_time = std::chrono::steady_clock::now();
lru_list_.splice(lru_list_.begin(), lru_list_, list_it);
}
/// Insert a new entry at the front of the LRU list.
void InsertEntry(const K& key, V value, int64_t weight) {
lru_list_.emplace_front(
CacheEntry{key, std::move(value), weight, std::chrono::steady_clock::now()});
lru_map_[key] = lru_list_.begin();
current_weight_ += weight;
}
/// Compare two values for equality. For pointers, compares the underlying
/// pointer first, then dereferences and compares the pointed-to objects.
/// For other types, uses operator==.
static bool ValuesEqual(const V& lhs, const V& rhs) {
if constexpr (is_pointer<V>::value) {
if (lhs == rhs) {
return true;
}
if (!lhs || !rhs) {
return false;
}
return *lhs == *rhs;
} else {
return lhs == rhs;
}
}
/// Replace the value of an existing entry, invoke the REPLACED callback for the old value,
/// and promote the entry to the front.
void ReplaceEntry(typename EntryList::iterator list_it, V new_value, int64_t new_weight) {
current_weight_ -= list_it->weight;
K key = list_it->key;
V old_value = std::move(list_it->value);
list_it->value = std::move(new_value);
list_it->weight = new_weight;
list_it->last_access_time = std::chrono::steady_clock::now();
current_weight_ += new_weight;
lru_list_.splice(lru_list_.begin(), lru_list_, list_it);
InvokeCallback(key, old_value, RemovalCause::REPLACED);
}
/// Remove an entry from the cache and invoke the removal callback.
void RemoveEntry(typename EntryList::iterator list_it, RemovalCause cause) {
lru_map_.erase(list_it->key);
K key = std::move(list_it->key);
V value = std::move(list_it->value);
current_weight_ -= list_it->weight;
lru_list_.erase(list_it);
InvokeCallback(key, value, cause);
}
/// Evict expired entries from the tail, then evict by weight if still over capacity.
void EvictIfNeeded() {
EvictExpired();
while (current_weight_ > options_.max_weight && !lru_list_.empty()) {
RemoveEntry(std::prev(lru_list_.end()), RemovalCause::SIZE);
}
}
/// Evict expired entries from the tail of the LRU list.
/// Since the tail has the oldest access time, we can stop as soon as we find
/// a non-expired entry.
void EvictExpired() {
if (options_.expire_after_access_ms < 0) {
return;
}
auto now = std::chrono::steady_clock::now();
while (!lru_list_.empty()) {
auto it = std::prev(lru_list_.end());
if (!IsExpired(it->last_access_time, now)) {
break;
}
RemoveEntry(it, RemovalCause::EXPIRED);
}
}
/// Compute the weight of an entry using the configured weigh function.
int64_t ComputeWeight(const K& key, const V& value) const {
if (options_.weigh_func) {
return options_.weigh_func(key, value);
}
return 1;
}
/// Check if an entry has expired based on its last access time.
bool IsExpired(
const std::chrono::steady_clock::time_point& last_access_time,
const std::chrono::steady_clock::time_point& now = std::chrono::steady_clock::now()) const {
if (options_.expire_after_access_ms < 0) {
return false;
}
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_access_time);
return elapsed.count() >= options_.expire_after_access_ms;
}
/// Invoke the removal callback if one is configured.
void InvokeCallback(const K& key, const V& value, RemovalCause cause) {
if (options_.removal_callback) {
options_.removal_callback(key, value, cause);
}
}
Options options_;
int64_t current_weight_ = 0;
EntryList lru_list_;
EntryMap lru_map_;
mutable std::shared_mutex mutex_;
};
} // namespace paimon