From 5069a69f69f423ba6bd72f98b8d4cbe156fc0a36 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:08:48 -0700 Subject: [PATCH] feat: Add the flag-overrides capability to the contract test service The contract test service advertises the flag-overrides capability and accepts the overrides object in the SDK configuration: filePaths, duplicateKeysHandling, changeDetection, and pollIntervalMs. The block configures FileOverrides.source() on the data system builder. An overrides block without a dataSystem block is rejected, because the override source is an option of the FDv2 data system. The service reads its listening port from the PORT environment variable when set, which the Makefile already passes, so several test services can run side by side. The default stays 8000. --- .../main/java/sdktest/Representations.java | 15 +++++++ .../main/java/sdktest/SdkClientEntity.java | 45 +++++++++++++++++++ .../src/main/java/sdktest/TestService.java | 14 ++++-- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java index cd7593e7..a2e03b1c 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java @@ -34,6 +34,21 @@ public static class SdkConfigParams { SdkConfigServiceEndpointParams serviceEndpoints; SdkConfigHookParams hooks; SdkConfigDataSystemParams dataSystem; + SdkConfigOverridesParams overrides; + } + + /** + * Configuration for the SDK's file-based flag overrides (the "flag-overrides" capability). + */ + public static class SdkConfigOverridesParams { + /** Absolute paths of the override files, in precedence order. */ + String[] filePaths; + /** "fail" (the default) or "ignore". */ + String duplicateKeysHandling; + /** "polling" or "watching"; null means the SDK default. */ + String changeDetection; + /** The polling interval in milliseconds; the SDK may raise it to its minimum. */ + Long pollIntervalMs; } public static class SdkConfigStreamParams { diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java index f6bb9d96..53da61ac 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java @@ -32,6 +32,9 @@ import com.launchdarkly.sdk.server.integrations.FDv2PollingInitializerBuilder; import com.launchdarkly.sdk.server.integrations.FDv2PollingSynchronizerBuilder; import com.launchdarkly.sdk.server.integrations.FDv2StreamingSynchronizerBuilder; +import com.launchdarkly.sdk.server.integrations.FileData; +import com.launchdarkly.sdk.server.integrations.FileOverrideSourceBuilder; +import com.launchdarkly.sdk.server.integrations.FileOverrides; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; import com.launchdarkly.sdk.server.subsystems.DataSourceBuilder; import com.launchdarkly.sdk.server.datasources.Initializer; @@ -68,6 +71,7 @@ import sdktest.Representations.SdkConfigParams; import sdktest.Representations.SdkConfigDataSystemParams; import sdktest.Representations.SdkConfigDataInitializerParams; +import sdktest.Representations.SdkConfigOverridesParams; import sdktest.Representations.SdkConfigSynchronizerParams; import sdktest.Representations.SdkConfigPollingParams; import sdktest.Representations.SdkConfigStreamingParams; @@ -587,12 +591,53 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { dataSystemBuilder.fDv1FallbackSynchronizer(fdv1Polling); } + if (params.overrides != null) { + dataSystemBuilder.overrides(makeOverridesConfig(params.overrides)); + } + builder.dataSystem(dataSystemBuilder); + } else if (params.overrides != null) { + throw new IllegalArgumentException("flag overrides require the data system to be configured"); } return builder.build(); } + private static FileOverrideSourceBuilder makeOverridesConfig(SdkConfigOverridesParams params) { + FileOverrideSourceBuilder overrides = FileOverrides.source(); + if (params.filePaths != null) { + overrides.filePaths(params.filePaths); + } + if (params.duplicateKeysHandling != null) { + switch (params.duplicateKeysHandling) { + case "fail": + overrides.duplicateKeysHandling(FileData.DuplicateKeysHandling.FAIL); + break; + case "ignore": + overrides.duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE); + break; + default: + throw new IllegalArgumentException("unknown duplicate keys handling: " + params.duplicateKeysHandling); + } + } + if (params.changeDetection != null) { + switch (params.changeDetection) { + case "polling": + overrides.changeDetection(FileOverrides.ChangeDetection.POLLING); + break; + case "watching": + overrides.changeDetection(FileOverrides.ChangeDetection.WATCHING); + break; + default: + throw new IllegalArgumentException("unknown change detection mode: " + params.changeDetection); + } + } + if (params.pollIntervalMs != null) { + overrides.pollInterval(Duration.ofMillis(params.pollIntervalMs)); + } + return overrides; + } + private DataSourceBuilder createSynchronizer( SdkConfigSynchronizerParams synchronizer, String payloadFilter) { diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java index 45eeead7..51f9fb4e 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java @@ -21,7 +21,7 @@ import sdktest.Representations.Status; public class TestService { - private static final int PORT = 8000; + private static final int DEFAULT_PORT = 8000; private static final String[] CAPABILITIES = new String[]{ "all-flags-client-side-only", "all-flags-details-only-for-tracked-flags", @@ -47,7 +47,8 @@ public class TestService { "fdv1-fallback", "instance-id", "retry-conformance-fdv1-streaming", - "retry-conformance-fdv1-polling" + "retry-conformance-fdv1-polling", + "flag-overrides" }; static final Gson gson = new GsonBuilder().serializeNulls().create(); @@ -83,10 +84,15 @@ public static void main(String[] args) throws Exception { .addRegex("POST", Pattern.compile("/clients/(.*)"), ctx -> service.postClientCommand(diableKeepAlive(ctx))) .addRegex("DELETE", Pattern.compile("/clients/(.*)"), ctx -> service.deleteClient(diableKeepAlive(ctx))); - HttpServer server = HttpServer.start(PORT, router); + int port = DEFAULT_PORT; + String portEnv = System.getenv("PORT"); + if (portEnv != null && !portEnv.isEmpty()) { + port = Integer.parseInt(portEnv); + } + HttpServer server = HttpServer.start(port, router); server.getRecorder().setEnabled(false); // don't accumulate a request log - System.out.println("Listening on port " + PORT); + System.out.println("Listening on port " + port); // need to explicitly sleep because HttpServer now starts as a daemon thread while (true) {