diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index 82a09db35..eca423d66 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -160,6 +160,7 @@ static bool IsInitializedSuccessfully(DataSourceStatus::DataSourceState state) { std::future ClientImpl::IdentifyAsync(Context context) { UpdateContextSynchronized(context); + flag_manager_.ClearSelector(); flag_manager_.LoadCache(context); event_processor_->SendAsync(events::IdentifyEventParams{ std::chrono::system_clock::now(), std::move(context)}); @@ -244,11 +245,12 @@ void ClientImpl::FlushAsync() { } template -EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, - Value default_value, - bool check_type, - bool detailed, - std::unordered_set* visited) { +EvaluationDetail ClientImpl::VariationInternal( + FlagKey const& key, + Value default_value, + bool check_type, + bool detailed, + std::unordered_set* visited) { auto desc = flag_manager_.Store().Get(key); events::FeatureEventParams event = { diff --git a/libs/client-sdk/src/flag_manager/flag_manager.cpp b/libs/client-sdk/src/flag_manager/flag_manager.cpp index bdeba00b8..68a0334fa 100644 --- a/libs/client-sdk/src/flag_manager/flag_manager.cpp +++ b/libs/client-sdk/src/flag_manager/flag_manager.cpp @@ -37,4 +37,8 @@ void FlagManager::LoadCache(Context const& context) { persistence_updater_.LoadCached(context); } +void FlagManager::ClearSelector() { + flag_store_.ClearSelector(); +} + } // namespace launchdarkly::client_side::flag_manager diff --git a/libs/client-sdk/src/flag_manager/flag_manager.hpp b/libs/client-sdk/src/flag_manager/flag_manager.hpp index ad09bb55e..1097d90c8 100644 --- a/libs/client-sdk/src/flag_manager/flag_manager.hpp +++ b/libs/client-sdk/src/flag_manager/flag_manager.hpp @@ -28,6 +28,9 @@ class FlagManager { void LoadCache(Context const& context); + /** Forgets the selector, leaving the stored flag data in place. */ + void ClearSelector(); + private: FlagStore flag_store_; FlagUpdater flag_updater_; diff --git a/libs/client-sdk/tests/fdv2_cache_initializer_test.cpp b/libs/client-sdk/tests/fdv2_cache_initializer_test.cpp index 837bd14e4..fd9ae82f1 100644 --- a/libs/client-sdk/tests/fdv2_cache_initializer_test.cpp +++ b/libs/client-sdk/tests/fdv2_cache_initializer_test.cpp @@ -120,6 +120,44 @@ TEST(FDv2CacheInitializerTest, NoPersistenceConfiguredProducesANoneIntent) { EXPECT_EQ(ChangeSetType::kNone, change_set->change_set.type); } +TEST(FDv2CacheInitializerTest, ReadsTheContextItWasBuiltFor) { + auto first = ContextBuilder().Kind("user", "first").Build(); + auto second = ContextBuilder().Kind("user", "second").Build(); + auto logger = launchdarkly::logging::NullLogger(); + auto persistence = + std::make_shared(TestPersistence::StoreType{ + {kEnvironment, + {{PersistenceEncodeKey(first.CanonicalKey()), + R"({"flagA":{"version":1,"value":"first-value"}})"}}}}); + FlagManager flag_manager("the-key", logger, 5, persistence); + + // Initialize for the context that has nothing cached. + FDv2CacheInitializer second_initializer(&flag_manager.Cache(), second, + logger); + auto second_future = second_initializer.Run(); + ASSERT_TRUE(second_future.IsFinished()); + auto second_result = second_future.GetResult(); + + // Another context's data in the same cache is not a hit. + auto* second_change_set = + std::get_if(&second_result->value); + ASSERT_NE(nullptr, second_change_set); + EXPECT_EQ(ChangeSetType::kNone, second_change_set->change_set.type); + + // Initialize for the context whose data is cached. + FDv2CacheInitializer first_initializer(&flag_manager.Cache(), first, + logger); + auto first_future = first_initializer.Run(); + ASSERT_TRUE(first_future.IsFinished()); + auto first_result = first_future.GetResult(); + + // That context's cached data comes back as a full data set. + auto* first_change_set = + std::get_if(&first_result->value); + ASSERT_NE(nullptr, first_change_set); + EXPECT_EQ(ChangeSetType::kFull, first_change_set->change_set.type); +} + // The orchestrator needs to tell cache initializers apart from network ones, // so that a miss with nothing else configured still starts the SDK. TEST(FDv2CacheInitializerTest, FactoryIdentifiesItselfAsReadingTheCache) { diff --git a/libs/client-sdk/tests/flag_persistence_test.cpp b/libs/client-sdk/tests/flag_persistence_test.cpp index ae449da35..d426e81ac 100644 --- a/libs/client-sdk/tests/flag_persistence_test.cpp +++ b/libs/client-sdk/tests/flag_persistence_test.cpp @@ -115,6 +115,80 @@ TEST(FlagPersistenceTests, CanLoadCache) { EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value().AsString()); } +TEST(FlagPersistenceTests, LoadingACacheMissRetainsTheExistingData) { + auto first = ContextBuilder().Kind("user", "first").Build(); + auto second = ContextBuilder().Kind("user", "second").Build(); + auto store = FlagStore(); + auto updater = FlagUpdater(store); + auto persistence = + std::make_shared(TestPersistence::StoreType()); + auto logger = launchdarkly::logging::NullLogger(); + + FlagPersistence flag_persistence("the-key", updater, store, persistence, + logger, 5); + + // Put the first context's flag data in the store. + flag_persistence.Apply( + first, + FlagChangeSet{ + ChangeSetType::kFull, + {FlagChange{ + "flagA", + ItemDescriptor{EvaluationResult{ + 1, std::nullopt, false, false, std::nullopt, + EvaluationDetailInternal{Value("first-value"), std::nullopt, + std::nullopt}}}}}, + Selector{}}, + /* from_cache= */ false); + + // Load a context that has nothing cached. + flag_persistence.LoadCached(second); + + // The flag data already in the store stays there. + ASSERT_TRUE(store.Get("flagA")); + EXPECT_EQ(Value("first-value"), store.Get("flagA")->item->Detail().Value()); +} + +TEST(FlagPersistenceTests, LoadingACacheHitReplacesTheExistingData) { + auto first = ContextBuilder().Kind("user", "first").Build(); + auto second = ContextBuilder().Kind("user", "second").Build(); + auto store = FlagStore(); + auto updater = FlagUpdater(store); + auto logger = launchdarkly::logging::NullLogger(); + + auto persistence = + std::make_shared(TestPersistence::StoreType{ + {"LaunchDarkly_rUTcjlHPv6Vegd27YmtGYkEGkEUGaEbn5M0JYTFQUpA=", + {{PersistenceEncodeKey(second.CanonicalKey()), + R"({"flagB":{"version":1,"value":"second-value"}})"}}}}); + + FlagPersistence flag_persistence("the-key", updater, store, persistence, + logger, 5); + + // Put the first context's flag data in the store. + flag_persistence.Apply( + first, + FlagChangeSet{ + ChangeSetType::kFull, + {FlagChange{ + "flagA", + ItemDescriptor{EvaluationResult{ + 1, std::nullopt, false, false, std::nullopt, + EvaluationDetailInternal{Value("first-value"), std::nullopt, + std::nullopt}}}}}, + Selector{}}, + /* from_cache= */ false); + + // Load a context that has cached data. + flag_persistence.LoadCached(second); + + // The cached data takes the place of what the store held. + EXPECT_FALSE(store.Get("flagA")); + ASSERT_TRUE(store.Get("flagB")); + EXPECT_EQ(Value("second-value"), + store.Get("flagB")->item->Detail().Value()); +} + TEST(FlagPersistenceTests, EvictsContextsBeyondMax) { auto store = FlagStore(); auto updater = FlagUpdater(store);