#include "LifecycleManager.h" #include "Merkle.h" #include "Types.h" #include namespace nt { LifecycleManager::LifecycleManager(ObjectManager& objects, IStorageBackend& storage) : objects_(objects), storage_(storage) { } void LifecycleManager::Monitor(ObjectManager::registry* object) { if (object != nullptr) ++object->head->handle_count; } void LifecycleManager::Unmonitor(ObjectManager::registry* object) { if (object == nullptr || object->head->handle_count == 0) return; --object->head->handle_count; TryCollect(object); } void LifecycleManager::Pin(ObjectManager::registry* object) { if (object != nullptr) ++object->head->reference_count; } void LifecycleManager::Unpin(ObjectManager::registry* object) { if (object == nullptr || object->head->reference_count == 0) return; --object->head->reference_count; TryCollect(object); } bool LifecycleManager::IsEligibleForGC(ObjectManager::registry* object) const { if (object == nullptr || object->head == nullptr) return false; return object->head->handle_count == 0 && object->head->reference_count == 0; } const bool LifecycleManager::Contention(ObjectManager::registry* object) { return object != nullptr; } void LifecycleManager::TryCollect(ObjectManager::registry* object) { if (!IsEligibleForGC(object)) return; if (object->head == nullptr || object->head->type == nullptr) return; // Named-lifetime types are managed explicitly and must not auto-collect: // BRANCH — named branches persist until a future explicit-delete API // tears them down. Their counters going to zero just means // nobody currently holds them open. // SESSION — rnt_session_close is the sole removal path. // Everything else (MULTIGROUP, BRANCH_TREE, RELATION, // EPHEMERAL_RELATION, TRANSACTION) is purely structurally referenced // and is GC-eligible by counters alone. const auto label = object->head->type->label; if (label == BRANCH || label == SESSION) return; if (label == MULTIGROUP) CascadeMultigroup(object); if (label == BRANCH_TREE) CascadeBranchTree(object); objects_.Unregister(object->head->path); } void LifecycleManager::CascadeMultigroup(ObjectManager::registry* multigroup) { if (multigroup == nullptr || multigroup->head == nullptr) return; const auto& mg_path = multigroup->head->path; if (mg_path.size() < 3) return; // /system/snapshots//relations/ entries are the relation // children pinned by this multigroup. Collect first, mutate after — // Unpin can Unregister the entry we're standing on if its counters // were already zero, and iteration must outlive that mutation. std::vector children; for (auto* cur = objects_.entries.get(); cur != nullptr; cur = cur->next.get()) { const auto& p = cur->head->path; if (p.size() != mg_path.size() + 2) continue; bool prefix_match = true; for (size_t i = 0; i < mg_path.size(); ++i) if (p[i] != mg_path[i]) { prefix_match = false; break; } if (!prefix_match) continue; if (p[mg_path.size()] != "relations") continue; children.push_back(cur); } for (auto* child : children) Unpin(child); } void LifecycleManager::CascadeBranchTree(ObjectManager::registry* branch_tree) { if (branch_tree == nullptr || branch_tree->head == nullptr) return; auto* bt = dynamic_cast(branch_tree->object.get()); if (bt == nullptr || bt->merkle_root.empty()) return; // Page through the (mg_name, mg_hash) tree and collect each referenced // multigroup before unpinning. Decoupling iteration from mutation // keeps us safe against Unpin cascading further GC under our feet. std::vector mgs; constexpr size_t kPageSize = 1024; size_t offset = 0; while (true) { auto page = Merkle::Page(storage_, bt->merkle_root, offset, kPageSize); if (page.empty()) break; for (const auto& entry : page) { static const Hash32 zero{}; if (entry.payload == zero) continue; const std::string mg_hash = bin_to_hex(entry.payload); auto* mg_entry = objects_.Find({"system", "snapshots", mg_hash}); if (mg_entry) mgs.push_back(mg_entry); } if (page.size() < kPageSize) break; offset += page.size(); } for (auto* mg : mgs) Unpin(mg); } } // namespace nt