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 9584f60d6..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 @@ -67,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; @@ -186,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 f908a8ac8..53bb49f08 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -283,58 +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(); + + // The data source status manager should have transitioned to kValid. + auto status = status_manager.Status(); + ASSERT_EQ(status.State(), DataSourceState::kValid); } -TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) { - using TimePoint = data_systems::LazyLoad::ClockType::time_point; - constexpr auto refresh_ttl = std::chrono::seconds(10); +TEST_F(LazyLoadTest, InitializeSetsValidEvenWhenStoreNotInitialized) { + 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 lazy_load(logger, config, status_manager); + + // Even when the store doesn't have $inited, status should be kValid. + lazy_load.Initialize(); + auto status = status_manager.Status(); + ASSERT_EQ(status.State(), DataSourceState::kValid); +} + +TEST_F(LazyLoadTest, InitializeLogsWarningWhenStoreNotInitialized) { 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); + lazy_load.Initialize(); - for (std::size_t i = 0; i < 10; i++) { - ASSERT_FALSE(lazy_load.Initialized()); - now += std::chrono::seconds(1); - } + // A warning should be logged about $inited not being found. + ASSERT_TRUE(spy_logger_backend->Contains( + 0, LogLevel::kWarn, "$inited")); +} - for (std::size_t i = 0; i < 10; i++) { - ASSERT_TRUE(lazy_load.Initialized()); - } +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(); + + // No warning should be logged when the store has $inited. + ASSERT_FALSE(spy_logger_backend->Contains( + 0, LogLevel::kWarn, "$inited")); }