Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2edfc8d
chore: Add support for persistent store contract tests.
kinyoklion Nov 3, 2025
baeb833
Merge main into rlamb/persistence-contract-tests
devin-ai-integration[bot] Mar 9, 2026
eb918dd
fix: add use_redis flag to server-redis.yml workflow steps
devin-ai-integration[bot] Mar 9, 2026
d960e6f
fix: update OpenSSL chocolatey version from 3.5.4 to 3.6.1 for Window…
devin-ai-integration[bot] Mar 9, 2026
14b2e22
Revert "fix: update OpenSSL chocolatey version from 3.5.4 to 3.6.1 fo…
devin-ai-integration[bot] Mar 9, 2026
d09ca12
Merge remote-tracking branch 'origin/main' into rlamb/persistence-con…
devin-ai-integration[bot] Mar 10, 2026
b9a13e4
fix: move enable_persistence_tests to Redis contract test jobs
devin-ai-integration[bot] Mar 12, 2026
9a95f3e
fix: change use_redis default to empty string to preserve auto-detection
devin-ai-integration[bot] Mar 12, 2026
bdcb385
fix: update contract-tests action to v1.3.0 for enable_persistence_te…
devin-ai-integration[bot] Mar 12, 2026
8d4e39c
fix: correct TTL field name mismatch in persistent cache config
devin-ai-integration[bot] Mar 12, 2026
e5bf095
fix: allow lazy load evaluations when $inited key is not set
devin-ai-integration[bot] Mar 12, 2026
cc44864
fix: move warn-and-proceed logic to evaluation path for lazy load
devin-ai-integration[bot] Mar 13, 2026
6cf1a3c
Merge branch 'devin/1773359647-lazy-load-initialized-warning' into rl…
devin-ai-integration[bot] Mar 13, 2026
3fe6eb3
chore: add suppressions for read-write persistent store tests
devin-ai-integration[bot] Mar 13, 2026
fc75921
Merge origin/main into rlamb/persistence-contract-tests
devin-ai-integration[bot] Aug 14, 2026
d308c56
chore: add DynamoDB persistent store contract test support
devin-ai-integration[bot] Aug 14, 2026
620d32b
ci: rename dynamodb contract test job
devin-ai-integration[bot] Aug 14, 2026
8b4f028
chore: Run persistence contract tests against both harness versions
devin-ai-integration[bot] Aug 21, 2026
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
12 changes: 10 additions & 2 deletions .github/actions/ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ inputs:
description: 'Whether to install CURL development libraries. Required for OpenTelemetry builds (server-sdk-otel), but does not enable CURL networking for the SDK itself.'
required: false
default: 'false'
use_redis:
description: 'Whether to enable Redis support (LD_BUILD_REDIS_SUPPORT=ON). Leave unset to preserve automatic detection for Redis targets.'
required: false
default: ''
use_dynamodb:
description: 'Whether to enable DynamoDB support (LD_BUILD_DYNAMODB_SUPPORT=ON). Leave unset to preserve automatic detection for DynamoDB targets.'
required: false
default: ''

runs:
using: composite
Expand All @@ -58,7 +66,7 @@ runs:
id: install-curl
- name: Build Library
shell: bash
run: ./scripts/build.sh ${{ inputs.cmake_target }} ON ${{ inputs.use_curl }}
run: ./scripts/build.sh ${{ inputs.cmake_target }} ON "${{ inputs.use_curl }}" "${{ inputs.use_redis }}" "${{ inputs.use_dynamodb }}"
env:
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
Boost_DIR: ${{ steps.install-boost.outputs.Boost_DIR }}
Expand All @@ -69,7 +77,7 @@ runs:
id: build-tests
if: inputs.run_tests == 'true'
shell: bash
run: ./scripts/build.sh gtest_${{ inputs.cmake_target }} ON ${{ inputs.use_curl }}
run: ./scripts/build.sh gtest_${{ inputs.cmake_target }} ON "${{ inputs.use_curl }}" "${{ inputs.use_redis }}" "${{ inputs.use_dynamodb }}"
env:
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
Boost_DIR: ${{ steps.install-boost.outputs.Boost_DIR }}
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/server-dynamodb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@ on:
- cron: '0 8 * * *'

