Skip to content

Commit 10c4a89

Browse files
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 <kingdewman@gmail.com>
1 parent cc44864 commit 10c4a89

5 files changed

Lines changed: 75 additions & 106 deletions

File tree

libs/server-sdk/src/client_impl.cpp

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -186,19 +186,11 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context,
186186
std::unordered_map<Client::FlagKey, Value> result;
187187

188188
if (!Initialized()) {
189-
if (data_system_->CanEvaluateWhenNotInitialized()) {
190-
LD_LOG(logger_, LogLevel::kWarn)
191-
<< "AllFlagsState() called before LaunchDarkly client "
192-
"initialization completed; using last known values "
193-
"from data store";
194-
} else {
195-
LD_LOG(logger_, LogLevel::kWarn)
196-
<< "AllFlagsState() called before client has finished "
197-
"initializing. Data source not available. Returning "
198-
"empty state";
189+
LD_LOG(logger_, LogLevel::kWarn)
190+
<< "AllFlagsState() called before client has finished "
191+
"initializing. Data source not available. Returning empty state";
199192

200-
return {};
201-
}
193+
return {};
202194
}
203195

204196
AllFlagsStateBuilder builder{options};
@@ -426,16 +418,7 @@ EvaluationDetail<Value> ClientImpl::VariationInternal(
426418
std::optional<enum EvaluationReason::ErrorKind> ClientImpl::PreEvaluationChecks(
427419
Context const& context) const {
428420
if (!Initialized()) {
429-
if (data_system_->CanEvaluateWhenNotInitialized()) {
430-
LD_LOG(logger_, LogLevel::kWarn)
431-
<< "Evaluation called before LaunchDarkly client "
432-
"initialization completed; using last known values "
433-
"from data store. The $inited key was not found in "
434-
"the store; typically a Relay Proxy or other SDK "
435-
"should set this key.";
436-
} else {
437-
return EvaluationReason::ErrorKind::kClientNotReady;
438-
}
421+
return EvaluationReason::ErrorKind::kClientNotReady;
439422
}
440423
if (!context.Valid()) {
441424
return EvaluationReason::ErrorKind::kUserNotSpecified;

libs/server-sdk/src/data_interfaces/system/idata_system.hpp

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,6 @@ class IDataSystem : public IStore {
2121
*/
2222
virtual void Initialize() = 0;
2323

24-
/**
25-
* @brief Returns true if the data system is capable of serving
26-
* flag evaluations even when Initialized() returns false.
27-
*
28-
* This is the case for Lazy Load (daemon mode), where data can be
29-
* fetched on-demand from the persistent store regardless of whether
30-
* the $inited key has been set. In contrast, Background Sync
31-
* cannot serve evaluations until initial data is received.
32-
*
33-
* When this returns true, the evaluation path should log a warning
34-
* (rather than returning CLIENT_NOT_READY) if Initialized() is false.
35-
*/
36-
[[nodiscard]] virtual bool CanEvaluateWhenNotInitialized() const {
37-
return false;
38-
}
39-
4024
virtual ~IDataSystem() override = default;
4125
IDataSystem(IDataSystem const& item) = delete;
4226
IDataSystem(IDataSystem&& item) = delete;

libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,25 @@ std::string const& LazyLoad::Identity() const {
7979

8080
void LazyLoad::Initialize() {
8181
status_manager_.SetState(DataSourceState::kInitializing);
82-
if (Initialized()) {
83-
status_manager_.SetState(DataSourceState::kValid);
82+
83+
// In lazy load (daemon) mode, the data system is always considered
84+
// initialized immediately — it can fetch data on demand from the
85+
// persistent store. This is consistent with Go, Java, and .NET SDKs
86+
// which use a NullDataSource that immediately reports initialized.
87+
//
88+
// The store's $inited key state is a separate concern: if a Relay
89+
// Proxy or other SDK hasn't set $inited, we log a warning but
90+
// proceed. This matches the Node SDK pattern where the data source
91+
// initializes immediately but the store state drives the warning.
92+
if (!reader_->Initialized()) {
93+
LD_LOG(logger_, LogLevel::kWarn)
94+
<< "LazyLoad: the $inited key was not found in the store. "
95+
"Evaluations will proceed using available data. Typically "
96+
"a Relay Proxy or other SDK should set this key; verify "
97+
"your configuration if this is unexpected.";
8498
}
99+
100+
status_manager_.SetState(DataSourceState::kValid);
85101
}
86102

87103
std::shared_ptr<data_model::FlagDescriptor> LazyLoad::GetFlag(
@@ -121,25 +137,13 @@ LazyLoad::AllSegments() const {
121137
}
122138

123139
bool LazyLoad::Initialized() const {
124-
/* Since the memory store isn't provisioned with an initial SDKDataSet
125-
* like in the Background Sync system, we can't forward this call to
126-
* MemoryStore::Initialized(). Instead, we need to check the state of the
127-
* underlying source. */
128-
129-
auto const state = tracker_.State(Keys::kInitialized, time_());
130-
if (initialized_.has_value()) {
131-
/* Once initialized, we can always return true. */
132-
if (initialized_.value()) {
133-
return true;
134-
}
135-
/* If not yet initialized, then we can return false only if the state is
136-
* fresh - otherwise we should make an attempt to refresh. */
137-
if (data_components::ExpirationTracker::TrackState::kFresh == state) {
138-
return false;
139-
}
140-
}
141-
RefreshInitState();
142-
return initialized_.value_or(false);
140+
/* In lazy load (daemon) mode, the data system is always considered
141+
* initialized. It can serve evaluations on demand from the persistent
142+
* store regardless of whether the $inited key has been set.
143+
*
144+
* This is consistent with Go/Java/.NET SDKs where the NullDataSource
145+
* used in daemon mode always returns IsInitialized() = true. */
146+
return true;
143147
}
144148

145149
void LazyLoad::RefreshAllFlags() const {
@@ -154,11 +158,6 @@ void LazyLoad::RefreshAllSegments() const {
154158
[this]() { return reader_->AllSegments(); });
155159
}
156160

157-
void LazyLoad::RefreshInitState() const {
158-
initialized_ = reader_->Initialized();
159-
tracker_.Add(Keys::kInitialized, ExpiryTime());
160-
}
161-
162161
void LazyLoad::RefreshSegment(std::string const& segment_key) const {
163162
RefreshItem<data_model::Segment>(
164163
data_components::DataKind::kSegment, segment_key,

libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ class LazyLoad final : public data_interfaces::IDataSystem {
5858

5959
bool Initialized() const override;
6060

61-
[[nodiscard]] bool CanEvaluateWhenNotInitialized() const override {
62-
return true;
63-
}
64-
6561
// Public for usage in tests.
6662
struct Kinds {
6763
static integrations::FlagKind const Flag;
@@ -71,7 +67,6 @@ class LazyLoad final : public data_interfaces::IDataSystem {
7167
private:
7268
void RefreshAllFlags() const;
7369
void RefreshAllSegments() const;
74-
void RefreshInitState() const;
7570
void RefreshFlag(std::string const& key) const;
7671
void RefreshSegment(std::string const& key) const;
7772

@@ -190,14 +185,12 @@ class LazyLoad final : public data_interfaces::IDataSystem {
190185

191186
mutable data_components::ExpirationTracker tracker_;
192187
TimeFn time_;
193-
mutable std::optional<bool> initialized_;
194188

195189
ClockType::duration fresh_duration_;
196190

197191
struct Keys {
198192
static inline std::string const kAllFlags = "allFlags";
199193
static inline std::string const kAllSegments = "allSegments";
200-
static inline std::string const kInitialized = "initialized";
201194
};
202195
};
203196
} // namespace launchdarkly::server_side::data_systems

libs/server-sdk/tests/lazy_load_system_test.cpp

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -283,70 +283,80 @@ TEST_F(LazyLoadTest, AllSegmentsRefreshesIndividualSegment) {
283283
ASSERT_EQ(segment2->version, 2);
284284
}
285285

286-
TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) {
286+
TEST_F(LazyLoadTest, InitializedAlwaysReturnsTrue) {
287287
built::LazyLoadConfig const config{
288288
built::LazyLoadConfig::EvictionPolicy::Disabled,
289289
std::chrono::seconds(10), mock_reader};
290290

291-
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false));
292-
293291
data_systems::LazyLoad const lazy_load(logger, config, status_manager);
294292

293+
// In lazy load (daemon) mode, Initialized() always returns true
294+
// regardless of whether $inited is set in the store. This is
295+
// consistent with Go/Java/.NET SDKs.
295296
for (std::size_t i = 0; i < 10; i++) {
296-
ASSERT_FALSE(lazy_load.Initialized());
297+
ASSERT_TRUE(lazy_load.Initialized());
297298
}
298299
}
299300

300-
TEST_F(LazyLoadTest, InitializeCalledOnceThenNeverAgainAfterReturningTrue) {
301+
TEST_F(LazyLoadTest, InitializeSetsValidImmediately) {
301302
built::LazyLoadConfig const config{
302303
built::LazyLoadConfig::EvictionPolicy::Disabled,
303304
std::chrono::seconds(10), mock_reader};
304305

305306
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true));
306307

307-
data_systems::LazyLoad const lazy_load(logger, config, status_manager);
308+
data_systems::LazyLoad lazy_load(logger, config, status_manager);
308309

309-
for (std::size_t i = 0; i < 10; i++) {
310-
ASSERT_TRUE(lazy_load.Initialized());
311-
}
312-
}
310+
// After Initialize(), status should be kValid immediately.
311+
lazy_load.Initialize();
313312

314-
TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) {
315-
using TimePoint = data_systems::LazyLoad::ClockType::time_point;
316-
constexpr auto refresh_ttl = std::chrono::seconds(10);
313+
// The data source status manager should have transitioned to kValid.
314+
auto status = status_manager.Status();
315+
ASSERT_EQ(status.State(), DataSourceState::kValid);
316+
}
317317

318+
TEST_F(LazyLoadTest, InitializeSetsValidEvenWhenStoreNotInitialized) {
318319
built::LazyLoadConfig const config{
319-
built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl,
320-
mock_reader};
320+
built::LazyLoadConfig::EvictionPolicy::Disabled,
321+
std::chrono::seconds(10), mock_reader};
321322

322-
{
323-
InSequence s;
324-
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false));
325-
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true));
326-
}
323+
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false));
327324

328-
TimePoint now{std::chrono::seconds(0)};
329-
data_systems::LazyLoad const lazy_load(logger, config, status_manager,
330-
[&]() { return now; });
325+
data_systems::LazyLoad lazy_load(logger, config, status_manager);
331326

332-
for (std::size_t i = 0; i < 10; i++) {
333-
ASSERT_FALSE(lazy_load.Initialized());
334-
now += std::chrono::seconds(1);
335-
}
327+
// Even when the store doesn't have $inited, status should be kValid.
328+
lazy_load.Initialize();
336329

337-
for (std::size_t i = 0; i < 10; i++) {
338-
ASSERT_TRUE(lazy_load.Initialized());
339-
}
330+
auto status = status_manager.Status();
331+
ASSERT_EQ(status.State(), DataSourceState::kValid);
340332
}
341333

