Skip to content

chore: Add support for persistent store contract tests.#502

Open
kinyoklion wants to merge 13 commits intomainfrom
rlamb/persistence-contract-tests
Open

chore: Add support for persistent store contract tests.#502
kinyoklion wants to merge 13 commits intomainfrom
rlamb/persistence-contract-tests

Conversation

@kinyoklion
Copy link
Member

@kinyoklion kinyoklion commented Nov 3, 2025

Summary

Adds support for running persistent data store contract tests against the C++ server SDK's Redis integration.

Key changes:

  • CI action (.github/actions/ci/action.yml): Added use_redis input, passed as $4 to build.sh to explicitly control LD_BUILD_REDIS_SUPPORT.
  • scripts/build.sh: Added $4 parameter for explicit Redis support toggle (overrides automatic target-name detection when provided).
  • server-redis.yml: Added use_redis: true to all existing Redis workflow jobs. Added new contract-tests and contract-tests-curl jobs with a Redis service container and enable_persistence_tests: true — these are the only jobs that run persistence contract tests.
  • Contract test data model: Added ConfigPersistentCache, ConfigPersistentStore, and ConfigPersistentDataStore structs; wired persistentDataStore into ConfigParams.
  • Contract test service (entity_manager.cpp): Implemented Redis persistent store creation and cache mode configuration (off, ttl, infinite) behind LD_REDIS_SUPPORT_ENABLED compile flag.
  • Capability registration (main.cpp): Advertises persistent-data-store-redis when Redis support is compiled in.
  • CMakeLists.txt: Conditionally links launchdarkly::server_redis_source and defines LD_REDIS_SUPPORT_ENABLED when LD_BUILD_REDIS_SUPPORT is ON.

Updates since last revision:

Review & Testing Checklist for Human

  • Verify the "infinite" cache mode implementation (24 * 365 hours TTL) matches what the contract test harness expects — confirm there isn't a dedicated "infinite" API on LazyLoadBuilder.
  • Verify the "off" cache mode (CacheRefresh(std::chrono::seconds(0))) actually disables caching as intended — i.e., 0 seconds means "don't cache" rather than "always stale".
  • Confirm that the build.sh $4 parameter logic doesn't break any other callers of build.sh that may pass positional arguments differently. When $4 is not provided, the variable is unset and the existing auto-detection logic is preserved.
  • Run the persistence contract tests end-to-end against a Redis instance to confirm they pass. CI runs these in server-redis.yml on Linux; Mac/Windows Redis tests are still TODO (no Redis service available on those runners).

Notes


Note

Medium Risk
Adds new CI jobs and contract-test harness logic for Redis-backed persistent stores and changes client evaluation behavior when not initialized for lazy-load mode; issues could surface as CI failures or altered early-evaluation semantics.

Overview
Adds Redis-backed persistent data store coverage to server SDK contract tests by introducing dedicated server-redis.yml contract-test jobs that spin up a Redis service and enable persistence tests (with and without CURL).

Extends the shared CI action and scripts/build.sh to accept an explicit use_redis toggle (controlling LD_BUILD_REDIS_SUPPORT), and wires the contract-test harness to parse persistentDataStore config, link Redis sources conditionally, advertise the persistent-data-store-redis capability, and build a lazy-load Redis data source with configurable cache modes (off/ttl/infinite).

Updates server SDK evaluation paths to allow lazy-load data systems to evaluate before "initialized" (logging warnings instead of returning CLIENT_NOT_READY) via a new IDataSystem::CanEvaluateWhenNotInitialized() override and accompanying unit test.

Written by Cursor Bugbot for commit 6cf1a3c. This will update automatically on new commits. Configure here.


Open with Devin

kinyoklion and others added 5 commits November 3, 2025 15:34
Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
…s CI

Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
@kinyoklion
Copy link
Member Author

Devin, please update with the latest changes from main.

@devin-ai-integration
Copy link
Contributor

Done — merged latest main (includes PR #505 and #506 changes) into this branch. Waiting for CI now.

@kinyoklion kinyoklion marked this pull request as ready for review March 10, 2026 21:29
@kinyoklion kinyoklion requested a review from a team as a code owner March 10, 2026 21:29
cursor[bot]

This comment was marked as resolved.

Removed enable_persistence_tests and use_redis from server.yml
contract tests (which have no Redis service). Added dedicated
contract-tests and contract-tests-curl jobs to server-redis.yml
with a Redis service container and enable_persistence_tests: true.

Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
cursor[bot]

This comment was marked as resolved.

The default of 'false' was always passed as $4 to build.sh, which
explicitly set build_redis='OFF' and overrode the auto-detection
for Redis target names. An empty default means $4 is empty, so
neither branch matches and auto-detection is preserved.

Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

devin-ai-integration bot and others added 2 commits March 12, 2026 22:47
…sts support

The pinned SHA (contract-tests-v1.1.0) did not have the
enable_persistence_tests input, causing persistence tests to be
skipped. Updated to contract-tests-v1.3.0 which supports this input.

Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
The Go test harness sends cache TTL as 'ttl' (in seconds), but the C++
data model expected 'ttlMs' (in milliseconds). This meant the TTL value
was never deserialized, and the default 5-minute cache TTL was always
used regardless of what the test requested.

- Rename ConfigPersistentCache::ttlMs to ttl to match JSON field name
- Change CacheRefresh from milliseconds to seconds to match harness units

Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
} else if (in.persistentDataStore->cache.mode == "ttl") {
if (in.persistentDataStore->cache.ttl) {
lazy_load.CacheRefresh(std::chrono::seconds(
*in.persistentDataStore->cache.ttl));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TTL value likely treated as wrong time unit

High Severity

The CacheRefresh API accepts std::chrono::milliseconds, and the LaunchDarkly SDK contract test harness protocol sends the ttl value in milliseconds. However, the code wraps the raw ttl integer in std::chrono::seconds, which implicitly converts to milliseconds by multiplying by 1000. This makes the effective cache TTL 1000× larger than intended (e.g., a harness-sent value of 30000 ms becomes 30,000 seconds instead of 30 seconds). The data model comment also incorrectly states the unit is seconds. The value likely needs to be wrapped in std::chrono::milliseconds instead.

Additional Locations (1)
Fix in Cursor Fix in Web

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-integration bot and others added 2 commits March 13, 2026 00:23
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>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

"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.";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning logged on every evaluation in lazy load

Medium Severity

In PreEvaluationChecks, a verbose warning is logged on every single evaluation call when Initialized() returns false and CanEvaluateWhenNotInitialized() is true. In LazyLoad mode where $inited is never set (common without Relay Proxy), this produces a warning on every flag evaluation, potentially thousands per second. Same issue exists in AllFlagsState. This warning likely needs to be logged only once or rate-limited.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants