Skip to content

Commit 342fc0a

Browse files
chore: adds DataSystem contract test support (#110)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** SDK-1631 **Describe the solution you've provided** Ports most of the dotnet logic for the TestService.java. Also adds payload filter query param handling to the FDv2 builders/sources so those filter tests pass. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces FDv2 payload filtering and data system wiring, plus contract test harness tweaks. > > - Adds `payloadFilter` support end-to-end: new setters on `FDv2*Initializer/Synchronizer` builders, `DataSystemComponents` plumbs it through, and `DefaultFDv2Requestor` now appends a `filter=` query param (with tests); `SdkClientEntity` consumes `dataSystem.payloadFilter` and configures FDv2 polling/streaming initializers/synchronizers accordingly > - Expands contract test representations with `dataSystem` config types (initializers, synchronizers, store stubs, cache modes) and adds handling in `SdkClientEntity` (persistent store code left TODO) > - Makefile: configurable `TEST_SERVICE_PORT`, introduce suppression files, update harness runs (v2 with `-skip-from`, add v3.0.0-alpha.1 run), and add `contract-tests/test-suppressions-fdv2.txt` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c3e5110. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
1 parent 3042ad4 commit 342fc0a

11 files changed

Lines changed: 389 additions & 7 deletions

File tree

lib/sdk/server/Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ test:
99
./gradlew test
1010

1111
TEMP_TEST_OUTPUT=/tmp/sdk-test-service.log
12+
TEST_SERVICE_PORT ?= 8000
13+
SUPPRESSION_FILE=contract-tests/test-suppressions.txt
14+
SUPPRESSION_FILE_FDV2=contract-tests/test-suppressions-fdv2.txt
1215

1316
# Add any extra sdk-test-harness parameters here, such as -skip for tests that are
1417
# temporarily not working.
@@ -18,15 +21,19 @@ build-contract-tests:
1821
@cd contract-tests && ../gradlew installDist
1922

2023
start-contract-test-service:
21-
@contract-tests/service/build/install/service/bin/service
24+
@PORT=$(TEST_SERVICE_PORT) contract-tests/service/build/install/service/bin/service
2225

2326
start-contract-test-service-bg:
2427
@echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)"
2528
@make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 &
2629

2730
run-contract-tests:
31+
@echo "Running SDK contract test v2..."
2832
@curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \
29-
| VERSION=v2 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh
33+
| VERSION=v2 PARAMS="-url http://localhost:$(TEST_SERVICE_PORT) -debug -skip-from=$(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS)" sh
34+
@echo "Running SDK contract test v3.0.0-alpha.1..."
35+
@curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.0.0-alpha.1/downloader/run.sh \
36+
| VERSION=v3.0.0-alpha.1 PARAMS="-url http://localhost:$(TEST_SERVICE_PORT) -debug -stop-service-at-end -skip-from=$(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS)" sh
3037

3138
contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests
3239

lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static class SdkConfigParams {
3232
SdkConfigTagParams tags;
3333
SdkConfigServiceEndpointParams serviceEndpoints;
3434
SdkConfigHookParams hooks;
35+
SdkConfigDataSystemParams dataSystem;
3536
}
3637

