diff --git a/include/matador/object/object_cache.hpp b/include/matador/object/object_cache.hpp new file mode 100644 index 0000000..42acaf8 --- /dev/null +++ b/include/matador/object/object_cache.hpp @@ -0,0 +1,339 @@ +#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 +#include +#include +#include +#include +#include + +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 +struct cache_entry : cache_entry_base { + std::weak_ptr > proxy; + std::weak_ptr 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) 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> oder @c std::weak_ptr>. + * @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 + std::shared_ptr> acquire_proxy(utils::identifier id, ResolverPtr &&resolver_ptr) { + const auto k = make_key(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_(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(std::forward(resolver_ptr)); + auto proxy_ptr = std::make_shared>(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>(); + auto *entry = entry_ptr.get(); + + // create a weak resolver and shared proxy + auto weak_resolver = to_weak(std::forward(resolver_ptr)); + auto proxy_ptr = std::make_shared>(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 + void attach_entity(const utils::identifier &id, std::shared_ptr obj) { + const auto k = make_key(id); + + std::unique_lock lock(mutex_); + + auto it = map_.find(k); + if (it == map_.end()) { + auto entry_ptr = std::make_unique>(); + auto *entry = entry_ptr.get(); + entry->entity = obj; + map_.emplace(k, std::move(entry_ptr)); + return; + } + + auto *entry = entry_cast_(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 + std::shared_ptr get_entity(const utils::identifier &id) { + const auto k = make_key(id); + + std::unique_lock lock(mutex_); + auto it = map_.find(k); + if (it == map_.end()) { + return {}; + } + + auto *entry = entry_cast_(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 + bool is_loaded(const utils::identifier &id) { + const auto k = make_key(id); + + std::unique_lock lock(mutex_); + auto it = map_.find(k); + if (it == map_.end()) { + return false; + } + + auto *entry = entry_cast_(it->second.get()); + const bool loaded = !entry->entity.expired(); + prune_if_dead(it); + return loaded; + } + + template + void erase(utils::identifier id) { + const auto k = make_key(id); + + std::unique_lock lock(mutex_); + auto it = map_.find(k); + if (it == map_.end()) { + return; + } + + auto *e = entry_cast_(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()(k.type); + const size_t h2 = std::hash()(k.id); + seed ^= h2 + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + return seed; + } + }; + + template + 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 + static cache_entry *entry_cast_(cache_entry_base *base) { +#ifndef NDEBUG + auto *p = dynamic_cast *>(base); + assert(p && "object_cache: Type mismatch in cache entry (key/type invariant violated)"); + return p; +#else + return static_cast *>(base); +#endif + } + + template + using enable_if_resolver_shared_ptr_t = + std::enable_if_t, U>, int>; + + template + using enable_if_resolver_weak_ptr_t = + std::enable_if_t, U>, int>; + + // shared_ptr -> weak_ptr + template = 0> + static std::weak_ptr> to_weak(const std::shared_ptr &s) { + return std::weak_ptr>(std::static_pointer_cast>(s)); + } + + // weak_ptr -> weak_ptr + template = 0> + static std::weak_ptr> to_weak(const std::weak_ptr &w) { + return std::weak_ptr>(w); + } + + // (2) Opportunistischer Cleanup: entferne Entry, wenn beide weak_ptr abgelaufen sind. + template + 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 + void prune_if_dead_typed(typename std::unordered_map, 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_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 diff --git a/include/matador/object/object_proxy.hpp b/include/matador/object/object_proxy.hpp index 633759e..e497331 100644 --- a/include/matador/object/object_proxy.hpp +++ b/include/matador/object/object_proxy.hpp @@ -44,6 +44,22 @@ public: , pk_(primary_key_resolver::resolve_object(*obj).pk) { } + void attach(std::shared_ptr obj) { + std::lock_guard lock(mutex_); + obj_ = std::move(obj); + if (obj_) { + pk_ = primary_key_resolver::resolve_object(*obj_).pk; + state_.store(object_state::Persistent, std::memory_order_release); + } + } + + void invalidate() { + std::lock_guard lock(mutex_); + obj_.reset(); + resolver_.reset(); + state_.store(object_state::Detached, std::memory_order_release); + } + [[nodiscard]] void *raw_pointer() const { return static_cast(pointer()); } Type *operator->() { return pointer(); } @@ -52,7 +68,7 @@ public: Type *pointer() const { return resolve(); } - [[nodiscard]] bool empty() const { return resolver_ == nullptr; } + [[nodiscard]] bool empty() const { return !obj_ && resolver_.expired(); } [[nodiscard]] bool valid() const { return !empty(); } [[nodiscard]] bool has_primary_key() const { return !pk_.is_null(); } [[nodiscard]] const utils::identifier &primary_key() const { return pk_; } diff --git a/include/matador/query/dependency_collector.hpp b/include/matador/query/dependency_collector.hpp new file mode 100644 index 0000000..ddfa8ea --- /dev/null +++ b/include/matador/query/dependency_collector.hpp @@ -0,0 +1,173 @@ +#ifndef MATADOR_DEPENDENCY_COLLECTOR_HPP +#define MATADOR_DEPENDENCY_COLLECTOR_HPP + +#include "matador/utils/field_attributes.hpp" +#include "matador/utils/foreign_attributes.hpp" +#include "matador/utils/primary_key_attribute.hpp" + +#include +#include +#include +#include +#include + +namespace matador::query { +enum class pk_strategy_kind { + manual, + sequence, + table, + identity +}; + +enum class pk_state { + unknown, // z.B. identity vor insert, oder manual aber nicht gesetzt + known // id ist gesetzt/verfügbar +}; + +struct fk_ref { + std::string fk_column; // z.B. "author_id" (optional, für Debug/SQL) + void* parent_object_ptr; // Adresse des referenzierten Objekts (type-erased) + std::type_index parent_type; +}; + +struct pk_accessor { + // obj zeigt auf konkrete Entity-Instanz + bool (*is_known)(void* obj) = nullptr; + void (*set_u64)(void* obj, std::uint64_t id) = nullptr; + std::uint64_t (*get_u64)(void* obj) = nullptr; +}; + +struct entity_meta { + pk_strategy_kind strategy{}; + pk_accessor pk; + // optional: table name, pk column name, etc. +}; + +struct meta_registry { + std::unordered_map by_type; + + const entity_meta& get(const std::type_index t) const { + return by_type.at(t); + } + + template + void register_type(entity_meta m) { + by_type.emplace(std::type_index(typeid(T)), m); + } +}; + +struct dependency_collector { + std::vector refs; + + template + static void on_primary_key(const char*, V&, const utils::primary_key_attribute& /*attr*/ = utils::default_pk_attributes) {} + + static void on_revision(const char*, std::uint64_t&) {} + + template + static void on_attribute(const char*, V&, const utils::field_attributes &/*attr*/ = utils::null_attributes) {} + + template + void on_belongs_to(const char* id, Pointer& p, utils::foreign_attributes &attr) { + // Erwartung: Pointer verhält sich wie matador::object::object_ptr + // also: p.get() liefert raw ptr, und Pointer::value_type / element_type ist T. + auto* raw = p.get(); + if (!raw) { + return; + } + + using parent_type = typename Pointer::value_type; // falls object_ptr so definiert ist + // Falls dein object_ptr anders heißt (z.B. element_type), hier anpassen. + + refs.push_back(fk_ref{ + id ? std::string{id} : std::string{}, + static_cast(raw), + typeid(parent_type) + }); + } + + template + static void on_has_one(const char*, Pointer&, const auto&) {} + + template + static void on_has_many(const char*, Container&, const char*, const auto&) {} + + template + static void on_has_many_to_many(const char*, Container&, const char*, const char*, const auto&) {} + + template + static void on_has_many_to_many(const char*, Container&, const auto&) {} +}; + +struct flush_node { + void* object_ptr = nullptr; // raw pointer (nicht owning) + std::type_index type = typeid(void); + + pk_strategy_kind pk_strategy = pk_strategy_kind::manual; + pk_accessor pk{}; + + pk_state state = pk_state::unknown; + + // eingehende Dependencies: child hängt von parents ab + std::vector depends_on; // Indizes in nodes[] + bool inserted = false; +}; + +struct flush_plan { + std::vector nodes; + + // map raw ptr -> node index + std::unordered_map index_by_ptr; +}; + +flush_plan build_nodes(const std::vector>& new_objects, const meta_registry& reg) { + flush_plan plan; + plan.nodes.reserve(new_objects.size()); + + for (const auto& [ptr, ti] : new_objects) { + const auto& meta = reg.get(ti); + + flush_node n; + n.object_ptr = ptr; + n.type = ti; + n.pk_strategy = meta.strategy; + n.pk = meta.pk; + n.state = n.pk.is_known(n.object_ptr) ? pk_state::known : pk_state::unknown; + + plan.index_by_ptr.emplace(ptr, plan.nodes.size()); + plan.nodes.push_back(n); + } + return plan; +} + +void add_edges_from_process(flush_plan& plan, const matador::query::process_registry& preg) { + using namespace matador::query; + for (std::size_t i = 0; i < plan.nodes.size(); ++i) { auto& node = plan.nodes[i]; + dependency_collector dc; + preg.get(node.type).collect_deps(node.object_ptr, dc); + + // 1) belongs_to: node (child) depends on parent + for (const auto& ref : dc.refs) { + auto it = plan.index_by_ptr.find(ref.parent_object_ptr); + if (it == plan.index_by_ptr.end()) { + // Parent ist nicht Teil des Plans (externe persistent entity?) + // Policy: wenn parent PK known -> ok; sonst Fehler oder cascade-add + continue; + } + node.depends_on.push_back(it->second); + } + + // 2) has_many: inverse; daraus macht man Kanten child -> this(parent) + for (const auto& child : dc.has_many_children) { + auto it_child = plan.index_by_ptr.find(child.child_object_ptr); + if (it_child == plan.index_by_ptr.end()) continue; + + auto& child_node = plan.nodes[it_child->second]; + child_node.depends_on.push_back(i); + } + + // 3) many-to-many: als separate work items (nicht in depends_on quetschen) + // dc.many_to_many_links -> später join-table tasks planen + } } +} +#endif //MATADOR_DEPENDENCY_COLLECTOR_HPP \ No newline at end of file diff --git a/include/matador/utils/identifier.hpp b/include/matador/utils/identifier.hpp index 08b433a..613c12a 100644 --- a/include/matador/utils/identifier.hpp +++ b/include/matador/utils/identifier.hpp @@ -23,7 +23,6 @@ public: virtual void serialize(uint16_t &, const field_attributes &) = 0; virtual void serialize(uint32_t &, const field_attributes &) = 0; virtual void serialize(uint64_t &, const field_attributes &) = 0; - virtual void serialize(const char *, const field_attributes &) = 0; virtual void serialize(std::string &, const field_attributes &) = 0; virtual void serialize(null_type_t &, const field_attributes &) = 0; }; @@ -149,7 +148,7 @@ public: } explicit identifier(const char *id) - : id_(std::make_shared >(id)) { + : id_(std::make_shared >(id)) { } identifier(const identifier &x); @@ -164,7 +163,7 @@ public: } identifier &operator=(const char *value) { - id_ = std::make_shared >(value); + id_ = std::make_shared >(value); return *this; } @@ -220,4 +219,13 @@ struct id_pk_hash { /// @endcond } +namespace std { +template<> +struct hash { + size_t operator()(const matador::utils::identifier &id) const noexcept { + return id.hash(); + } +}; +} // namespace std + #endif //MATADOR_IDENTIFIER_HPP diff --git a/include/matador/utils/identifier_to_value_converter.hpp b/include/matador/utils/identifier_to_value_converter.hpp index 38d25ff..3224fad 100644 --- a/include/matador/utils/identifier_to_value_converter.hpp +++ b/include/matador/utils/identifier_to_value_converter.hpp @@ -17,7 +17,6 @@ public: void serialize(uint16_t &, const field_attributes &) override; void serialize(uint32_t &, const field_attributes &) override; void serialize(uint64_t &, const field_attributes &) override; - void serialize(const char *, const field_attributes &) override; void serialize(std::string &, const field_attributes &) override; void serialize(null_type_t &, const field_attributes &) override; diff --git a/source/core/CMakeLists.txt b/source/core/CMakeLists.txt index badead0..ff8d19f 100644 --- a/source/core/CMakeLists.txt +++ b/source/core/CMakeLists.txt @@ -123,6 +123,7 @@ add_library(matador-core STATIC ../../include/matador/object/collection_utils.hpp object/collection_utils.cpp ../../include/matador/object/pk_field_locator.hpp + ../../include/matador/object/object_cache.hpp ) target_link_libraries(matador-core ${CMAKE_DL_LIBS}) diff --git a/source/core/utils/identifier_to_value_converter.cpp b/source/core/utils/identifier_to_value_converter.cpp index 5a5d6fc..e0622fb 100644 --- a/source/core/utils/identifier_to_value_converter.cpp +++ b/source/core/utils/identifier_to_value_converter.cpp @@ -39,10 +39,6 @@ void identifier_to_value_converter::serialize(uint64_t &x, const field_attribute value_ = x; } -void identifier_to_value_converter::serialize(const char *x, const field_attributes &) { - value_ = x; -} - void identifier_to_value_converter::serialize(std::string &x, const field_attributes &) { value_ = x; } diff --git a/test/core/CMakeLists.txt b/test/core/CMakeLists.txt index 602b50d..16f3b41 100644 --- a/test/core/CMakeLists.txt +++ b/test/core/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(CoreTests utils/ThreadPoolTest.cpp utils/VersionTest.cpp utils/IsDatabasePrimitiveTest.cpp + object/ObjectCacheTest.cpp ) target_link_libraries(CoreTests matador-core Catch2::Catch2WithMain) diff --git a/test/core/object/ObjectCacheTest.cpp b/test/core/object/ObjectCacheTest.cpp new file mode 100644 index 0000000..a0621f7 --- /dev/null +++ b/test/core/object/ObjectCacheTest.cpp @@ -0,0 +1,126 @@ +#include + +#include "matador/object/object_cache.hpp" +#include "matador/object/object_resolver.hpp" +#include "matador/utils/identifier.hpp" + +#include "../test/models/person.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace matador::test; + +namespace { +class dummy_resolver final : public matador::object::object_resolver { +public: + explicit dummy_resolver(std::shared_ptr shared, std::atomic_int &calls) + : shared_(std::move(shared)) + , calls_(calls) {} + + std::shared_ptr resolve(const matador::utils::identifier & /*id*/) override { + ++calls_; + return shared_; + } + +private: + std::shared_ptr shared_; + std::atomic_int &calls_; +}; + +class start_latch { +public: + explicit start_latch(int participants) + : participants_(participants) {} + + void arrive_and_wait() { + std::unique_lock lock(m_); + ++arrived_; + if (arrived_ >= participants_) { + open_ = true; + cv_.notify_all(); + return; + } + cv_.wait(lock, [&] { return open_; }); + } + +private: + std::mutex m_; + std::condition_variable cv_; + int participants_{0}; + int arrived_{0}; + bool open_{false}; +}; +} // namespace + +TEST_CASE("object_cache: acquire_proxy returns the same proxy instance across threads", "[object][cache][threadsafe]") { + matador::object::object_cache cache; + const matador::utils::identifier id{123}; + + std::atomic_int calls{0}; + auto entity = std::make_shared(); + auto resolver = std::make_shared(entity, calls); + + constexpr int threads = 16; + start_latch latch(threads); + + std::vector>> proxies(threads); + std::vector ts; + ts.reserve(threads); + + for (int i = 0; i < threads; ++i) { + ts.emplace_back([&, i]() { + latch.arrive_and_wait(); + proxies[i] = cache.acquire_proxy(id, resolver); + }); + } + for (auto &t : ts) t.join(); + + REQUIRE(proxies[0]); + const auto *p0 = proxies[0].get(); + for (int i = 1; i < threads; ++i) { + REQUIRE(proxies[i]); + REQUIRE(proxies[i].get() == p0); + } +} + +TEST_CASE("object_cache: attach_entity makes is_loaded/get_entity reflect presence", "[object][cache]") { + matador::object::object_cache cache; + const matador::utils::identifier id{42}; + + REQUIRE_FALSE(cache.is_loaded(id)); + REQUIRE(cache.get_entity(id) == nullptr); + + auto entity = std::make_shared(); + entity->name = "hans"; + + cache.attach_entity(id, entity); + + REQUIRE(cache.is_loaded(id)); + auto got = cache.get_entity(id); + REQUIRE(got); + REQUIRE(got->name == "hans"); +} + +TEST_CASE("object_cache: erase invalidates existing proxies", "[object][cache]") { + matador::object::object_cache cache; + const matador::utils::identifier id{9}; + + std::atomic_int calls{0}; + auto entity = std::make_shared(); + auto resolver = std::make_shared(entity, calls); + + auto proxy = cache.acquire_proxy(id, resolver); + REQUIRE(proxy); + REQUIRE(proxy->valid()); + + cache.erase(id); + + // Proxy lebt noch (shared_ptr), aber ist invalidiert. + REQUIRE_FALSE(proxy->valid()); + REQUIRE(proxy->pointer() == nullptr); +} \ No newline at end of file