Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 25 additions & 26 deletions libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<data_model::FlagDescriptor> LazyLoad::GetFlag(
Expand Down Expand Up @@ -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 {
Expand All @@ -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_model::Segment>(
data_components::DataKind::kSegment, segment_key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -186,14 +185,12 @@ class LazyLoad final : public data_interfaces::IDataSystem {

mutable data_components::ExpirationTracker tracker_;
TimeFn time_;
mutable std::optional<bool> 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
80 changes: 51 additions & 29 deletions libs/server-sdk/tests/lazy_load_system_test.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>

Expand Down Expand Up @@ -283,58 +283,80 @@
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"));
}
Loading