fix: allow lazy load evaluations when $inited key is not set#508
fix: allow lazy load evaluations when $inited key is not set#508kinyoklion wants to merge 4 commits intomainfrom
Conversation
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 <kingdewman@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
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 <kingdewman@gmail.com>
|
Devin, review the human checklist for items which you can verify. |
|
I've reviewed the human checklist items. Here's what I can verify: 1. Warning log volume
|
- 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>
Summary
In lazy load / daemon mode, the SDK previously gated all flag evaluations on whether the
$initedkey existed in the persistent store (e.g. Redis). In daemon mode, an external process (Relay Proxy, another SDK) populates the store, but$initedmay not always be present — causing every evaluation to returnCLIENT_NOT_READYeven though flag data is available for on-demand fetching.This PR changes
LazyLoadto match the Go, Java, and .NET SDK daemon-mode pattern:Initialized()always returnstrue— the data system is always considered initialized since it can fetch on demand. This matches Go/Java/.NET'sNullDataSource.IsInitialized() → true.Initialize()always setskValidimmediately — theStartAsyncfuture resolves right away, matchingGo: close(closeWhenReady),Java: CompletableFuture.completedFuture(null),.NET: Task.FromResult(true).$initedkey is missing, consistent with the Node SDK pattern where the data source initializes immediately but the store state drives the warning.client_impl.cpp—PreEvaluationChecksandAllFlagsStateuse their original logic; sinceInitialized()returnstrue, evaluations proceed naturally without special casing.Removed
CanEvaluateWhenNotInitialized()virtual method (removed fromIDataSysteminterface andLazyLoad)RefreshInitState(),initialized_member,Keys::kInitialized— no longer needed sinceInitialized()is unconditionalFiles changed:
lazy_load_system.cpp—Initialize()always setskValid;Initialized()always returnstrue; removedRefreshInitState()lazy_load_system.hpp— removedRefreshInitState()declaration,initialized_member,kInitializedkeylazy_load_system_test.cpp— replaced oldInitialized()caching/TTL tests with tests for: always-trueInitialized(), immediatekValidstatus, warning behavior based on store stateReview & Testing Checklist for Human
reader_->Initialized()error handling inInitialize(): The storeInitialized()check is called directly (not cached/TTL-gated). If the persistent store is unreachable at SDK startup, verify this doesn't throw or block indefinitely. The warning path handlesfalse, but an exception or hang would be problematic.Initialized()always returnstrue,PreEvaluationCheckswill never returnCLIENT_NOT_READYfor LazyLoad. If the store is completely down, evaluations will attempt to fetch and return defaults. Verify this is acceptable behavior (Go SDK behaves identically viaNullDataSource).spy_logger_backendtest API: Tests useContains(0, LogLevel::kWarn, "$inited")— verify the0parameter (message index?) is correct. If it means "first message," then tests are correct; if it's a match count, could be wrong.Notes
NullDataSource.IsInitialized() → true,Start() → close(closeWhenReady)), Java (NullDataSource.isInitialized() → true,start() → CompletableFuture.completedFuture(null),updateStatus(VALID)), and .NET (NullDataSource.Initialized → true,Start() → Task.FromResult(true)) daemon mode patterns.Link to Devin session | Requested by: @kinyoklion