diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java index d996d3b..f30ad0d 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java @@ -26,6 +26,7 @@ import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder; import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder; import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder; +import com.launchdarkly.sdk.server.integrations.DataSystemModes; import com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder; import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; @@ -476,4 +477,21 @@ public static PluginsConfigurationBuilder plugins() { * @since 7.1.0 */ public static WrapperInfoBuilder wrapperInfo() { return new WrapperInfoBuilderImpl(); } + + /** + * This API is under active development. Do not use. + * + * Returns a set of builder options for configuring the SDK data system. When the data system configuration + * is used it overrides {@link LDConfig.Builder#dataSource(ComponentConfigurer)} and + * {@link LDConfig.Builder#dataStore(ComponentConfigurer)} in the configuration. + *

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

+ * + * @return a configuration builder + */ + public static DataSystemModes dataSystem() { + return new DataSystemModes(); + } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java index d200c49..76aff81 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java @@ -210,6 +210,14 @@ private boolean hasFlagChangeEventListeners() { return flagChangeEventNotifier.hasListeners(); } + void addFlagChangeListener(FlagChangeListener listener) { + flagChangeEventNotifier.register(listener); + } + + void removeFlagChangeListener(FlagChangeListener listener) { + flagChangeEventNotifier.unregister(listener); + } + private void sendChangeEvents(Iterable affectedItems) { for (KindAndKey item: affectedItems) { if (item.kind == FEATURES) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java new file mode 100644 index 0000000..0568ddd --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java @@ -0,0 +1,120 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; + +import java.util.concurrent.Future; + +/** + * Internal interface for the data system abstraction. + *

+ * This interface is package-private and should not be used by application code. + *

+ * 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 + */ +interface DataSystem { + /** + * Returns the read-only store interface. + * + * @return the read-only store + */ + ReadOnlyStore getStore(); + + /** + * Starts the data system. + * + * @return a Future that completes when initialization is complete + */ + Future start(); + + /** + * Returns whether the data system has been initialized. + * + * @return true if initialized + */ + boolean isInitialized(); + + /** + * Returns the flag change notifier interface. + * + * @return the flag change notifier + */ + FlagChangeNotifier getFlagChanged(); + + /** + * Returns the data source status provider. + * + * @return the data source status provider + */ + DataSourceStatusProvider getDataSourceStatusProvider(); + + /** + * Returns the data store status provider. + * + * @return the data store status provider + */ + DataStoreStatusProvider getDataStoreStatusProvider(); +} + +/** + * Internal interface for read-only access to a data store. + *

+ * This interface is package-private and should not be used by application code. + */ +interface ReadOnlyStore { + /** + * Retrieves an item from the specified collection, if available. + *

+ * If the item has been deleted and the store contains a placeholder, it should + * return that placeholder rather than null. + * + * @param kind specifies which collection to use + * @param key the unique key of the item within that collection + * @return a versioned item that contains the stored data (or placeholder for deleted data); + * null if the key is unknown + */ + ItemDescriptor get(DataKind kind, String key); + + /** + * Retrieves all items from the specified collection. + *

+ * If the store contains placeholders for deleted items, it should include them in + * the results, not filter them out. + * + * @param kind specifies which collection to use + * @return a collection of key-value pairs; the ordering is not significant + */ + KeyedItems getAll(DataKind kind); + + /** + * Checks whether this store has been initialized with any data yet. + * + * @return true if the store contains data + */ + boolean isInitialized(); +} + +/** + * Internal interface for flag change notifications. + *

+ * This interface is package-private and should not be used by application code. + */ +interface FlagChangeNotifier { + /** + * Adds a listener for flag change events. + * + * @param listener the listener to add + */ + void addFlagChangeListener(com.launchdarkly.sdk.server.interfaces.FlagChangeListener listener); + + /** + * Removes a listener for flag change events. + * + * @param listener the listener to remove + */ + void removeFlagChangeListener(com.launchdarkly.sdk.server.interfaces.FlagChangeListener listener); +} diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java new file mode 100644 index 0000000..ef1d340 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java @@ -0,0 +1,176 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; +import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; +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.LoggingConfiguration; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.Future; + +/** + * Internal implementation of the FDv1 data system. + *

+ * This class is package-private and should not be used by application code. + */ +final class FDv1DataSystem implements DataSystem, Closeable { + private final DataSource dataSource; + private final DataStore dataStore; + private final ReadOnlyStore store; + private final FlagChangeNotifier flagChanged; + private final DataSourceStatusProvider dataSourceStatusProvider; + private final DataStoreStatusProvider dataStoreStatusProvider; + private boolean disposed = false; + + /** + * Testing access to internal components. + */ + static final class TestingAccess { + final DataSource dataSource; + + TestingAccess(DataSource dataSource) { + this.dataSource = dataSource; + } + } + + final TestingAccess testing; + + /** + * Gets the underlying data store. This is needed for the evaluator. + * @return the underlying data store + */ + DataStore getUnderlyingStore() { + return dataStore; + } + + private FDv1DataSystem( + DataStore store, + DataStoreStatusProvider dataStoreStatusProvider, + DataSourceStatusProvider dataSourceStatusProvider, + DataSource dataSource, + FlagChangeNotifier flagChanged + ) { + this.dataStoreStatusProvider = dataStoreStatusProvider; + this.dataSourceStatusProvider = dataSourceStatusProvider; + this.store = new ReadonlyStoreFacade(store); + this.flagChanged = flagChanged; + this.dataSource = dataSource; + this.dataStore = store; + this.testing = new TestingAccess(dataSource); + } + + /** + * Creates a new FDv1DataSystem instance. + * + * @param logger the logger + * @param config the SDK configuration + * @param clientContext the client context + * @param logConfig the logging configuration + * @return a new FDv1DataSystem instance + */ + static FDv1DataSystem create( + LDLogger logger, + LDConfig config, + ClientContextImpl clientContext, + LoggingConfiguration logConfig + ) { + DataStoreUpdatesImpl dataStoreUpdates = new DataStoreUpdatesImpl( + EventBroadcasterImpl.forDataStoreStatus(clientContext.sharedExecutor, logger)); + + DataStore dataStore = (config.dataStore == null ? Components.inMemoryDataStore() : config.dataStore) + .build(clientContext.withDataStoreUpdateSink(dataStoreUpdates)); + + DataStoreStatusProvider dataStoreStatusProvider = new DataStoreStatusProviderImpl(dataStore, dataStoreUpdates); + + // Create a single flag change broadcaster to be shared between DataSourceUpdatesImpl and FlagTrackerImpl + EventBroadcasterImpl flagChangeBroadcaster = + EventBroadcasterImpl.forFlagChangeEvents(clientContext.sharedExecutor, logger); + + // Create a single data source status broadcaster to be shared between DataSourceUpdatesImpl and DataSourceStatusProviderImpl + EventBroadcasterImpl dataSourceStatusBroadcaster = + EventBroadcasterImpl.forDataSourceStatus(clientContext.sharedExecutor, logger); + + DataSourceUpdatesImpl dataSourceUpdates = new DataSourceUpdatesImpl( + dataStore, + dataStoreStatusProvider, + flagChangeBroadcaster, + dataSourceStatusBroadcaster, + clientContext.sharedExecutor, + logConfig.getLogDataSourceOutageAsErrorAfter(), + logger + ); + + ComponentConfigurer dataSourceFactory = config.offline + ? Components.externalUpdatesOnly() + : (config.dataSource == null ? Components.streamingDataSource() : config.dataSource); + DataSource dataSource = dataSourceFactory.build(clientContext.withDataSourceUpdateSink(dataSourceUpdates)); + DataSourceStatusProvider dataSourceStatusProvider = new DataSourceStatusProviderImpl( + dataSourceStatusBroadcaster, + dataSourceUpdates); + + FlagChangeNotifier flagChanged = new FlagChangedFacade(dataSourceUpdates); + + return new FDv1DataSystem( + dataStore, + dataStoreStatusProvider, + dataSourceStatusProvider, + dataSource, + flagChanged + ); + } + + @Override + public ReadOnlyStore getStore() { + return store; + } + + @Override + public Future start() { + return dataSource.start(); + } + + @Override + public boolean isInitialized() { + return dataSource.isInitialized(); + } + + @Override + public FlagChangeNotifier getFlagChanged() { + return flagChanged; + } + + @Override + public DataSourceStatusProvider getDataSourceStatusProvider() { + return dataSourceStatusProvider; + } + + @Override + public DataStoreStatusProvider getDataStoreStatusProvider() { + return dataStoreStatusProvider; + } + + @Override + public void close() throws IOException { + if (disposed) { + return; + } + try { + if (dataSource instanceof Closeable) { + ((Closeable) dataSource).close(); + } + if (dataStore instanceof Closeable) { + ((Closeable) dataStore).close(); + } + // DataSourceUpdatesImpl doesn't implement Closeable + } finally { + disposed = true; + } + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java new file mode 100644 index 0000000..1481de1 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java @@ -0,0 +1,120 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.Future; + +/** + * Internal implementation of the FDv2 data system. + *

+ * This class is package-private and should not be used by application code. + *

+ * This is a placeholder implementation. Not all dependencies are yet implemented. + */ +final class FDv2DataSystem implements DataSystem, Closeable { + private final DataSource dataSource; + private final DataStore store; + private final ReadOnlyStore readOnlyStore; + private final FlagChangeNotifier flagChanged; + private final DataSourceStatusProvider dataSourceStatusProvider; + private final DataStoreStatusProvider dataStoreStatusProvider; + private boolean disposed = false; + + private FDv2DataSystem( + DataStore store, + DataSource dataSource, + DataSourceStatusProvider dataSourceStatusProvider, + DataStoreStatusProvider dataStoreStatusProvider, + FlagChangeNotifier flagChanged + ) { + this.store = store; + this.dataSource = dataSource; + this.dataStoreStatusProvider = dataStoreStatusProvider; + this.dataSourceStatusProvider = dataSourceStatusProvider; + this.flagChanged = flagChanged; + this.readOnlyStore = new ReadonlyStoreFacade(store); + } + + /** + * Creates a new FDv2DataSystem instance. + *

+ * This is a placeholder implementation. Not all dependencies are yet implemented. + * + * @param logger the logger + * @param config the SDK configuration + * @param clientContext the client context + * @param logConfig the logging configuration + * @return a new FDv2DataSystem instance + * @throws UnsupportedOperationException since this is not yet fully implemented + */ + static FDv2DataSystem create( + LDLogger logger, + LDConfig config, + ClientContextImpl clientContext, + LoggingConfiguration logConfig + ) { + if (config.dataSystem == null) { + throw new IllegalArgumentException("DataSystem configuration is required for FDv2DataSystem"); + } + + // TODO: Implement FDv2DataSystem once all dependencies are available + + throw new UnsupportedOperationException("FDv2DataSystem is not yet fully implemented"); + } + + @Override + public ReadOnlyStore getStore() { + return readOnlyStore; + } + + @Override + public Future start() { + // TODO: Implement FDv2DataSystem.start() once all dependencies are available + throw new UnsupportedOperationException("FDv2DataSystem.start() is not yet implemented"); + } + + @Override + public boolean isInitialized() { + return dataSource.isInitialized(); + } + + @Override + public FlagChangeNotifier getFlagChanged() { + return flagChanged; + } + + @Override + public DataSourceStatusProvider getDataSourceStatusProvider() { + return dataSourceStatusProvider; + } + + @Override + public DataStoreStatusProvider getDataStoreStatusProvider() { + return dataStoreStatusProvider; + } + + @Override + public void close() throws IOException { + if (disposed) { + return; + } + try { + if (dataSource instanceof Closeable) { + ((Closeable) dataSource).close(); + } + if (store instanceof Closeable) { + ((Closeable) store).close(); + } + } finally { + disposed = true; + } + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagChangedFacade.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagChangedFacade.java new file mode 100644 index 0000000..2815c5c --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagChangedFacade.java @@ -0,0 +1,28 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; +import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; + +/** + * Internal facade that wraps DataSourceUpdatesImpl to provide flag change notifications. + *

+ * This class is package-private and should not be used by application code. + */ +final class FlagChangedFacade implements FlagChangeNotifier { + private final DataSourceUpdatesImpl dataSourceUpdates; + + FlagChangedFacade(DataSourceUpdatesImpl dataSourceUpdates) { + this.dataSourceUpdates = dataSourceUpdates; + } + + @Override + public void addFlagChangeListener(FlagChangeListener listener) { + dataSourceUpdates.addFlagChangeListener(listener); + } + + @Override + public void removeFlagChangeListener(FlagChangeListener listener) { + dataSourceUpdates.removeFlagChangeListener(listener); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java index 3f9fb1a..11d4faf 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java @@ -12,25 +12,25 @@ import java.util.function.BiFunction; final class FlagTrackerImpl implements FlagTracker { - private final EventBroadcasterImpl flagChangeBroadcaster; + private final FlagChangeNotifier flagChangeNotifier; private final BiFunction evaluateFn; FlagTrackerImpl( - EventBroadcasterImpl flagChangeBroadcaster, + FlagChangeNotifier flagChangeNotifier, BiFunction evaluateFn ) { - this.flagChangeBroadcaster = flagChangeBroadcaster; + this.flagChangeNotifier = flagChangeNotifier; this.evaluateFn = evaluateFn; } @Override public void addFlagChangeListener(FlagChangeListener listener) { - flagChangeBroadcaster.register(listener); + flagChangeNotifier.addFlagChangeListener(listener); } @Override public void removeFlagChangeListener(FlagChangeListener listener) { - flagChangeBroadcaster.unregister(listener); + flagChangeNotifier.removeFlagChangeListener(listener); } @Override diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java index 526e151..8e7b61f 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java @@ -8,7 +8,6 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.subsystems.DataStore; import com.launchdarkly.sdk.server.subsystems.DataStoreTypes; import com.launchdarkly.sdk.server.subsystems.EventProcessor; @@ -28,7 +27,7 @@ class InputValidatingEvaluator implements EvaluatorInterface { private final Evaluator evaluator; - private final DataStore store; + private final ReadOnlyStore store; private final LDLogger logger; // these are created at construction to avoid recreation during each evaluation @@ -45,7 +44,7 @@ class InputValidatingEvaluator implements EvaluatorInterface { * @param eventProcessor will be used to record events during evaluations as necessary * @param logger for logging messages and errors during evaluations */ - InputValidatingEvaluator(DataStore store, BigSegmentStoreWrapper segmentStore, @Nonnull EventProcessor eventProcessor, LDLogger logger) { + InputValidatingEvaluator(ReadOnlyStore store, BigSegmentStoreWrapper segmentStore, @Nonnull EventProcessor eventProcessor, LDLogger logger) { this.evaluator = new Evaluator(new Evaluator.Getters() { public DataModel.FeatureFlag getFlag(String key) { return InputValidatingEvaluator.getFlag(store, key); @@ -206,12 +205,12 @@ public FeatureFlagsState allFlagsState(LDContext context, FlagsStateOption... op return builder.build(); } - private static DataModel.FeatureFlag getFlag(DataStore store, String key) { + private static DataModel.FeatureFlag getFlag(ReadOnlyStore store, String key) { DataStoreTypes.ItemDescriptor item = store.get(FEATURES, key); return item == null ? null : (DataModel.FeatureFlag) item.getItem(); } - private static DataModel.Segment getSegment(DataStore store, String key) { + private static DataModel.Segment getSegment(ReadOnlyStore store, String key) { DataStoreTypes.ItemDescriptor item = store.get(SEGMENTS, key); return item == null ? null : (DataModel.Segment) item.getItem(); } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java index 9b29053..ba0e4c9 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -17,12 +17,8 @@ import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; -import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; import com.launchdarkly.sdk.server.interfaces.FlagTracker; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; -import com.launchdarkly.sdk.server.subsystems.DataSource; -import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; import com.launchdarkly.sdk.server.subsystems.DataStore; import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import com.launchdarkly.sdk.server.subsystems.EventProcessor; @@ -30,6 +26,7 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import java.io.Closeable; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; @@ -61,15 +58,11 @@ public final class LDClient implements LDClientInterface { final EvaluatorInterface evaluator; final EvaluatorInterface migrationEvaluator; final EventProcessor eventProcessor; - final DataSource dataSource; - final DataStore dataStore; + @VisibleForTesting + final DataSystem dataSystem; + private final FlagTrackerImpl flagTracker; private final BigSegmentStoreStatusProvider bigSegmentStoreStatusProvider; private final BigSegmentStoreWrapper bigSegmentStoreWrapper; - private final DataSourceUpdateSink dataSourceUpdates; - private final DataStoreStatusProviderImpl dataStoreStatusProvider; - private final DataSourceStatusProviderImpl dataSourceStatusProvider; - private final FlagTrackerImpl flagTracker; - private final EventBroadcasterImpl flagChangeBroadcaster; private final ScheduledExecutorService sharedExecutor; private final LDLogger baseLogger; private final LDLogger evaluationLogger; @@ -206,12 +199,14 @@ public LDClient(String sdkKey, LDConfig config) { } bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderImpl(bigSegmentStoreStatusNotifier, bigSegmentStoreWrapper); - EventBroadcasterImpl dataStoreStatusNotifier = - EventBroadcasterImpl.forDataStoreStatus(sharedExecutor, baseLogger); - DataStoreUpdatesImpl dataStoreUpdates = new DataStoreUpdatesImpl(dataStoreStatusNotifier); - this.dataStore = config.dataStore.build(context.withDataStoreUpdateSink(dataStoreUpdates)); + // Create DataSystem - FDv2 if configured, otherwise FDv1 + if (config.dataSystem != null) { + this.dataSystem = FDv2DataSystem.create(baseLogger, config, context, context.getLogging()); + } else { + this.dataSystem = FDv1DataSystem.create(baseLogger, config, context, context.getLogging()); + } - EvaluatorInterface evaluator = new InputValidatingEvaluator(dataStore, bigSegmentStoreWrapper, eventProcessor, evaluationLogger); + EvaluatorInterface evaluator = new InputValidatingEvaluator(this.dataSystem.getStore(), bigSegmentStoreWrapper, eventProcessor, evaluationLogger); // build environment metadata for plugins SdkMetadata sdkMetadata; @@ -242,27 +237,11 @@ public LDClient(String sdkKey, LDConfig config) { this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME)); } - this.flagChangeBroadcaster = EventBroadcasterImpl.forFlagChangeEvents(sharedExecutor, baseLogger); - this.flagTracker = new FlagTrackerImpl(flagChangeBroadcaster, + // Create FlagTracker using the dataSystem's flag change notifier + this.flagTracker = new FlagTrackerImpl( + this.dataSystem.getFlagChanged(), (key, ctx) -> jsonValueVariation(key, ctx, LDValue.ofNull())); - this.dataStoreStatusProvider = new DataStoreStatusProviderImpl(this.dataStore, dataStoreUpdates); - - EventBroadcasterImpl dataSourceStatusNotifier = - EventBroadcasterImpl.forDataSourceStatus(sharedExecutor, baseLogger); - DataSourceUpdatesImpl dataSourceUpdates = new DataSourceUpdatesImpl( - dataStore, - dataStoreStatusProvider, - flagChangeBroadcaster, - dataSourceStatusNotifier, - sharedExecutor, - context.getLogging().getLogDataSourceOutageAsErrorAfter(), - baseLogger - ); - this.dataSourceUpdates = dataSourceUpdates; - this.dataSource = config.dataSource.build(context.withDataSourceUpdateSink(dataSourceUpdates)); - this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates); - // register plugins as soon as possible after client is valid for (Plugin plugin : config.plugins.getPlugins()) { try { @@ -272,9 +251,10 @@ public LDClient(String sdkKey, LDConfig config) { } } - Future startFuture = dataSource.start(); + // Start the data system + Future startFuture = dataSystem.start(); if (!config.startWait.isZero() && !config.startWait.isNegative()) { - if (!(dataSource instanceof ComponentsImpl.NullDataSource)) { + if (!dataSystem.isInitialized()) { baseLogger.info("Waiting up to {} milliseconds for LaunchDarkly client to start...", config.startWait.toMillis()); if (config.startWait.toMillis() > EXCESSIVE_INIT_WAIT_MILLIS) { @@ -290,7 +270,7 @@ public LDClient(String sdkKey, LDConfig config) { LogValues.exceptionSummary(e)); baseLogger.debug("{}", LogValues.exceptionTrace(e)); } - if (!dataSource.isInitialized()) { + if (!dataSystem.isInitialized()) { baseLogger.warn("LaunchDarkly client was not successfully initialized"); } } @@ -298,7 +278,7 @@ public LDClient(String sdkKey, LDConfig config) { @Override public boolean isInitialized() { - return dataSource.isInitialized(); + return dataSystem.isInitialized(); } @Override @@ -443,8 +423,10 @@ public MigrationVariation migrationVariation(String key, LDContext context, Migr @Override public boolean isFlagKnown(String featureKey) { + ReadOnlyStore store = dataSystem.getStore(); + if (!isInitialized()) { - if (dataStore.isInitialized()) { + if (store.isInitialized()) { baseLogger.warn("isFlagKnown called before client initialized for feature flag \"{}\"; using last known values from data store", featureKey); } else { baseLogger.warn("isFlagKnown called before client initialized for feature flag \"{}\"; data store unavailable, returning false", featureKey); @@ -453,7 +435,7 @@ public boolean isFlagKnown(String featureKey) { } try { - if (getFlag(dataStore, featureKey) != null) { + if (store.get(DataModel.FEATURES, featureKey) != null) { return true; } } catch (Exception e) { @@ -477,7 +459,7 @@ public BigSegmentStoreStatusProvider getBigSegmentStoreStatusProvider() { @Override public DataStoreStatusProvider getDataStoreStatusProvider() { - return dataStoreStatusProvider; + return dataSystem.getDataStoreStatusProvider(); } @Override @@ -487,7 +469,7 @@ public LDLogger getLogger() { @Override public DataSourceStatusProvider getDataSourceStatusProvider() { - return dataSourceStatusProvider; + return dataSystem.getDataSourceStatusProvider(); } /** @@ -499,10 +481,10 @@ public DataSourceStatusProvider getDataSourceStatusProvider() { @Override public void close() throws IOException { baseLogger.info("Closing LaunchDarkly Client"); - this.dataStore.close(); + if (this.dataSystem instanceof Closeable) { + ((Closeable) this.dataSystem).close(); + } this.eventProcessor.close(); - this.dataSource.close(); - this.dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.OFF, null); if (this.bigSegmentStoreWrapper != null) { this.bigSegmentStoreWrapper.close(); } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java index c840a50..797add7 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java @@ -3,6 +3,7 @@ import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder; +import com.launchdarkly.sdk.server.integrations.DataSystemBuilder; import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder; @@ -48,6 +49,7 @@ public final class LDConfig { final Duration startWait; final int threadPriority; final WrapperInfo wrapperInfo; + final DataSystemBuilder dataSystem; protected LDConfig(Builder builder) { if (builder.offline) { @@ -74,6 +76,7 @@ protected LDConfig(Builder builder) { this.startWait = builder.startWait; this.threadPriority = builder.threadPriority; this.wrapperInfo = builder.wrapperBuilder != null ? builder.wrapperBuilder.build() : null; + this.dataSystem = builder.dataSystem; } /** @@ -103,6 +106,7 @@ public static class Builder { private Duration startWait = DEFAULT_START_WAIT; private int threadPriority = Thread.MIN_PRIORITY; private WrapperInfoBuilder wrapperBuilder = null; + private DataSystemBuilder dataSystem = null; /** * Creates a builder with all configuration parameters set to the default @@ -136,6 +140,7 @@ public static Builder fromConfig(LDConfig config) { newBuilder.threadPriority = config.threadPriority; newBuilder.wrapperBuilder = config.wrapperInfo != null ? ComponentsImpl.WrapperInfoBuilderImpl.fromInfo(config.wrapperInfo) : null; + newBuilder.dataSystem = config.dataSystem; return newBuilder; } @@ -198,6 +203,8 @@ public Builder bigSegments(ComponentConfigurer bigSegm * {@link Components#pollingDataSource()}, or a test fixture such as * {@link com.launchdarkly.sdk.server.integrations.FileData#dataSource()}. See those methods * for details on how to configure them. + *

+ * Note: If {@link #dataSystem(DataSystemBuilder)} is used, it will override this setting. * * @param dataSourceConfigurer the data source configuration builder * @return the main configuration builder @@ -213,6 +220,8 @@ public Builder dataSource(ComponentConfigurer dataSourceConfigurer) * related data received from LaunchDarkly, using a factory object. The default is * {@link Components#inMemoryDataStore()}; for database integrations, use * {@link Components#persistentDataStore(ComponentConfigurer)}. + *

+ * Note: If {@link #dataSystem(DataSystemBuilder)} is used, it will override this setting. * * @param dataStoreConfigurer the data store configuration builder * @return the main configuration builder @@ -406,6 +415,32 @@ public Builder wrapper(WrapperInfoBuilder wrapperBuilder) { return this; } + /** + * Sets the data system configuration. + *

+ * When the data system configuration is used it overrides {@link #dataSource(ComponentConfigurer)} and + * {@link #dataStore(ComponentConfigurer)} in the configuration. + *

+ * 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().defaultMode())
+     *       .build();
+     * 
+ * + * @param dataSystemBuilder the data system builder + * @return the builder + */ + public Builder dataSystem(DataSystemBuilder dataSystemBuilder) { + this.dataSystem = dataSystemBuilder; + return this; + } + /** * Builds the configured {@link com.launchdarkly.sdk.server.LDConfig} object. * diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ReadonlyStoreFacade.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ReadonlyStoreFacade.java new file mode 100644 index 0000000..f183a7d --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ReadonlyStoreFacade.java @@ -0,0 +1,35 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; + +/** + * Internal facade that wraps a DataStore to provide read-only access. + *

+ * This class is package-private and should not be used by application code. + */ +final class ReadonlyStoreFacade implements ReadOnlyStore { + private final DataStore store; + + ReadonlyStoreFacade(DataStore store) { + this.store = store; + } + + @Override + public ItemDescriptor get(DataKind kind, String key) { + return store.get(kind, key); + } + + @Override + public KeyedItems getAll(DataKind kind) { + return store.getAll(kind); + } + + @Override + public boolean isInitialized() { + return store.isInitialized(); + } +} + diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index 85a5238..c92affa 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,7 +4,5 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed - // x-release-please-start-version static final String SDK_VERSION = "7.10.2"; - // x-release-please-end } diff --git a/lib/sdk/server/src/templates/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/templates/java/com/launchdarkly/sdk/server/Version.java index acfd3be..e3987ce 100644 --- a/lib/sdk/server/src/templates/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/templates/java/com/launchdarkly/sdk/server/Version.java @@ -4,5 +4,7 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed + // x-release-please-start-version static final String SDK_VERSION = "@VERSION@"; + // x-release-please-end } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java index ebf93d6..8aa805f 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java @@ -29,7 +29,20 @@ public void flagChangeListeners() throws Exception { EventBroadcasterImpl broadcaster = EventBroadcasterImpl.forFlagChangeEvents(TestComponents.sharedExecutor, testLogger); - FlagTrackerImpl tracker = new FlagTrackerImpl(broadcaster, null); + // Create a test FlagChangeNotifier that wraps the broadcaster + FlagChangeNotifier notifier = new FlagChangeNotifier() { + @Override + public void addFlagChangeListener(FlagChangeListener listener) { + broadcaster.register(listener); + } + + @Override + public void removeFlagChangeListener(FlagChangeListener listener) { + broadcaster.unregister(listener); + } + }; + + FlagTrackerImpl tracker = new FlagTrackerImpl(notifier, null); BlockingQueue eventSink1 = new LinkedBlockingQueue<>(); BlockingQueue eventSink2 = new LinkedBlockingQueue<>(); @@ -69,7 +82,20 @@ public void flagValueChangeListener() throws Exception { EventBroadcasterImpl.forFlagChangeEvents(TestComponents.sharedExecutor, testLogger); Map, LDValue> resultMap = new HashMap<>(); - FlagTrackerImpl tracker = new FlagTrackerImpl(broadcaster, + // Create a test FlagChangeNotifier that wraps the broadcaster + FlagChangeNotifier notifier = new FlagChangeNotifier() { + @Override + public void addFlagChangeListener(FlagChangeListener listener) { + broadcaster.register(listener); + } + + @Override + public void removeFlagChangeListener(FlagChangeListener listener) { + broadcaster.unregister(listener); + } + }; + + FlagTrackerImpl tracker = new FlagTrackerImpl(notifier, (k, u) -> LDValue.normalize(resultMap.get(new AbstractMap.SimpleEntry<>(k, u)))); resultMap.put(new AbstractMap.SimpleEntry<>(flagKey, user), LDValue.of(false)); diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java index ca91095..83c2913 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java @@ -23,8 +23,9 @@ public void externalUpdatesOnlyClientHasNullDataSource() throws Exception { LDConfig config = baseConfig() .dataSource(Components.externalUpdatesOnly()) .build(); - try (LDClient client = new LDClient("SDK_KEY", config)) { - assertEquals(ComponentsImpl.NullDataSource.class, client.dataSource.getClass()); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertTrue(client.dataSystem instanceof FDv1DataSystem); + assertEquals(ComponentsImpl.NullDataSource.class, ((FDv1DataSystem) client.dataSystem).testing.dataSource.getClass()); } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java index ccbdc1a..5780d77 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java @@ -26,8 +26,9 @@ public void offlineClientHasNullDataSource() throws IOException { LDConfig config = baseConfig() .offline(true) .build(); - try (LDClient client = new LDClient("SDK_KEY", config)) { - assertEquals(ComponentsImpl.NullDataSource.class, client.dataSource.getClass()); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertTrue(client.dataSystem instanceof FDv1DataSystem); + assertEquals(ComponentsImpl.NullDataSource.class, ((FDv1DataSystem) client.dataSystem).testing.dataSource.getClass()); } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java index a5540ac..1be860f 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java @@ -169,7 +169,8 @@ public void streamingClientHasStreamProcessor() throws Exception { .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertEquals(StreamProcessor.class, client.dataSource.getClass()); + assertTrue(client.dataSystem instanceof FDv1DataSystem); + assertEquals(StreamProcessor.class, ((FDv1DataSystem) client.dataSystem).testing.dataSource.getClass()); } } @@ -185,7 +186,9 @@ public void canSetCustomStreamingEndpoint() throws Exception { .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertEquals(expected, ((StreamProcessor) client.dataSource).streamUri.toString()); + assertTrue(client.dataSystem instanceof FDv1DataSystem); + DataSource dataSource = ((FDv1DataSystem) client.dataSystem).testing.dataSource; + assertEquals(expected, ((StreamProcessor) dataSource).streamUri.toString()); } } @@ -199,7 +202,8 @@ public void pollingClientHasPollingProcessor() throws IOException { .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertEquals(PollingProcessor.class, client.dataSource.getClass()); + assertTrue(client.dataSystem instanceof FDv1DataSystem); + assertEquals(PollingProcessor.class, ((FDv1DataSystem) client.dataSystem).testing.dataSource.getClass()); } } @@ -214,7 +218,9 @@ public void canSetCustomPollingEndpoint() throws Exception { .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient(SDK_KEY, config)) { - String actual = ((DefaultFeatureRequestor) ((PollingProcessor) client.dataSource).requestor).pollingUri.toString(); + assertTrue(client.dataSystem instanceof FDv1DataSystem); + DataSource dataSource = ((FDv1DataSystem) client.dataSystem).testing.dataSource; + String actual = ((DefaultFeatureRequestor) ((PollingProcessor) dataSource).requestor).pollingUri.toString(); assertThat(actual, containsString(pu.toString())); } }