340 lines
10 KiB
C++
340 lines
10 KiB
C++
#ifndef MATADOR_OBJECT_CACHE_HPP
|
|
#define MATADOR_OBJECT_CACHE_HPP
|
|
|
|
#include "matador/object/object_proxy.hpp"
|
|
#include "matador/object/object_resolver.hpp"
|
|
|
|
#include "matador/utils/identifier.hpp"
|
|
|
|
#include <unordered_map>
|
|
#include <memory>
|
|
#include <shared_mutex>
|
|
#include <typeindex>
|
|
#include <utility>
|
|
#include <cassert>
|
|
|
|
namespace matador::object {
|
|
struct cache_entry_base {
|
|
virtual ~cache_entry_base() = default;
|
|
|
|
/**
|
|
* @brief Gibt an, ob der Eintrag vollständig "tot" ist (weder Proxy noch Entity leben).
|
|
*
|
|
* Wird von @ref object_cache::sweep() verwendet, um Einträge ohne T-Kenntnis zu bereinigen.
|
|
*/
|
|
[[nodiscard]] virtual bool is_dead() const noexcept = 0;
|
|
};
|
|
|
|
template<typename T>
|
|
struct cache_entry : cache_entry_base {
|
|
std::weak_ptr<object_proxy<T> > proxy;
|
|
std::weak_ptr<T> entity;
|
|
|
|
[[nodiscard]] bool is_dead() const noexcept override {
|
|
return proxy.expired() && entity.expired();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief Thread-sicherer Cache für Objekt-Proxies und (optional) geladene Entities.
|
|
*
|
|
* Der Cache verwaltet Einträge pro Kombination aus Objekt-Typ @p T und Primärschlüssel/Identifier.
|
|
* Für jede (T, id)-Kombination können sowohl
|
|
* - ein Proxy (@c object_proxy<T>) als auch
|
|
* - die dazugehörige Entity (@c T)
|
|
* zwischengespeichert werden.
|
|
*
|
|
* Der Cache hält dabei nur @c std::weak_ptr auf Proxy und Entity, d.h. er verlängert deren Lebensdauer nicht.
|
|
*
|
|
* @note Thread-Safety: Alle öffentlichen Methoden sind threadsafe (intern synchronisiert).
|
|
* @note Semantik: Ein Eintrag kann aus dem Cache opportunistisch entfernt werden, sobald Proxy und Entity abgelaufen sind.
|
|
*/
|
|
class object_cache {
|
|
public:
|
|
/**
|
|
* @brief Erzeugt einen leeren Cache.
|
|
*/
|
|
object_cache() = default;
|
|
|
|
/**
|
|
* @brief Liefert einen Proxy für (T, id) und erstellt ihn bei Bedarf.
|
|
*
|
|
* Falls bereits ein lebender Proxy im Cache existiert, wird er wiederverwendet.
|
|
* Andernfalls wird ein neuer Proxy erzeugt, im Cache abgelegt und zurückgegeben.
|
|
* Falls für (T, id) bereits eine Entity vorhanden ist, wird sie an den Proxy gebunden.
|
|
*
|
|
* @tparam T Entity-Typ.
|
|
* @tparam ResolverPtr Zeiger-Typ auf einen Resolver, typischerweise
|
|
* @c std::shared_ptr<object_resolver<T>> oder @c std::weak_ptr<object_resolver<T>>.
|
|
* @param id Identifier/Primärschlüssel der Entity.
|
|
* @param resolver_ptr Resolver (shared/weak), der zur Lazy-Auflösung durch den Proxy genutzt wird.
|
|
* @return Shared-Pointer auf den (ggf. neu erstellten) Proxy.
|
|
*
|
|
* @note Diese Methode ist threadsafe.
|
|
*/
|
|
template<typename T, typename ResolverPtr>
|
|
std::shared_ptr<object_proxy<T>> acquire_proxy(utils::identifier id, ResolverPtr &&resolver_ptr) {
|
|
const auto k = make_key<T>(id);
|
|
|
|
std::unique_lock lock(mutex_);
|
|
|
|
auto it = map_.find(k);
|
|
if (it != map_.end()) {
|
|
// found entry, return std::shared_ptr of proxy
|
|
auto *entry = entry_cast_<T>(it->second.get());
|
|
if (auto proxy_ptr = entry->proxy.lock()) {
|
|
return proxy_ptr;
|
|
}
|
|
|
|
// if the proxy is dead, but the entity is alive, create a new proxy and attach entity
|
|
auto weak_resolver = to_weak<T>(std::forward<ResolverPtr>(resolver_ptr));
|
|
auto proxy_ptr = std::make_shared<object_proxy<T>>(weak_resolver, id);
|
|
|
|
if (auto obj = entry->entity.lock()) {
|
|
proxy_ptr->attach(std::move(obj));
|
|
}
|
|
|
|
entry->proxy = proxy_ptr;
|
|
|
|
// return the shared_ptr of the proxy
|
|
return proxy_ptr;
|
|
}
|
|
|
|
// entry couldn't be found, create a new entry
|
|
auto entry_ptr = std::make_unique<cache_entry<T>>();
|
|
auto *entry = entry_ptr.get();
|
|
|
|
// create a weak resolver and shared proxy
|
|
auto weak_resolver = to_weak<T>(std::forward<ResolverPtr>(resolver_ptr));
|
|
auto proxy_ptr = std::make_shared<object_proxy<T>>(weak_resolver, id);
|
|
|
|
// lock entity and attach to proxy
|
|
if (auto obj = entry->entity.lock()) {
|
|
proxy_ptr->attach(std::move(obj));
|
|
}
|
|
|
|
// set the new proxy into cache entry
|
|
entry->proxy = proxy_ptr;
|
|
map_.emplace(k, std::move(entry_ptr));
|
|
|
|
// return the shared_ptr of the proxy
|
|
return proxy_ptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Verknüpft eine geladene Entity mit einem Cache-Eintrag (Type, id).
|
|
*
|
|
* Speichert die Entity als @c weak_ptr im Cache. Falls bereits ein Proxy existiert,
|
|
* wird die Entity sofort an den Proxy gebunden.
|
|
*
|
|
* @tparam Type Entity-Typ.
|
|
* @param id Identifier/Primärschlüssel der Entity.
|
|
* @param obj Geladene Entity als @c shared_ptr.
|
|
*
|
|
* @note Diese Methode ist threadsafe.
|
|
*/
|
|
template<typename Type>
|
|
void attach_entity(const utils::identifier &id, std::shared_ptr<Type> obj) {
|
|
const auto k = make_key<Type>(id);
|
|
|
|
std::unique_lock lock(mutex_);
|
|
|
|
auto it = map_.find(k);
|
|
if (it == map_.end()) {
|
|
auto entry_ptr = std::make_unique<cache_entry<Type>>();
|
|
auto *entry = entry_ptr.get();
|
|
entry->entity = obj;
|
|
map_.emplace(k, std::move(entry_ptr));
|
|
return;
|
|
}
|
|
|
|
auto *entry = entry_cast_<Type>(it->second.get());
|
|
entry->entity = obj;
|
|
|
|
if (auto proxy = entry->proxy.lock()) {
|
|
proxy->attach(std::move(obj));
|
|
}
|
|
|
|
prune_if_dead(it);
|
|
}
|
|
|
|
/**
|
|
* @brief Liefert die aktuell geladene Entity für (Type, id), falls vorhanden.
|
|
*
|
|
* @tparam Type Entity-Typ.
|
|
* @param id Identifier/Primärschlüssel der Entity.
|
|
* @return @c shared_ptr auf die Entity oder ein leerer Pointer, falls nicht geladen/abgelaufen.
|
|
*
|
|
* @note Diese Methode ist threadsafe.
|
|
*/
|
|
template<typename Type>
|
|
std::shared_ptr<Type> get_entity(const utils::identifier &id) {
|
|
const auto k = make_key<Type>(id);
|
|
|
|
std::unique_lock lock(mutex_);
|
|
auto it = map_.find(k);
|
|
if (it == map_.end()) {
|
|
return {};
|
|
}
|
|
|
|
auto *entry = entry_cast_<Type>(it->second.get());
|
|
auto entity_ptr = entry->entity.lock();
|
|
prune_if_dead(it);
|
|
return entity_ptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Prüft, ob für (Type, id) aktuell eine lebende Entity verfügbar ist.
|
|
*
|
|
* @tparam Type Entity-Typ.
|
|
* @param id Identifier/Primärschlüssel der Entity.
|
|
* @return @c true, wenn die Entity-Referenz nicht abgelaufen ist; sonst @c false.
|
|
*
|
|
* @note Diese Methode ist threadsafe.
|
|
*/
|
|
template<typename Type>
|
|
bool is_loaded(const utils::identifier &id) {
|
|
const auto k = make_key<Type>(id);
|
|
|
|
std::unique_lock lock(mutex_);
|
|
auto it = map_.find(k);
|
|
if (it == map_.end()) {
|
|
return false;
|
|
}
|
|
|
|
auto *entry = entry_cast_<Type>(it->second.get());
|
|
const bool loaded = !entry->entity.expired();
|
|
prune_if_dead(it);
|
|
return loaded;
|
|
}
|
|
|
|
template<typename T>
|
|
void erase(utils::identifier id) {
|
|
const auto k = make_key<T>(id);
|
|
|
|
std::unique_lock lock(mutex_);
|
|
auto it = map_.find(k);
|
|
if (it == map_.end()) {
|
|
return;
|
|
}
|
|
|
|
auto *e = entry_cast_<T>(it->second.get());
|
|
if (auto p = e->proxy.lock()) {
|
|
p->invalidate();
|
|
}
|
|
|
|
map_.erase(it);
|
|
}
|
|
|
|
/**
|
|
* @brief Führt einen Cleanup-Lauf aus und entfernt Einträge, deren Proxy und Entity abgelaufen sind.
|
|
*
|
|
* @return Anzahl der entfernten Einträge.
|
|
*
|
|
* @note Diese Methode ist threadsafe.
|
|
*/
|
|
[[nodiscard]] std::size_t sweep() {
|
|
std::unique_lock lock(mutex_);
|
|
std::size_t removed = 0;
|
|
|
|
for (auto it = map_.begin(); it != map_.end(); ) {
|
|
if (it->second->is_dead()) {
|
|
it = map_.erase(it);
|
|
++removed;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
private:
|
|
struct key {
|
|
std::type_index type;
|
|
utils::identifier id{};
|
|
bool operator==(key const &other) const {
|
|
return type == other.type && id == other.id;
|
|
}
|
|
};
|
|
|
|
struct key_hash {
|
|
size_t operator()(key const &k) const noexcept {
|
|
// (3) robustere Hash-Kombination als XOR
|
|
size_t seed = std::hash<std::type_index>()(k.type);
|
|
const size_t h2 = std::hash<utils::identifier>()(k.id);
|
|
seed ^= h2 + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
|
|
return seed;
|
|
}
|
|
};
|
|
|
|
template<typename T>
|
|
static key make_key(const utils::identifier &id) {
|
|
return key{std::type_index(typeid(T)), id};
|
|
}
|
|
|
|
// (4) Debug-Absicherung für type-erased Casts
|
|
template<typename T>
|
|
static cache_entry<T> *entry_cast_(cache_entry_base *base) {
|
|
#ifndef NDEBUG
|
|
auto *p = dynamic_cast<cache_entry<T> *>(base);
|
|
assert(p && "object_cache: Type mismatch in cache entry (key/type invariant violated)");
|
|
return p;
|
|
#else
|
|
return static_cast<cache_entry<T> *>(base);
|
|
#endif
|
|
}
|
|
|
|
template<typename T, typename U>
|
|
using enable_if_resolver_shared_ptr_t =
|
|
std::enable_if_t<std::is_base_of_v<object_resolver<T>, U>, int>;
|
|
|
|
template<typename T, typename U>
|
|
using enable_if_resolver_weak_ptr_t =
|
|
std::enable_if_t<std::is_base_of_v<object_resolver<T>, U>, int>;
|
|
|
|
// shared_ptr<Derived> -> weak_ptr<Base>
|
|
template<typename T, typename U, enable_if_resolver_shared_ptr_t<T, U> = 0>
|
|
static std::weak_ptr<object_resolver<T>> to_weak(const std::shared_ptr<U> &s) {
|
|
return std::weak_ptr<object_resolver<T>>(std::static_pointer_cast<object_resolver<T>>(s));
|
|
}
|
|
|
|
// weak_ptr<Derived> -> weak_ptr<Base>
|
|
template<typename T, typename U, enable_if_resolver_weak_ptr_t<T, U> = 0>
|
|
static std::weak_ptr<object_resolver<T>> to_weak(const std::weak_ptr<U> &w) {
|
|
return std::weak_ptr<object_resolver<T>>(w);
|
|
}
|
|
|
|
// (2) Opportunistischer Cleanup: entferne Entry, wenn beide weak_ptr abgelaufen sind.
|
|
template<typename It>
|
|
bool prune_if_dead(It &it) {
|
|
if (it == map_.end()) {
|
|
return false;
|
|
}
|
|
if (it->second && it->second->is_dead()) {
|
|
map_.erase(it);
|
|
it = map_.end();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template<typename T>
|
|
void prune_if_dead_typed(typename std::unordered_map<key, std::unique_ptr<cache_entry_base>, key_hash>::iterator &it) {
|
|
(void)T{}; // T bleibt im Interface, damit bestehende Call-Sites unverändert bleiben können.
|
|
prune_if_dead(it);
|
|
}
|
|
|
|
private:
|
|
mutable std::shared_mutex mutex_{};
|
|
std::unordered_map<key, std::unique_ptr<cache_entry_base>, key_hash> map_;
|
|
};
|
|
|
|
// --- Inline Anpassungen: typed prune nach Zugriff (damit wir T haben) ---
|
|
|
|
// ... existing code ...
|
|
// (Die typed prune wird oben benutzt; wir rufen sie direkt im Codepfad anstelle von prune_if_dead_)
|
|
|
|
} // namespace matador::object
|
|
#endif //MATADOR_OBJECT_CACHE_HPP
|