342-
TEST_F(LazyLoadTest, CanEvaluateWhenNotInitialized) {
334+
TEST_F(LazyLoadTest, InitializeLogsWarningWhenStoreNotInitialized) {
343335
built::LazyLoadConfig const config{
344336
built::LazyLoadConfig::EvictionPolicy::Disabled,
345337
std::chrono::seconds(10), mock_reader};
346338

347-
data_systems::LazyLoad const lazy_load(logger, config, status_manager);
339+
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false));
340+
341+
data_systems::LazyLoad lazy_load(logger, config, status_manager);
342+
lazy_load.Initialize();
343+
344+
// A warning should be logged about $inited not being found.
345+
ASSERT_TRUE(spy_logger_backend->Contains(
346+
0, LogLevel::kWarn, "$inited"));
347+
}
348+
349+
TEST_F(LazyLoadTest, InitializeDoesNotLogWarningWhenStoreIsInitialized) {
350+
built::LazyLoadConfig const config{
351+
built::LazyLoadConfig::EvictionPolicy::Disabled,
352+
std::chrono::seconds(10), mock_reader};
353+
354+
EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true));
355+
356+
data_systems::LazyLoad lazy_load(logger, config, status_manager);
357+
lazy_load.Initialize();
348358

349-
// LazyLoad can always serve evaluations on demand, even if not
350-
// initialized (i.e. $inited key not found in store).
351-
ASSERT_TRUE(lazy_load.CanEvaluateWhenNotInitialized());
359+
// No warning should be logged when the store has $inited.
360+
ASSERT_FALSE(spy_logger_backend->Contains(
361+
0, LogLevel::kWarn, "$inited"));
352362
}

0 commit comments

Comments
 (0)