From bf945b3b745029d9c6cc0d793612622fbac3ba88 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 27 Mar 2026 14:02:33 -0700 Subject: [PATCH 1/6] refactor: add Apply method to MemoryStore for transactional change sets --- .../memory_store/memory_store.cpp | 49 ++ .../memory_store/memory_store.hpp | 4 + .../tests/memory_store_apply_test.cpp | 551 ++++++++++++++++++ 3 files changed, 604 insertions(+) create mode 100644 libs/server-sdk/tests/memory_store_apply_test.cpp diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 95cac8748..6e0365d7f 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -1,5 +1,9 @@ #include "memory_store.hpp" +#include + +#include + namespace launchdarkly::server_side::data_components { std::shared_ptr MemoryStore::GetFlag( @@ -82,4 +86,49 @@ bool MemoryStore::RemoveSegment(std::string const& key) { return segments_.erase(key) == 1; } +void MemoryStore::Apply(data_model::FDv2ChangeSet const& changeSet) { + std::lock_guard lock{data_mutex_}; + + if (changeSet.type == data_model::FDv2ChangeSet::Type::kNone) { + return; + } + + if (changeSet.type == data_model::FDv2ChangeSet::Type::kFull) { + initialized_ = true; + flags_.clear(); + segments_.clear(); + } + + for (auto change : changeSet.changes) { + if (std::holds_alternative(change.object)) { + auto& flag_descriptor = + std::get(change.object); + + auto existing_flag = flags_.find(change.key); + if (existing_flag != flags_.end() && + existing_flag->second->version >= flag_descriptor.version) { + continue; + } + + flags_[change.key] = std::make_shared( + std::move(flag_descriptor)); + } else if (std::holds_alternative( + change.object)) { + auto& segment_descriptor = + std::get(change.object); + + auto existing_segment = segments_.find(change.key); + if (existing_segment != segments_.end() && + existing_segment->second->version >= + segment_descriptor.version) { + continue; + } + + segments_[change.key] = + std::make_shared( + std::move(segment_descriptor)); + } + } +} + } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 93dfca485..81846dfab 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -3,6 +3,8 @@ #include "../../data_interfaces/destination/idestination.hpp" #include "../../data_interfaces/store/istore.hpp" +#include + #include #include #include @@ -44,6 +46,8 @@ class MemoryStore final : public data_interfaces::IStore, bool RemoveSegment(std::string const& key); + void Apply(data_model::FDv2ChangeSet const& changeSet); + MemoryStore() = default; ~MemoryStore() override = default; diff --git a/libs/server-sdk/tests/memory_store_apply_test.cpp b/libs/server-sdk/tests/memory_store_apply_test.cpp new file mode 100644 index 000000000..219e36a15 --- /dev/null +++ b/libs/server-sdk/tests/memory_store_apply_test.cpp @@ -0,0 +1,551 @@ +#include + +#include +#include + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::data_components; + +// --------------------------------------------------------------------------- +// kNone tests +// --------------------------------------------------------------------------- + +TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { + MemoryStore store; + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, + }); + + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + + auto fetched_flag = store.GetFlag("flagA"); + ASSERT_TRUE(fetched_flag); + EXPECT_EQ(1u, fetched_flag->version); + auto fetched_seg = store.GetSegment("segA"); + ASSERT_TRUE(fetched_seg); + EXPECT_EQ(1u, fetched_seg->version); +} + +TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { + MemoryStore store; + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + EXPECT_FALSE(store.Initialized()); +} + +// --------------------------------------------------------------------------- +// kFull tests +// --------------------------------------------------------------------------- + +TEST(MemoryStoreApplyTest, ApplyFull_SetsInitialized) { + MemoryStore store; + ASSERT_FALSE(store.Initialized()); + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); + EXPECT_TRUE(store.Initialized()); +} + +TEST(MemoryStoreApplyTest, ApplyFull_WithFlag) { + MemoryStore store; + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kFull, + std::vector{{"flagA", FlagDescriptor(flag_a)}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_TRUE(fetched->item.has_value()); + EXPECT_EQ("flagA", fetched->item->key); + EXPECT_EQ(1u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyFull_WithSegment) { + MemoryStore store; + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kFull, + std::vector{{"segA", SegmentDescriptor(seg_a)}}, + Selector{}, + }); + + auto fetched = store.GetSegment("segA"); + ASSERT_TRUE(fetched); + EXPECT_TRUE(fetched->item.has_value()); + EXPECT_EQ("segA", fetched->item->key); + EXPECT_EQ(1u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingFlags) { + MemoryStore store; + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + Flag flag_b; + flag_b.version = 1; + flag_b.key = "flagB"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_c; + flag_c.version = 1; + flag_c.key = "flagC"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kFull, + std::vector{{"flagC", FlagDescriptor(flag_c)}}, + Selector{}, + }); + + EXPECT_FALSE(store.GetFlag("flagA")); + EXPECT_FALSE(store.GetFlag("flagB")); + ASSERT_TRUE(store.GetFlag("flagC")); + EXPECT_EQ("flagC", store.GetFlag("flagC")->item->key); +} + +TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingSegments) { + MemoryStore store; + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, + }); + + Segment seg_b; + seg_b.version = 1; + seg_b.key = "segB"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kFull, + std::vector{{"segB", SegmentDescriptor(seg_b)}}, + Selector{}, + }); + + EXPECT_FALSE(store.GetSegment("segA")); + ASSERT_TRUE(store.GetSegment("segB")); +} + +TEST(MemoryStoreApplyTest, ApplyFull_EmptyChangeSetClearsStore) { + MemoryStore store; + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, + }); + + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); + + EXPECT_EQ(0u, store.AllFlags().size()); + EXPECT_EQ(0u, store.AllSegments().size()); +} + +TEST(MemoryStoreApplyTest, ApplyFull_WithFlagTombstone) { + MemoryStore store; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kFull, + std::vector{{"flagA", FlagDescriptor(Tombstone(5))}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(5u, fetched->version); + EXPECT_FALSE(fetched->item.has_value()); +} + +TEST(MemoryStoreApplyTest, ApplyFull_WithSegmentTombstone) { + MemoryStore store; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kFull, + std::vector{{"segA", SegmentDescriptor(Tombstone(3))}}, + Selector{}, + }); + + auto fetched = store.GetSegment("segA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(3u, fetched->version); + EXPECT_FALSE(fetched->item.has_value()); +} + +// --------------------------------------------------------------------------- +// kPartial tests +// --------------------------------------------------------------------------- + +TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewFlag) { + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(flag_a)}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_TRUE(fetched->item.has_value()); + EXPECT_EQ("flagA", fetched->item->key); + EXPECT_EQ(1u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewSegment) { + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"segA", SegmentDescriptor(seg_a)}}, + Selector{}, + }); + + auto fetched = store.GetSegment("segA"); + ASSERT_TRUE(fetched); + EXPECT_TRUE(fetched->item.has_value()); + EXPECT_EQ("segA", fetched->item->key); + EXPECT_EQ(1u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithLowerVersion) { + MemoryStore store; + Flag flag_a; + flag_a.version = 5; + flag_a.key = "flagA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_stale; + flag_a_stale.version = 3; + flag_a_stale.key = "flagA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(flag_a_stale)}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(5u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithEqualVersion) { + MemoryStore store; + Flag flag_a; + flag_a.version = 5; + flag_a.key = "flagA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_same; + flag_a_same.version = 5; + flag_a_same.key = "flagA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(flag_a_same)}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(5u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFlagWithHigherVersion) { + MemoryStore store; + Flag flag_a; + flag_a.version = 5; + flag_a.key = "flagA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_new; + flag_a_new.version = 6; + flag_a_new.key = "flagA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(flag_a_new)}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(6u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_SkipsSegmentWithLowerVersion) { + MemoryStore store; + Segment seg_a; + seg_a.version = 5; + seg_a.key = "segA"; + + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, + }); + + Segment seg_a_stale; + seg_a_stale.version = 3; + seg_a_stale.key = "segA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"segA", SegmentDescriptor(seg_a_stale)}}, + Selector{}, + }); + + auto fetched = store.GetSegment("segA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(5u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_AppliesSegmentWithHigherVersion) { + MemoryStore store; + Segment seg_a; + seg_a.version = 5; + seg_a.key = "segA"; + + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, + }); + + Segment seg_a_new; + seg_a_new.version = 6; + seg_a_new.key = "segA"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"segA", SegmentDescriptor(seg_a_new)}}, + Selector{}, + }); + + auto fetched = store.GetSegment("segA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(6u, fetched->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedFlags) { + MemoryStore store; + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + Flag flag_b; + flag_b.version = 1; + flag_b.key = "flagB"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_b_new; + flag_b_new.version = 2; + flag_b_new.key = "flagB"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagB", FlagDescriptor(flag_b_new)}}, + Selector{}, + }); + + auto fetched_a = store.GetFlag("flagA"); + ASSERT_TRUE(fetched_a); + EXPECT_EQ(1u, fetched_a->version); + + auto fetched_b = store.GetFlag("flagB"); + ASSERT_TRUE(fetched_b); + EXPECT_EQ(2u, fetched_b->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedSegments) { + MemoryStore store; + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + + Segment seg_b; + seg_b.version = 1; + seg_b.key = "segB"; + + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}, + {"segB", SegmentDescriptor(seg_b)}}, + }); + + Segment seg_b_new; + seg_b_new.version = 2; + seg_b_new.key = "segB"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"segB", SegmentDescriptor(seg_b_new)}}, + Selector{}, + }); + + auto fetched_a = store.GetSegment("segA"); + ASSERT_TRUE(fetched_a); + EXPECT_EQ(1u, fetched_a->version); + + auto fetched_b = store.GetSegment("segB"); + ASSERT_TRUE(fetched_b); + EXPECT_EQ(2u, fetched_b->version); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_WithFlagTombstone) { + MemoryStore store; + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(Tombstone(2))}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(2u, fetched->version); + EXPECT_FALSE(fetched->item.has_value()); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_TombstoneSkippedIfVersionNotNewer) { + MemoryStore store; + Flag flag_a; + flag_a.version = 5; + flag_a.key = "flagA"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + // Tombstone at version 3 < stored version 5: should be ignored. + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(Tombstone(3))}}, + Selector{}, + }); + + auto fetched = store.GetFlag("flagA"); + ASSERT_TRUE(fetched); + EXPECT_EQ(5u, fetched->version); + EXPECT_TRUE(fetched->item.has_value()); +} + +TEST(MemoryStoreApplyTest, ApplyPartial_MixedStaleAndFreshItems) { + MemoryStore store; + Flag flag_a; + flag_a.version = 10; + flag_a.key = "flagA"; + + Flag flag_b; + flag_b.version = 1; + flag_b.key = "flagB"; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_a_stale; + flag_a_stale.version = 5; + flag_a_stale.key = "flagA"; + + Flag flag_b_new; + flag_b_new.version = 2; + flag_b_new.key = "flagB"; + + store.Apply(FDv2ChangeSet{ + FDv2ChangeSet::Type::kPartial, + std::vector{{"flagA", FlagDescriptor(flag_a_stale)}, + {"flagB", FlagDescriptor(flag_b_new)}}, + Selector{}, + }); + + // flagA version 5 < 10: skip. + EXPECT_EQ(10u, store.GetFlag("flagA")->version); + // flagB version 2 > 1: apply. + EXPECT_EQ(2u, store.GetFlag("flagB")->version); +} From cc59635bccfd2990cc53ae65f5256f83f1fb5027 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 27 Mar 2026 14:19:23 -0700 Subject: [PATCH 2/6] fix an unneeded copy, and warn on missing enum case --- .../memory_store/memory_store.cpp | 28 ++++++++++--------- .../memory_store/memory_store.hpp | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 6e0365d7f..7dc5105e3 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -1,8 +1,6 @@ #include "memory_store.hpp" -#include - -#include +#include namespace launchdarkly::server_side::data_components { @@ -86,20 +84,24 @@ bool MemoryStore::RemoveSegment(std::string const& key) { return segments_.erase(key) == 1; } -void MemoryStore::Apply(data_model::FDv2ChangeSet const& changeSet) { +void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { std::lock_guard lock{data_mutex_}; - if (changeSet.type == data_model::FDv2ChangeSet::Type::kNone) { - return; - } - - if (changeSet.type == data_model::FDv2ChangeSet::Type::kFull) { - initialized_ = true; - flags_.clear(); - segments_.clear(); + switch (changeSet.type) { + case data_model::FDv2ChangeSet::Type::kNone: + return; + case data_model::FDv2ChangeSet::Type::kPartial: + break; + case data_model::FDv2ChangeSet::Type::kFull: + initialized_ = true; + flags_.clear(); + segments_.clear(); + break; + default: + detail::unreachable(); } - for (auto change : changeSet.changes) { + for (auto& change : changeSet.changes) { if (std::holds_alternative(change.object)) { auto& flag_descriptor = std::get(change.object); diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 81846dfab..e9a067881 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -46,7 +46,7 @@ class MemoryStore final : public data_interfaces::IStore, bool RemoveSegment(std::string const& key); - void Apply(data_model::FDv2ChangeSet const& changeSet); + void Apply(data_model::FDv2ChangeSet changeSet); MemoryStore() = default; ~MemoryStore() override = default; From 82d09d26bd805d72012bf329d90933db9678aaef Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 27 Mar 2026 16:02:23 -0700 Subject: [PATCH 3/6] add an ApplyResult with the changed keys --- .../memory_store/memory_store.cpp | 17 ++- .../memory_store/memory_store.hpp | 8 +- .../tests/memory_store_apply_test.cpp | 123 +++++++++++++++--- 3 files changed, 125 insertions(+), 23 deletions(-) diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 7dc5105e3..7680853a7 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -84,15 +84,24 @@ bool MemoryStore::RemoveSegment(std::string const& key) { return segments_.erase(key) == 1; } -void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { +ApplyResult MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { + ApplyResult result; std::lock_guard lock{data_mutex_}; switch (changeSet.type) { case data_model::FDv2ChangeSet::Type::kNone: - return; + return result; case data_model::FDv2ChangeSet::Type::kPartial: break; case data_model::FDv2ChangeSet::Type::kFull: + // When there's a full change, any current keys are considered + // changed, regardless of whether they are in the new set. + for (auto const& [key, _] : flags_) { + result.flags.insert(key); + } + for (auto const& [key, _] : segments_) { + result.segments.insert(key); + } initialized_ = true; flags_.clear(); segments_.clear(); @@ -114,6 +123,7 @@ void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { flags_[change.key] = std::make_shared( std::move(flag_descriptor)); + result.flags.insert(change.key); } else if (std::holds_alternative( change.object)) { auto& segment_descriptor = @@ -129,8 +139,11 @@ void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { segments_[change.key] = std::make_shared( std::move(segment_descriptor)); + result.segments.insert(change.key); } } + + return result; } } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index e9a067881..7712eb67e 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -9,9 +9,15 @@ #include #include #include +#include namespace launchdarkly::server_side::data_components { +struct ApplyResult { + std::unordered_set flags; + std::unordered_set segments; +}; + class MemoryStore final : public data_interfaces::IStore, public data_interfaces::IDestination { public: @@ -46,7 +52,7 @@ class MemoryStore final : public data_interfaces::IStore, bool RemoveSegment(std::string const& key); - void Apply(data_model::FDv2ChangeSet changeSet); + ApplyResult Apply(data_model::FDv2ChangeSet changeSet); MemoryStore() = default; ~MemoryStore() override = default; diff --git a/libs/server-sdk/tests/memory_store_apply_test.cpp b/libs/server-sdk/tests/memory_store_apply_test.cpp index 219e36a15..d0ab96868 100644 --- a/libs/server-sdk/tests/memory_store_apply_test.cpp +++ b/libs/server-sdk/tests/memory_store_apply_test.cpp @@ -27,7 +27,8 @@ TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { {"segA", SegmentDescriptor(seg_a)}}, }); - store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + auto result = + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); auto fetched_flag = store.GetFlag("flagA"); ASSERT_TRUE(fetched_flag); @@ -35,6 +36,9 @@ TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { auto fetched_seg = store.GetSegment("segA"); ASSERT_TRUE(fetched_seg); EXPECT_EQ(1u, fetched_seg->version); + + EXPECT_TRUE(result.flags.empty()); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { @@ -60,7 +64,7 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithFlag) { flag_a.version = 1; flag_a.key = "flagA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"flagA", FlagDescriptor(flag_a)}}, Selector{}, @@ -71,6 +75,10 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithFlag) { EXPECT_TRUE(fetched->item.has_value()); EXPECT_EQ("flagA", fetched->item->key); EXPECT_EQ(1u, fetched->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyFull_WithSegment) { @@ -79,7 +87,7 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithSegment) { seg_a.version = 1; seg_a.key = "segA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"segA", SegmentDescriptor(seg_a)}}, Selector{}, @@ -90,6 +98,10 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithSegment) { EXPECT_TRUE(fetched->item.has_value()); EXPECT_EQ("segA", fetched->item->key); EXPECT_EQ(1u, fetched->version); + + EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segA")); } TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingFlags) { @@ -113,7 +125,7 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingFlags) { flag_c.version = 1; flag_c.key = "flagC"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"flagC", FlagDescriptor(flag_c)}}, Selector{}, @@ -123,6 +135,13 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingFlags) { EXPECT_FALSE(store.GetFlag("flagB")); ASSERT_TRUE(store.GetFlag("flagC")); EXPECT_EQ("flagC", store.GetFlag("flagC")->item->key); + + // Cleared keys (flagA, flagB) and new key (flagC) all reported as changed. + ASSERT_EQ(3u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + EXPECT_EQ(1u, result.flags.count("flagB")); + EXPECT_EQ(1u, result.flags.count("flagC")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingSegments) { @@ -141,7 +160,7 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingSegments) { seg_b.version = 1; seg_b.key = "segB"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"segB", SegmentDescriptor(seg_b)}}, Selector{}, @@ -149,6 +168,12 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingSegments) { EXPECT_FALSE(store.GetSegment("segA")); ASSERT_TRUE(store.GetSegment("segB")); + + // Cleared key (segA) and new key (segB) both reported as changed. + EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(2u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segA")); + EXPECT_EQ(1u, result.segments.count("segB")); } TEST(MemoryStoreApplyTest, ApplyFull_EmptyChangeSetClearsStore) { @@ -168,16 +193,22 @@ TEST(MemoryStoreApplyTest, ApplyFull_EmptyChangeSetClearsStore) { {"segA", SegmentDescriptor(seg_a)}}, }); - store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); + auto result = + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); EXPECT_EQ(0u, store.AllFlags().size()); EXPECT_EQ(0u, store.AllSegments().size()); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + ASSERT_EQ(1u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segA")); } TEST(MemoryStoreApplyTest, ApplyFull_WithFlagTombstone) { MemoryStore store; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"flagA", FlagDescriptor(Tombstone(5))}}, Selector{}, @@ -187,12 +218,16 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithFlagTombstone) { ASSERT_TRUE(fetched); EXPECT_EQ(5u, fetched->version); EXPECT_FALSE(fetched->item.has_value()); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyFull_WithSegmentTombstone) { MemoryStore store; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"segA", SegmentDescriptor(Tombstone(3))}}, Selector{}, @@ -202,6 +237,10 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithSegmentTombstone) { ASSERT_TRUE(fetched); EXPECT_EQ(3u, fetched->version); EXPECT_FALSE(fetched->item.has_value()); + + EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segA")); } // --------------------------------------------------------------------------- @@ -219,7 +258,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewFlag) { flag_a.version = 1; flag_a.key = "flagA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(flag_a)}}, Selector{}, @@ -230,6 +269,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewFlag) { EXPECT_TRUE(fetched->item.has_value()); EXPECT_EQ("flagA", fetched->item->key); EXPECT_EQ(1u, fetched->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewSegment) { @@ -243,7 +286,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewSegment) { seg_a.version = 1; seg_a.key = "segA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"segA", SegmentDescriptor(seg_a)}}, Selector{}, @@ -254,6 +297,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewSegment) { EXPECT_TRUE(fetched->item.has_value()); EXPECT_EQ("segA", fetched->item->key); EXPECT_EQ(1u, fetched->version); + + EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segA")); } TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithLowerVersion) { @@ -272,7 +319,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithLowerVersion) { flag_a_stale.version = 3; flag_a_stale.key = "flagA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(flag_a_stale)}}, Selector{}, @@ -281,6 +328,9 @@ TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithLowerVersion) { auto fetched = store.GetFlag("flagA"); ASSERT_TRUE(fetched); EXPECT_EQ(5u, fetched->version); + + EXPECT_TRUE(result.flags.empty()); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithEqualVersion) { @@ -299,7 +349,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithEqualVersion) { flag_a_same.version = 5; flag_a_same.key = "flagA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(flag_a_same)}}, Selector{}, @@ -308,6 +358,9 @@ TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithEqualVersion) { auto fetched = store.GetFlag("flagA"); ASSERT_TRUE(fetched); EXPECT_EQ(5u, fetched->version); + + EXPECT_TRUE(result.flags.empty()); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFlagWithHigherVersion) { @@ -326,7 +379,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFlagWithHigherVersion) { flag_a_new.version = 6; flag_a_new.key = "flagA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(flag_a_new)}}, Selector{}, @@ -335,6 +388,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFlagWithHigherVersion) { auto fetched = store.GetFlag("flagA"); ASSERT_TRUE(fetched); EXPECT_EQ(6u, fetched->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_SkipsSegmentWithLowerVersion) { @@ -353,7 +410,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_SkipsSegmentWithLowerVersion) { seg_a_stale.version = 3; seg_a_stale.key = "segA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"segA", SegmentDescriptor(seg_a_stale)}}, Selector{}, @@ -362,6 +419,9 @@ TEST(MemoryStoreApplyTest, ApplyPartial_SkipsSegmentWithLowerVersion) { auto fetched = store.GetSegment("segA"); ASSERT_TRUE(fetched); EXPECT_EQ(5u, fetched->version); + + EXPECT_TRUE(result.flags.empty()); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_AppliesSegmentWithHigherVersion) { @@ -380,7 +440,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesSegmentWithHigherVersion) { seg_a_new.version = 6; seg_a_new.key = "segA"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"segA", SegmentDescriptor(seg_a_new)}}, Selector{}, @@ -389,6 +449,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesSegmentWithHigherVersion) { auto fetched = store.GetSegment("segA"); ASSERT_TRUE(fetched); EXPECT_EQ(6u, fetched->version); + + EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segA")); } TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedFlags) { @@ -412,7 +476,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedFlags) { flag_b_new.version = 2; flag_b_new.key = "flagB"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagB", FlagDescriptor(flag_b_new)}}, Selector{}, @@ -425,6 +489,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedFlags) { auto fetched_b = store.GetFlag("flagB"); ASSERT_TRUE(fetched_b); EXPECT_EQ(2u, fetched_b->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagB")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedSegments) { @@ -448,7 +516,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedSegments) { seg_b_new.version = 2; seg_b_new.key = "segB"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"segB", SegmentDescriptor(seg_b_new)}}, Selector{}, @@ -461,6 +529,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedSegments) { auto fetched_b = store.GetSegment("segB"); ASSERT_TRUE(fetched_b); EXPECT_EQ(2u, fetched_b->version); + + EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.segments.size()); + EXPECT_EQ(1u, result.segments.count("segB")); } TEST(MemoryStoreApplyTest, ApplyPartial_WithFlagTombstone) { @@ -475,7 +547,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_WithFlagTombstone) { std::unordered_map(), }); - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(Tombstone(2))}}, Selector{}, @@ -485,6 +557,10 @@ TEST(MemoryStoreApplyTest, ApplyPartial_WithFlagTombstone) { ASSERT_TRUE(fetched); EXPECT_EQ(2u, fetched->version); EXPECT_FALSE(fetched->item.has_value()); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_TombstoneSkippedIfVersionNotNewer) { @@ -500,7 +576,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_TombstoneSkippedIfVersionNotNewer) { }); // Tombstone at version 3 < stored version 5: should be ignored. - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(Tombstone(3))}}, Selector{}, @@ -510,6 +586,9 @@ TEST(MemoryStoreApplyTest, ApplyPartial_TombstoneSkippedIfVersionNotNewer) { ASSERT_TRUE(fetched); EXPECT_EQ(5u, fetched->version); EXPECT_TRUE(fetched->item.has_value()); + + EXPECT_TRUE(result.flags.empty()); + EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyPartial_MixedStaleAndFreshItems) { @@ -537,7 +616,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_MixedStaleAndFreshItems) { flag_b_new.version = 2; flag_b_new.key = "flagB"; - store.Apply(FDv2ChangeSet{ + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(flag_a_stale)}, {"flagB", FlagDescriptor(flag_b_new)}}, @@ -548,4 +627,8 @@ TEST(MemoryStoreApplyTest, ApplyPartial_MixedStaleAndFreshItems) { EXPECT_EQ(10u, store.GetFlag("flagA")->version); // flagB version 2 > 1: apply. EXPECT_EQ(2u, store.GetFlag("flagB")->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagB")); + EXPECT_TRUE(result.segments.empty()); } From 259d8e901e9b4d2f76a85e63f423213e6cbf6340 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 27 Mar 2026 16:08:41 -0700 Subject: [PATCH 4/6] mark the apply result as nodiscard for now --- .../src/data_components/memory_store/memory_store.cpp | 2 +- .../src/data_components/memory_store/memory_store.hpp | 2 +- libs/server-sdk/tests/memory_store_apply_test.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 7680853a7..d8e9474af 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -85,8 +85,8 @@ bool MemoryStore::RemoveSegment(std::string const& key) { } ApplyResult MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { - ApplyResult result; std::lock_guard lock{data_mutex_}; + ApplyResult result; switch (changeSet.type) { case data_model::FDv2ChangeSet::Type::kNone: diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 7712eb67e..5013adcce 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -52,7 +52,7 @@ class MemoryStore final : public data_interfaces::IStore, bool RemoveSegment(std::string const& key); - ApplyResult Apply(data_model::FDv2ChangeSet changeSet); + [[nodiscard]] ApplyResult Apply(data_model::FDv2ChangeSet changeSet); MemoryStore() = default; ~MemoryStore() override = default; diff --git a/libs/server-sdk/tests/memory_store_apply_test.cpp b/libs/server-sdk/tests/memory_store_apply_test.cpp index d0ab96868..844076559 100644 --- a/libs/server-sdk/tests/memory_store_apply_test.cpp +++ b/libs/server-sdk/tests/memory_store_apply_test.cpp @@ -43,7 +43,7 @@ TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { MemoryStore store; - store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + std::ignore = store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); EXPECT_FALSE(store.Initialized()); } @@ -54,7 +54,7 @@ TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { TEST(MemoryStoreApplyTest, ApplyFull_SetsInitialized) { MemoryStore store; ASSERT_FALSE(store.Initialized()); - store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); + std::ignore = store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); EXPECT_TRUE(store.Initialized()); } From f5b82bf6240b0cec36943d2c8de7cd5162414df6 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 27 Mar 2026 16:19:50 -0700 Subject: [PATCH 5/6] simplify tests --- .../tests/memory_store_apply_test.cpp | 381 ++++++------------ 1 file changed, 120 insertions(+), 261 deletions(-) diff --git a/libs/server-sdk/tests/memory_store_apply_test.cpp b/libs/server-sdk/tests/memory_store_apply_test.cpp index 844076559..619fe380b 100644 --- a/libs/server-sdk/tests/memory_store_apply_test.cpp +++ b/libs/server-sdk/tests/memory_store_apply_test.cpp @@ -43,7 +43,8 @@ TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { MemoryStore store; - std::ignore = store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + std::ignore = store.Apply( + FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); EXPECT_FALSE(store.Initialized()); } @@ -54,57 +55,47 @@ TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { TEST(MemoryStoreApplyTest, ApplyFull_SetsInitialized) { MemoryStore store; ASSERT_FALSE(store.Initialized()); - std::ignore = store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); + std::ignore = store.Apply( + FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); EXPECT_TRUE(store.Initialized()); } -TEST(MemoryStoreApplyTest, ApplyFull_WithFlag) { +TEST(MemoryStoreApplyTest, ApplyFull_StoresItems) { MemoryStore store; Flag flag_a; flag_a.version = 1; flag_a.key = "flagA"; - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kFull, - std::vector{{"flagA", FlagDescriptor(flag_a)}}, - Selector{}, - }); - - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_TRUE(fetched->item.has_value()); - EXPECT_EQ("flagA", fetched->item->key); - EXPECT_EQ(1u, fetched->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyFull_WithSegment) { - MemoryStore store; Segment seg_a; seg_a.version = 1; seg_a.key = "segA"; auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, - std::vector{{"segA", SegmentDescriptor(seg_a)}}, + std::vector{{"flagA", FlagDescriptor(flag_a)}, + {"segA", SegmentDescriptor(seg_a)}}, Selector{}, }); - auto fetched = store.GetSegment("segA"); - ASSERT_TRUE(fetched); - EXPECT_TRUE(fetched->item.has_value()); - EXPECT_EQ("segA", fetched->item->key); - EXPECT_EQ(1u, fetched->version); + auto fetched_flag = store.GetFlag("flagA"); + ASSERT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item.has_value()); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1u, fetched_flag->version); - EXPECT_TRUE(result.flags.empty()); + auto fetched_seg = store.GetSegment("segA"); + ASSERT_TRUE(fetched_seg); + EXPECT_TRUE(fetched_seg->item.has_value()); + EXPECT_EQ("segA", fetched_seg->item->key); + EXPECT_EQ(1u, fetched_seg->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); ASSERT_EQ(1u, result.segments.size()); EXPECT_EQ(1u, result.segments.count("segA")); } -TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingFlags) { +TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingItems) { MemoryStore store; Flag flag_a; flag_a.version = 1; @@ -114,63 +105,44 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingFlags) { flag_b.version = 1; flag_b.key = "flagB"; + Segment seg_a; + seg_a.version = 1; + seg_a.key = "segA"; + store.Init(SDKDataSet{ std::unordered_map{ {"flagA", FlagDescriptor(flag_a)}, {"flagB", FlagDescriptor(flag_b)}}, - std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, }); Flag flag_c; flag_c.version = 1; flag_c.key = "flagC"; + Segment seg_b; + seg_b.version = 1; + seg_b.key = "segB"; + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, - std::vector{{"flagC", FlagDescriptor(flag_c)}}, + std::vector{{"flagC", FlagDescriptor(flag_c)}, + {"segB", SegmentDescriptor(seg_b)}}, Selector{}, }); EXPECT_FALSE(store.GetFlag("flagA")); EXPECT_FALSE(store.GetFlag("flagB")); ASSERT_TRUE(store.GetFlag("flagC")); - EXPECT_EQ("flagC", store.GetFlag("flagC")->item->key); + EXPECT_FALSE(store.GetSegment("segA")); + ASSERT_TRUE(store.GetSegment("segB")); - // Cleared keys (flagA, flagB) and new key (flagC) all reported as changed. + // Cleared keys and new keys all reported as changed. ASSERT_EQ(3u, result.flags.size()); EXPECT_EQ(1u, result.flags.count("flagA")); EXPECT_EQ(1u, result.flags.count("flagB")); EXPECT_EQ(1u, result.flags.count("flagC")); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingSegments) { - MemoryStore store; - Segment seg_a; - seg_a.version = 1; - seg_a.key = "segA"; - - store.Init(SDKDataSet{ - std::unordered_map(), - std::unordered_map{ - {"segA", SegmentDescriptor(seg_a)}}, - }); - - Segment seg_b; - seg_b.version = 1; - seg_b.key = "segB"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kFull, - std::vector{{"segB", SegmentDescriptor(seg_b)}}, - Selector{}, - }); - - EXPECT_FALSE(store.GetSegment("segA")); - ASSERT_TRUE(store.GetSegment("segB")); - - // Cleared key (segA) and new key (segB) both reported as changed. - EXPECT_TRUE(result.flags.empty()); ASSERT_EQ(2u, result.segments.size()); EXPECT_EQ(1u, result.segments.count("segA")); EXPECT_EQ(1u, result.segments.count("segB")); @@ -224,30 +196,11 @@ TEST(MemoryStoreApplyTest, ApplyFull_WithFlagTombstone) { EXPECT_TRUE(result.segments.empty()); } -TEST(MemoryStoreApplyTest, ApplyFull_WithSegmentTombstone) { - MemoryStore store; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kFull, - std::vector{{"segA", SegmentDescriptor(Tombstone(3))}}, - Selector{}, - }); - - auto fetched = store.GetSegment("segA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(3u, fetched->version); - EXPECT_FALSE(fetched->item.has_value()); - - EXPECT_TRUE(result.flags.empty()); - ASSERT_EQ(1u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segA")); -} - // --------------------------------------------------------------------------- // kPartial tests // --------------------------------------------------------------------------- -TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewFlag) { +TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewItems) { MemoryStore store; store.Init(SDKDataSet{ std::unordered_map(), @@ -258,183 +211,137 @@ TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewFlag) { flag_a.version = 1; flag_a.key = "flagA"; - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a)}}, - Selector{}, - }); - - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_TRUE(fetched->item.has_value()); - EXPECT_EQ("flagA", fetched->item->key); - EXPECT_EQ(1u, fetched->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewSegment) { - MemoryStore store; - store.Init(SDKDataSet{ - std::unordered_map(), - std::unordered_map(), - }); - Segment seg_a; seg_a.version = 1; seg_a.key = "segA"; auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, - std::vector{{"segA", SegmentDescriptor(seg_a)}}, + std::vector{{"flagA", FlagDescriptor(flag_a)}, + {"segA", SegmentDescriptor(seg_a)}}, Selector{}, }); - auto fetched = store.GetSegment("segA"); - ASSERT_TRUE(fetched); - EXPECT_TRUE(fetched->item.has_value()); - EXPECT_EQ("segA", fetched->item->key); - EXPECT_EQ(1u, fetched->version); + auto fetched_flag = store.GetFlag("flagA"); + ASSERT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item.has_value()); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1u, fetched_flag->version); - EXPECT_TRUE(result.flags.empty()); + auto fetched_seg = store.GetSegment("segA"); + ASSERT_TRUE(fetched_seg); + EXPECT_TRUE(fetched_seg->item.has_value()); + EXPECT_EQ("segA", fetched_seg->item->key); + EXPECT_EQ(1u, fetched_seg->version); + + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); ASSERT_EQ(1u, result.segments.size()); EXPECT_EQ(1u, result.segments.count("segA")); } -TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithLowerVersion) { +TEST(MemoryStoreApplyTest, ApplyPartial_SkipsStaleItems) { MemoryStore store; Flag flag_a; flag_a.version = 5; flag_a.key = "flagA"; + Segment seg_a; + seg_a.version = 5; + seg_a.key = "segA"; + store.Init(SDKDataSet{ std::unordered_map{ {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, }); Flag flag_a_stale; flag_a_stale.version = 3; flag_a_stale.key = "flagA"; + Segment seg_a_stale; + seg_a_stale.version = 3; + seg_a_stale.key = "segA"; + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a_stale)}}, + std::vector{{"flagA", FlagDescriptor(flag_a_stale)}, + {"segA", SegmentDescriptor(seg_a_stale)}}, Selector{}, }); - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(5u, fetched->version); + ASSERT_TRUE(store.GetFlag("flagA")); + EXPECT_EQ(5u, store.GetFlag("flagA")->version); + ASSERT_TRUE(store.GetSegment("segA")); + EXPECT_EQ(5u, store.GetSegment("segA")->version); EXPECT_TRUE(result.flags.empty()); EXPECT_TRUE(result.segments.empty()); } -TEST(MemoryStoreApplyTest, ApplyPartial_SkipsFlagWithEqualVersion) { +TEST(MemoryStoreApplyTest, ApplyPartial_SkipsItemsWithEqualVersion) { MemoryStore store; Flag flag_a; flag_a.version = 5; flag_a.key = "flagA"; + Segment seg_a; + seg_a.version = 5; + seg_a.key = "segA"; + store.Init(SDKDataSet{ std::unordered_map{ {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map(), + std::unordered_map{ + {"segA", SegmentDescriptor(seg_a)}}, }); Flag flag_a_same; flag_a_same.version = 5; flag_a_same.key = "flagA"; + Segment seg_a_same; + seg_a_same.version = 5; + seg_a_same.key = "segA"; + auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a_same)}}, + std::vector{{"flagA", FlagDescriptor(flag_a_same)}, + {"segA", SegmentDescriptor(seg_a_same)}}, Selector{}, }); - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(5u, fetched->version); + ASSERT_TRUE(store.GetFlag("flagA")); + EXPECT_EQ(5u, store.GetFlag("flagA")->version); + ASSERT_TRUE(store.GetSegment("segA")); + EXPECT_EQ(5u, store.GetSegment("segA")->version); EXPECT_TRUE(result.flags.empty()); EXPECT_TRUE(result.segments.empty()); } -TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFlagWithHigherVersion) { +TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFreshItems) { MemoryStore store; Flag flag_a; flag_a.version = 5; flag_a.key = "flagA"; - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map(), - }); - - Flag flag_a_new; - flag_a_new.version = 6; - flag_a_new.key = "flagA"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a_new)}}, - Selector{}, - }); - - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(6u, fetched->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_SkipsSegmentWithLowerVersion) { - MemoryStore store; Segment seg_a; seg_a.version = 5; seg_a.key = "segA"; store.Init(SDKDataSet{ - std::unordered_map(), + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, std::unordered_map{ {"segA", SegmentDescriptor(seg_a)}}, }); - Segment seg_a_stale; - seg_a_stale.version = 3; - seg_a_stale.key = "segA"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"segA", SegmentDescriptor(seg_a_stale)}}, - Selector{}, - }); - - auto fetched = store.GetSegment("segA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(5u, fetched->version); - - EXPECT_TRUE(result.flags.empty()); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_AppliesSegmentWithHigherVersion) { - MemoryStore store; - Segment seg_a; - seg_a.version = 5; - seg_a.key = "segA"; - - store.Init(SDKDataSet{ - std::unordered_map(), - std::unordered_map{ - {"segA", SegmentDescriptor(seg_a)}}, - }); + Flag flag_a_new; + flag_a_new.version = 6; + flag_a_new.key = "flagA"; Segment seg_a_new; seg_a_new.version = 6; @@ -442,20 +349,23 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesSegmentWithHigherVersion) { auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, - std::vector{{"segA", SegmentDescriptor(seg_a_new)}}, + std::vector{{"flagA", FlagDescriptor(flag_a_new)}, + {"segA", SegmentDescriptor(seg_a_new)}}, Selector{}, }); - auto fetched = store.GetSegment("segA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(6u, fetched->version); + ASSERT_TRUE(store.GetFlag("flagA")); + EXPECT_EQ(6u, store.GetFlag("flagA")->version); + ASSERT_TRUE(store.GetSegment("segA")); + EXPECT_EQ(6u, store.GetSegment("segA")->version); - EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagA")); ASSERT_EQ(1u, result.segments.size()); EXPECT_EQ(1u, result.segments.count("segA")); } -TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedFlags) { +TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedItems) { MemoryStore store; Flag flag_a; flag_a.version = 1; @@ -465,38 +375,6 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedFlags) { flag_b.version = 1; flag_b.key = "flagB"; - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}, - {"flagB", FlagDescriptor(flag_b)}}, - std::unordered_map(), - }); - - Flag flag_b_new; - flag_b_new.version = 2; - flag_b_new.key = "flagB"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagB", FlagDescriptor(flag_b_new)}}, - Selector{}, - }); - - auto fetched_a = store.GetFlag("flagA"); - ASSERT_TRUE(fetched_a); - EXPECT_EQ(1u, fetched_a->version); - - auto fetched_b = store.GetFlag("flagB"); - ASSERT_TRUE(fetched_b); - EXPECT_EQ(2u, fetched_b->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagB")); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedSegments) { - MemoryStore store; Segment seg_a; seg_a.version = 1; seg_a.key = "segA"; @@ -506,31 +384,40 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedSegments) { seg_b.key = "segB"; store.Init(SDKDataSet{ - std::unordered_map(), + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, std::unordered_map{ {"segA", SegmentDescriptor(seg_a)}, {"segB", SegmentDescriptor(seg_b)}}, }); + Flag flag_b_new; + flag_b_new.version = 2; + flag_b_new.key = "flagB"; + Segment seg_b_new; seg_b_new.version = 2; seg_b_new.key = "segB"; auto result = store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, - std::vector{{"segB", SegmentDescriptor(seg_b_new)}}, + std::vector{{"flagB", FlagDescriptor(flag_b_new)}, + {"segB", SegmentDescriptor(seg_b_new)}}, Selector{}, }); - auto fetched_a = store.GetSegment("segA"); - ASSERT_TRUE(fetched_a); - EXPECT_EQ(1u, fetched_a->version); - - auto fetched_b = store.GetSegment("segB"); - ASSERT_TRUE(fetched_b); - EXPECT_EQ(2u, fetched_b->version); + ASSERT_TRUE(store.GetFlag("flagA")); + EXPECT_EQ(1u, store.GetFlag("flagA")->version); + ASSERT_TRUE(store.GetFlag("flagB")); + EXPECT_EQ(2u, store.GetFlag("flagB")->version); + ASSERT_TRUE(store.GetSegment("segA")); + EXPECT_EQ(1u, store.GetSegment("segA")->version); + ASSERT_TRUE(store.GetSegment("segB")); + EXPECT_EQ(2u, store.GetSegment("segB")->version); - EXPECT_TRUE(result.flags.empty()); + ASSERT_EQ(1u, result.flags.size()); + EXPECT_EQ(1u, result.flags.count("flagB")); ASSERT_EQ(1u, result.segments.size()); EXPECT_EQ(1u, result.segments.count("segB")); } @@ -563,34 +450,6 @@ TEST(MemoryStoreApplyTest, ApplyPartial_WithFlagTombstone) { EXPECT_TRUE(result.segments.empty()); } -TEST(MemoryStoreApplyTest, ApplyPartial_TombstoneSkippedIfVersionNotNewer) { - MemoryStore store; - Flag flag_a; - flag_a.version = 5; - flag_a.key = "flagA"; - - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map(), - }); - - // Tombstone at version 3 < stored version 5: should be ignored. - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(Tombstone(3))}}, - Selector{}, - }); - - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(5u, fetched->version); - EXPECT_TRUE(fetched->item.has_value()); - - EXPECT_TRUE(result.flags.empty()); - EXPECT_TRUE(result.segments.empty()); -} - TEST(MemoryStoreApplyTest, ApplyPartial_MixedStaleAndFreshItems) { MemoryStore store; Flag flag_a; From 7826357582c1e65c305951b6c6a7a31a13c17144 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Mon, 30 Mar 2026 10:14:54 -0700 Subject: [PATCH 6/6] simplify, since FDv2 doesnt require version checking in memory store --- .../memory_store/memory_store.cpp | 42 +-- .../memory_store/memory_store.hpp | 8 +- .../tests/memory_store_apply_test.cpp | 286 +----------------- 3 files changed, 14 insertions(+), 322 deletions(-) diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index d8e9474af..c71acf08a 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -84,24 +84,15 @@ bool MemoryStore::RemoveSegment(std::string const& key) { return segments_.erase(key) == 1; } -ApplyResult MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { +void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { std::lock_guard lock{data_mutex_}; - ApplyResult result; switch (changeSet.type) { case data_model::FDv2ChangeSet::Type::kNone: - return result; + return; case data_model::FDv2ChangeSet::Type::kPartial: break; case data_model::FDv2ChangeSet::Type::kFull: - // When there's a full change, any current keys are considered - // changed, regardless of whether they are in the new set. - for (auto const& [key, _] : flags_) { - result.flags.insert(key); - } - for (auto const& [key, _] : segments_) { - result.segments.insert(key); - } initialized_ = true; flags_.clear(); segments_.clear(); @@ -112,38 +103,15 @@ ApplyResult MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) { for (auto& change : changeSet.changes) { if (std::holds_alternative(change.object)) { - auto& flag_descriptor = - std::get(change.object); - - auto existing_flag = flags_.find(change.key); - if (existing_flag != flags_.end() && - existing_flag->second->version >= flag_descriptor.version) { - continue; - } - flags_[change.key] = std::make_shared( - std::move(flag_descriptor)); - result.flags.insert(change.key); + std::move(std::get(change.object))); } else if (std::holds_alternative( change.object)) { - auto& segment_descriptor = - std::get(change.object); - - auto existing_segment = segments_.find(change.key); - if (existing_segment != segments_.end() && - existing_segment->second->version >= - segment_descriptor.version) { - continue; - } - segments_[change.key] = - std::make_shared( - std::move(segment_descriptor)); - result.segments.insert(change.key); + std::make_shared(std::move( + std::get(change.object))); } } - - return result; } } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 5013adcce..e9a067881 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -9,15 +9,9 @@ #include #include #include -#include namespace launchdarkly::server_side::data_components { -struct ApplyResult { - std::unordered_set flags; - std::unordered_set segments; -}; - class MemoryStore final : public data_interfaces::IStore, public data_interfaces::IDestination { public: @@ -52,7 +46,7 @@ class MemoryStore final : public data_interfaces::IStore, bool RemoveSegment(std::string const& key); - [[nodiscard]] ApplyResult Apply(data_model::FDv2ChangeSet changeSet); + void Apply(data_model::FDv2ChangeSet changeSet); MemoryStore() = default; ~MemoryStore() override = default; diff --git a/libs/server-sdk/tests/memory_store_apply_test.cpp b/libs/server-sdk/tests/memory_store_apply_test.cpp index 619fe380b..003285c53 100644 --- a/libs/server-sdk/tests/memory_store_apply_test.cpp +++ b/libs/server-sdk/tests/memory_store_apply_test.cpp @@ -27,8 +27,7 @@ TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { {"segA", SegmentDescriptor(seg_a)}}, }); - auto result = - store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); auto fetched_flag = store.GetFlag("flagA"); ASSERT_TRUE(fetched_flag); @@ -36,15 +35,11 @@ TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) { auto fetched_seg = store.GetSegment("segA"); ASSERT_TRUE(fetched_seg); EXPECT_EQ(1u, fetched_seg->version); - - EXPECT_TRUE(result.flags.empty()); - EXPECT_TRUE(result.segments.empty()); } TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { MemoryStore store; - std::ignore = store.Apply( - FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}}); EXPECT_FALSE(store.Initialized()); } @@ -55,8 +50,7 @@ TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) { TEST(MemoryStoreApplyTest, ApplyFull_SetsInitialized) { MemoryStore store; ASSERT_FALSE(store.Initialized()); - std::ignore = store.Apply( - FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); + store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); EXPECT_TRUE(store.Initialized()); } @@ -70,7 +64,7 @@ TEST(MemoryStoreApplyTest, ApplyFull_StoresItems) { seg_a.version = 1; seg_a.key = "segA"; - auto result = store.Apply(FDv2ChangeSet{ + store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"flagA", FlagDescriptor(flag_a)}, {"segA", SegmentDescriptor(seg_a)}}, @@ -88,11 +82,6 @@ TEST(MemoryStoreApplyTest, ApplyFull_StoresItems) { EXPECT_TRUE(fetched_seg->item.has_value()); EXPECT_EQ("segA", fetched_seg->item->key); EXPECT_EQ(1u, fetched_seg->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - ASSERT_EQ(1u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segA")); } TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingItems) { @@ -125,7 +114,7 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingItems) { seg_b.version = 1; seg_b.key = "segB"; - auto result = store.Apply(FDv2ChangeSet{ + store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kFull, std::vector{{"flagC", FlagDescriptor(flag_c)}, {"segB", SegmentDescriptor(seg_b)}}, @@ -137,192 +126,13 @@ TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingItems) { ASSERT_TRUE(store.GetFlag("flagC")); EXPECT_FALSE(store.GetSegment("segA")); ASSERT_TRUE(store.GetSegment("segB")); - - // Cleared keys and new keys all reported as changed. - ASSERT_EQ(3u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - EXPECT_EQ(1u, result.flags.count("flagB")); - EXPECT_EQ(1u, result.flags.count("flagC")); - ASSERT_EQ(2u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segA")); - EXPECT_EQ(1u, result.segments.count("segB")); -} - -TEST(MemoryStoreApplyTest, ApplyFull_EmptyChangeSetClearsStore) { - MemoryStore store; - Flag flag_a; - flag_a.version = 1; - flag_a.key = "flagA"; - - Segment seg_a; - seg_a.version = 1; - seg_a.key = "segA"; - - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map{ - {"segA", SegmentDescriptor(seg_a)}}, - }); - - auto result = - store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}}); - - EXPECT_EQ(0u, store.AllFlags().size()); - EXPECT_EQ(0u, store.AllSegments().size()); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - ASSERT_EQ(1u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segA")); -} - -TEST(MemoryStoreApplyTest, ApplyFull_WithFlagTombstone) { - MemoryStore store; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kFull, - std::vector{{"flagA", FlagDescriptor(Tombstone(5))}}, - Selector{}, - }); - - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(5u, fetched->version); - EXPECT_FALSE(fetched->item.has_value()); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - EXPECT_TRUE(result.segments.empty()); } // --------------------------------------------------------------------------- // kPartial tests // --------------------------------------------------------------------------- -TEST(MemoryStoreApplyTest, ApplyPartial_UpsertsNewItems) { - MemoryStore store; - store.Init(SDKDataSet{ - std::unordered_map(), - std::unordered_map(), - }); - - Flag flag_a; - flag_a.version = 1; - flag_a.key = "flagA"; - - Segment seg_a; - seg_a.version = 1; - seg_a.key = "segA"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a)}, - {"segA", SegmentDescriptor(seg_a)}}, - Selector{}, - }); - - auto fetched_flag = store.GetFlag("flagA"); - ASSERT_TRUE(fetched_flag); - EXPECT_TRUE(fetched_flag->item.has_value()); - EXPECT_EQ("flagA", fetched_flag->item->key); - EXPECT_EQ(1u, fetched_flag->version); - - auto fetched_seg = store.GetSegment("segA"); - ASSERT_TRUE(fetched_seg); - EXPECT_TRUE(fetched_seg->item.has_value()); - EXPECT_EQ("segA", fetched_seg->item->key); - EXPECT_EQ(1u, fetched_seg->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - ASSERT_EQ(1u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segA")); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_SkipsStaleItems) { - MemoryStore store; - Flag flag_a; - flag_a.version = 5; - flag_a.key = "flagA"; - - Segment seg_a; - seg_a.version = 5; - seg_a.key = "segA"; - - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map{ - {"segA", SegmentDescriptor(seg_a)}}, - }); - - Flag flag_a_stale; - flag_a_stale.version = 3; - flag_a_stale.key = "flagA"; - - Segment seg_a_stale; - seg_a_stale.version = 3; - seg_a_stale.key = "segA"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a_stale)}, - {"segA", SegmentDescriptor(seg_a_stale)}}, - Selector{}, - }); - - ASSERT_TRUE(store.GetFlag("flagA")); - EXPECT_EQ(5u, store.GetFlag("flagA")->version); - ASSERT_TRUE(store.GetSegment("segA")); - EXPECT_EQ(5u, store.GetSegment("segA")->version); - - EXPECT_TRUE(result.flags.empty()); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_SkipsItemsWithEqualVersion) { - MemoryStore store; - Flag flag_a; - flag_a.version = 5; - flag_a.key = "flagA"; - - Segment seg_a; - seg_a.version = 5; - seg_a.key = "segA"; - - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map{ - {"segA", SegmentDescriptor(seg_a)}}, - }); - - Flag flag_a_same; - flag_a_same.version = 5; - flag_a_same.key = "flagA"; - - Segment seg_a_same; - seg_a_same.version = 5; - seg_a_same.key = "segA"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a_same)}, - {"segA", SegmentDescriptor(seg_a_same)}}, - Selector{}, - }); - - ASSERT_TRUE(store.GetFlag("flagA")); - EXPECT_EQ(5u, store.GetFlag("flagA")->version); - ASSERT_TRUE(store.GetSegment("segA")); - EXPECT_EQ(5u, store.GetSegment("segA")->version); - - EXPECT_TRUE(result.flags.empty()); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFreshItems) { +TEST(MemoryStoreApplyTest, ApplyPartial_AppliesItems) { MemoryStore store; Flag flag_a; flag_a.version = 5; @@ -347,7 +157,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFreshItems) { seg_a_new.version = 6; seg_a_new.key = "segA"; - auto result = store.Apply(FDv2ChangeSet{ + store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagA", FlagDescriptor(flag_a_new)}, {"segA", SegmentDescriptor(seg_a_new)}}, @@ -358,11 +168,6 @@ TEST(MemoryStoreApplyTest, ApplyPartial_AppliesFreshItems) { EXPECT_EQ(6u, store.GetFlag("flagA")->version); ASSERT_TRUE(store.GetSegment("segA")); EXPECT_EQ(6u, store.GetSegment("segA")->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - ASSERT_EQ(1u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segA")); } TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedItems) { @@ -400,7 +205,7 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedItems) { seg_b_new.version = 2; seg_b_new.key = "segB"; - auto result = store.Apply(FDv2ChangeSet{ + store.Apply(FDv2ChangeSet{ FDv2ChangeSet::Type::kPartial, std::vector{{"flagB", FlagDescriptor(flag_b_new)}, {"segB", SegmentDescriptor(seg_b_new)}}, @@ -415,79 +220,4 @@ TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedItems) { EXPECT_EQ(1u, store.GetSegment("segA")->version); ASSERT_TRUE(store.GetSegment("segB")); EXPECT_EQ(2u, store.GetSegment("segB")->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagB")); - ASSERT_EQ(1u, result.segments.size()); - EXPECT_EQ(1u, result.segments.count("segB")); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_WithFlagTombstone) { - MemoryStore store; - Flag flag_a; - flag_a.version = 1; - flag_a.key = "flagA"; - - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}}, - std::unordered_map(), - }); - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(Tombstone(2))}}, - Selector{}, - }); - - auto fetched = store.GetFlag("flagA"); - ASSERT_TRUE(fetched); - EXPECT_EQ(2u, fetched->version); - EXPECT_FALSE(fetched->item.has_value()); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagA")); - EXPECT_TRUE(result.segments.empty()); -} - -TEST(MemoryStoreApplyTest, ApplyPartial_MixedStaleAndFreshItems) { - MemoryStore store; - Flag flag_a; - flag_a.version = 10; - flag_a.key = "flagA"; - - Flag flag_b; - flag_b.version = 1; - flag_b.key = "flagB"; - - store.Init(SDKDataSet{ - std::unordered_map{ - {"flagA", FlagDescriptor(flag_a)}, - {"flagB", FlagDescriptor(flag_b)}}, - std::unordered_map(), - }); - - Flag flag_a_stale; - flag_a_stale.version = 5; - flag_a_stale.key = "flagA"; - - Flag flag_b_new; - flag_b_new.version = 2; - flag_b_new.key = "flagB"; - - auto result = store.Apply(FDv2ChangeSet{ - FDv2ChangeSet::Type::kPartial, - std::vector{{"flagA", FlagDescriptor(flag_a_stale)}, - {"flagB", FlagDescriptor(flag_b_new)}}, - Selector{}, - }); - - // flagA version 5 < 10: skip. - EXPECT_EQ(10u, store.GetFlag("flagA")->version); - // flagB version 2 > 1: apply. - EXPECT_EQ(2u, store.GetFlag("flagB")->version); - - ASSERT_EQ(1u, result.flags.size()); - EXPECT_EQ(1u, result.flags.count("flagB")); - EXPECT_TRUE(result.segments.empty()); }