Skip to content

Commit d427e51

Browse files
committed
feat: Discard the FDv2 selector when the evaluation context changes
1 parent a1f1bec commit d427e51

4 files changed

Lines changed: 190 additions & 0 deletions

File tree

‎libs/client-sdk/src/client_impl.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ static bool IsInitializedSuccessfully(DataSourceStatus::DataSourceState state) {
160160

161161
std::future<bool> ClientImpl::IdentifyAsync(Context context) {
162162
UpdateContextSynchronized(context);
163+
// A selector describes one context's data, so it is never carried over.
164+
// Any flag data already loaded stays available for evaluation until a
165+
// full data set arrives for the new context.
166+
flag_manager_.ClearSelector();
163167
flag_manager_.LoadCache(context);
164168
event_processor_->SendAsync(events::IdentifyEventParams{
165169
std::chrono::system_clock::now(), std::move(context)});

‎libs/client-sdk/src/flag_manager/flag_manager.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,8 @@ void FlagManager::LoadCache(Context const& context) {
3737
persistence_updater_.LoadCached(context);
3838
}
3939

40+
void FlagManager::ClearSelector() {
41+
flag_store_.ClearSelector();
42+
}
43+
4044
} // namespace launchdarkly::client_side::flag_manager

‎libs/client-sdk/src/flag_manager/flag_manager.hpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ class FlagManager {
2828

2929
void LoadCache(Context const& context);
3030

31+
/**
32+
* Forgets the selector for the data currently held, leaving the data in
33+
* place. Called when the evaluation context changes, since a selector
34+
* describes one context's data and is never reused for another.
35+
*/
36+
void ClearSelector();
37+
3138
private:
3239
FlagStore flag_store_;
3340
FlagUpdater flag_updater_;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <data_sources/fdv2/cache_initializer.hpp>
4+
#include <flag_manager/flag_manager.hpp>
5+
6+
#include <launchdarkly/context_builder.hpp>
7+
#include <launchdarkly/logging/null_logger.hpp>
8+
9+
#include <map>
10+
#include <memory>
11+
#include <optional>
12+
#include <string>
13+
14+
using launchdarkly::Context;
15+
using launchdarkly::ContextBuilder;
16+
using launchdarkly::EvaluationDetailInternal;
17+
using launchdarkly::EvaluationResult;
18+
using launchdarkly::Value;
19+
using launchdarkly::client_side::FlagChange;
20+
using launchdarkly::client_side::FlagChangeSet;
21+
using launchdarkly::client_side::ItemDescriptor;
22+
using launchdarkly::client_side::flag_manager::FlagManager;
23+
using launchdarkly::client_side::flag_manager::PersistenceEncodeKey;
24+
using launchdarkly::data_model::ChangeSetType;
25+
using launchdarkly::data_model::Selector;
26+
27+
namespace {
28+
29+
class TestPersistence : public IPersistence {
30+
public:
31+
using StoreType =
32+
std::map<std::string,
33+
std::map<std::string, std::optional<std::string>>>;
34+
35+
explicit TestPersistence(StoreType store) : store_(std::move(store)) {}
36+
37+
void Set(std::string storageNamespace,
38+
std::string key,
39+
std::string data) noexcept override {
40+
store_[storageNamespace][key] = data;
41+
}
42+
43+
void Remove(std::string storageNamespace,
44+
std::string key) noexcept override {
45+
store_[storageNamespace].erase(key);
46+
}
47+
48+
std::optional<std::string> Read(std::string storageNamespace,
49+
std::string key) noexcept override {
50+
return store_[storageNamespace][key];
51+
}
52+
53+
StoreType store_;
54+
};
55+
56+
ItemDescriptor Flag(std::uint64_t version, Value value) {
57+
return ItemDescriptor{
58+
EvaluationResult{version, std::nullopt, false, false, std::nullopt,
59+
EvaluationDetailInternal{std::move(value),
60+
std::nullopt, std::nullopt}}};
61+
}
62+
63+
char const* const kEnvironment =
64+
"LaunchDarkly_rUTcjlHPv6Vegd27YmtGYkEGkEUGaEbn5M0JYTFQUpA=";
65+
66+
} // namespace
67+
68+
// A selector names a state the service can compute changes against for one
69+
// context. Sending it for another would ask for the wrong delta.
70+
TEST(FDv2ContextSwitchTest, ClearSelectorForgetsTheBasisButKeepsTheData) {
71+
auto logger = launchdarkly::logging::NullLogger();
72+
FlagManager flag_manager("the-key", logger, 5, nullptr);
73+
74+
flag_manager.Updater().Apply(
75+
ContextBuilder().Kind("user", "first").Build(),
76+
FlagChangeSet{ChangeSetType::kFull,
77+
{FlagChange{"flagA", Flag(1, Value("a"))}},
78+
Selector{Selector::State{1, "state-1"}}},
79+
/* from_cache= */ false);
80+
81+
ASSERT_TRUE(flag_manager.Store().CurrentSelector().value.has_value());
82+
83+
flag_manager.ClearSelector();
84+
85+
EXPECT_FALSE(flag_manager.Store().CurrentSelector().value.has_value());
86+
ASSERT_TRUE(flag_manager.Store().Get("flagA"));
87+
EXPECT_EQ(Value("a"),
88+
flag_manager.Store().Get("flagA")->item->Detail().Value());
89+
}
90+
91+
// Nothing is available to evaluate against for the new context yet, so the
92+
// previous context's data has to stay until a full data set arrives.
93+
TEST(FDv2ContextSwitchTest, CacheMissRetainsThePreviousContextsData) {
94+
auto logger = launchdarkly::logging::NullLogger();
95+
auto persistence =
96+
std::make_shared<TestPersistence>(TestPersistence::StoreType());
97+
FlagManager flag_manager("the-key", logger, 5, persistence);
98+
99+
auto first = ContextBuilder().Kind("user", "first").Build();
100+
flag_manager.Updater().Apply(
101+
first,
102+
FlagChangeSet{ChangeSetType::kFull,
103+
{FlagChange{"flagA", Flag(1, Value("first-value"))}},
104+
Selector{Selector::State{1, "state-1"}}},
105+
/* from_cache= */ false);
106+
107+
auto second = ContextBuilder().Kind("user", "second").Build();
108+
flag_manager.ClearSelector();
109+
flag_manager.LoadCache(second);
110+
111+
ASSERT_TRUE(flag_manager.Store().Get("flagA"));
112+
EXPECT_EQ(Value("first-value"),
113+
flag_manager.Store().Get("flagA")->item->Detail().Value());
114+
}
115+
116+
TEST(FDv2ContextSwitchTest, CacheHitReplacesThePreviousContextsData) {
117+
auto logger = launchdarkly::logging::NullLogger();
118+
auto second = ContextBuilder().Kind("user", "second").Build();
119+
auto persistence =
120+
std::make_shared<TestPersistence>(TestPersistence::StoreType{
121+
{kEnvironment,
122+
{{PersistenceEncodeKey(second.CanonicalKey()),
123+
R"({"flagB":{"version":1,"value":"second-value"}})"}}}});
124+
FlagManager flag_manager("the-key", logger, 5, persistence);
125+
126+
flag_manager.Updater().Apply(
127+
ContextBuilder().Kind("user", "first").Build(),
128+
FlagChangeSet{ChangeSetType::kFull,
129+
{FlagChange{"flagA", Flag(1, Value("first-value"))}},
130+
Selector{Selector::State{1, "state-1"}}},
131+
/* from_cache= */ false);
132+
133+
flag_manager.ClearSelector();
134+
flag_manager.LoadCache(second);
135+
136+
EXPECT_FALSE(flag_manager.Store().Get("flagA"));
137+
ASSERT_TRUE(flag_manager.Store().Get("flagB"));
138+
EXPECT_EQ(Value("second-value"),
139+
flag_manager.Store().Get("flagB")->item->Detail().Value());
140+
}
141+
142+
// The cache initializer for a new context reports a hit or a miss for that
143+
// context alone, whatever the store currently holds.
144+
TEST(FDv2ContextSwitchTest, CacheInitializerReadsTheNewContext) {
145+
auto logger = launchdarkly::logging::NullLogger();
146+
auto first = ContextBuilder().Kind("user", "first").Build();
147+
auto second = ContextBuilder().Kind("user", "second").Build();
148+
auto persistence =
149+
std::make_shared<TestPersistence>(TestPersistence::StoreType{
150+
{kEnvironment,
151+
{{PersistenceEncodeKey(first.CanonicalKey()),
152+
R"({"flagA":{"version":1,"value":"first-value"}})"}}}});
153+
FlagManager flag_manager("the-key", logger, 5, persistence);
154+
155+
using launchdarkly::client_side::data_sources::FDv2CacheInitializer;
156+
using launchdarkly::client_side::data_sources::FDv2SourceResult;
157+
158+
auto second_result =
159+
FDv2CacheInitializer(&flag_manager.Cache(), second, logger)
160+
.Run()
161+
.GetResult();
162+
auto* second_change_set =
163+
std::get_if<FDv2SourceResult::ChangeSet>(&second_result->value);
164+
ASSERT_NE(nullptr, second_change_set);
165+
EXPECT_EQ(ChangeSetType::kNone, second_change_set->change_set.type);
166+
167+
auto first_result =
168+
FDv2CacheInitializer(&flag_manager.Cache(), first, logger)
169+
.Run()
170+
.GetResult();
171+
auto* first_change_set =
172+
std::get_if<FDv2SourceResult::ChangeSet>(&first_result->value);
173+
ASSERT_NE(nullptr, first_change_set);
174+
EXPECT_EQ(ChangeSetType::kFull, first_change_set->change_set.type);
175+
}

0 commit comments

Comments
 (0)