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()));
}
}