diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 35c1ca23b..a68d659de 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -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)' + required: false + default: 'false' runs: using: composite @@ -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 }} @@ -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 }} diff --git a/.github/workflows/server-redis.yml b/.github/workflows/server-redis.yml index 0a73c4786..74f3c50a6 100644 --- a/.github/workflows/server-redis.yml +++ b/.github/workflows/server-redis.yml @@ -28,6 +28,7 @@ jobs: with: cmake_target: launchdarkly-cpp-server-redis-source simulate_release: true + use_redis: true build-redis-mac: runs-on: macos-15 steps: @@ -39,6 +40,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: @@ -57,3 +59,4 @@ jobs: toolset: msvc run_tests: false # TODO: figure out how to run Redis service on Windows simulate_windows_release: true + use_redis: true diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index f1b996dda..5f836700b 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -27,6 +27,7 @@ jobs: 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 @@ -35,6 +36,7 @@ jobs: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} token: ${{ secrets.GITHUB_TOKEN }} + enable_persistence_tests: true contract-tests-curl: runs-on: ubuntu-22.04 @@ -50,6 +52,7 @@ jobs: 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 @@ -58,6 +61,7 @@ jobs: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} token: ${{ secrets.GITHUB_TOKEN }} + enable_persistence_tests: true build-test-server: runs-on: ubuntu-22.04 diff --git a/contract-tests/data-model/include/data_model/data_model.hpp b/contract-tests/data-model/include/data_model/data_model.hpp index 679fd0015..ef3aab779 100644 --- a/contract-tests/data-model/include/data_model/data_model.hpp +++ b/contract-tests/data-model/include/data_model/data_model.hpp @@ -150,6 +150,34 @@ struct ConfigWrapper { NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigWrapper, name, version); +struct ConfigPersistentCache { + std::string mode; // "off", "ttl", "infinite" + std::optional ttlMs; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigPersistentCache, + mode, + ttlMs); + +struct ConfigPersistentStore { + std::string type; // "redis", "consul", "dynamodb" + std::string dsn; + std::optional 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 startWaitTimeMs; @@ -164,6 +192,7 @@ struct ConfigParams { std::optional proxy; std::optional hooks; std::optional wrapper; + std::optional persistentDataStore; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, @@ -179,7 +208,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, tls, proxy, hooks, - wrapper); + wrapper, + persistentDataStore); struct ContextSingleParams { std::optional kind; diff --git a/contract-tests/server-contract-tests/CMakeLists.txt b/contract-tests/server-contract-tests/CMakeLists.txt index 9455cd27e..faeca51eb 100644 --- a/contract-tests/server-contract-tests/CMakeLists.txt +++ b/contract-tests/server-contract-tests/CMakeLists.txt @@ -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) diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index 085df1c2d..b337d5598 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -7,6 +7,10 @@ #include +#ifdef LD_REDIS_SUPPORT_ENABLED +#include +#endif + using launchdarkly::LogLevel; using namespace launchdarkly::server_side; @@ -154,6 +158,56 @@ std::optional EntityManager::create(ConfigParams const& in) { } } +#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) diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp index e017a8634..481997961 100644 --- a/contract-tests/server-contract-tests/src/main.cpp +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -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}; diff --git a/scripts/build.sh b/scripts/build.sh index 7fd27a47e..f766f0583 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -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 .. @@ -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