#include #include "InMemoryBackend.h" #include "LifecycleManager.h" #include "ObjectManager.h" #include // --------------------------------------------------------------------------- // Step 6 — Pin/Unpin and the cascade // // LifecycleManager owns object lifetime. Pin/Unpin adjust reference_count; // Monitor/Unmonitor adjust handle_count. Both decrement paths call into // TryCollect, which gates GC on the joint condition (both counters zero) // and, for MULTIGROUP entries, cascades through CascadeMultigroup to // unpin every Relation registered under their snapshot subtree. // --------------------------------------------------------------------------- namespace { std::unique_ptr make_type(OBJECT_TYPE label) { auto t = std::make_unique(); t->label = label; t->methods = { OPEN, CLOSE }; return t; } } TEST_CASE("Pin and Unpin adjust reference_count symmetrically", "[step6][lifecycle][pin]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"r1"}, std::make_unique(), make_type(RELATION)); auto* entry = om.Find({"r1"}); REQUIRE(entry->head->reference_count == 0); lm.Pin(entry); lm.Pin(entry); REQUIRE(entry->head->reference_count == 2); lm.Unpin(entry); REQUIRE(entry->head->reference_count == 1); // Still alive at ref_count=1. REQUIRE(om.Find({"r1"}) != nullptr); } TEST_CASE("Pin on nullptr is a no-op", "[step6][lifecycle][pin]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); REQUIRE_NOTHROW(lm.Pin(nullptr)); REQUIRE_NOTHROW(lm.Unpin(nullptr)); } TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"r1"}, std::make_unique(), make_type(RELATION)); auto* entry = om.Find({"r1"}); lm.Unpin(entry); REQUIRE(entry->head->reference_count == 0); } TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"r1"}, std::make_unique(), make_type(RELATION)); auto* entry = om.Find({"r1"}); lm.Monitor(entry); lm.Pin(entry); // handle_count=1, ref_count=1. lm.Unpin(entry); // ref_count → 0, but handle_count still 1. REQUIRE(om.Find({"r1"}) != nullptr); lm.Pin(entry); lm.Unmonitor(entry); // handle_count → 0, but ref_count still 1. REQUIRE(om.Find({"r1"}) != nullptr); lm.Unpin(entry); // both zero → GC. REQUIRE(om.Find({"r1"}) == nullptr); } TEST_CASE("Unmonitor that brings both counters to zero collects the entry", "[step6][lifecycle][gc]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"r1"}, std::make_unique(), make_type(RELATION)); auto* entry = om.Find({"r1"}); lm.Monitor(entry); REQUIRE(entry->head->handle_count == 1); lm.Unmonitor(entry); REQUIRE(om.Find({"r1"}) == nullptr); } TEST_CASE("IsEligibleForGC reflects the joint-zero rule", "[step6][lifecycle][gc]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"r1"}, std::make_unique(), make_type(RELATION)); auto* entry = om.Find({"r1"}); REQUIRE(lm.IsEligibleForGC(entry)); lm.Pin(entry); REQUIRE_FALSE(lm.IsEligibleForGC(entry)); } TEST_CASE("CascadeMultigroup unpins child Relations on snapshot collection", "[step6][lifecycle][cascade]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"system", "snapshots", "H1"}, std::make_unique(), make_type(MULTIGROUP)); auto* mg_entry = om.Find({"system", "snapshots", "H1"}); for (const auto& n : { std::string{"foo"}, std::string{"bar"} }) { om.Register({"system", "snapshots", "H1", "relations", n}, std::make_unique(), make_type(RELATION)); // The parent Multigroup's pin on each child, mirroring what // register_snapshot does in the C API layer. lm.Pin(om.Find({"system", "snapshots", "H1", "relations", n})); } REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) != nullptr); REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) != nullptr); // One external pin on the Multigroup (e.g. a branch). Release it: cascade. lm.Pin(mg_entry); lm.Unpin(mg_entry); REQUIRE(om.Find({"system", "snapshots", "H1"}) == nullptr); REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) == nullptr); REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) == nullptr); } TEST_CASE("Cascade survives a child with an open handle", "[step6][lifecycle][cascade]") { nt::InMemoryBackend store; nt::ObjectManager om; nt::LifecycleManager lm(om, store); om.Register({"system", "snapshots", "H2"}, std::make_unique(), make_type(MULTIGROUP)); auto* mg_entry = om.Find({"system", "snapshots", "H2"}); om.Register({"system", "snapshots", "H2", "relations", "live"}, std::make_unique(), make_type(RELATION)); auto* child = om.Find({"system", "snapshots", "H2", "relations", "live"}); lm.Pin(child); // parent's pin lm.Monitor(child); // simulate an open handle on the child // Drop the Multigroup's external ref: cascade unpins child (ref → 0), // but handle_count is still 1 — child must survive. lm.Pin(mg_entry); lm.Unpin(mg_entry); REQUIRE(om.Find({"system", "snapshots", "H2"}) == nullptr); REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) != nullptr); // Closing the handle now releases the child. lm.Unmonitor(child); REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) == nullptr); }