From e5bf095dee7f6971ba839641bf422d710a712963 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:56:07 +0000 Subject: [PATCH 1/3] fix: allow lazy load evaluations when $inited key is not set In lazy load / daemon mode, the SDK's Initialized() check was blocking all flag evaluations when the $inited key was not found in the persistent store. This is problematic because in daemon mode, an external process (like Relay Proxy) populates the store, and the $inited key may not always be present. The fix changes LazyLoad::Initialized() to always return true, allowing evaluations to proceed using available data. When the underlying source reports not initialized ($inited key not found), a warning is logged to alert operators that a Relay Proxy or other SDK should set this key. This aligns with the Go SDK behavior where daemon mode (ExternalUpdatesOnly) always considers the data source initialized. Updated unit tests to reflect the new behavior and added tests verifying the warning is logged appropriately. Co-Authored-By: rlamb@launchdarkly.com --- .../lazy_load/lazy_load_system.cpp | 37 ++++++++++------ .../lazy_load/lazy_load_system.hpp | 1 + .../tests/lazy_load_system_test.cpp | 42 +++++++++++++++++-- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4235b9676..196ee0747 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -79,9 +79,12 @@ std::string const& LazyLoad::Identity() const { void LazyLoad::Initialize() { status_manager_.SetState(DataSourceState::kInitializing); - if (Initialized()) { - status_manager_.SetState(DataSourceState::kValid); - } + // In lazy load mode, we always consider the system ready for + // evaluations. The Initialized() call here will log a warning if + // the underlying source reports not initialized (e.g. $inited key + // not found), but we proceed regardless. + Initialized(); + status_manager_.SetState(DataSourceState::kValid); } std::shared_ptr LazyLoad::GetFlag( @@ -121,25 +124,35 @@ LazyLoad::AllSegments() const { } bool LazyLoad::Initialized() const { - /* Since the memory store isn't provisioned with an initial SDKDataSet - * like in the Background Sync system, we can't forward this call to - * MemoryStore::Initialized(). Instead, we need to check the state of the - * underlying source. */ + /* In lazy load mode, the system is always considered initialized for + * the purpose of flag evaluations. Data is fetched on-demand from the + * underlying source regardless of the $inited key state. + * + * However, we still check the underlying source's initialized state + * so we can warn if $inited is not set. A properly configured + * Relay Proxy or other SDK populating the store should set the + * $inited key. */ auto const state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { - /* Once initialized, we can always return true. */ if (initialized_.value()) { return true; } - /* If not yet initialized, then we can return false only if the state is - * fresh - otherwise we should make an attempt to refresh. */ if (data_components::ExpirationTracker::TrackState::kFresh == state) { - return false; + return true; } } RefreshInitState(); - return initialized_.value_or(false); + if (!initialized_.value_or(false) && !logged_init_warning_) { + LD_LOG(logger_, LogLevel::kWarn) + << "LazyLoad: data source reports not initialized " + "(the $inited key was not found in the store). " + "Evaluations will proceed using available data. " + "Typically a Relay Proxy or other SDK should set this " + "key; verify your configuration if this is unexpected."; + logged_init_warning_ = true; + } + return true; } void LazyLoad::RefreshAllFlags() const { diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 9584f60d6..960a641b1 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -187,6 +187,7 @@ class LazyLoad final : public data_interfaces::IDataSystem { mutable data_components::ExpirationTracker tracker_; TimeFn time_; mutable std::optional initialized_; + mutable bool logged_init_warning_{false}; ClockType::duration fresh_duration_; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index f908a8ac8..b32723691 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -283,7 +283,7 @@ TEST_F(LazyLoadTest, AllSegmentsRefreshesIndividualSegment) { ASSERT_EQ(segment2->version, 2); } -TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) { +TEST_F(LazyLoadTest, InitializedReturnsTrueEvenWhenSourceNotInitialized) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; @@ -292,8 +292,10 @@ TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) { data_systems::LazyLoad const lazy_load(logger, config, status_manager); + // In lazy load mode, Initialized() always returns true even when the + // underlying source reports not initialized ($inited key not found). for (std::size_t i = 0; i < 10; i++) { - ASSERT_FALSE(lazy_load.Initialized()); + ASSERT_TRUE(lazy_load.Initialized()); } } @@ -329,12 +331,46 @@ TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) { data_systems::LazyLoad const lazy_load(logger, config, status_manager, [&]() { return now; }); + // Always returns true even when source reports not initialized. for (std::size_t i = 0; i < 10; i++) { - ASSERT_FALSE(lazy_load.Initialized()); + ASSERT_TRUE(lazy_load.Initialized()); now += std::chrono::seconds(1); } + // Still true after TTL when source now reports initialized. for (std::size_t i = 0; i < 10; i++) { ASSERT_TRUE(lazy_load.Initialized()); } } + +TEST_F(LazyLoadTest, InitializedLogsWarningWhenSourceNotInitialized) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); + + data_systems::LazyLoad const lazy_load(logger, config, status_manager); + + ASSERT_TRUE(lazy_load.Initialized()); + + // A warning should have been logged about $inited not being found. + ASSERT_TRUE(spy_logger_backend->Contains( + 0, LogLevel::kWarn, "$inited")); +} + +TEST_F(LazyLoadTest, InitializedDoesNotLogWarningWhenSourceIsInitialized) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); + + data_systems::LazyLoad const lazy_load(logger, config, status_manager); + + ASSERT_TRUE(lazy_load.Initialized()); + + // No warning should be logged when source is properly initialized. + // Only debug-level messages should be present (or none at all). + ASSERT_TRUE(spy_logger_backend->Count(0)); +} From cc44864970c1baaadf00bcacb0a429b40e364017 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:23:26 +0000 Subject: [PATCH 2/3] fix: move warn-and-proceed logic to evaluation path for lazy load Reworked approach based on review feedback: Initialized() should return false when $inited is not set (consistent with other SDK implementations), and the evaluation path should handle this case by warning and proceeding rather than blocking. Changes: - Added CanEvaluateWhenNotInitialized() virtual method to IDataSystem interface (defaults to false) - LazyLoad overrides to return true (can serve on demand) - PreEvaluationChecks warns and proceeds when data system can evaluate while not initialized, instead of returning CLIENT_NOT_READY - AllFlagsState similarly warns and proceeds instead of returning empty - Reverted LazyLoad::Initialized() to original behavior (truthfully reports whether $inited key exists) - Added unit test for CanEvaluateWhenNotInitialized() This matches the pattern used in the Erlang SDK where the evaluation path distinguishes between 'not initialized' (blocks) and 'store initialized' (warns but proceeds). Co-Authored-By: rlamb@launchdarkly.com --- libs/server-sdk/src/client_impl.cpp | 27 ++++++++++--- .../data_interfaces/system/idata_system.hpp | 16 ++++++++ .../lazy_load/lazy_load_system.cpp | 37 ++++++------------ .../lazy_load/lazy_load_system.hpp | 5 ++- .../tests/lazy_load_system_test.cpp | 38 ++++--------------- 5 files changed, 61 insertions(+), 62 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 0c53440d7..64d9f17b3 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -186,11 +186,19 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, std::unordered_map result; if (!Initialized()) { - LD_LOG(logger_, LogLevel::kWarn) - << "AllFlagsState() called before client has finished " - "initializing. Data source not available. Returning empty state"; + if (data_system_->CanEvaluateWhenNotInitialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before LaunchDarkly client " + "initialization completed; using last known values " + "from data store"; + } else { + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing. Data source not available. Returning " + "empty state"; - return {}; + return {}; + } } AllFlagsStateBuilder builder{options}; @@ -418,7 +426,16 @@ EvaluationDetail ClientImpl::VariationInternal( std::optional ClientImpl::PreEvaluationChecks( Context const& context) const { if (!Initialized()) { - return EvaluationReason::ErrorKind::kClientNotReady; + if (data_system_->CanEvaluateWhenNotInitialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "Evaluation called before LaunchDarkly client " + "initialization completed; using last known values " + "from data store. The $inited key was not found in " + "the store; typically a Relay Proxy or other SDK " + "should set this key."; + } else { + return EvaluationReason::ErrorKind::kClientNotReady; + } } if (!context.Valid()) { return EvaluationReason::ErrorKind::kUserNotSpecified; diff --git a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp index 0edf778db..73b1eefef 100644 --- a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp +++ b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp @@ -21,6 +21,22 @@ class IDataSystem : public IStore { */ virtual void Initialize() = 0; + /** + * @brief Returns true if the data system is capable of serving + * flag evaluations even when Initialized() returns false. + * + * This is the case for Lazy Load (daemon mode), where data can be + * fetched on-demand from the persistent store regardless of whether + * the $inited key has been set. In contrast, Background Sync + * cannot serve evaluations until initial data is received. + * + * When this returns true, the evaluation path should log a warning + * (rather than returning CLIENT_NOT_READY) if Initialized() is false. + */ + [[nodiscard]] virtual bool CanEvaluateWhenNotInitialized() const { + return false; + } + virtual ~IDataSystem() override = default; IDataSystem(IDataSystem const& item) = delete; IDataSystem(IDataSystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 196ee0747..4235b9676 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -79,12 +79,9 @@ std::string const& LazyLoad::Identity() const { void LazyLoad::Initialize() { status_manager_.SetState(DataSourceState::kInitializing); - // In lazy load mode, we always consider the system ready for - // evaluations. The Initialized() call here will log a warning if - // the underlying source reports not initialized (e.g. $inited key - // not found), but we proceed regardless. - Initialized(); - status_manager_.SetState(DataSourceState::kValid); + if (Initialized()) { + status_manager_.SetState(DataSourceState::kValid); + } } std::shared_ptr LazyLoad::GetFlag( @@ -124,35 +121,25 @@ LazyLoad::AllSegments() const { } bool LazyLoad::Initialized() const { - /* In lazy load mode, the system is always considered initialized for - * the purpose of flag evaluations. Data is fetched on-demand from the - * underlying source regardless of the $inited key state. - * - * However, we still check the underlying source's initialized state - * so we can warn if $inited is not set. A properly configured - * Relay Proxy or other SDK populating the store should set the - * $inited key. */ + /* Since the memory store isn't provisioned with an initial SDKDataSet + * like in the Background Sync system, we can't forward this call to + * MemoryStore::Initialized(). Instead, we need to check the state of the + * underlying source. */ auto const state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { + /* Once initialized, we can always return true. */ if (initialized_.value()) { return true; } + /* If not yet initialized, then we can return false only if the state is + * fresh - otherwise we should make an attempt to refresh. */ if (data_components::ExpirationTracker::TrackState::kFresh == state) { - return true; + return false; } } RefreshInitState(); - if (!initialized_.value_or(false) && !logged_init_warning_) { - LD_LOG(logger_, LogLevel::kWarn) - << "LazyLoad: data source reports not initialized " - "(the $inited key was not found in the store). " - "Evaluations will proceed using available data. " - "Typically a Relay Proxy or other SDK should set this " - "key; verify your configuration if this is unexpected."; - logged_init_warning_ = true; - } - return true; + return initialized_.value_or(false); } void LazyLoad::RefreshAllFlags() const { diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 960a641b1..f1730fafa 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -58,6 +58,10 @@ class LazyLoad final : public data_interfaces::IDataSystem { bool Initialized() const override; + [[nodiscard]] bool CanEvaluateWhenNotInitialized() const override { + return true; + } + // Public for usage in tests. struct Kinds { static integrations::FlagKind const Flag; @@ -187,7 +191,6 @@ class LazyLoad final : public data_interfaces::IDataSystem { mutable data_components::ExpirationTracker tracker_; TimeFn time_; mutable std::optional initialized_; - mutable bool logged_init_warning_{false}; ClockType::duration fresh_duration_; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index b32723691..c2b99bd45 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -283,7 +283,7 @@ TEST_F(LazyLoadTest, AllSegmentsRefreshesIndividualSegment) { ASSERT_EQ(segment2->version, 2); } -TEST_F(LazyLoadTest, InitializedReturnsTrueEvenWhenSourceNotInitialized) { +TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; @@ -292,10 +292,8 @@ TEST_F(LazyLoadTest, InitializedReturnsTrueEvenWhenSourceNotInitialized) { data_systems::LazyLoad const lazy_load(logger, config, status_manager); - // In lazy load mode, Initialized() always returns true even when the - // underlying source reports not initialized ($inited key not found). for (std::size_t i = 0; i < 10; i++) { - ASSERT_TRUE(lazy_load.Initialized()); + ASSERT_FALSE(lazy_load.Initialized()); } } @@ -331,46 +329,24 @@ TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) { data_systems::LazyLoad const lazy_load(logger, config, status_manager, [&]() { return now; }); - // Always returns true even when source reports not initialized. for (std::size_t i = 0; i < 10; i++) { - ASSERT_TRUE(lazy_load.Initialized()); + ASSERT_FALSE(lazy_load.Initialized()); now += std::chrono::seconds(1); } - // Still true after TTL when source now reports initialized. for (std::size_t i = 0; i < 10; i++) { ASSERT_TRUE(lazy_load.Initialized()); } } -TEST_F(LazyLoadTest, InitializedLogsWarningWhenSourceNotInitialized) { - built::LazyLoadConfig const config{ - built::LazyLoadConfig::EvictionPolicy::Disabled, - std::chrono::seconds(10), mock_reader}; - - EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); - - data_systems::LazyLoad const lazy_load(logger, config, status_manager); - - ASSERT_TRUE(lazy_load.Initialized()); - - // A warning should have been logged about $inited not being found. - ASSERT_TRUE(spy_logger_backend->Contains( - 0, LogLevel::kWarn, "$inited")); -} - -TEST_F(LazyLoadTest, InitializedDoesNotLogWarningWhenSourceIsInitialized) { +TEST_F(LazyLoadTest, CanEvaluateWhenNotInitialized) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); - data_systems::LazyLoad const lazy_load(logger, config, status_manager); - ASSERT_TRUE(lazy_load.Initialized()); - - // No warning should be logged when source is properly initialized. - // Only debug-level messages should be present (or none at all). - ASSERT_TRUE(spy_logger_backend->Count(0)); + // LazyLoad can always serve evaluations on demand, even if not + // initialized (i.e. $inited key not found in store). + ASSERT_TRUE(lazy_load.CanEvaluateWhenNotInitialized()); } From 10c4a89c63f8e87beae904229f87a2ef01e290f2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:16:17 +0000 Subject: [PATCH 3/3] fix: match Go/Java/.NET daemon mode pattern for lazy load initialization - LazyLoad::Initialize() always sets kValid immediately (resolves StartAsync future) - LazyLoad::Initialized() always returns true (data system is always initialized) - Warning logged in Initialize() when store's $inited key is missing - Removed CanEvaluateWhenNotInitialized() from IDataSystem interface - Reverted PreEvaluationChecks/AllFlagsState to original behavior - Removed unused RefreshInitState(), initialized_ member, kInitialized key - Updated tests to verify new behavior Co-Authored-By: rlamb@launchdarkly.com --- libs/server-sdk/src/client_impl.cpp | 27 ++----- .../data_interfaces/system/idata_system.hpp | 16 ---- .../lazy_load/lazy_load_system.cpp | 51 ++++++------ .../lazy_load/lazy_load_system.hpp | 7 -- .../tests/lazy_load_system_test.cpp | 80 +++++++++++-------- 5 files changed, 75 insertions(+), 106 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 64d9f17b3..0c53440d7 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -186,19 +186,11 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, std::unordered_map result; if (!Initialized()) { - if (data_system_->CanEvaluateWhenNotInitialized()) { - LD_LOG(logger_, LogLevel::kWarn) - << "AllFlagsState() called before LaunchDarkly client " - "initialization completed; using last known values " - "from data store"; - } else { - LD_LOG(logger_, LogLevel::kWarn) - << "AllFlagsState() called before client has finished " - "initializing. Data source not available. Returning " - "empty state"; + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing. Data source not available. Returning empty state"; - return {}; - } + return {}; } AllFlagsStateBuilder builder{options}; @@ -426,16 +418,7 @@ EvaluationDetail ClientImpl::VariationInternal( std::optional ClientImpl::PreEvaluationChecks( Context const& context) const { if (!Initialized()) { - if (data_system_->CanEvaluateWhenNotInitialized()) { - LD_LOG(logger_, LogLevel::kWarn) - << "Evaluation called before LaunchDarkly client " - "initialization completed; using last known values " - "from data store. The $inited key was not found in " - "the store; typically a Relay Proxy or other SDK " - "should set this key."; - } else { - return EvaluationReason::ErrorKind::kClientNotReady; - } + return EvaluationReason::ErrorKind::kClientNotReady; } if (!context.Valid()) { return EvaluationReason::ErrorKind::kUserNotSpecified; diff --git a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp index 73b1eefef..0edf778db 100644 --- a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp +++ b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp @@ -21,22 +21,6 @@ class IDataSystem : public IStore { */ virtual void Initialize() = 0; - /** - * @brief Returns true if the data system is capable of serving - * flag evaluations even when Initialized() returns false. - * - * This is the case for Lazy Load (daemon mode), where data can be - * fetched on-demand from the persistent store regardless of whether - * the $inited key has been set. In contrast, Background Sync - * cannot serve evaluations until initial data is received. - * - * When this returns true, the evaluation path should log a warning - * (rather than returning CLIENT_NOT_READY) if Initialized() is false. - */ - [[nodiscard]] virtual bool CanEvaluateWhenNotInitialized() const { - return false; - } - virtual ~IDataSystem() override = default; IDataSystem(IDataSystem const& item) = delete; IDataSystem(IDataSystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4235b9676..9f5d24b5d 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -79,9 +79,25 @@ std::string const& LazyLoad::Identity() const { void LazyLoad::Initialize() { status_manager_.SetState(DataSourceState::kInitializing); - if (Initialized()) { - status_manager_.SetState(DataSourceState::kValid); + + // In lazy load (daemon) mode, the data system is always considered + // initialized immediately — it can fetch data on demand from the + // persistent store. This is consistent with Go, Java, and .NET SDKs + // which use a NullDataSource that immediately reports initialized. + // + // The store's $inited key state is a separate concern: if a Relay + // Proxy or other SDK hasn't set $inited, we log a warning but + // proceed. This matches the Node SDK pattern where the data source + // initializes immediately but the store state drives the warning. + if (!reader_->Initialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "LazyLoad: the $inited key was not found in the store. " + "Evaluations will proceed using available data. Typically " + "a Relay Proxy or other SDK should set this key; verify " + "your configuration if this is unexpected."; } + + status_manager_.SetState(DataSourceState::kValid); } std::shared_ptr LazyLoad::GetFlag( @@ -121,25 +137,13 @@ LazyLoad::AllSegments() const { } bool LazyLoad::Initialized() const { - /* Since the memory store isn't provisioned with an initial SDKDataSet - * like in the Background Sync system, we can't forward this call to - * MemoryStore::Initialized(). Instead, we need to check the state of the - * underlying source. */ - - auto const state = tracker_.State(Keys::kInitialized, time_()); - if (initialized_.has_value()) { - /* Once initialized, we can always return true. */ - if (initialized_.value()) { - return true; - } - /* If not yet initialized, then we can return false only if the state is - * fresh - otherwise we should make an attempt to refresh. */ - if (data_components::ExpirationTracker::TrackState::kFresh == state) { - return false; - } - } - RefreshInitState(); - return initialized_.value_or(false); + /* In lazy load (daemon) mode, the data system is always considered + * initialized. It can serve evaluations on demand from the persistent + * store regardless of whether the $inited key has been set. + * + * This is consistent with Go/Java/.NET SDKs where the NullDataSource + * used in daemon mode always returns IsInitialized() = true. */ + return true; } void LazyLoad::RefreshAllFlags() const { @@ -154,11 +158,6 @@ void LazyLoad::RefreshAllSegments() const { [this]() { return reader_->AllSegments(); }); } -void LazyLoad::RefreshInitState() const { - initialized_ = reader_->Initialized(); - tracker_.Add(Keys::kInitialized, ExpiryTime()); -} - void LazyLoad::RefreshSegment(std::string const& segment_key) const { RefreshItem( data_components::DataKind::kSegment, segment_key, diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index f1730fafa..5f1c5644a 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -58,10 +58,6 @@ class LazyLoad final : public data_interfaces::IDataSystem { bool Initialized() const override; - [[nodiscard]] bool CanEvaluateWhenNotInitialized() const override { - return true; - } - // Public for usage in tests. struct Kinds { static integrations::FlagKind const Flag; @@ -71,7 +67,6 @@ class LazyLoad final : public data_interfaces::IDataSystem { private: void RefreshAllFlags() const; void RefreshAllSegments() const; - void RefreshInitState() const; void RefreshFlag(std::string const& key) const; void RefreshSegment(std::string const& key) const; @@ -190,14 +185,12 @@ class LazyLoad final : public data_interfaces::IDataSystem { mutable data_components::ExpirationTracker tracker_; TimeFn time_; - mutable std::optional initialized_; ClockType::duration fresh_duration_; struct Keys { static inline std::string const kAllFlags = "allFlags"; static inline std::string const kAllSegments = "allSegments"; - static inline std::string const kInitialized = "initialized"; }; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index c2b99bd45..53bb49f08 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -283,70 +283,80 @@ TEST_F(LazyLoadTest, AllSegmentsRefreshesIndividualSegment) { ASSERT_EQ(segment2->version, 2); } -TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) { +TEST_F(LazyLoadTest, InitializedAlwaysReturnsTrue) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); - data_systems::LazyLoad const lazy_load(logger, config, status_manager); + // In lazy load (daemon) mode, Initialized() always returns true + // regardless of whether $inited is set in the store. This is + // consistent with Go/Java/.NET SDKs. for (std::size_t i = 0; i < 10; i++) { - ASSERT_FALSE(lazy_load.Initialized()); + ASSERT_TRUE(lazy_load.Initialized()); } } -TEST_F(LazyLoadTest, InitializeCalledOnceThenNeverAgainAfterReturningTrue) { +TEST_F(LazyLoadTest, InitializeSetsValidImmediately) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); - data_systems::LazyLoad const lazy_load(logger, config, status_manager); + data_systems::LazyLoad lazy_load(logger, config, status_manager); - for (std::size_t i = 0; i < 10; i++) { - ASSERT_TRUE(lazy_load.Initialized()); - } -} + // After Initialize(), status should be kValid immediately. + lazy_load.Initialize(); -TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) { - using TimePoint = data_systems::LazyLoad::ClockType::time_point; - constexpr auto refresh_ttl = std::chrono::seconds(10); + // The data source status manager should have transitioned to kValid. + auto status = status_manager.Status(); + ASSERT_EQ(status.State(), DataSourceState::kValid); +} +TEST_F(LazyLoadTest, InitializeSetsValidEvenWhenStoreNotInitialized) { built::LazyLoadConfig const config{ - built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl, - mock_reader}; + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; - { - InSequence s; - EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); - EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); - } + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); - TimePoint now{std::chrono::seconds(0)}; - data_systems::LazyLoad const lazy_load(logger, config, status_manager, - [&]() { return now; }); + data_systems::LazyLoad lazy_load(logger, config, status_manager); - for (std::size_t i = 0; i < 10; i++) { - ASSERT_FALSE(lazy_load.Initialized()); - now += std::chrono::seconds(1); - } + // Even when the store doesn't have $inited, status should be kValid. + lazy_load.Initialize(); - for (std::size_t i = 0; i < 10; i++) { - ASSERT_TRUE(lazy_load.Initialized()); - } + auto status = status_manager.Status(); + ASSERT_EQ(status.State(), DataSourceState::kValid); } -TEST_F(LazyLoadTest, CanEvaluateWhenNotInitialized) { +TEST_F(LazyLoadTest, InitializeLogsWarningWhenStoreNotInitialized) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - data_systems::LazyLoad const lazy_load(logger, config, status_manager); + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); + + data_systems::LazyLoad lazy_load(logger, config, status_manager); + lazy_load.Initialize(); + + // A warning should be logged about $inited not being found. + ASSERT_TRUE(spy_logger_backend->Contains( + 0, LogLevel::kWarn, "$inited")); +} + +TEST_F(LazyLoadTest, InitializeDoesNotLogWarningWhenStoreIsInitialized) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); + + data_systems::LazyLoad lazy_load(logger, config, status_manager); + lazy_load.Initialize(); - // LazyLoad can always serve evaluations on demand, even if not - // initialized (i.e. $inited key not found in store). - ASSERT_TRUE(lazy_load.CanEvaluateWhenNotInitialized()); + // No warning should be logged when the store has $inited. + ASSERT_FALSE(spy_logger_backend->Contains( + 0, LogLevel::kWarn, "$inited")); }