3738
public static class SdkConfigStreamParams {
@@ -73,6 +74,113 @@ public static class SdkConfigHookParams {
7374
List<HookConfig> hooks;
7475
}
7576

77+
/**
78+
* Constants for store mode values.
79+
*/
80+
public static class StoreMode {
81+
/**
82+
* Read-only mode - the data system will only read from the persistent store.
83+
*/
84+
public static final int READ = 0;
85+
86+
/**
87+
* Read-write mode - the data system can read from, and write to, the persistent store.
88+
*/
89+
public static final int READ_WRITE = 1;
90+
}
91+
92+
/**
93+
* Constants for persistent store type values.
94+
*/
95+
public static class PersistentStoreType {
96+
/**
97+
* Redis persistent store type.
98+
*/
99+
public static final String REDIS = "redis";
100+
101+
/**
102+
* DynamoDB persistent store type.
103+
*/
104+
public static final String DYNAMODB = "dynamodb";
105+
106+
/**
107+
* Consul persistent store type.
108+
*/
109+
public static final String CONSUL = "consul";
110+
}
111+
112+
/**
113+
* Constants for persistent cache mode values.
114+
*/
115+
public static class PersistentCacheMode {
116+
/**
117+
* Cache disabled mode.
118+
*/
119+
public static final String OFF = "off";
120+
121+
/**
122+
* Time-to-live cache mode with a specified TTL.
123+
*/
124+
public static final String TTL = "ttl";
125+
126+
/**
127+
* Infinite cache mode - cache forever.
128+
*/
129+
public static final String INFINITE = "infinite";
130+
}
131+
132+
public static class SdkConfigDataSystemParams {
133+
SdkConfigDataStoreParams store;
134+
Integer storeMode;
135+
SdkConfigDataInitializerParams[] initializers;
136+
SdkConfigSynchronizersParams synchronizers;
137+
String payloadFilter;
138+
}
139+
140+
public static class SdkConfigDataStoreParams {
141+
SdkConfigPersistentDataStoreParams persistentDataStore;
142+
}
143+
144+
public static class SdkConfigPersistentDataStoreParams {
145+
SdkConfigPersistentStoreParams store;
146+
SdkConfigPersistentCacheParams cache;
147+
}
148+
149+
public static class SdkConfigPersistentStoreParams {
150+
String type;
151+
String prefix;
152+
String dsn;
153+
}
154+
155+
public static class SdkConfigPersistentCacheParams {
156+
String mode;
157+
Integer ttl;
158+
}
159+
160+
public static class SdkConfigDataInitializerParams {
161+
SdkConfigPollingParams polling;
162+
}
163+
164+
public static class SdkConfigSynchronizersParams {
165+
SdkConfigSynchronizerParams primary;
166+
SdkConfigSynchronizerParams secondary;
167+
}
168+
169+
public static class SdkConfigSynchronizerParams {
170+
SdkConfigStreamingParams streaming;
171+
SdkConfigPollingParams polling;
172+
}
173+
174+
public static class SdkConfigPollingParams {
175+
URI baseUri;
176+
Long pollIntervalMs;
177+
}
178+
179+
public static class SdkConfigStreamingParams {
180+
URI baseUri;
181+
Long initialRetryDelayMs;
182+
}
183+
76184
public static class HookConfig {
77185
String name;
78186
URI callbackUri;

lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@
2525
import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder;
2626
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
2727
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
28+
import com.launchdarkly.sdk.server.integrations.DataSystemBuilder;
29+
import com.launchdarkly.sdk.server.DataSystemComponents;
30+
import com.launchdarkly.sdk.server.integrations.FDv2PollingInitializerBuilder;
31+
import com.launchdarkly.sdk.server.integrations.FDv2PollingSynchronizerBuilder;
32+
import com.launchdarkly.sdk.server.integrations.FDv2StreamingSynchronizerBuilder;
2833
import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider;
34+
import com.launchdarkly.sdk.server.subsystems.DataSourceBuilder;
35+
import com.launchdarkly.sdk.server.datasources.Initializer;
36+
import com.launchdarkly.sdk.server.datasources.Synchronizer;
37+
import com.launchdarkly.sdk.server.subsystems.DataSystemConfiguration;
2938

3039
import org.jetbrains.annotations.NotNull;
3140
import org.slf4j.Logger;
@@ -55,6 +64,12 @@
5564
import sdktest.Representations.HookConfig;
5665
import sdktest.Representations.SdkConfigHookParams;
5766
import sdktest.Representations.SdkConfigParams;
67+
import sdktest.Representations.SdkConfigDataSystemParams;
68+
import sdktest.Representations.SdkConfigDataInitializerParams;
69+
import sdktest.Representations.SdkConfigSynchronizersParams;
70+
import sdktest.Representations.SdkConfigSynchronizerParams;
71+
import sdktest.Representations.SdkConfigPollingParams;
72+
import sdktest.Representations.SdkConfigStreamingParams;
5873
import sdktest.Representations.SecureModeHashParams;
5974
import sdktest.Representations.SecureModeHashResponse;
6075

@@ -465,6 +480,125 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) {
465480
builder.hooks(Components.hooks().setHooks(hookList));
466481
}
467482

483+
if (params.dataSystem != null) {
484+
DataSystemBuilder dataSystemBuilder = Components.dataSystem().custom();
485+
486+
// TODO: enable this code in the future and determine which dependencies on persistent stores need to be added to contract test build process
487+
// Configure persistent store if provided
488+
// if (params.dataSystem.store != null && params.dataSystem.store.persistentDataStore != null) {
489+
// var storeConfig = params.dataSystem.store.persistentDataStore;
490+
// var storeType = storeConfig.store.type.toLowerCase();
491+
// ComponentConfigurer<DataStore> persistentStore = null;
492+
//
493+
// switch (storeType) {
494+
// case "redis":
495+
// // Redis store configuration
496+
// break;
497+
// case "dynamodb":
498+
// // DynamoDB store configuration
499+
// break;
500+
// case "consul":
501+
// // Consul store configuration
502+
// break;
503+
// }
504+
//
505+
// if (persistentStore != null) {
506+
// // Configure cache
507+
// var cacheMode = storeConfig.cache != null ? storeConfig.cache.mode.toLowerCase() : null;
508+
// // ... cache configuration ...
509+
//
510+
// // Determine store mode
511+
// var storeMode = params.dataSystem.storeMode == 0
512+
// ? DataSystemConfiguration.DataStoreMode.READ_ONLY
513+
// : DataSystemConfiguration.DataStoreMode.READ_WRITE;
514+
//
515+
// dataSystemBuilder.persistentStore(persistentStore, storeMode);
516+
// }
517+
// }
518+
519+
// Configure initializers
520+
if (params.dataSystem.initializers != null && params.dataSystem.initializers.length > 0) {
521+
List<DataSourceBuilder<Initializer>> initializers = new ArrayList<>();
522+
for (SdkConfigDataInitializerParams initializer : params.dataSystem.initializers) {
523+
if (initializer.polling != null) {
524+
FDv2PollingInitializerBuilder pollingBuilder = DataSystemComponents.pollingInitializer();
525+
if (initializer.polling.baseUri != null) {
526+
ServiceEndpointsBuilder endpointOverride = Components.serviceEndpoints().polling(initializer.polling.baseUri);
527+
pollingBuilder.serviceEndpointsOverride(endpointOverride);
528+
}
529+
// Note: pollInterval is not available for initializers, only for synchronizers
530+
if (params.dataSystem.payloadFilter != null && !params.dataSystem.payloadFilter.isEmpty()) {
531+
pollingBuilder.payloadFilter(params.dataSystem.payloadFilter);
532+
}
533+
initializers.add(pollingBuilder);
534+
}
535+
}
536+
if (!initializers.isEmpty()) {
537+
dataSystemBuilder.initializers(initializers.toArray(new DataSourceBuilder[0]));
538+
}
539+
}
540+
541+
// Configure synchronizers
542+
if (params.dataSystem.synchronizers != null) {
543+
List<DataSourceBuilder<Synchronizer>> synchronizers = new ArrayList<>();
544+
545+
// Primary synchronizer
546+
if (params.dataSystem.synchronizers.primary != null) {
547+
DataSourceBuilder<Synchronizer> primary = createSynchronizer(params.dataSystem.synchronizers.primary, params.dataSystem.payloadFilter);
548+
if (primary != null) {
549+
synchronizers.add(primary);
550+
}
551+
}
552+
553+
// Secondary synchronizer (optional)
554+
if (params.dataSystem.synchronizers.secondary != null) {
555+
DataSourceBuilder<Synchronizer> secondary = createSynchronizer(params.dataSystem.synchronizers.secondary, params.dataSystem.payloadFilter);
556+
if (secondary != null) {
557+
synchronizers.add(secondary);
558+
}
559+
}
560+
561+
if (!synchronizers.isEmpty()) {
562+
dataSystemBuilder.synchronizers(synchronizers.toArray(new DataSourceBuilder[0]));
563+
}
564+
}
565+
566+
builder.dataSystem(dataSystemBuilder);
567+
}
568+
468569
return builder.build();
469570
}
571+
572+
private DataSourceBuilder<Synchronizer> createSynchronizer(
573+
SdkConfigSynchronizerParams synchronizer,
574+
String payloadFilter) {
575+
if (synchronizer.polling != null) {
576+
FDv2PollingSynchronizerBuilder pollingBuilder = DataSystemComponents.pollingSynchronizer();
577+
if (synchronizer.polling.baseUri != null) {
578+
ServiceEndpointsBuilder endpointOverride = Components.serviceEndpoints().polling(synchronizer.polling.baseUri);
579+
pollingBuilder.serviceEndpointsOverride(endpointOverride);
580+
}
581+
if (synchronizer.polling.pollIntervalMs != null) {
582+
pollingBuilder.pollInterval(Duration.ofMillis(synchronizer.polling.pollIntervalMs));
583+
}
584+
if (payloadFilter != null && !payloadFilter.isEmpty()) {
585+
pollingBuilder.payloadFilter(payloadFilter);
586+
}
587+
return pollingBuilder;
588+
} else if (synchronizer.streaming != null) {
589+
FDv2StreamingSynchronizerBuilder streamingBuilder = DataSystemComponents.streamingSynchronizer();
590+
if (synchronizer.streaming.baseUri != null) {
591+
ServiceEndpointsBuilder endpointOverride = Components.serviceEndpoints().streaming(synchronizer.streaming.baseUri);
592+
streamingBuilder.serviceEndpointsOverride(endpointOverride);
593+
}
594+
if (synchronizer.streaming.initialRetryDelayMs != null) {
595+
streamingBuilder.initialReconnectDelay(Duration.ofMillis(synchronizer.streaming.initialRetryDelayMs));
596+
}
597+
if (payloadFilter != null && !payloadFilter.isEmpty()) {
598+
streamingBuilder.payloadFilter(payloadFilter);
599+
}
600+
return streamingBuilder;
601+
}
602+
return null;
603+
}
470604
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
streaming/validation/unrecognized data that can be safely ignored/unknown event name with JSON body
2+
streaming/validation/unrecognized data that can be safely ignored/unknown event name with non-JSON body
3+
streaming/validation/unrecognized data that can be safely ignored/patch event with unrecognized path kind
4+
streaming/fdv2/fallback to FDv1 handling

