Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
8 changes: 6 additions & 2 deletions .github/actions/ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ 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: ''

runs:
using: composite
Expand All @@ -58,7 +62,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 }}
env:
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
Boost_DIR: ${{ steps.install-boost.outputs.Boost_DIR }}
Expand All @@ -69,7 +73,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 }}
env:
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
Boost_DIR: ${{ steps.install-boost.outputs.Boost_DIR }}
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/server-redis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,61 @@ 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
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.1.0
- uses: launchdarkly/gh-actions/actions/contract-tests@2715574e04448246bc529a23a81766491bbc4aae
with:
test_service_port: ${{ env.TEST_SERVICE_PORT }}
token: ${{ secrets.GITHUB_TOKEN }}
enable_persistence_tests: true

contract-tests-curl:
runs-on: ubuntu-22.04
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_curl: true
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.1.0
- uses: launchdarkly/gh-actions/actions/contract-tests@2715574e04448246bc529a23a81766491bbc4aae
with:
test_service_port: ${{ env.TEST_SERVICE_PORT }}
token: ${{ secrets.GITHUB_TOKEN }}
enable_persistence_tests: true

build-test-redis:
runs-on: ubuntu-22.04
services:
Expand All @@ -28,6 +83,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 +95,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 +114,4 @@ jobs:
toolset: msvc
run_tests: false # TODO: figure out how to run Redis service on Windows
simulate_windows_release: true
use_redis: true
32 changes: 31 additions & 1 deletion contract-tests/data-model/include/data_model/data_model.hpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#pragma once

#include <optional>
Expand Down Expand Up @@ -150,6 +150,34 @@

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigWrapper, name, version);

struct ConfigPersistentCache {
std::string mode; // "off", "ttl", "infinite"
std::optional<int> ttlMs;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigPersistentCache,
mode,
ttlMs);

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 ConfigParams {
std::string credential;
std::optional<uint32_t> startWaitTimeMs;
Expand All @@ -164,6 +192,7 @@
std::optional<ConfigProxyParams> proxy;
std::optional<ConfigHooksParams> hooks;
std::optional<ConfigWrapper> wrapper;
std::optional<ConfigPersistentDataStore> persistentDataStore;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
Expand All @@ -179,7 +208,8 @@
tls,
proxy,
hooks,
wrapper);
wrapper,
persistentDataStore);

struct ContextSingleParams {
std::optional<std::string> kind;
Expand Down
5 changes: 5 additions & 0 deletions contract-tests/server-contract-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ 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 ()

target_include_directories(server-tests PUBLIC include)
54 changes: 54 additions & 0 deletions contract-tests/server-contract-tests/src/entity_manager.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include "entity_manager.hpp"
#include "contract_test_hook.hpp"

Expand All @@ -7,6 +7,10 @@

#include <boost/json.hpp>

#ifdef LD_REDIS_SUPPORT_ENABLED
#include <launchdarkly/server_side/integrations/redis/redis_source.hpp>
#endif

using launchdarkly::LogLevel;
using namespace launchdarkly::server_side;

Expand Down Expand Up @@ -154,6 +158,56 @@
}
}

#ifdef LD_REDIS_SUPPORT_ENABLED
if (in.persistentDataStore) {
if (in.persistentDataStore->store.type == "redis") {
std::string prefix =
in.persistentDataStore->store.prefix.value_or("launchdarkly");

auto redis_result = launchdarkly::server_side::integrations::
RedisDataSource::Create(in.persistentDataStore->store.dsn,
prefix);

if (!redis_result) {
LD_LOG(logger_, LogLevel::kWarn)
<< "entity_manager: couldn't create Redis data source: "
<< redis_result.error();
return std::nullopt;
}

auto lazy_load = config::builders::LazyLoadBuilder();
lazy_load.Source(std::move(*redis_result));

// Configure cache mode
// Default is 5 minutes, but contract tests may specify:
// - "off": disable caching (fetch from DB every time)
// - "ttl": custom TTL in milliseconds
// - "infinite": never expire cached items
if (in.persistentDataStore->cache.mode == "off") {
lazy_load.CacheRefresh(std::chrono::seconds(0));
} else if (in.persistentDataStore->cache.mode == "ttl") {
if (in.persistentDataStore->cache.ttlMs) {
lazy_load.CacheRefresh(std::chrono::milliseconds(
*in.persistentDataStore->cache.ttlMs));
}
} else if (in.persistentDataStore->cache.mode == "infinite") {
// Use a very large TTL to effectively never expire
lazy_load.CacheRefresh(std::chrono::hours(24 * 365));
}
// If no mode specified, the default 5-minute TTL is used

config_builder.DataSystem().Method(
config::builders::DataSystemBuilder::LazyLoad(
std::move(lazy_load)));
} else {
LD_LOG(logger_, LogLevel::kWarn)
<< "entity_manager: unsupported persistent store type: "
<< in.persistentDataStore->store.type;
return std::nullopt;
}
}
#endif

auto config = config_builder.Build();
if (!config) {
LD_LOG(logger_, LogLevel::kWarn)
Expand Down
3 changes: 3 additions & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ int main(int argc, char* argv[]) {
srv.add_capability("evaluation-hooks");
srv.add_capability("track-hooks");
srv.add_capability("wrapper");
#ifdef LD_REDIS_SUPPORT_ENABLED
srv.add_capability("persistent-data-store-redis");
#endif

net::signal_set signals{ioc, SIGINT, SIGTERM};

Expand Down
8 changes: 8 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# $1 the name of the target. For example "launchdarkly-cpp-common".
# $2 ON/OFF which enables/disables building in a test configuration (unit tests + contract tests.)
# $3 (optional) true/false to enable/disable CURL networking (LD_CURL_NETWORKING)
# $4 (optional) true/false to enable/disable Redis support (LD_BUILD_REDIS_SUPPORT)

function cleanup {
cd ..
Expand All @@ -31,6 +32,13 @@ if [ "$3" == "true" ]; then
build_curl="ON"
fi

# Check for Redis support option (override the automatic detection if explicitly passed)
if [ "$4" == "true" ]; then
build_redis="ON"
elif [ "$4" == "false" ]; then
build_redis="OFF"
fi

# Set build type to Debug when testing is enabled
build_type="Release"
if [ "$2" == "ON" ]; then
Expand Down
Loading