Skip to content

Commit 6f795b2

Browse files
authored
refactor: add Apply method to MemoryStore for transactional change sets (#515)
This is a simple PR to add an equivalent of Java's `InMemoryStore#apply`. Unit tests are added. I don't predict anything controversial here. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces new mutation path that can clear and repopulate the in-memory flag/segment cache, so incorrect change-set composition or missed object types could cause stale or missing data. Scope is limited to `MemoryStore` and is covered by focused unit tests. > > **Overview** > Adds `MemoryStore::Apply(FDv2ChangeSet)` to apply FDv2 transactional updates: *no-op* for `kNone`, *merge* for `kPartial`, and *clear + reinitialize* for `kFull` (also setting `initialized_`). > > Includes a new `memory_store_apply_test.cpp` test suite validating initialization behavior, full-replace semantics, and partial update/preservation for flags and segments. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7826357. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 235d4c1 commit 6f795b2

3 files changed

Lines changed: 259 additions & 0 deletions

File tree

libs/server-sdk/src/data_components/memory_store/memory_store.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "memory_store.hpp"
22

3+
#include <launchdarkly/detail/unreachable.hpp>
4+
35
namespace launchdarkly::server_side::data_components {
46

57
std::shared_ptr<data_model::FlagDescriptor> MemoryStore::GetFlag(
@@ -82,4 +84,34 @@ bool MemoryStore::RemoveSegment(std::string const& key) {
8284
return segments_.erase(key) == 1;
8385
}
8486

87+
void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) {
88+
std::lock_guard lock{data_mutex_};
89+
90+
switch (changeSet.type) {
91+
case data_model::FDv2ChangeSet::Type::kNone:
92+
return;
93+
case data_model::FDv2ChangeSet::Type::kPartial:
94+
break;
95+
case data_model::FDv2ChangeSet::Type::kFull:
96+
initialized_ = true;
97+
flags_.clear();
98+
segments_.clear();
99+
break;
100+
default:
101+
detail::unreachable();
102+
}
103+
104+
for (auto& change : changeSet.changes) {
105+
if (std::holds_alternative<data_model::FlagDescriptor>(change.object)) {
106+
flags_[change.key] = std::make_shared<data_model::FlagDescriptor>(
107+
std::move(std::get<data_model::FlagDescriptor>(change.object)));
108+
} else if (std::holds_alternative<data_model::SegmentDescriptor>(
109+
change.object)) {
110+
segments_[change.key] =
111+
std::make_shared<data_model::SegmentDescriptor>(std::move(
112+
std::get<data_model::SegmentDescriptor>(change.object)));
113+
}
114+
}
115+
}
116+
85117
} // namespace launchdarkly::server_side::data_components

libs/server-sdk/src/data_components/memory_store/memory_store.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include "../../data_interfaces/destination/idestination.hpp"
44
#include "../../data_interfaces/store/istore.hpp"
55

6+
#include <launchdarkly/data_model/fdv2_change.hpp>
7+
68
#include <memory>
79
#include <mutex>
810
#include <string>
@@ -44,6 +46,8 @@ class MemoryStore final : public data_interfaces::IStore,
4446

4547
bool RemoveSegment(std::string const& key);
4648

49+
void Apply(data_model::FDv2ChangeSet changeSet);
50+
4751
MemoryStore() = default;
4852
~MemoryStore() override = default;
4953

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <data_components/memory_store/memory_store.hpp>
4+
#include <launchdarkly/data_model/fdv2_change.hpp>
5+
6+
using namespace launchdarkly::data_model;
7+
using namespace launchdarkly::server_side::data_components;
8+
9+
// ---------------------------------------------------------------------------
10+
// kNone tests
11+
// ---------------------------------------------------------------------------
12+
13+
TEST(MemoryStoreApplyTest, ApplyNone_IsNoOp) {
14+
MemoryStore store;
15+
Flag flag_a;
16+
flag_a.version = 1;
17+
flag_a.key = "flagA";
18+
19+
Segment seg_a;
20+
seg_a.version = 1;
21+
seg_a.key = "segA";
22+
23+
store.Init(SDKDataSet{
24+
std::unordered_map<std::string, FlagDescriptor>{
25+
{"flagA", FlagDescriptor(flag_a)}},
26+
std::unordered_map<std::string, SegmentDescriptor>{
27+
{"segA", SegmentDescriptor(seg_a)}},
28+
});
29+
30+
store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}});
31+
32+
auto fetched_flag = store.GetFlag("flagA");
33+
ASSERT_TRUE(fetched_flag);
34+
EXPECT_EQ(1u, fetched_flag->version);
35+
auto fetched_seg = store.GetSegment("segA");
36+
ASSERT_TRUE(fetched_seg);
37+
EXPECT_EQ(1u, fetched_seg->version);
38+
}
39+
40+
TEST(MemoryStoreApplyTest, ApplyNone_DoesNotInitialize) {
41+
MemoryStore store;
42+
store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kNone, {}, Selector{}});
43+
EXPECT_FALSE(store.Initialized());
44+
}
45+
46+
// ---------------------------------------------------------------------------
47+
// kFull tests
48+
// ---------------------------------------------------------------------------
49+
50+
TEST(MemoryStoreApplyTest, ApplyFull_SetsInitialized) {
51+
MemoryStore store;
52+
ASSERT_FALSE(store.Initialized());
53+
store.Apply(FDv2ChangeSet{FDv2ChangeSet::Type::kFull, {}, Selector{}});
54+
EXPECT_TRUE(store.Initialized());
55+
}
56+
57+
TEST(MemoryStoreApplyTest, ApplyFull_StoresItems) {
58+
MemoryStore store;
59+
Flag flag_a;
60+
flag_a.version = 1;
61+
flag_a.key = "flagA";
62+
63+
Segment seg_a;
64+
seg_a.version = 1;
65+
seg_a.key = "segA";
66+
67+
store.Apply(FDv2ChangeSet{
68+
FDv2ChangeSet::Type::kFull,
69+
std::vector<FDv2Change>{{"flagA", FlagDescriptor(flag_a)},
70+
{"segA", SegmentDescriptor(seg_a)}},
71+
Selector{},
72+
});
73+
74+
auto fetched_flag = store.GetFlag("flagA");
75+
ASSERT_TRUE(fetched_flag);
76+
EXPECT_TRUE(fetched_flag->item.has_value());
77+
EXPECT_EQ("flagA", fetched_flag->item->key);
78+
EXPECT_EQ(1u, fetched_flag->version);
79+
80+
auto fetched_seg = store.GetSegment("segA");
81+
ASSERT_TRUE(fetched_seg);
82+
EXPECT_TRUE(fetched_seg->item.has_value());
83+
EXPECT_EQ("segA", fetched_seg->item->key);
84+
EXPECT_EQ(1u, fetched_seg->version);
85+
}
86+
87+
TEST(MemoryStoreApplyTest, ApplyFull_ClearsExistingItems) {
88+
MemoryStore store;
89+
Flag flag_a;
90+
flag_a.version = 1;
91+
flag_a.key = "flagA";
92+
93+
Flag flag_b;
94+
flag_b.version = 1;
95+
flag_b.key = "flagB";
96+
97+
Segment seg_a;
98+
seg_a.version = 1;
99+
seg_a.key = "segA";
100+
101+
store.Init(SDKDataSet{
102+
std::unordered_map<std::string, FlagDescriptor>{
103+
{"flagA", FlagDescriptor(flag_a)},
104+
{"flagB", FlagDescriptor(flag_b)}},
105+
std::unordered_map<std::string, SegmentDescriptor>{
106+
{"segA", SegmentDescriptor(seg_a)}},
107+
});
108+
109+
Flag flag_c;
110+
flag_c.version = 1;
111+
flag_c.key = "flagC";
112+
113+
Segment seg_b;
114+
seg_b.version = 1;
115+
seg_b.key = "segB";
116+
117+
store.Apply(FDv2ChangeSet{
118+
FDv2ChangeSet::Type::kFull,
119+
std::vector<FDv2Change>{{"flagC", FlagDescriptor(flag_c)},
120+
{"segB", SegmentDescriptor(seg_b)}},
121+
Selector{},
122+
});
123+
124+
EXPECT_FALSE(store.GetFlag("flagA"));
125+
EXPECT_FALSE(store.GetFlag("flagB"));
126+
ASSERT_TRUE(store.GetFlag("flagC"));
127+
EXPECT_FALSE(store.GetSegment("segA"));
128+
ASSERT_TRUE(store.GetSegment("segB"));
129+
}
130+
131+
// ---------------------------------------------------------------------------
132+
// kPartial tests
133+
// ---------------------------------------------------------------------------
134+
135+
TEST(MemoryStoreApplyTest, ApplyPartial_AppliesItems) {
136+
MemoryStore store;
137+
Flag flag_a;
138+
flag_a.version = 5;
139+
flag_a.key = "flagA";
140+
141+
Segment seg_a;
142+
seg_a.version = 5;
143+
seg_a.key = "segA";
144+
145+
store.Init(SDKDataSet{
146+
std::unordered_map<std::string, FlagDescriptor>{
147+
{"flagA", FlagDescriptor(flag_a)}},
148+
std::unordered_map<std::string, SegmentDescriptor>{
149+
{"segA", SegmentDescriptor(seg_a)}},
150+
});
151+
152+
Flag flag_a_new;
153+
flag_a_new.version = 6;
154+
flag_a_new.key = "flagA";
155+
156+
Segment seg_a_new;
157+
seg_a_new.version = 6;
158+
seg_a_new.key = "segA";
159+
160+
store.Apply(FDv2ChangeSet{
161+
FDv2ChangeSet::Type::kPartial,
162+
std::vector<FDv2Change>{{"flagA", FlagDescriptor(flag_a_new)},
163+
{"segA", SegmentDescriptor(seg_a_new)}},
164+
Selector{},
165+
});
166+
167+
ASSERT_TRUE(store.GetFlag("flagA"));
168+
EXPECT_EQ(6u, store.GetFlag("flagA")->version);
169+
ASSERT_TRUE(store.GetSegment("segA"));
170+
EXPECT_EQ(6u, store.GetSegment("segA")->version);
171+
}
172+
173+
TEST(MemoryStoreApplyTest, ApplyPartial_PreservesUnchangedItems) {
174+
MemoryStore store;
175+
Flag flag_a;
176+
flag_a.version = 1;
177+
flag_a.key = "flagA";
178+
179+
Flag flag_b;
180+
flag_b.version = 1;
181+
flag_b.key = "flagB";
182+
183+
Segment seg_a;
184+
seg_a.version = 1;
185+
seg_a.key = "segA";
186+
187+
Segment seg_b;
188+
seg_b.version = 1;
189+
seg_b.key = "segB";
190+
191+
store.Init(SDKDataSet{
192+
std::unordered_map<std::string, FlagDescriptor>{
193+
{"flagA", FlagDescriptor(flag_a)},
194+
{"flagB", FlagDescriptor(flag_b)}},
195+
std::unordered_map<std::string, SegmentDescriptor>{
196+
{"segA", SegmentDescriptor(seg_a)},
197+
{"segB", SegmentDescriptor(seg_b)}},
198+
});
199+
200+
Flag flag_b_new;
201+
flag_b_new.version = 2;
202+
flag_b_new.key = "flagB";
203+
204+
Segment seg_b_new;
205+
seg_b_new.version = 2;
206+
seg_b_new.key = "segB";
207+
208+
store.Apply(FDv2ChangeSet{
209+
FDv2ChangeSet::Type::kPartial,
210+
std::vector<FDv2Change>{{"flagB", FlagDescriptor(flag_b_new)},
211+
{"segB", SegmentDescriptor(seg_b_new)}},
212+
Selector{},
213+
});
214+
215+
ASSERT_TRUE(store.GetFlag("flagA"));
216+
EXPECT_EQ(1u, store.GetFlag("flagA")->version);
217+
ASSERT_TRUE(store.GetFlag("flagB"));
218+
EXPECT_EQ(2u, store.GetFlag("flagB")->version);
219+
ASSERT_TRUE(store.GetSegment("segA"));
220+
EXPECT_EQ(1u, store.GetSegment("segA")->version);
221+
ASSERT_TRUE(store.GetSegment("segB"));
222+
EXPECT_EQ(2u, store.GetSegment("segB")->version);
223+
}

0 commit comments

Comments
 (0)