From f71655e0d6add3112358e5edb5e31a4c430f0093 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:30:38 +0000 Subject: [PATCH 1/2] ci: Run the persistence contract tests The contract test service had no persistent store support: the data system store configuration was commented out, and the FDv1 configuration was not handled at all, so CI ran the harness with persistence tests disabled. - The service configures a Redis, DynamoDB, or Consul store from the FDv2 dataSystem.store configuration (harness v3) and from the FDv1 persistentDataStore configuration (harness v2). An FDv1 configuration with a store and no data source runs in daemon mode, as the harness expects. The store clients fail fast, as the store recovery tests require. - The service references the three store projects in this repository. They reference the published SDK package unless UseLocalServerSdk is set, which the service sets, so the service always runs against the local SDK. - The service advertises the persistent-data-store-redis, -dynamodb, -consul and -recovery capabilities. - CI starts the stores on the Linux runner and enables the persistence tests there. The service listens on port 9000, its first argument, since DynamoDB uses port 8000. Pre-existing store format defects that the tests found are suppressed: a deleted flag is written without a "key" property (Redis, Consul) or as the placeholder "null" (DynamoDB), and the Consul store fails to read an empty collection. --- .github/actions/contract-tests/action.yml | 26 ++- .github/workflows/sdk-server-ci.yml | 3 + .../src/LaunchDarkly.ServerSdk.Consul.csproj | 10 +- .../LaunchDarkly.ServerSdk.DynamoDB.csproj | 10 +- .../src/LaunchDarkly.ServerSdk.Redis.csproj | 10 +- pkgs/sdk/server/contract-tests/README.md | 4 + .../server/contract-tests/Representations.cs | 1 + .../server/contract-tests/SdkClientEntity.cs | 175 ++++++++++-------- pkgs/sdk/server/contract-tests/TestService.cs | 9 +- .../server/contract-tests/TestService.csproj | 9 + .../contract-tests/test-supressions-fdv2.txt | 25 +++ .../contract-tests/test-supressions.txt | 9 + 12 files changed, 197 insertions(+), 94 deletions(-) diff --git a/.github/actions/contract-tests/action.yml b/.github/actions/contract-tests/action.yml index 17eaf4026..71264623a 100644 --- a/.github/actions/contract-tests/action.yml +++ b/.github/actions/contract-tests/action.yml @@ -15,6 +15,14 @@ inputs: description: 'Whether to run contract tests from the latest v3 release' required: false default: 'false' + enable_persistence_tests: + description: 'Whether to start Redis, DynamoDB and Consul on their default ports and run the persistent store tests.' + required: false + default: 'false' + test_service_port: + description: 'The port of the contract test service. The default is not 8000, the port that the harness expects DynamoDB on.' + required: false + default: '9000' runs: using: composite @@ -36,10 +44,18 @@ runs: shell: bash run: dotnet build -p:Configuration=debug ${{ inputs.service_project_file }} + - name: Start Persistent Stores + if: inputs.enable_persistence_tests == 'true' + uses: launchdarkly/gh-actions/actions/persistent-stores@persistent-stores-v0 + with: + redis: true + dynamodb: true + consul: true + - name: Launch Contract Tests id: launch-contract-tests shell: bash - run: dotnet ${{ inputs.service_dll_file }} > test-service.log 2>&1 & disown + run: dotnet ${{ inputs.service_dll_file }} ${{ inputs.test_service_port }} > test-service.log 2>&1 & disown - name: Suppression Path shell: bash @@ -50,8 +66,8 @@ runs: - name: Run Contract Tests uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1 with: - test_service_port: 8000 - enable_persistence_tests: 'false' + test_service_port: ${{ inputs.test_service_port }} + enable_persistence_tests: ${{ inputs.enable_persistence_tests }} # Leave the service up for the FDv2 run that follows. stop_service: ${{ inputs.run_fdv2_tests == 'true' && 'false' || 'true' }} extra_params: '-status-timeout=360 -skip-from=${{ env.SUPPRESSION_FILE }}' @@ -61,10 +77,10 @@ runs: if: inputs.run_fdv2_tests == 'true' uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1 with: - test_service_port: 8000 + test_service_port: ${{ inputs.test_service_port }} version: 'v3' debug_logging: 'true' - enable_persistence_tests: 'false' + enable_persistence_tests: ${{ inputs.enable_persistence_tests }} extra_params: '-status-timeout=360 -skip-from=${{ env.SUPPRESSION_FILE_FDV2 }}' token: ${{ inputs.token }} diff --git a/.github/workflows/sdk-server-ci.yml b/.github/workflows/sdk-server-ci.yml index 8da16ed63..208f0cf25 100644 --- a/.github/workflows/sdk-server-ci.yml +++ b/.github/workflows/sdk-server-ci.yml @@ -37,6 +37,9 @@ jobs: service_dll_file: ${{ env.CONTRACT_TEST_DLL_FILE}} token: ${{ secrets.GITHUB_TOKEN }} run_fdv2_tests: 'true' + # The persistence tests run on the Linux runner only, where the stores start quickly in Docker. Their + # behavior does not depend on the platform. + enable_persistence_tests: ${{ runner.os == 'Linux' && 'true' || 'false' }} - uses: ./.github/actions/build-docs with: diff --git a/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj b/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj index d470c576a..c328d018b 100644 --- a/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj +++ b/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj @@ -34,13 +34,15 @@ - + - - - + + + diff --git a/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj b/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj index f1c6b36b3..0a74b9a00 100644 --- a/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj +++ b/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj @@ -34,13 +34,15 @@ - + - - - + + + diff --git a/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj b/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj index 298b304b7..2c2fce535 100644 --- a/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj +++ b/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj @@ -34,13 +34,15 @@ - + - - - + + + diff --git a/pkgs/sdk/server/contract-tests/README.md b/pkgs/sdk/server/contract-tests/README.md index b75f5fbb6..ecc79afd5 100644 --- a/pkgs/sdk/server/contract-tests/README.md +++ b/pkgs/sdk/server/contract-tests/README.md @@ -7,3 +7,7 @@ To run these tests locally, run `make contract-tests` from the SDK project root Or, to test against an in-progress local version of the test harness, run `make start-contract-test-service` from the SDK project root directory; then, in the root directory of the `sdk-test-harness` project, build the test harness and run it from the command line. Currently, the project does _not_ automatically detect the available target frameworks. It will default to building and running for .NET 8.0. To use a different target framework, set the environment variable `TESTFRAMEWORK` to the name of the application runtime framework (such as `net8.0`), and set the environment variable `BUILDFRAMEWORKS` (note the S at the end) to the target framework that the SDK should be built for (which may or may not be the same). + +The service references the Redis, DynamoDB, and Consul integrations in this repository, built against the local SDK rather than the published SDK package that they reference by default. Those integrations target `netstandard2.0` and `net462`, so if you set `BUILDFRAMEWORKS`, it must include both. + +To run the persistent store tests, start the stores on their default ports, for example with `docker run -p 6379:6379 redis`, `docker run -p 8000:8000 amazon/dynamodb-local`, and `docker run -p 8500:8500 hashicorp/consul`, and pass `-enable-persistence-tests` to the test harness. DynamoDB uses port 8000, the service's default port, so pass another port as the service's first argument, for example `dotnet bin/Debug/net8.0/ContractTestService.dll 9000`. diff --git a/pkgs/sdk/server/contract-tests/Representations.cs b/pkgs/sdk/server/contract-tests/Representations.cs index 9efeb5ce7..4ea828708 100644 --- a/pkgs/sdk/server/contract-tests/Representations.cs +++ b/pkgs/sdk/server/contract-tests/Representations.cs @@ -36,6 +36,7 @@ public class SdkConfigParams public SdkHookParams Hooks { get; set; } public SdkConfigDataSystemParams DataSystem { get; set; } public SdkConfigOverridesParams Overrides { get; set; } + public SdkConfigPersistentDataStoreParams PersistentDataStore { get; set; } } public class SdkConfigOverridesParams diff --git a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs index 2007cca9d..58f2fdca9 100644 --- a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs +++ b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using Amazon.DynamoDBv2; +using Amazon.Runtime; using LaunchDarkly.Logging; using LaunchDarkly.Sdk; using LaunchDarkly.Sdk.Json; @@ -436,6 +438,17 @@ private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapt builder.DataSource(dataSource); } + if (sdkParams.PersistentDataStore != null) + { + builder.DataStore(CreatePersistentDataStore(sdkParams.PersistentDataStore)); + // With a persistent store and no data source, the SDK runs in daemon mode: it reads the data + // that another process, such as the Relay Proxy, writes to the store. + if (streamingParams == null && pollingParams == null) + { + builder.DataSource(Components.ExternalUpdatesOnly); + } + } + var eventParams = sdkParams.Events; if (eventParams == null) { @@ -516,81 +529,14 @@ private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapt { var dataSystemBuilder = Components.DataSystem().Custom(); - // TODO: re-enable this code in the future and determine which dependencies on persistent stores need to be added to contract test build process - // Configure persistent store if provided - // if (sdkParams.DataSystem.Store?.PersistentDataStore != null) - // { - // var storeConfig = sdkParams.DataSystem.Store.PersistentDataStore; - // var storeType = storeConfig.Store.Type.ToLower(); - // IComponentConfigurer persistentStore = null; - - // PersistentDataStoreBuilder persistentStoreBuilder = null; - - // switch (storeType) - // { - // case "redis": - // var redisBuilder = Redis.DataStore(); - // if (!string.IsNullOrEmpty(storeConfig.Store.DSN)) - // { - // redisBuilder.Uri(storeConfig.Store.DSN); - // } - // if (!string.IsNullOrEmpty(storeConfig.Store.Prefix)) - // { - // redisBuilder.Prefix(storeConfig.Store.Prefix); - // } - // persistentStoreBuilder = Components.PersistentDataStore(redisBuilder); - // break; - - // case "dynamodb": - // // For DynamoDB, DSN is the table name - // var dynamoBuilder = DynamoDB.DataStore(storeConfig.Store.DSN ?? "sdk-contract-tests"); - // if (!string.IsNullOrEmpty(storeConfig.Store.Prefix)) - // { - // dynamoBuilder.Prefix(storeConfig.Store.Prefix); - // } - // // DynamoDB uses IPersistentDataStoreAsync, so use the async overload - // persistentStoreBuilder = Components.PersistentDataStore(dynamoBuilder); - // break; - - // case "consul": - // var consulBuilder = Consul.DataStore(); - // if (!string.IsNullOrEmpty(storeConfig.Store.DSN)) - // { - // consulBuilder.Address(storeConfig.Store.DSN); - // } - // if (!string.IsNullOrEmpty(storeConfig.Store.Prefix)) - // { - // consulBuilder.Prefix(storeConfig.Store.Prefix); - // } - // persistentStoreBuilder = Components.PersistentDataStore(consulBuilder); - // break; - // } - - // if (persistentStoreBuilder != null) - // { - // // Configure cache - // var cacheMode = storeConfig.Cache?.Mode?.ToLower(); - // if (cacheMode == "off") - // { - // persistentStoreBuilder.NoCaching(); - // } - // else if (cacheMode == "ttl" && storeConfig.Cache.TTL.HasValue) - // { - // persistentStoreBuilder.CacheTime(TimeSpan.FromSeconds(storeConfig.Cache.TTL.Value)); - // } - // else if (cacheMode == "infinite") - // { - // persistentStoreBuilder.CacheForever(); - // } - - // // Determine store mode - // var storeMode = sdkParams.DataSystem.StoreMode == 0 - // ? DataSystemConfiguration.DataStoreMode.ReadOnly - // : DataSystemConfiguration.DataStoreMode.ReadWrite; - - // dataSystemBuilder.PersistentStore(persistentStoreBuilder, storeMode); - // } - // } + var storeParams = sdkParams.DataSystem.Store?.PersistentDataStore; + if (storeParams != null) + { + var storeMode = sdkParams.DataSystem.StoreMode == StoreMode.ReadWrite + ? DataSystemConfiguration.DataStoreMode.ReadWrite + : DataSystemConfiguration.DataStoreMode.ReadOnly; + dataSystemBuilder.PersistentStore(CreatePersistentDataStore(storeParams), storeMode); + } // Configure initializers if (sdkParams.DataSystem.Initializers != null && sdkParams.DataSystem.Initializers.Length > 0) @@ -721,6 +667,85 @@ private static FileOverrideSourceBuilder BuildOverrideSource(SdkConfigOverridesP return source; } + // The Redis and DynamoDB clients are configured so that a failed operation surfaces well within a + // second, as the harness's store recovery tests require: no retries, and short timeouts. The Consul + // client does not retry. + private static readonly TimeSpan StoreOperationTimeout = TimeSpan.FromMilliseconds(500); + + // The table that the test harness creates in DynamoDB. + private const string DynamoDBTableName = "sdk-contract-tests"; + + private static PersistentDataStoreBuilder CreatePersistentDataStore(SdkConfigPersistentDataStoreParams storeParams) + { + var store = storeParams.Store; + PersistentDataStoreBuilder builder; + switch (store.Type) + { + case PersistentStoreType.Redis: + var redis = Redis.DataStore() + .Uri(store.DSN) + .ConnectTimeout(StoreOperationTimeout) + .OperationTimeout(StoreOperationTimeout); + if (store.Prefix != null) + { + redis.Prefix(store.Prefix); + } + builder = Components.PersistentDataStore(redis); + break; + + case PersistentStoreType.DynamoDB: + // The DSN is the endpoint of the local DynamoDB instance. + var dynamoDB = DynamoDB.DataStore(DynamoDBTableName) + .Configuration(new AmazonDynamoDBConfig + { + ServiceURL = store.DSN, + AuthenticationRegion = "us-east-1", + MaxErrorRetry = 0, + Timeout = StoreOperationTimeout + }) + .Credentials(new BasicAWSCredentials("dummy", "dummy")); + if (store.Prefix != null) + { + dynamoDB.Prefix(store.Prefix); + } + builder = Components.PersistentDataStore(dynamoDB); + break; + + case PersistentStoreType.Consul: + // The DSN is a host and port, without a scheme. The Consul package's namespace is also + // named Consul, so the SDK's class is named in full. + var consul = LaunchDarkly.Sdk.Server.Integrations.Consul.DataStore() + .Address(store.DSN.Contains("://") ? store.DSN : "http://" + store.DSN); + if (store.Prefix != null) + { + consul.Prefix(store.Prefix); + } + builder = Components.PersistentDataStore(consul); + break; + + default: + throw new ArgumentException("Unsupported persistent store type: " + store.Type); + } + + switch (storeParams.Cache?.Mode) + { + case PersistentCacheMode.Off: + builder.NoCaching(); + break; + case PersistentCacheMode.TTL: + if (storeParams.Cache.TTL.HasValue) + { + builder.CacheTime(TimeSpan.FromSeconds(storeParams.Cache.TTL.Value)); + } + break; + case PersistentCacheMode.Infinite: + builder.CacheForever(); + break; + } + + return builder; + } + private static IComponentConfigurer CreateSynchronizer( SdkConfigDataSynchronizerParams synchronizer, string payloadFilter) diff --git a/pkgs/sdk/server/contract-tests/TestService.cs b/pkgs/sdk/server/contract-tests/TestService.cs index 9ce3be0b2..0d3e5518e 100644 --- a/pkgs/sdk/server/contract-tests/TestService.cs +++ b/pkgs/sdk/server/contract-tests/TestService.cs @@ -12,7 +12,8 @@ public class Program { public static void Main(string[] args) { - // An optional first argument selects the port, so that several test services can run at once. + // An optional first argument selects the port. In CI, port 8000 belongs to the local DynamoDB + // instance that the persistent store tests use. var port = args.Length > 0 ? int.Parse(args[0]) : 8000; var quitSignal = new EventWaitHandle(false, EventResetMode.AutoReset); var app = new Webapp(quitSignal); @@ -52,7 +53,11 @@ public class Webapp "instance-id", "retry-conformance-fdv1-streaming", "retry-conformance-fdv1-polling", - "flag-overrides" + "flag-overrides", + "persistent-data-store-redis", + "persistent-data-store-dynamodb", + "persistent-data-store-consul", + "persistent-data-store-recovery" }; public readonly Handler Handler; diff --git a/pkgs/sdk/server/contract-tests/TestService.csproj b/pkgs/sdk/server/contract-tests/TestService.csproj index 8123e1389..053b47c79 100644 --- a/pkgs/sdk/server/contract-tests/TestService.csproj +++ b/pkgs/sdk/server/contract-tests/TestService.csproj @@ -13,6 +13,7 @@ false false false + true @@ -24,4 +25,12 @@ + + + + + + + diff --git a/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt b/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt index 854fa2d1c..f355754a7 100644 --- a/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt +++ b/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt @@ -1 +1,26 @@ streaming/fdv2/disconnects on goodbye +# The harness reads the lines below as test IDs; a line that starts with "#" matches no test. +# Pre-existing store format defects that the persistence tests found: +# - A deleted flag is written as {"version":N,"deleted":true}, with no "key" property (Redis, Consul), or +# as the placeholder "null" (DynamoDB). The harness expects a JSON tombstone with a "key" property. The +# store recovery tests check a deleted flag after the write back. +persistent data store/redis/read-write/data source deletions respect versioning +persistent data store/redis/store recovery/no cache - writes back full state on recovery +persistent data store/redis/store recovery/ttl cache - writes back full state on recovery +persistent data store/redis/store recovery/infinite cache - writes back full state on recovery +persistent data store/redis/store recovery/retries write back after another failure +persistent data store/redis/store recovery/recovers from repeated outages +persistent data store/consul/read-write/data source deletions respect versioning +persistent data store/consul/store recovery/no cache - writes back full state on recovery +persistent data store/consul/store recovery/ttl cache - writes back full state on recovery +persistent data store/consul/store recovery/infinite cache - writes back full state on recovery +persistent data store/consul/store recovery/retries write back after another failure +persistent data store/consul/store recovery/recovers from repeated outages +persistent data store/dynamodb/read-write/data source deletions respect versioning +persistent data store/dynamodb/store recovery/no cache - writes back full state on recovery +persistent data store/dynamodb/store recovery/ttl cache - writes back full state on recovery +persistent data store/dynamodb/store recovery/infinite cache - writes back full state on recovery +persistent data store/dynamodb/store recovery/retries write back after another failure +persistent data store/dynamodb/store recovery/recovers from repeated outages +# - The Consul store fails to read a collection that has no items. +persistent data store/consul/collection cardinality/collection holds no flags diff --git a/pkgs/sdk/server/contract-tests/test-supressions.txt b/pkgs/sdk/server/contract-tests/test-supressions.txt index 29151d21b..d1615f6c5 100644 --- a/pkgs/sdk/server/contract-tests/test-supressions.txt +++ b/pkgs/sdk/server/contract-tests/test-supressions.txt @@ -1 +1,10 @@ tags/disallowed characters +# The harness reads the lines below as test IDs; a line that starts with "#" matches no test. +# Pre-existing store format defects that the persistence tests found: +# - A deleted flag is written as {"version":N,"deleted":true}, with no "key" property (Redis, Consul), or +# as the placeholder "null" (DynamoDB). The harness expects a JSON tombstone with a "key" property. +persistent data store/redis/read-write/data source deletions respect versioning +persistent data store/consul/read-write/data source deletions respect versioning +persistent data store/dynamodb/read-write/data source deletions respect versioning +# - The Consul store fails to read a collection that has no items. +persistent data store/consul/daemon mode/collection cardinality/collection holds no flags From 8f77f3f693f5da0bd45363ee08abe7f6a14f1fcb Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:21:49 +0000 Subject: [PATCH 2/2] ci: Fix the client CI port and build the stores without changing their projects From the review of this change: - The contract-tests action defaulted its new port input to 9000, which the client SDK CI also uses, while the client test service always listens on 8000. The default is 8000 again, and the server CI and nightly pass 9000. - Switching the store projects to the local SDK with a property broke solution builds: a solution restore resolves the published SDK package for them, and the build then compiled them against both SDKs. The store projects are unchanged now. A small project under contract-tests/Stores builds each store from its sources against the local SDK; one project per store, because the stores share internal type names. - The action waits until the stores accept connections and Consul has a leader before the tests start. - The service treats a configuration with service endpoint URIs as having a data source when it decides on daemon mode, treats an empty prefix as no prefix, and rejects a store configuration without a DSN. --- .github/actions/contract-tests/action.yml | 17 ++++++++++++-- .github/workflows/sdk-server-ci.yml | 2 ++ .../src/LaunchDarkly.ServerSdk.Consul.csproj | 10 ++++----- .../LaunchDarkly.ServerSdk.DynamoDB.csproj | 10 ++++----- .../src/LaunchDarkly.ServerSdk.Redis.csproj | 10 ++++----- pkgs/sdk/server/contract-tests/README.md | 4 ++-- .../server/contract-tests/SdkClientEntity.cs | 13 +++++++---- .../Consul/ContractTestService.Consul.csproj | 22 +++++++++++++++++++ .../ContractTestService.DynamoDB.csproj | 22 +++++++++++++++++++ .../Redis/ContractTestService.Redis.csproj | 22 +++++++++++++++++++ .../server/contract-tests/TestService.csproj | 12 +++++----- 11 files changed, 112 insertions(+), 32 deletions(-) create mode 100644 pkgs/sdk/server/contract-tests/Stores/Consul/ContractTestService.Consul.csproj create mode 100644 pkgs/sdk/server/contract-tests/Stores/DynamoDB/ContractTestService.DynamoDB.csproj create mode 100644 pkgs/sdk/server/contract-tests/Stores/Redis/ContractTestService.Redis.csproj diff --git a/.github/actions/contract-tests/action.yml b/.github/actions/contract-tests/action.yml index 71264623a..b003705c8 100644 --- a/.github/actions/contract-tests/action.yml +++ b/.github/actions/contract-tests/action.yml @@ -20,9 +20,9 @@ inputs: required: false default: 'false' test_service_port: - description: 'The port of the contract test service. The default is not 8000, the port that the harness expects DynamoDB on.' + description: 'The port of the contract test service. A caller that enables the persistence tests must pass another port than 8000, the port that the harness expects DynamoDB on.' required: false - default: '9000' + default: '8000' runs: using: composite @@ -52,6 +52,19 @@ runs: dynamodb: true consul: true + # The stores start in the background. The harness's DynamoDB setup fails the whole DynamoDB suite if the store + # does not answer yet, and Consul rejects writes until it has elected a leader. + - name: Wait for Persistent Stores + if: inputs.enable_persistence_tests == 'true' + shell: bash + run: | + for port in 6379 8000 8500; do + timeout 120 bash -c "until (echo > /dev/tcp/127.0.0.1/$port) 2>/dev/null; do sleep 1; done" \ + || { echo "The store on port $port did not start"; exit 1; } + done + timeout 120 bash -c 'until curl -sf http://127.0.0.1:8500/v1/status/leader | grep -q ":"; do sleep 1; done' \ + || { echo "Consul did not elect a leader"; exit 1; } + - name: Launch Contract Tests id: launch-contract-tests shell: bash diff --git a/.github/workflows/sdk-server-ci.yml b/.github/workflows/sdk-server-ci.yml index 208f0cf25..07afa7257 100644 --- a/.github/workflows/sdk-server-ci.yml +++ b/.github/workflows/sdk-server-ci.yml @@ -40,6 +40,8 @@ jobs: # The persistence tests run on the Linux runner only, where the stores start quickly in Docker. Their # behavior does not depend on the platform. enable_persistence_tests: ${{ runner.os == 'Linux' && 'true' || 'false' }} + # DynamoDB uses port 8000, the service's default port. + test_service_port: '9000' - uses: ./.github/actions/build-docs with: diff --git a/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj b/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj index c328d018b..d470c576a 100644 --- a/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj +++ b/pkgs/dotnet-server-sdk-consul/src/LaunchDarkly.ServerSdk.Consul.csproj @@ -34,15 +34,13 @@ - + - - - + + + diff --git a/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj b/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj index 0a74b9a00..f1c6b36b3 100644 --- a/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj +++ b/pkgs/dotnet-server-sdk-dynamodb/src/LaunchDarkly.ServerSdk.DynamoDB.csproj @@ -34,15 +34,13 @@ - + - - - + + + diff --git a/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj b/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj index 2c2fce535..298b304b7 100644 --- a/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj +++ b/pkgs/dotnet-server-sdk-redis/src/LaunchDarkly.ServerSdk.Redis.csproj @@ -34,15 +34,13 @@ - + - - - + + + diff --git a/pkgs/sdk/server/contract-tests/README.md b/pkgs/sdk/server/contract-tests/README.md index ecc79afd5..27c749b64 100644 --- a/pkgs/sdk/server/contract-tests/README.md +++ b/pkgs/sdk/server/contract-tests/README.md @@ -8,6 +8,6 @@ Or, to test against an in-progress local version of the test harness, run `make Currently, the project does _not_ automatically detect the available target frameworks. It will default to building and running for .NET 8.0. To use a different target framework, set the environment variable `TESTFRAMEWORK` to the name of the application runtime framework (such as `net8.0`), and set the environment variable `BUILDFRAMEWORKS` (note the S at the end) to the target framework that the SDK should be built for (which may or may not be the same). -The service references the Redis, DynamoDB, and Consul integrations in this repository, built against the local SDK rather than the published SDK package that they reference by default. Those integrations target `netstandard2.0` and `net462`, so if you set `BUILDFRAMEWORKS`, it must include both. +The service compiles the sources of the Redis, DynamoDB, and Consul integrations in this repository, so that they run against the local SDK rather than the published SDK package that their projects reference. -To run the persistent store tests, start the stores on their default ports, for example with `docker run -p 6379:6379 redis`, `docker run -p 8000:8000 amazon/dynamodb-local`, and `docker run -p 8500:8500 hashicorp/consul`, and pass `-enable-persistence-tests` to the test harness. DynamoDB uses port 8000, the service's default port, so pass another port as the service's first argument, for example `dotnet bin/Debug/net8.0/ContractTestService.dll 9000`. +To run the persistent store tests, start the stores on their default ports, for example with `docker run -p 127.0.0.1:6379:6379 redis`, `docker run -p 127.0.0.1:8000:8000 amazon/dynamodb-local`, and `docker run -p 127.0.0.1:8500:8500 hashicorp/consul`, and pass `-enable-persistence-tests` to the test harness. DynamoDB uses port 8000, the service's default port, so pass another port as the service's first argument, for example `dotnet bin/Debug/net8.0/ContractTestService.dll 9000`. diff --git a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs index 58f2fdca9..db460e570 100644 --- a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs +++ b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs @@ -443,7 +443,8 @@ private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapt builder.DataStore(CreatePersistentDataStore(sdkParams.PersistentDataStore)); // With a persistent store and no data source, the SDK runs in daemon mode: it reads the data // that another process, such as the Relay Proxy, writes to the store. - if (streamingParams == null && pollingParams == null) + if (streamingParams == null && pollingParams == null && + serviceEndpointsParams?.Streaming == null && serviceEndpointsParams?.Polling == null) { builder.DataSource(Components.ExternalUpdatesOnly); } @@ -678,6 +679,10 @@ private static FileOverrideSourceBuilder BuildOverrideSource(SdkConfigOverridesP private static PersistentDataStoreBuilder CreatePersistentDataStore(SdkConfigPersistentDataStoreParams storeParams) { var store = storeParams.Store; + if (string.IsNullOrEmpty(store.DSN)) + { + throw new ArgumentException("The persistent store configuration has no DSN"); + } PersistentDataStoreBuilder builder; switch (store.Type) { @@ -686,7 +691,7 @@ private static PersistentDataStoreBuilder CreatePersistentDataStore(SdkConfigPer .Uri(store.DSN) .ConnectTimeout(StoreOperationTimeout) .OperationTimeout(StoreOperationTimeout); - if (store.Prefix != null) + if (!string.IsNullOrEmpty(store.Prefix)) { redis.Prefix(store.Prefix); } @@ -704,7 +709,7 @@ private static PersistentDataStoreBuilder CreatePersistentDataStore(SdkConfigPer Timeout = StoreOperationTimeout }) .Credentials(new BasicAWSCredentials("dummy", "dummy")); - if (store.Prefix != null) + if (!string.IsNullOrEmpty(store.Prefix)) { dynamoDB.Prefix(store.Prefix); } @@ -716,7 +721,7 @@ private static PersistentDataStoreBuilder CreatePersistentDataStore(SdkConfigPer // named Consul, so the SDK's class is named in full. var consul = LaunchDarkly.Sdk.Server.Integrations.Consul.DataStore() .Address(store.DSN.Contains("://") ? store.DSN : "http://" + store.DSN); - if (store.Prefix != null) + if (!string.IsNullOrEmpty(store.Prefix)) { consul.Prefix(store.Prefix); } diff --git a/pkgs/sdk/server/contract-tests/Stores/Consul/ContractTestService.Consul.csproj b/pkgs/sdk/server/contract-tests/Stores/Consul/ContractTestService.Consul.csproj new file mode 100644 index 000000000..3987f2e13 --- /dev/null +++ b/pkgs/sdk/server/contract-tests/Stores/Consul/ContractTestService.Consul.csproj @@ -0,0 +1,22 @@ + + + + + net8.0 + $(TESTFRAMEWORK) + ContractTestService.Consul + false + + + + + + + + + + + + + diff --git a/pkgs/sdk/server/contract-tests/Stores/DynamoDB/ContractTestService.DynamoDB.csproj b/pkgs/sdk/server/contract-tests/Stores/DynamoDB/ContractTestService.DynamoDB.csproj new file mode 100644 index 000000000..8e375a0ab --- /dev/null +++ b/pkgs/sdk/server/contract-tests/Stores/DynamoDB/ContractTestService.DynamoDB.csproj @@ -0,0 +1,22 @@ + + + + + net8.0 + $(TESTFRAMEWORK) + ContractTestService.DynamoDB + false + + + + + + + + + + + + + diff --git a/pkgs/sdk/server/contract-tests/Stores/Redis/ContractTestService.Redis.csproj b/pkgs/sdk/server/contract-tests/Stores/Redis/ContractTestService.Redis.csproj new file mode 100644 index 000000000..a03593e1d --- /dev/null +++ b/pkgs/sdk/server/contract-tests/Stores/Redis/ContractTestService.Redis.csproj @@ -0,0 +1,22 @@ + + + + + net8.0 + $(TESTFRAMEWORK) + ContractTestService.Redis + false + + + + + + + + + + + + + diff --git a/pkgs/sdk/server/contract-tests/TestService.csproj b/pkgs/sdk/server/contract-tests/TestService.csproj index 053b47c79..43b665086 100644 --- a/pkgs/sdk/server/contract-tests/TestService.csproj +++ b/pkgs/sdk/server/contract-tests/TestService.csproj @@ -13,7 +13,6 @@ false false false - true @@ -25,12 +24,13 @@ - + - - - + + + +