Skip to content

Commit 74a0f88

Browse files
authored
chore: Improve testing for atomic memory store (#355)
1 parent 595f91a commit 74a0f88

3 files changed

Lines changed: 1064 additions & 0 deletions

File tree

spec/impl/data_store/in_memory_feature_store_v2_spec.rb

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,28 @@ module DataStore
2020
}
2121
end
2222

23+
let(:segment_key) { "test-segment" }
24+
let(:segment) do
25+
{
26+
key: segment_key,
27+
version: 1,
28+
included: ["user1"],
29+
excluded: [],
30+
rules: [],
31+
}
32+
end
33+
34+
describe "#initialized?" do
35+
it "returns false before initialization" do
36+
expect(subject.initialized?).to be false
37+
end
38+
39+
it "returns true after set_basis" do
40+
subject.set_basis({ FEATURES => {} })
41+
expect(subject.initialized?).to be true
42+
end
43+
end
44+
2345
describe "#get with string/symbol key compatibility" do
2446
before do
2547
# Store items with symbol keys (as done by FDv2 protocol layer)
@@ -44,6 +66,280 @@ module DataStore
4466
it "returns nil for non-existent keys" do
4567
expect(subject.get(FEATURES, "nonexistent")).to be_nil
4668
end
69+
70+
it "returns nil for deleted items" do
71+
deleted_flag = flag.merge(deleted: true)
72+
collections = { FEATURES => { flag_key.to_sym => deleted_flag } }
73+
subject.set_basis(collections)
74+
expect(subject.get(FEATURES, flag_key)).to be_nil
75+
end
76+
end
77+
78+
describe "#all" do
79+
it "returns empty hash when no data" do
80+
expect(subject.all(FEATURES)).to eq({})
81+
end
82+
83+
it "returns all non-deleted items" do
84+
collections = {
85+
FEATURES => {
86+
flag_key.to_sym => flag,
87+
"deleted-flag".to_sym => flag.merge(key: "deleted-flag", deleted: true),
88+
},
89+
}
90+
subject.set_basis(collections)
91+
92+
result = subject.all(FEATURES)
93+
expect(result.keys).to contain_exactly(flag_key.to_sym)
94+
expect(result[flag_key.to_sym].key).to eq(flag_key)
95+
end
96+
97+
it "returns items for both flags and segments" do
98+
collections = {
99+
FEATURES => { flag_key.to_sym => flag },
100+
SEGMENTS => { segment_key.to_sym => segment },
101+
}
102+
subject.set_basis(collections)
103+
104+
expect(subject.all(FEATURES).keys).to contain_exactly(flag_key.to_sym)
105+
expect(subject.all(SEGMENTS).keys).to contain_exactly(segment_key.to_sym)
106+
end
107+
end
108+
109+
describe "#set_basis" do
110+
it "initializes the store with valid data" do
111+
collections = {
112+
FEATURES => { flag_key.to_sym => flag },
113+
SEGMENTS => { segment_key.to_sym => segment },
114+
}
115+
116+
result = subject.set_basis(collections)
117+
expect(result).to be true
118+
expect(subject.initialized?).to be true
119+
expect(subject.get(FEATURES, flag_key)).not_to be_nil
120+
expect(subject.get(SEGMENTS, segment_key)).not_to be_nil
121+
end
122+
123+
it "replaces existing data" do
124+
# Set initial data
125+
initial_collections = {
126+
FEATURES => { flag_key.to_sym => flag },
127+
}
128+
subject.set_basis(initial_collections)
129+
130+
# Replace with new data
131+
new_flag = flag.merge(key: "new-flag", version: 2)
132+
new_collections = {
133+
FEATURES => { "new-flag".to_sym => new_flag },
134+
}
135+
result = subject.set_basis(new_collections)
136+
137+
expect(result).to be true
138+
expect(subject.get(FEATURES, flag_key)).to be_nil # Old flag gone
139+
expect(subject.get(FEATURES, "new-flag")).not_to be_nil
140+
end
141+
142+
it "clears all data before setting new data" do
143+
subject.set_basis({
144+
FEATURES => { flag_key.to_sym => flag },
145+
SEGMENTS => { segment_key.to_sym => segment },
146+
})
147+
148+
# Replace with data that only has flags
149+
new_collections = {
150+
FEATURES => { "new-flag".to_sym => flag.merge(key: "new-flag") },
151+
SEGMENTS => {},
152+
}
153+
subject.set_basis(new_collections)
154+
155+
expect(subject.all(SEGMENTS)).to be_empty
156+
end
157+
158+
it "handles multiple flags and segments" do
159+
flag1 = flag.merge(key: "flag-1")
160+
flag2 = flag.merge(key: "flag-2", version: 2)
161+
flag3 = flag.merge(key: "flag-3", version: 3)
162+
163+
segment1 = segment.merge(key: "segment-1")
164+
segment2 = segment.merge(key: "segment-2", version: 2)
165+
166+
collections = {
167+
FEATURES => {
168+
"flag-1".to_sym => flag1,
169+
"flag-2".to_sym => flag2,
170+
"flag-3".to_sym => flag3,
171+
},
172+
SEGMENTS => {
173+
"segment-1".to_sym => segment1,
174+
"segment-2".to_sym => segment2,
175+
},
176+
}
177+
178+
result = subject.set_basis(collections)
179+
expect(result).to be true
180+
181+
expect(subject.all(FEATURES).size).to eq(3)
182+
expect(subject.all(SEGMENTS).size).to eq(2)
183+
end
184+
185+
it "returns false and logs error on deserialization failure" do
186+
allow(LaunchDarkly::Impl::Model).to receive(:deserialize).and_raise(StandardError.new("test error"))
187+
188+
collections = { FEATURES => { flag_key.to_sym => flag } }
189+
result = subject.set_basis(collections)
190+
191+
expect(result).to be false
192+
expect(subject.initialized?).to be false
193+
end
194+
195+
it "handles empty collections" do
196+
result = subject.set_basis({ FEATURES => {}, SEGMENTS => {} })
197+
expect(result).to be true
198+
expect(subject.initialized?).to be true
199+
end
200+
end
201+
202+
describe "#apply_delta" do
203+
before do
204+
# Set initial data
205+
collections = {
206+
FEATURES => { flag_key.to_sym => flag },
207+
SEGMENTS => { segment_key.to_sym => segment },
208+
}
209+
subject.set_basis(collections)
210+
end
211+
212+
it "adds new items without clearing existing data" do
213+
new_flag = flag.merge(key: "new-flag", version: 2)
214+
delta = {
215+
FEATURES => { "new-flag".to_sym => new_flag },
216+
}
217+
218+
result = subject.apply_delta(delta)
219+
expect(result).to be true
220+
221+
# Original flag should still exist
222+
expect(subject.get(FEATURES, flag_key)).not_to be_nil
223+
# New flag should be added
224+
expect(subject.get(FEATURES, "new-flag")).not_to be_nil
225+
# Segment should be unchanged
226+
expect(subject.get(SEGMENTS, segment_key)).not_to be_nil
227+
end
228+
229+
it "updates existing items" do
230+
updated_flag = flag.merge(version: 2, on: false)
231+
delta = {
232+
FEATURES => { flag_key.to_sym => updated_flag },
233+
}
234+
235+
result = subject.apply_delta(delta)
236+
expect(result).to be true
237+
238+
result = subject.get(FEATURES, flag_key)
239+
expect(result.version).to eq(2)
240+
expect(result.on).to be false
241+
end
242+
243+
it "handles multiple updates in one delta" do
244+
flag2 = flag.merge(key: "flag-2", version: 2)
245+
flag3 = flag.merge(key: "flag-3", version: 3)
246+
segment2 = segment.merge(key: "segment-2", version: 2)
247+
248+
delta = {
249+
FEATURES => {
250+
"flag-2".to_sym => flag2,
251+
"flag-3".to_sym => flag3,
252+
},
253+
SEGMENTS => {
254+
"segment-2".to_sym => segment2,
255+
},
256+
}
257+
258+
result = subject.apply_delta(delta)
259+
expect(result).to be true
260+
261+
# Original items unchanged
262+
expect(subject.get(FEATURES, flag_key)).not_to be_nil
263+
expect(subject.get(SEGMENTS, segment_key)).not_to be_nil
264+
265+
# New items added
266+
expect(subject.get(FEATURES, "flag-2")).not_to be_nil
267+
expect(subject.get(FEATURES, "flag-3")).not_to be_nil
268+
expect(subject.get(SEGMENTS, "segment-2")).not_to be_nil
269+
end
270+
271+
it "handles delete operations" do
272+
deleted_flag = { key: flag_key, version: 2, deleted: true }
273+
delta = {
274+
FEATURES => { flag_key.to_sym => deleted_flag },
275+
}
276+
277+
result = subject.apply_delta(delta)
278+
expect(result).to be true
279+
280+
# Deleted flag should return nil
281+
expect(subject.get(FEATURES, flag_key)).to be_nil
282+
end
283+
284+
it "returns false and logs error on deserialization failure" do
285+
allow(LaunchDarkly::Impl::Model).to receive(:deserialize).and_raise(StandardError.new("test error"))
286+
287+
delta = { FEATURES => { "new-flag".to_sym => flag } }
288+
result = subject.apply_delta(delta)
289+
290+
expect(result).to be false
291+
# Original data should be intact
292+
expect(subject.get(FEATURES, flag_key)).not_to be_nil
293+
end
294+
295+
it "handles empty delta" do
296+
result = subject.apply_delta({ FEATURES => {}, SEGMENTS => {} })
297+
expect(result).to be true
298+
299+
# Original data unchanged
300+
expect(subject.get(FEATURES, flag_key)).not_to be_nil
301+
expect(subject.get(SEGMENTS, segment_key)).not_to be_nil
302+
end
303+
end
304+
305+
describe "thread safety" do
306+
it "handles concurrent reads and writes" do
307+
subject.set_basis({ FEATURES => { flag_key.to_sym => flag } })
308+
309+
threads = []
310+
errors = []
311+
312+
# Writer threads
313+
5.times do |i|
314+
threads << Thread.new do
315+
begin
316+
10.times do |j|
317+
new_flag = flag.merge(key: "flag-#{i}-#{j}", version: j + 1)
318+
subject.apply_delta({ FEATURES => { "flag-#{i}-#{j}".to_sym => new_flag } })
319+
end
320+
rescue => e
321+
errors << e
322+
end
323+
end
324+
end
325+
326+
# Reader threads
327+
5.times do
328+
threads << Thread.new do
329+
begin
330+
20.times do
331+
subject.get(FEATURES, flag_key)
332+
subject.all(FEATURES)
333+
end
334+
rescue => e
335+
errors << e
336+
end
337+
end
338+
end
339+
340+
threads.each(&:join)
341+
expect(errors).to be_empty
342+
end
47343
end
48344
end
49345
end

0 commit comments

Comments
 (0)