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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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