jobs:
contract-tests-dynamodb:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
include:
- harness: v2
suppressions: contract-tests/server-contract-tests/persistence-suppressions.txt
- harness: v3
suppressions: contract-tests/server-contract-tests/persistence-suppressions-fdv2.txt
services:
dynamodb:
image: amazon/dynamodb-local
ports:
- 8000:8000
env:
TEST_SERVICE_PORT: 8123
TEST_SERVICE_BINARY: ./build/contract-tests/server-contract-tests/server-tests
steps:
# https://github.com/actions/checkout/releases/tag/v4.3.0
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: ./.github/actions/ci
with:
cmake_target: server-tests
run_tests: false
# AWS C++ SDK requires libcurl at link time on Linux/macOS.
install_curl: true
use_dynamodb: true
- name: 'Launch test service as background task'
run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 &
# https://github.com/launchdarkly/gh-actions/releases/tag/contract-tests-v1.3.0
- uses: launchdarkly/gh-actions/actions/contract-tests@5adb11fd6953e1bc35d9cf1fc1b4374c464e3a8b
with:
test_service_port: ${{ env.TEST_SERVICE_PORT }}
token: ${{ secrets.GITHUB_TOKEN }}
enable_persistence_tests: true
version: ${{ matrix.harness }}
branch: ${{ matrix.harness }}
# The other suites are covered by libs/server-sdk's workflow.
extra_params: -run=persistent.data.store -skip-from=${{ matrix.suppressions }}

build-test-dynamodb:
runs-on: ubuntu-22.04
services:
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/server-redis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,45 @@ on:
- cron: '0 8 * * *'

jobs:
contract-tests:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. Is this redundant redundant with the existing contract test CI runner?
  2. There is currently one workflow for v2 tests, and one for v3, which includes fdv2. But I think these new workflows only cover the v2 tests without fdv2. If we add redis/dynamodb as a second dimension, should we set up a test matrix with all the combinations, or...?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both good catches — addressed in 8b4f028.

  1. It was partly redundant: the store jobs also re-ran the whole non-persistence v2 suite that libs/server-sdk already runs. They now pass -run=persistent.data.store, so each job only runs the persistence suite (the store-specific dimension). I also dropped the redis contract-tests-curl job — the HTTP backend has no bearing on store behavior and libs/server-sdk already covers curl.

  2. Each store workflow is now a matrix over harness v2/v3, which required a code fix: the v3 harness nests the store config under dataSystem.store.persistentDataStore (plus storeMode), so previously every v3 persistence test failed. The service now reads either location. Current per-store results:

harness run suppressed
v2 14 12
v3 15 19

The v3 suppressions (persistence-suppressions-fdv2.txt) are the read-write tests plus all with data source cases — the data system method is a variant (Lazy Load or Background Sync or FDv2), so a store can't be combined with a data source, and the store integrations are read-only sources. Both are SDK gaps rather than harness/test-service gaps.

One caveat with -run: if the harness ever renames the persistent data store group, these jobs would pass while running zero tests.

runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
include:
- harness: v2
suppressions: contract-tests/server-contract-tests/persistence-suppressions.txt
- harness: v3
suppressions: contract-tests/server-contract-tests/persistence-suppressions-fdv2.txt
services:
redis:
image: redis
ports:
- 6379:6379
env:
TEST_SERVICE_PORT: 8123
TEST_SERVICE_BINARY: ./build/contract-tests/server-contract-tests/server-tests
steps:
# https://github.com/actions/checkout/releases/tag/v4.3.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
- uses: ./.github/actions/ci
with:
cmake_target: server-tests
run_tests: false
use_redis: true
- name: 'Launch test service as background task'
run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 &
# https://github.com/launchdarkly/gh-actions/releases/tag/contract-tests-v1.3.0
- uses: launchdarkly/gh-actions/actions/contract-tests@5adb11fd6953e1bc35d9cf1fc1b4374c464e3a8b
with:
test_service_port: ${{ env.TEST_SERVICE_PORT }}
token: ${{ secrets.GITHUB_TOKEN }}
enable_persistence_tests: true
version: ${{ matrix.harness }}
branch: ${{ matrix.harness }}
# The other suites are covered by libs/server-sdk's workflow.
extra_params: -run=persistent.data.store -skip-from=${{ matrix.suppressions }}

build-test-redis:
runs-on: ubuntu-22.04
services:
Expand All @@ -28,6 +67,7 @@ jobs:
with:
cmake_target: launchdarkly-cpp-server-redis-source
simulate_release: true
use_redis: true
build-redis-mac:
runs-on: macos-15
steps:
Expand All @@ -39,6 +79,7 @@ jobs:
platform_version: 12
run_tests: false # TODO: figure out how to run Redis service on Mac
simulate_release: true
use_redis: true
build-test-redis-windows:
runs-on: windows-2022
steps:
Expand All @@ -57,3 +98,4 @@ jobs:
toolset: msvc
run_tests: false # TODO: figure out how to run Redis service on Windows
simulate_windows_release: true
use_redis: true
42 changes: 41 additions & 1 deletion contract-tests/data-model/include/data_model/data_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include <optional>
#include <string>
#include <unordered_map>
#include "nlohmann/json.hpp"

Check failure on line 6 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:6:10 [clang-diagnostic-error]

'nlohmann/json.hpp' file not found

namespace nlohmann {
template <typename T>
Expand Down Expand Up @@ -65,7 +65,7 @@
pollIntervalMs,
filter);

struct ConfigEventParams {

Check warning on line 68 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:68:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: globalPrivateAttributes
std::optional<std::string> baseUri;
std::optional<uint32_t> capacity;
std::optional<bool> enableDiagnostics;
Expand Down Expand Up @@ -93,7 +93,7 @@
polling,
events);

struct ConfigClientSideParams {

Check warning on line 96 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:96:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: initialContext
nlohmann::json initialContext;
std::optional<bool> evaluationReasons;
std::optional<bool> useReport;
Expand Down Expand Up @@ -136,7 +136,7 @@
data,
errors);

struct ConfigHooksParams {

Check warning on line 139 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:139:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: hooks
std::vector<ConfigHookInstance> hooks;
};

Expand All @@ -149,6 +149,42 @@

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigWrapper, name, version);

struct ConfigPersistentCache {
std::string mode; // "off", "ttl", "infinite"
std::optional<int> ttl; // TTL in seconds (sent by test harness as "ttl")
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigPersistentCache,
mode,
ttl);

struct ConfigPersistentStore {
std::string type; // "redis", "consul", "dynamodb"
std::string dsn;
std::optional<std::string> prefix;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigPersistentStore,
type,
dsn,
prefix);

struct ConfigPersistentDataStore {
ConfigPersistentStore store;
ConfigPersistentCache cache;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigPersistentDataStore,
store,
cache);

struct ConfigDataSystemStore {
std::optional<ConfigPersistentDataStore> persistentDataStore;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigDataSystemStore,
persistentDataStore);

struct ConfigDataSynchronizerParams {
std::optional<ConfigStreamingParams> streaming;
std::optional<ConfigPollingParams> polling;
Expand All @@ -170,13 +206,15 @@
std::optional<std::vector<ConfigDataSynchronizerParams>> synchronizers;
std::optional<ConfigPollingParams> fdv1Fallback;
std::optional<std::string> payloadFilter;
std::optional<ConfigDataSystemStore> store;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigDataSystemParams,
initializers,
synchronizers,
fdv1Fallback,
payloadFilter);
payloadFilter,
store);

