From 5eeeb095e06b7d3b4c006107442feabef22f97e6 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Tue, 13 Jan 2026 13:58:12 -0500 Subject: [PATCH] chore: adds data system configuration APIs --- .../sdk/server/StandardEndpoints.java | 24 +- .../integrations/DataSystemBuilder.java | 134 +++++++++ .../integrations/DataSystemComponents.java | 46 +++ .../server/integrations/DataSystemModes.java | 158 ++++++++++ .../FDv2PollingDataSourceBuilder.java | 129 +++++++++ .../FDv2StreamingDataSourceBuilder.java | 120 ++++++++ .../subsystems/DataSystemConfiguration.java | 119 ++++++++ .../sdk/server/ConfigurationTest.java | 273 ++++++++++++++++++ 8 files changed, 995 insertions(+), 8 deletions(-) create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemBuilder.java create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemComponents.java create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemModes.java create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingDataSourceBuilder.java create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingDataSourceBuilder.java create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSystemConfiguration.java create mode 100644 lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/ConfigurationTest.java diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java index 867d0e86..e29a7e7a 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java @@ -4,15 +4,23 @@ import java.net.URI; -abstract class StandardEndpoints { +/** + * Internal utility class for resolving service endpoint URIs. + *

+ * This class is for internal LaunchDarkly SDK use only. It is public only because it needs to be + * accessible to SDK code in different packages. It is not part of the public supported API and + * should not be referenced by application code. This class is subject to change without notice. + *

+ */ +public abstract class StandardEndpoints { private StandardEndpoints() {} - static final URI DEFAULT_STREAMING_BASE_URI = URI.create("https://stream.launchdarkly.com"); - static final URI DEFAULT_POLLING_BASE_URI = URI.create("https://app.launchdarkly.com"); - static final URI DEFAULT_EVENTS_BASE_URI = URI.create("https://events.launchdarkly.com"); + public static final URI DEFAULT_STREAMING_BASE_URI = URI.create("https://stream.launchdarkly.com"); + public static final URI DEFAULT_POLLING_BASE_URI = URI.create("https://app.launchdarkly.com"); + public static final URI DEFAULT_EVENTS_BASE_URI = URI.create("https://events.launchdarkly.com"); - static final String STREAMING_REQUEST_PATH = "/all"; - static final String POLLING_REQUEST_PATH = "/sdk/latest-all"; + public static final String STREAMING_REQUEST_PATH = "/all"; + public static final String POLLING_REQUEST_PATH = "/sdk/latest-all"; /** * Internal method to decide which URI a given component should connect to. @@ -26,7 +34,7 @@ private StandardEndpoints() {} * @param logger the logger to which we should print the warning, if needed * @return the base URI we should connect to */ - static URI selectBaseUri(URI serviceEndpointsValue, URI defaultValue, String description, LDLogger logger) { + public static URI selectBaseUri(URI serviceEndpointsValue, URI defaultValue, String description, LDLogger logger) { if (serviceEndpointsValue != null) { return serviceEndpointsValue; } @@ -46,7 +54,7 @@ static URI selectBaseUri(URI serviceEndpointsValue, URI defaultValue, String des * @param defaultValue the constant default URI value defined in StandardEndpoints * @return true iff the base URI was customized */ - static boolean isCustomBaseUri(URI serviceEndpointsValue, URI defaultValue) { + public static boolean isCustomBaseUri(URI serviceEndpointsValue, URI defaultValue) { return serviceEndpointsValue != null && !serviceEndpointsValue.equals(defaultValue); } } \ No newline at end of file diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemBuilder.java new file mode 100644 index 00000000..cf063777 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemBuilder.java @@ -0,0 +1,134 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.google.common.collect.ImmutableList; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataSystemConfiguration; + +import java.util.ArrayList; +import java.util.List; + +/** + * Configuration builder for the SDK's data acquisition and storage strategy. + *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode + *

+ */ +public final class DataSystemBuilder { + + private final List> initializers = new ArrayList<>(); + private final List> synchronizers = new ArrayList<>(); + private ComponentConfigurer fDv1FallbackSynchronizer; + private ComponentConfigurer persistentStore; + private DataSystemConfiguration.DataStoreMode persistentDataStoreMode; + + /** + * Add one or more initializers to the builder. + * To replace initializers, please refer to {@link #replaceInitializers(ComponentConfigurer[])}. + * + * @param initializers the initializers to add + * @return a reference to the builder + */ + public DataSystemBuilder initializers(ComponentConfigurer... initializers) { + for (ComponentConfigurer initializer : initializers) { + this.initializers.add(initializer); + } + return this; + } + + /** + * Replaces any existing initializers with the given initializers. + * To add initializers, please refer to {@link #initializers(ComponentConfigurer[])}. + * + * @param initializers the initializers to replace the current initializers with + * @return a reference to this builder + */ + public DataSystemBuilder replaceInitializers(ComponentConfigurer... initializers) { + this.initializers.clear(); + for (ComponentConfigurer initializer : initializers) { + this.initializers.add(initializer); + } + return this; + } + + /** + * Add one or more synchronizers to the builder. + * To replace synchronizers, please refer to {@link #replaceSynchronizers(ComponentConfigurer[])}. + * + * @param synchronizers the synchronizers to add + * @return a reference to the builder + */ + public DataSystemBuilder synchronizers(ComponentConfigurer... synchronizers) { + for (ComponentConfigurer synchronizer : synchronizers) { + this.synchronizers.add(synchronizer); + } + return this; + } + + /** + * Replaces any existing synchronizers with the given synchronizers. + * To add synchronizers, please refer to {@link #synchronizers(ComponentConfigurer[])}. + * + * @param synchronizers the synchronizers to replace the current synchronizers with + * @return a reference to this builder + */ + public DataSystemBuilder replaceSynchronizers(ComponentConfigurer... synchronizers) { + this.synchronizers.clear(); + for (ComponentConfigurer synchronizer : synchronizers) { + this.synchronizers.add(synchronizer); + } + return this; + } + + /** + * Configure the FDv1 fallback synchronizer. + *

+ * LaunchDarkly can instruct the SDK to fall back to this synchronizer. + *

+ * + * @param fDv1FallbackSynchronizer the FDv1 fallback synchronizer + * @return a reference to the builder + */ + public DataSystemBuilder fDv1FallbackSynchronizer(ComponentConfigurer fDv1FallbackSynchronizer) { + this.fDv1FallbackSynchronizer = fDv1FallbackSynchronizer; + return this; + } + + /** + * Configures the persistent data store. + *

+ * The SDK will use the persistent data store to store feature flag data. + *

+ * + * @param persistentStore the persistent data store + * @param mode the mode for the persistent data store + * @return a reference to the builder + * @see DataSystemConfiguration.DataStoreMode + */ + public DataSystemBuilder persistentStore(ComponentConfigurer persistentStore, DataSystemConfiguration.DataStoreMode mode) { + this.persistentStore = persistentStore; + this.persistentDataStoreMode = mode; + return this; + } + + /** + * Build the data system configuration. + *

+ * This method is internal and should not be called by application code. + * This function should remain internal. + *

+ * + * @return the data system configuration + */ + public DataSystemConfiguration build() { + return new DataSystemConfiguration( + ImmutableList.copyOf(initializers), + ImmutableList.copyOf(synchronizers), + fDv1FallbackSynchronizer, + persistentStore, + persistentDataStoreMode); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemComponents.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemComponents.java new file mode 100644 index 00000000..43d58548 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemComponents.java @@ -0,0 +1,46 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.server.Components; + +/** + * Components for use with the data system. + *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode + *

+ */ +public final class DataSystemComponents { + + private DataSystemComponents() {} + + /** + * Get a builder for a polling data source. + * + * @return the polling data source builder + */ + public static FDv2PollingDataSourceBuilder polling() { + return new FDv2PollingDataSourceBuilder(); + } + + /** + * Get a builder for a streaming data source. + * + * @return the streaming data source builder + */ + public static FDv2StreamingDataSourceBuilder streaming() { + return new FDv2StreamingDataSourceBuilder(); + } + + /** + * Get a builder for a FDv1 compatible polling data source. + *

+ * This is intended for use as a fallback. + *

+ * + * @return the FDv1 compatible polling data source builder + */ + public static PollingDataSourceBuilder fDv1Polling() { + return Components.pollingDataSource(); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemModes.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemModes.java new file mode 100644 index 00000000..82544157 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/DataSystemModes.java @@ -0,0 +1,158 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataSystemConfiguration; + +/** + * A set of different data system modes which provide pre-configured {@link DataSystemBuilder}s. + *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode + *

+ *

+ * This implementation is non-static to allow for easy usage with "Components". + * Where we can return an instance of this object, and the user can chain into their desired configuration. + *

+ */ +public final class DataSystemModes { + // This implementation is non-static to allow for easy usage with "Components". + // Where we can return an instance of this object, and the user can chain into their desired configuration. + + /** + * Configure's LaunchDarkly's recommended flag data acquisition strategy. + *

+ * Currently, it operates a two-phase method for getting data: first, it requests data from LaunchDarkly's + * global CDN. Then, it initiates a streaming connection to LaunchDarkly's Flag Delivery services to receive + * real-time updates. If the streaming connection is interrupted for an extended period of time, the SDK will + * automatically fall back to polling the global CDN for updates. + *

+ *

+ * Example: + *

+ *

+   *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+   *       .dataSystem(Components.dataSystem().defaultMode());
+   * 
+ * + * @return a builder containing our default configuration + */ + public DataSystemBuilder defaultMode() { + return custom() + .initializers(DataSystemComponents.polling()) + .synchronizers(DataSystemComponents.streaming(), DataSystemComponents.polling()) + .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()); + } + + /** + * Configures the SDK to stream data without polling for the initial payload. + *

+ * This is not our recommended strategy, which is {@link #defaultMode()}, but it may be + * suitable for some situations. + *

+ *

+ * This configuration will not automatically fall back to polling, but it can be instructed by LaunchDarkly + * to fall back to polling in certain situations. + *

+ *

+ * Example: + *

+ *

+   *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+   *       .dataSystem(Components.dataSystem().streaming());
+   * 
+ * + * @return a builder containing a primarily streaming configuration + */ + public DataSystemBuilder streaming() { + return custom() + .synchronizers(DataSystemComponents.streaming()) + .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()); + } + + /** + * Configure the SDK to poll data instead of receiving real-time updates via a stream. + *

+ * This is not our recommended strategy, which is {@link #defaultMode()}, but it may be + * required for certain network configurations. + *

+ *

+ * Example: + *

+ *

+   *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+   *       .dataSystem(Components.dataSystem().polling());
+   * 
+ * + * @return a builder containing a polling-only configuration + */ + public DataSystemBuilder polling() { + return custom() + .synchronizers(DataSystemComponents.polling()) + .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()); + } + + /** + * Configures the SDK to read from a persistent store integration that is populated by Relay Proxy + * or other SDKs. The SDK will not connect to LaunchDarkly. In this mode, the SDK never writes to the data + * store. + *

+ * Example: + *

+ *

+   *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+   *       .dataSystem(Components.dataSystem().daemon(persistentStore));
+   * 
+ * + * @param persistentStore the persistent store configurer + * @return a builder which is configured for daemon mode + */ + public DataSystemBuilder daemon(ComponentConfigurer persistentStore) { + return custom() + .persistentStore(persistentStore, DataSystemConfiguration.DataStoreMode.READ_ONLY); + } + + /** + * PersistentStore is similar to Default, with the addition of a persistent store integration. Before data has + * arrived from LaunchDarkly, the SDK is able to evaluate flags using data from the persistent store. + * Once fresh data is available, the SDK will no longer read from the persistent store, although it will keep + * it up to date. + *

+ * Example: + *

+ *

+   *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+   *       .dataSystem(Components.dataSystem()
+   *         .persistentStore(Components.persistentDataStore(SomeDatabaseName.dataStore())));
+   * 
+ * + * @param persistentStore the persistent store configurer + * @return a builder which is configured for persistent store mode + */ + public DataSystemBuilder persistentStore(ComponentConfigurer persistentStore) { + return defaultMode() + .persistentStore(persistentStore, DataSystemConfiguration.DataStoreMode.READ_WRITE); + } + + /** + * Custom returns a builder suitable for creating a custom data acquisition strategy. You may configure + * how the SDK uses a Persistent Store, how the SDK obtains an initial set of data, and how the SDK keeps data + * up to date. + *

+ * Example: + *

+ *

+   *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+   *       .dataSystem(Components.dataSystem().custom()
+   *         .initializers(DataSystemComponents.polling())
+   *         .synchronizers(DataSystemComponents.streaming(), DataSystemComponents.polling())
+   *         .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()));
+   * 
+ * + * @return a builder without any base configuration + */ + public DataSystemBuilder custom() { + return new DataSystemBuilder(); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingDataSourceBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingDataSourceBuilder.java new file mode 100644 index 00000000..81a40aae --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingDataSourceBuilder.java @@ -0,0 +1,129 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.internal.events.DiagnosticConfigProperty; +import com.launchdarkly.sdk.server.StandardEndpoints; +import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DiagnosticDescription; + +import java.net.URI; +import java.time.Duration; + +/** + * Contains methods for configuring the polling data source. + *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode + *

+ *

+ * Example: + *

+ *

+ *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+ *         .dataSystem(Components.dataSystem().custom()
+ *             // DataSystemComponents.polling() returns an instance of this builder.
+ *             .initializers(DataSystemComponents.polling()
+ *                 .pollInterval(Duration.ofMinutes(10)))
+ *             .synchronizers(DataSystemComponents.streaming(), DataSystemComponents.polling())
+ *             .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()));
+ * 
+ */ +public final class FDv2PollingDataSourceBuilder implements ComponentConfigurer, DiagnosticDescription { + /** + * The default value for {@link #pollInterval(Duration)}: 30 seconds. + */ + public static final Duration DEFAULT_POLL_INTERVAL = Duration.ofSeconds(30); + + Duration pollInterval = DEFAULT_POLL_INTERVAL; + + private ServiceEndpoints serviceEndpointsOverride; + + /** + * Sets the interval at which the SDK will poll for feature flag updates. + *

+ * The default and minimum value is {@link #DEFAULT_POLL_INTERVAL}. Values less than this will + * be set to the default. + *

+ * + * @param pollInterval the polling interval + * @return the builder + */ + public FDv2PollingDataSourceBuilder pollInterval(Duration pollInterval) { + this.pollInterval = pollInterval != null && pollInterval.compareTo(DEFAULT_POLL_INTERVAL) >= 0 + ? pollInterval + : DEFAULT_POLL_INTERVAL; + return this; + } + + /** + * Exposed internally for testing. + * + * @param pollInterval the polling interval + * @return the builder + */ + FDv2PollingDataSourceBuilder pollIntervalNoMinimum(Duration pollInterval) { + this.pollInterval = pollInterval; + return this; + } + + /** + * Sets overrides for the service endpoints. In typical usage, the data source will use the commonly defined + * service endpoints, but for cases where they need to be controlled at the source level, this method can + * be used. This data source will only use the endpoints applicable to it. + * + * @param serviceEndpointsOverride the service endpoints to override the base endpoints + * @return the builder + */ + public FDv2PollingDataSourceBuilder serviceEndpointsOverride(ServiceEndpointsBuilder serviceEndpointsOverride) { + this.serviceEndpointsOverride = serviceEndpointsOverride.createServiceEndpoints(); + return this; + } + + @Override + public DataSource build(ClientContext context) { + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : context.getServiceEndpoints(); + URI configuredBaseUri = StandardEndpoints.selectBaseUri( + endpoints.getPollingBaseUri(), + StandardEndpoints.DEFAULT_POLLING_BASE_URI, + "Polling", + context.getBaseLogger()); + + // TODO: Implement FDv2PollingRequestor + // var requestor = new FDv2PollingRequestor(context, configuredBaseUri); + + // TODO: Implement FDv2PollingDataSource + // return new FDv2PollingDataSource( + // context, + // context.getDataSourceUpdateSink(), + // requestor, + // pollInterval, + // () -> context.getSelectorSource() != null ? context.getSelectorSource().getSelector() : Selector.empty() + // ); + + // Placeholder - this will not compile until FDv2PollingDataSource is implemented + throw new UnsupportedOperationException("FDv2PollingDataSource is not yet implemented"); + } + + @Override + public LDValue describeConfiguration(ClientContext context) { + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : context.getServiceEndpoints(); + + boolean customPollingBaseUri = StandardEndpoints.isCustomBaseUri( + endpoints.getPollingBaseUri(), StandardEndpoints.DEFAULT_POLLING_BASE_URI); + + return LDValue.buildObject() + .put(DiagnosticConfigProperty.STREAMING_DISABLED.name, true) + .put(DiagnosticConfigProperty.CUSTOM_BASE_URI.name, customPollingBaseUri) + .put(DiagnosticConfigProperty.POLLING_INTERVAL_MILLIS.name, pollInterval.toMillis()) + .put(DiagnosticConfigProperty.USING_RELAY_DAEMON.name, false) + .build(); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingDataSourceBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingDataSourceBuilder.java new file mode 100644 index 00000000..8e08531b --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingDataSourceBuilder.java @@ -0,0 +1,120 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.internal.events.DiagnosticConfigProperty; +import com.launchdarkly.sdk.server.StandardEndpoints; +import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DiagnosticDescription; + +import java.net.URI; +import java.time.Duration; + +/** + * Contains methods for configuring the streaming data source. + *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode + *

+ *

+ * Example: + *

+ *

+ *     LDConfig config = new LDConfig.Builder("my-sdk-key")
+ *         .dataSystem(Components.dataSystem().custom()
+ *             .initializers(DataSystemComponents.polling())
+ *             // DataSystemComponents.streaming() returns an instance of this builder.
+ *             .synchronizers(DataSystemComponents.streaming()
+ *                 .initialReconnectDelay(Duration.ofSeconds(5)), DataSystemComponents.polling())
+ *             .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()));
+ * 
+ */ +public final class FDv2StreamingDataSourceBuilder implements ComponentConfigurer, DiagnosticDescription { + /** + * The default value for {@link #initialReconnectDelay(Duration)}: 1000 milliseconds. + */ + public static final Duration DEFAULT_INITIAL_RECONNECT_DELAY = Duration.ofSeconds(1); + + private Duration initialReconnectDelay = DEFAULT_INITIAL_RECONNECT_DELAY; + + private ServiceEndpoints serviceEndpointsOverride; + + /** + * Sets the initial reconnect delay for the streaming connection. + *

+ * The streaming service uses a backoff algorithm (with jitter) every time the connection needs + * to be reestablished. The delay for the first reconnection will start near this value, and then + * increase exponentially for any subsequent connection failures. + *

+ *

+ * The default value is {@link #DEFAULT_INITIAL_RECONNECT_DELAY}. + *

+ * + * @param initialReconnectDelay the reconnect time base value + * @return the builder + */ + public FDv2StreamingDataSourceBuilder initialReconnectDelay(Duration initialReconnectDelay) { + this.initialReconnectDelay = initialReconnectDelay != null ? initialReconnectDelay : DEFAULT_INITIAL_RECONNECT_DELAY; + return this; + } + + /** + * Sets overrides for the service endpoints. In typical usage, the data source will use the commonly defined + * service endpoints, but for cases where they need to be controlled at the source level, this method can + * be used. This data source will only use the endpoints applicable to it. + * + * @param serviceEndpointsOverride the service endpoints to override the base endpoints + * @return the builder + */ + public FDv2StreamingDataSourceBuilder serviceEndpointsOverride(ServiceEndpointsBuilder serviceEndpointsOverride) { + this.serviceEndpointsOverride = serviceEndpointsOverride.createServiceEndpoints(); + return this; + } + + @Override + public DataSource build(ClientContext context) { + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : context.getServiceEndpoints(); + URI configuredBaseUri = StandardEndpoints.selectBaseUri( + endpoints.getStreamingBaseUri(), + StandardEndpoints.DEFAULT_STREAMING_BASE_URI, + "Streaming", + context.getBaseLogger()); + + // TODO: Implement FDv2StreamingDataSource + // return new FDv2StreamingDataSource( + // context, + // context.getDataSourceUpdateSink(), + // configuredBaseUri, + // initialReconnectDelay, + // () -> context.getSelectorSource() != null ? context.getSelectorSource().getSelector() : Selector.empty() + // ); + + // Placeholder - this will not compile until FDv2StreamingDataSource is implemented + throw new UnsupportedOperationException("FDv2StreamingDataSource is not yet implemented"); + } + + @Override + public LDValue describeConfiguration(ClientContext context) { + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : context.getServiceEndpoints(); + + boolean customStreamingBaseUri = StandardEndpoints.isCustomBaseUri( + endpoints.getStreamingBaseUri(), StandardEndpoints.DEFAULT_STREAMING_BASE_URI); + boolean customPollingBaseUri = StandardEndpoints.isCustomBaseUri( + endpoints.getPollingBaseUri(), StandardEndpoints.DEFAULT_POLLING_BASE_URI); + + return LDValue.buildObject() + .put(DiagnosticConfigProperty.STREAMING_DISABLED.name, false) + .put(DiagnosticConfigProperty.CUSTOM_BASE_URI.name, customPollingBaseUri) + .put(DiagnosticConfigProperty.CUSTOM_STREAM_URI.name, customStreamingBaseUri) + .put(DiagnosticConfigProperty.RECONNECT_TIME_MILLIS.name, initialReconnectDelay.toMillis()) + .put(DiagnosticConfigProperty.USING_RELAY_DAEMON.name, false) + .build(); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSystemConfiguration.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSystemConfiguration.java new file mode 100644 index 00000000..489cae83 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSystemConfiguration.java @@ -0,0 +1,119 @@ +package com.launchdarkly.sdk.server.subsystems; + +import com.google.common.collect.ImmutableList; + +/** + * Configuration for the SDK's data acquisition and storage strategy. + *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode + *

+ *

+ * Applications should use {@link com.launchdarkly.sdk.server.integrations.DataSystemBuilder} or + * {@link com.launchdarkly.sdk.server.integrations.DataSystemModes} to create instances of this class, + * rather than calling the constructor directly. + *

+ */ +public final class DataSystemConfiguration { + + /** + * The persistent data store mode. + *

+ * This enum can be extended without a major version. Code should provide this value in configuration, + * but it should not use the enum itself, for example, in a switch-case. + *

+ */ + public enum DataStoreMode { + /** + * The data system will only read from the persistent store. + */ + READ_ONLY, + + /** + * The data system can read from, and write to, the persistent store. + */ + READ_WRITE + } + + private final ImmutableList> initializers; + private final ImmutableList> synchronizers; + private final ComponentConfigurer fDv1FallbackSynchronizer; + private final ComponentConfigurer persistentStore; + private final DataStoreMode persistentDataStoreMode; + + /** + * Creates an instance. + *

+ * This constructor is internal and should not be called by application code. + *

+ * + * @param initializers see {@link #getInitializers()} + * @param synchronizers see {@link #getSynchronizers()} + * @param fDv1FallbackSynchronizer see {@link #getFDv1FallbackSynchronizer()} + * @param persistentStore see {@link #getPersistentStore()} + * @param persistentDataStoreMode see {@link #getPersistentDataStoreMode()} + */ + public DataSystemConfiguration( + ImmutableList> initializers, + ImmutableList> synchronizers, + ComponentConfigurer fDv1FallbackSynchronizer, + ComponentConfigurer persistentStore, + DataStoreMode persistentDataStoreMode) { + this.initializers = initializers; + this.synchronizers = synchronizers; + this.fDv1FallbackSynchronizer = fDv1FallbackSynchronizer; + this.persistentStore = persistentStore; + this.persistentDataStoreMode = persistentDataStoreMode; + } + + /** + * A list of factories for creating data sources for initialization. + * + * @return the list of initializer configurers + */ + public ImmutableList> getInitializers() { + return initializers; + } + + /** + * A list of factories for creating data sources for synchronization. + * + * @return the list of synchronizer configurers + */ + public ImmutableList> getSynchronizers() { + return synchronizers; + } + + /** + * A synchronizer to fall back to when FDv1 fallback has been requested. + * + * @return the FDv1 fallback synchronizer configurer, or null + */ + public ComponentConfigurer getFDv1FallbackSynchronizer() { + return fDv1FallbackSynchronizer; + } + + /** + * An optional factory for creating a persistent data store. This is optional, and if no persistent store is configured, it will be + * null. + *

+ * The persistent store itself will implement {@link PersistentDataStore}, but we expect that to be wrapped by a factory which can + * operates at the {@link DataStore} level. + *

+ * + * @return the persistent store configurer, or null + */ + public ComponentConfigurer getPersistentStore() { + return persistentStore; + } + + /** + * The mode of operation for the persistent data store. + * + * @return the persistent data store mode + */ + public DataStoreMode getPersistentDataStoreMode() { + return persistentDataStoreMode; + } +} + diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/ConfigurationTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/ConfigurationTest.java new file mode 100644 index 00000000..13aa825c --- /dev/null +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/ConfigurationTest.java @@ -0,0 +1,273 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.server.integrations.DataSystemBuilder; +import com.launchdarkly.sdk.server.integrations.DataSystemComponents; +import com.launchdarkly.sdk.server.integrations.DataSystemModes; +import com.launchdarkly.sdk.server.integrations.FDv2PollingDataSourceBuilder; +import com.launchdarkly.sdk.server.integrations.FDv2StreamingDataSourceBuilder; +import com.launchdarkly.sdk.server.integrations.MockPersistentDataStore; +import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder; +import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder; +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSystemConfiguration; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreUpdateSink; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings("javadoc") +public class ConfigurationTest { + private static final String SDK_KEY = "any-key"; + + private static ClientContext clientContextWithDataStoreUpdateSink() { + ClientContextImpl baseContext = TestComponents.clientContext("", LDConfig.DEFAULT); + // Create a mock DataStoreUpdateSink for testing + DataStoreUpdateSink mockSink = new DataStoreUpdateSink() { + @Override + public void updateStatus(DataStoreStatusProvider.Status newStatus) { + // No-op for testing + } + }; + return baseContext.withDataStoreUpdateSink(mockSink); + } + + @Test + public void defaultSetsKey() { + LDConfig config = LDConfig.DEFAULT; + // Note: LDConfig doesn't expose SDK key directly, but this test verifies default config exists + assertNotNull(config); + } + + @Test + public void builderSetsKey() { + LDConfig config = new LDConfig.Builder().build(); + assertNotNull(config); + } + + // Note: The following tests for DataSystem configuration are adapted to test DataSystemBuilder + // and DataSystemModes directly, since LDConfig.Builder doesn't have a dataSystem() method yet. + // Once that method is added, these tests can be updated to use it. + + @Test + public void canConfigureDefaultDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.defaultMode(); + DataSystemConfiguration dataSystemConfig = builder.build(); + + assertEquals(1, dataSystemConfig.getInitializers().size()); + assertTrue(dataSystemConfig.getInitializers().get(0) instanceof FDv2PollingDataSourceBuilder); + assertEquals(2, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2StreamingDataSourceBuilder); + assertTrue(dataSystemConfig.getSynchronizers().get(1) instanceof FDv2PollingDataSourceBuilder); + assertTrue(dataSystemConfig.getFDv1FallbackSynchronizer() instanceof PollingDataSourceBuilder); + assertNull(dataSystemConfig.getPersistentStore()); + } + + @Test + public void canConfigureStreamingDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.streaming(); + DataSystemConfiguration dataSystemConfig = builder.build(); + + assertTrue(dataSystemConfig.getInitializers().isEmpty()); + assertEquals(1, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2StreamingDataSourceBuilder); + assertTrue(dataSystemConfig.getFDv1FallbackSynchronizer() instanceof PollingDataSourceBuilder); + assertNull(dataSystemConfig.getPersistentStore()); + } + + @Test + public void canConfigurePollingDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.polling(); + DataSystemConfiguration dataSystemConfig = builder.build(); + + assertTrue(dataSystemConfig.getInitializers().isEmpty()); + assertEquals(1, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2PollingDataSourceBuilder); + assertTrue(dataSystemConfig.getFDv1FallbackSynchronizer() instanceof PollingDataSourceBuilder); + assertNull(dataSystemConfig.getPersistentStore()); + } + + @Test + public void canConfigureDaemonDataSystem() { + MockPersistentDataStore mockStore = new MockPersistentDataStore(); + ComponentConfigurer storeConfigurer = TestComponents.specificComponent(mockStore); + PersistentDataStoreBuilder persistentStoreBuilder = Components.persistentDataStore(storeConfigurer); + ComponentConfigurer dataStoreConfigurer = TestComponents.specificComponent( + Components.persistentDataStore(storeConfigurer).build(clientContextWithDataStoreUpdateSink())); + + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.daemon(dataStoreConfigurer); + DataSystemConfiguration dataSystemConfig = builder.build(); + + assertTrue(dataSystemConfig.getInitializers().isEmpty()); + assertTrue(dataSystemConfig.getSynchronizers().isEmpty()); + assertNull(dataSystemConfig.getFDv1FallbackSynchronizer()); + assertNotNull(dataSystemConfig.getPersistentStore()); + assertEquals(DataSystemConfiguration.DataStoreMode.READ_ONLY, dataSystemConfig.getPersistentDataStoreMode()); + } + + @Test + public void canConfigurePersistentStoreDataSystem() { + MockPersistentDataStore mockStore = new MockPersistentDataStore(); + ComponentConfigurer storeConfigurer = TestComponents.specificComponent(mockStore); + ComponentConfigurer dataStoreConfigurer = TestComponents.specificComponent( + Components.persistentDataStore(storeConfigurer).build(clientContextWithDataStoreUpdateSink())); + + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.persistentStore(dataStoreConfigurer); + DataSystemConfiguration dataSystemConfig = builder.build(); + + assertEquals(1, dataSystemConfig.getInitializers().size()); + assertTrue(dataSystemConfig.getInitializers().get(0) instanceof FDv2PollingDataSourceBuilder); + assertEquals(2, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2StreamingDataSourceBuilder); + assertTrue(dataSystemConfig.getSynchronizers().get(1) instanceof FDv2PollingDataSourceBuilder); + assertTrue(dataSystemConfig.getFDv1FallbackSynchronizer() instanceof PollingDataSourceBuilder); + assertNotNull(dataSystemConfig.getPersistentStore()); + assertEquals(DataSystemConfiguration.DataStoreMode.READ_WRITE, dataSystemConfig.getPersistentDataStoreMode()); + } + + @Test + public void canConfigureCustomDataSystemWithAllOptions() { + MockPersistentDataStore mockStore = new MockPersistentDataStore(); + ComponentConfigurer storeConfigurer = TestComponents.specificComponent(mockStore); + ComponentConfigurer dataStoreConfigurer = TestComponents.specificComponent( + Components.persistentDataStore(storeConfigurer).build(clientContextWithDataStoreUpdateSink())); + + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .initializers(DataSystemComponents.polling()) + .synchronizers(DataSystemComponents.streaming(), DataSystemComponents.polling()) + .fDv1FallbackSynchronizer(DataSystemComponents.fDv1Polling()) + .persistentStore(dataStoreConfigurer, DataSystemConfiguration.DataStoreMode.READ_WRITE); + + DataSystemConfiguration dataSystemConfig = builder.build(); + + // Verify initializers + assertEquals(1, dataSystemConfig.getInitializers().size()); + assertTrue(dataSystemConfig.getInitializers().get(0) instanceof FDv2PollingDataSourceBuilder); + + // Verify synchronizers + assertEquals(2, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2StreamingDataSourceBuilder); + assertTrue(dataSystemConfig.getSynchronizers().get(1) instanceof FDv2PollingDataSourceBuilder); + + // Verify FDv1 fallback + assertTrue(dataSystemConfig.getFDv1FallbackSynchronizer() instanceof PollingDataSourceBuilder); + + // Verify persistent store and mode + assertNotNull(dataSystemConfig.getPersistentStore()); + assertEquals(DataSystemConfiguration.DataStoreMode.READ_WRITE, dataSystemConfig.getPersistentDataStoreMode()); + } + + @Test + public void canReplaceInitializersInCustomDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .initializers(DataSystemComponents.polling()) + .replaceInitializers(DataSystemComponents.streaming()); + + DataSystemConfiguration dataSystemConfig = builder.build(); + assertEquals(1, dataSystemConfig.getInitializers().size()); + assertTrue(dataSystemConfig.getInitializers().get(0) instanceof FDv2StreamingDataSourceBuilder); + } + + @Test + public void canReplaceSynchronizersInCustomDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .synchronizers(DataSystemComponents.polling()) + .replaceSynchronizers(DataSystemComponents.streaming()); + + DataSystemConfiguration dataSystemConfig = builder.build(); + assertEquals(1, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2StreamingDataSourceBuilder); + } + + @Test + public void canAddMultipleInitializersToCustomDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .initializers(DataSystemComponents.polling()) + .initializers(DataSystemComponents.streaming()); + + DataSystemConfiguration dataSystemConfig = builder.build(); + assertEquals(2, dataSystemConfig.getInitializers().size()); + assertTrue(dataSystemConfig.getInitializers().get(0) instanceof FDv2PollingDataSourceBuilder); + assertTrue(dataSystemConfig.getInitializers().get(1) instanceof FDv2StreamingDataSourceBuilder); + } + + @Test + public void canAddMultipleSynchronizersToCustomDataSystem() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .synchronizers(DataSystemComponents.polling()) + .synchronizers(DataSystemComponents.streaming()) + .synchronizers(DataSystemComponents.fDv1Polling()); + + DataSystemConfiguration dataSystemConfig = builder.build(); + assertEquals(3, dataSystemConfig.getSynchronizers().size()); + assertTrue(dataSystemConfig.getSynchronizers().get(0) instanceof FDv2PollingDataSourceBuilder); + assertTrue(dataSystemConfig.getSynchronizers().get(1) instanceof FDv2StreamingDataSourceBuilder); + assertTrue(dataSystemConfig.getSynchronizers().get(2) instanceof PollingDataSourceBuilder); + } + + @Test + public void customDataSystemWithNoConfigurationHasEmptyLists() { + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom(); + DataSystemConfiguration dataSystemConfig = builder.build(); + + assertTrue(dataSystemConfig.getInitializers().isEmpty()); + assertTrue(dataSystemConfig.getSynchronizers().isEmpty()); + assertNull(dataSystemConfig.getFDv1FallbackSynchronizer()); + assertNull(dataSystemConfig.getPersistentStore()); + } + + @Test + public void canConfigureDaemonDataSystemWithReadOnlyMode() { + MockPersistentDataStore mockStore = new MockPersistentDataStore(); + ComponentConfigurer storeConfigurer = TestComponents.specificComponent(mockStore); + ComponentConfigurer dataStoreConfigurer = TestComponents.specificComponent( + Components.persistentDataStore(storeConfigurer).build(clientContextWithDataStoreUpdateSink())); + + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .persistentStore(dataStoreConfigurer, DataSystemConfiguration.DataStoreMode.READ_ONLY); + + DataSystemConfiguration dataSystemConfig = builder.build(); + assertNotNull(dataSystemConfig.getPersistentStore()); + assertEquals(DataSystemConfiguration.DataStoreMode.READ_ONLY, dataSystemConfig.getPersistentDataStoreMode()); + assertTrue(dataSystemConfig.getInitializers().isEmpty()); + assertTrue(dataSystemConfig.getSynchronizers().isEmpty()); + } + + @Test + public void canConfigureCustomDataSystemWithReadWritePersistentStore() { + MockPersistentDataStore mockStore = new MockPersistentDataStore(); + ComponentConfigurer storeConfigurer = TestComponents.specificComponent(mockStore); + ComponentConfigurer dataStoreConfigurer = TestComponents.specificComponent( + Components.persistentDataStore(storeConfigurer).build(clientContextWithDataStoreUpdateSink())); + + DataSystemModes modes = new DataSystemModes(); + DataSystemBuilder builder = modes.custom() + .persistentStore(dataStoreConfigurer, DataSystemConfiguration.DataStoreMode.READ_WRITE) + .synchronizers(DataSystemComponents.streaming()); + + DataSystemConfiguration dataSystemConfig = builder.build(); + assertNotNull(dataSystemConfig.getPersistentStore()); + assertEquals(DataSystemConfiguration.DataStoreMode.READ_WRITE, dataSystemConfig.getPersistentDataStoreMode()); + assertEquals(1, dataSystemConfig.getSynchronizers().size()); + } +} +