Skip to content

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

Draft
kinyoklion wants to merge 14 commits intomainfrom
rlamb/persistence-contract-tests
Draft

chore: Add support for persistent store contract tests.#502
kinyoklion wants to merge 14 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, enable_persistence_tests: true, and --skip-from suppression file.
  • Contract test data model: Added ConfigPersistentCache, ConfigPersistentStore, and ConfigPersistentDataStore structs; wired persistentDataStore into ConfigParams. TTL field uses ttl (seconds) to match the Go test harness.
  • 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.
  • Daemon mode fix (client_impl.cpp, idata_system.hpp, lazy_load_system.hpp): Added IDataSystem::CanEvaluateWhenNotInitialized() virtual method. LazyLoad overrides it to return true, allowing flag evaluations in daemon mode when $inited is not set (logs a warning instead of returning CLIENT_NOT_READY). This matches Go/Erlang SDK behavior where a Relay Proxy sets the $inited key but evaluations should still proceed without it.
  • Suppressions file (suppressions.txt): 12 read-write persistent store tests are suppressed because the C++ SDK does not yet support write-through from BackgroundSync to persistent store. These tests require the SDK to write received streaming data to Redis alongside the in-memory store.

Updates since last revision:

  • Added suppressions.txt for 12 read-write persistent store tests and wired it into both contract test jobs via --skip-from. The C++ SDK's BackgroundSync does not yet support writing to a persistent store (it only reads from memory). This is a known feature gap tracked separately.
  • Updated contract-tests action from v1.1.0 to v1.3.0 (required for enable_persistence_tests and extra_params inputs to work).
  • Merged CanEvaluateWhenNotInitialized() fix from PR fix: allow lazy load evaluations when $inited key is not set #508 — daemon mode tests now pass (previously all returned CLIENT_NOT_READY).
  • Moved enable_persistence_tests and use_redis out of the general server contract tests in server.yml (which have no Redis service) into dedicated contract test jobs in server-redis.yml.
  • Merged latest main which includes PR chore: update CI runners and dependencies for macOS and Windows #505 (CI runner + OpenSSL + Boost pin fixes) and PR fix: update foxy for Boost 1.90 compatibility and unpin macOS Boost #506 (foxy Boost 1.90 compatibility + Boost unpin).

CI results: All 44 checks pass. Persistence tests: 8 daemon-mode + 2 prefix tests pass; 12 read-write tests skipped via suppressions.

Review & Testing Checklist for Human

  • Review CanEvaluateWhenNotInitialized() behavioral change: When $inited is missing in daemon mode, PreEvaluationChecks now logs a warning on every evaluation call (no rate limiting). Consider whether this is acceptable log volume or if a once-per-session guard is needed.
  • Verify the "infinite" cache mode implementation (24 * 365 hours TTL) matches contract test expectations. 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".
  • Review the 12 suppressed read-write tests in suppressions.txt — these represent missing BackgroundSync write-through functionality. Confirm this is an acceptable gap for now or if a follow-up issue should be filed.
  • Run the persistence contract tests end-to-end against a Redis instance to confirm the 8 daemon-mode + 2 prefix tests pass as expected. CI validates this, but local verification is recommended.

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

Copy link
Member Author

Choose a reason for hiding this comment

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

ttl (number, optional): If the cache mode is ttl, this value will be the time-to-live for cache entries in seconds.

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

Copy link
Member Author

Choose a reason for hiding this comment

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

This is consistent with other implementations.
https://github.com/launchdarkly/go-server-sdk/blob/ef9846a95f060076b19b405c26ad452dca574b1e/ldclient.go#L1389

@keelerm84 Relay does write $inited, correct?

@kinyoklion kinyoklion marked this pull request as draft March 13, 2026 22:14
Co-Authored-By: rlamb@launchdarkly.com <kingdewman@gmail.com>
Copy link
Member Author

Choose a reason for hiding this comment

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

Currently this SDK doesn't support read-write mode. It supports a purely lazy system for use with daemon mode.

Copy link
Member Author

Choose a reason for hiding this comment

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

With FDv2 support we will want to standardize this with the FDv2 behavior.

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