struct ConfigBigSegmentsParams {
std::string callbackUri;
Expand Down Expand Up @@ -208,6 +246,7 @@
std::optional<ConfigProxyParams> proxy;
std::optional<ConfigHooksParams> hooks;
std::optional<ConfigWrapper> wrapper;
std::optional<ConfigPersistentDataStore> persistentDataStore;
std::optional<ConfigBigSegmentsParams> bigSegments;
};

Expand All @@ -226,9 +265,10 @@
proxy,
hooks,
wrapper,
persistentDataStore,
bigSegments);

struct ContextSingleParams {

Check warning on line 271 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:271:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: custom
std::optional<std::string> kind;
std::string key;
std::optional<std::string> name;
Expand Down Expand Up @@ -323,7 +363,7 @@
defaultValue,
detail);

struct EvaluateFlagResponse {

Check warning on line 366 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:366:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: value, reason
nlohmann::json value;
std::optional<uint32_t> variationIndex;
std::optional<nlohmann::json> reason;
Expand All @@ -334,7 +374,7 @@
variationIndex,
reason);

struct EvaluateAllFlagParams {

Check warning on line 377 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:377:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: context
std::optional<nlohmann::json> context;
std::optional<bool> withReasons;
std::optional<bool> clientSideOnly;
Expand All @@ -347,7 +387,7 @@
clientSideOnly,
detailsOnlyForTrackedFlags);

struct EvaluateAllFlagsResponse {

Check warning on line 390 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:390:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: state
nlohmann::json state;
};

Expand All @@ -369,7 +409,7 @@
omitNullData,
metricValue);

struct IdentifyEventParams {

Check warning on line 412 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/data-model/include/data_model/data_model.hpp:412:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: context
nlohmann::json context;
};

Expand Down
10 changes: 10 additions & 0 deletions contract-tests/server-contract-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,14 @@ target_link_libraries(server-tests PRIVATE
contract-test-data-model
)

if (LD_BUILD_REDIS_SUPPORT)
target_link_libraries(server-tests PRIVATE launchdarkly::server_redis_source)
target_compile_definitions(server-tests PRIVATE LD_REDIS_SUPPORT_ENABLED)
endif ()

if (LD_BUILD_DYNAMODB_SUPPORT)
target_link_libraries(server-tests PRIVATE launchdarkly::server_dynamodb_source)
target_compile_definitions(server-tests PRIVATE LD_DYNAMODB_SUPPORT_ENABLED)
endif ()

target_include_directories(server-tests PUBLIC include)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# The SDK's store integrations are read-only sources for the Lazy Load data
# system, so it can't write data it receives, and a store can't be combined
# with a data source (the data system method is one of Lazy Load, Background
# Sync, or FDv2).

persistent data store/redis/read-write/initializes store when data received
persistent data store/redis/read-write/applies updates to store
persistent data store/redis/read-write/evaluation reflects streaming updates
persistent data store/redis/read-write/data source updates respect versioning
persistent data store/redis/read-write/data source deletions respect versioning
persistent data store/redis/read-write/ignores direct database modifications
persistent data store/redis/read-write/ignores dropped flags
persistent data store/redis/store mode read-write - with data source/no cache - ignores database until init key is set
persistent data store/redis/store mode read-write - with data source/no cache - ignores database when ds sends data
persistent data store/redis/store mode read-write - with data source/infinite cache - ignores database until init key is set
persistent data store/redis/store mode read-write - with data source/infinite cache - ignores database when ds sends data
persistent data store/redis/store mode read-write - with data source/ttl cache - ignores database until init key is set
persistent data store/redis/store mode read-write - with data source/ttl cache - ignores database when ds sends data
persistent data store/redis/store mode read - with data source/no cache - ignores database until init key is set
persistent data store/redis/store mode read - with data source/no cache - ignores database when ds sends data
persistent data store/redis/store mode read - with data source/infinite cache - ignores database until init key is set
persistent data store/redis/store mode read - with data source/infinite cache - ignores database when ds sends data
persistent data store/redis/store mode read - with data source/ttl cache - ignores database until init key is set
persistent data store/redis/store mode read - with data source/ttl cache - ignores database when ds sends data