lib/sdk/server/contract-tests/test-suppressions.txt

Whitespace-only changes.

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystemComponents.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public Initializer build(DataSourceBuildInputs context) {
3838
toHttpProperties(context.getHttp()),
3939
configuredBaseUri,
4040
StandardEndpoints.FDV2_POLLING_REQUEST_PATH,
41+
payloadFilter,
4142
context.getBaseLogger());
4243

4344
return new PollingInitializerImpl(
@@ -64,6 +65,7 @@ public Synchronizer build(DataSourceBuildInputs context) {
6465
toHttpProperties(context.getHttp()),
6566
configuredBaseUri,
6667
StandardEndpoints.FDV2_POLLING_REQUEST_PATH,
68+
payloadFilter,
6769
context.getBaseLogger());
6870

6971
return new PollingSynchronizerImpl(
@@ -94,7 +96,7 @@ public Synchronizer build(DataSourceBuildInputs context) {
9496
StandardEndpoints.FDV2_STREAMING_REQUEST_PATH,
9597
context.getBaseLogger(),
9698
context.getSelectorSource(),
97-
null,
99+
payloadFilter,
98100
initialReconnectDelay
99101
);
100102
}

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFDv2Requestor.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,29 @@
3232
*/
3333
public class DefaultFDv2Requestor implements FDv2Requestor, Closeable {
3434
private static final String BASIS_QUERY_PARAM = "basis";
35+
private static final String FILTER_QUERY_PARAM = "filter";
3536

3637
private final OkHttpClient httpClient;
3738
private final URI pollingUri;
3839
private final Headers headers;
3940
private final LDLogger logger;
4041
private final Map<URI, String> etags;
42+
private final String payloadFilter;
4143

4244
/**
4345
* Creates a DefaultFDv2Requestor.
4446
*
4547
* @param httpProperties HTTP configuration properties
4648
* @param baseUri base URI for the FDv2 polling endpoint
4749
* @param requestPath the request path to append to the base URI (e.g., "/sdk/poll")
50+
* @param payloadFilter optional payload filter to add as a query parameter
4851
* @param logger logger for diagnostic output
4952
*/
50-
public DefaultFDv2Requestor(HttpProperties httpProperties, URI baseUri, String requestPath, LDLogger logger) {
53+
public DefaultFDv2Requestor(HttpProperties httpProperties, URI baseUri, String requestPath, String payloadFilter, LDLogger logger) {
5154
this.logger = logger;
5255
this.pollingUri = HttpHelpers.concatenateUriPath(baseUri, requestPath);
5356
this.etags = new HashMap<>();
57+
this.payloadFilter = payloadFilter;
5458

5559
OkHttpClient.Builder httpBuilder = httpProperties.toHttpClientBuilder();
5660
this.headers = httpProperties.toHeadersBuilder().build();
@@ -69,6 +73,11 @@ public CompletableFuture<FDv2PayloadResponse> Poll(Selector selector) {
6973
requestUri = HttpHelpers.addQueryParam(requestUri, BASIS_QUERY_PARAM, selector.getState());
7074
}
7175

76+
// Add payload filter query parameter if present
77+
if (payloadFilter != null && !payloadFilter.isEmpty()) {
78+
requestUri = HttpHelpers.addQueryParam(requestUri, FILTER_QUERY_PARAM, payloadFilter);
79+
}
80+
7281
logger.debug("Making FDv2 polling request to: {}", requestUri);
7382

7483
// Build the HTTP request

0 commit comments

Comments
 (0)