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/2] 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/2] 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()); }