persistent data store/dynamodb/read-write/initializes store when data received
persistent data store/dynamodb/read-write/applies updates to store
persistent data store/dynamodb/read-write/evaluation reflects streaming updates
persistent data store/dynamodb/read-write/data source updates respect versioning
persistent data store/dynamodb/read-write/data source deletions respect versioning
persistent data store/dynamodb/read-write/ignores direct database modifications
persistent data store/dynamodb/read-write/ignores dropped flags
persistent data store/dynamodb/store mode read-write - with data source/no cache - ignores database until init key is set
persistent data store/dynamodb/store mode read-write - with data source/no cache - ignores database when ds sends data
persistent data store/dynamodb/store mode read-write - with data source/infinite cache - ignores database until init key is set
persistent data store/dynamodb/store mode read-write - with data source/infinite cache - ignores database when ds sends data
persistent data store/dynamodb/store mode read-write - with data source/ttl cache - ignores database until init key is set
persistent data store/dynamodb/store mode read-write - with data source/ttl cache - ignores database when ds sends data
persistent data store/dynamodb/store mode read - with data source/no cache - ignores database until init key is set
persistent data store/dynamodb/store mode read - with data source/no cache - ignores database when ds sends data
persistent data store/dynamodb/store mode read - with data source/infinite cache - ignores database until init key is set
persistent data store/dynamodb/store mode read - with data source/infinite cache - ignores database when ds sends data
persistent data store/dynamodb/store mode read - with data source/ttl cache - ignores database until init key is set
persistent data store/dynamodb/store mode read - with data source/ttl cache - ignores database when ds sends data

Copy link
Copy Markdown
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
Copy Markdown
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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The SDK's store integrations are read-only sources for the Lazy Load data
# system, so it can't write data it receives back to the store.
persistent data store/redis/read-write/initializes store when data received
persistent data store/redis/read-write/applies updates to store
persistent data store/redis/read-write/data source updates respect versioning
persistent data store/redis/read-write/data source deletions respect versioning
persistent data store/redis/read-write/cache mode infinite/does not cache flag miss
persistent data store/redis/read-write/cache mode infinite/sdk reflects data source updates even with cache
persistent data store/redis/read-write/cache mode infinite/ignores direct database modifications
persistent data store/redis/read-write/cache mode infinite/ignores dropped flags
persistent data store/redis/read-write/cache mode ttl/does not cache flag miss
persistent data store/redis/read-write/cache mode ttl/sdk reflects data source updates even with cache
persistent data store/redis/read-write/cache mode ttl/ignores direct database modifications
persistent data store/redis/read-write/cache mode ttl/ignores dropped flags
persistent data store/dynamodb/read-write/initializes store when data received
persistent data store/dynamodb/read-write/applies updates to store
persistent data store/dynamodb/read-write/data source updates respect versioning
persistent data store/dynamodb/read-write/data source deletions respect versioning
persistent data store/dynamodb/read-write/cache mode infinite/does not cache flag miss
persistent data store/dynamodb/read-write/cache mode infinite/sdk reflects data source updates even with cache
persistent data store/dynamodb/read-write/cache mode infinite/ignores direct database modifications
persistent data store/dynamodb/read-write/cache mode infinite/ignores dropped flags
persistent data store/dynamodb/read-write/cache mode ttl/does not cache flag miss
persistent data store/dynamodb/read-write/cache mode ttl/sdk reflects data source updates even with cache
persistent data store/dynamodb/read-write/cache mode ttl/ignores direct database modifications
persistent data store/dynamodb/read-write/cache mode ttl/ignores dropped flags
Loading
Loading