From 1a548dd28dd13b205689a3f5b46787ea67eb49a1 Mon Sep 17 00:00:00 2001
From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Date: Thu, 29 Jan 2026 09:17:54 -0800
Subject: [PATCH 01/15] chore: Add file data source support for FDv2.
---
.../sdk/server/integrations/FileData.java | 239 +++++++++++-
.../integrations/FileDataSourceBase.java | 223 ++++++++++++
.../integrations/FileDataSourceBuilder.java | 76 +++-
.../integrations/FileDataSourceImpl.java | 344 +++++-------------
.../server/integrations/FileInitializer.java | 41 +++
.../server/integrations/FileSynchronizer.java | 208 +++++++++++
.../server/integrations/DataLoaderTest.java | 4 +-
.../integrations/FileInitializerTest.java | 135 +++++++
.../integrations/FileSynchronizerTest.java | 181 +++++++++
.../TestDataSourceBuildInputs.java | 28 ++
10 files changed, 1214 insertions(+), 265 deletions(-)
create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBase.java
create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileInitializer.java
create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileSynchronizer.java
create mode 100644 lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java
create mode 100644 lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java
create mode 100644 lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataSourceBuildInputs.java
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java
index c75b64c0..54356b14 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java
@@ -1,6 +1,13 @@
package com.launchdarkly.sdk.server.integrations;
import com.launchdarkly.sdk.server.LDConfig.Builder;
+import com.launchdarkly.sdk.server.datasources.Initializer;
+import com.launchdarkly.sdk.server.datasources.Synchronizer;
+import com.launchdarkly.sdk.server.subsystems.DataSourceBuildInputs;
+import com.launchdarkly.sdk.server.subsystems.DataSourceBuilder;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
/**
* Integration between the LaunchDarkly SDK and file data.
@@ -137,6 +144,236 @@ public enum DuplicateKeysHandling {
public static FileDataSourceBuilder dataSource() {
return new FileDataSourceBuilder();
}
-
+
+ /**
+ * Creates a builder for an FDv2 Initializer that loads file data.
+ *
+ * An initializer performs a one-shot load of the file data. This is used with the FDv2 data system
+ * for initial data loading.
+ *
+ * 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 builder for configuring the file data initializer
+ */
+ public static FileInitializerBuilder initializer() {
+ return new FileInitializerBuilder();
+ }
+
+ /**
+ * Creates a builder for an FDv2 Synchronizer that loads and watches file data.
+ *
+ * A synchronizer loads file data and can watch for changes (if autoUpdate is enabled).
+ * This is used with the FDv2 data system for ongoing data synchronization.
+ *
+ * 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 builder for configuring the file data synchronizer
+ */
+ public static FileSynchronizerBuilder synchronizer() {
+ return new FileSynchronizerBuilder();
+ }
+
private FileData() {}
+
+ /**
+ * Builder for creating an FDv2 {@link Initializer} that loads file data.
+ *
+ * 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 static final class FileInitializerBuilder implements DataSourceBuilder {
+ private final FileDataSourceBuilder delegate = new FileDataSourceBuilder();
+
+ FileInitializerBuilder() {
+ delegate.shouldPersist(false);
+ }
+
+ /**
+ * Adds any number of source files for loading flag data, specifying each file path as a string.
+ *
+ * @param filePaths path(s) to the source file(s); may be absolute or relative to the current working directory
+ * @return the same builder
+ * @throws InvalidPathException if one of the parameters is not a valid file path
+ * @see FileDataSourceBuilder#filePaths(String...)
+ */
+ public FileInitializerBuilder filePaths(String... filePaths) throws InvalidPathException {
+ delegate.filePaths(filePaths);
+ return this;
+ }
+
+ /**
+ * Adds any number of source files for loading flag data, specifying each file path as a Path.
+ *
+ * @param filePaths path(s) to the source file(s); may be absolute or relative to the current working directory
+ * @return the same builder
+ * @see FileDataSourceBuilder#filePaths(Path...)
+ */
+ public FileInitializerBuilder filePaths(Path... filePaths) {
+ delegate.filePaths(filePaths);
+ return this;
+ }
+
+ /**
+ * Adds any number of classpath resources for loading flag data.
+ *
+ * @param resourceLocations resource location(s) in the format used by {@code ClassLoader.getResource()}
+ * @return the same builder
+ * @see FileDataSourceBuilder#classpathResources(String...)
+ */
+ public FileInitializerBuilder classpathResources(String... resourceLocations) {
+ delegate.classpathResources(resourceLocations);
+ return this;
+ }
+
+ /**
+ * Specifies how to handle keys that are duplicated across files.
+ *
+ * @param duplicateKeysHandling specifies how to handle duplicate keys
+ * @return the same builder
+ * @see FileDataSourceBuilder#duplicateKeysHandling(DuplicateKeysHandling)
+ */
+ public FileInitializerBuilder duplicateKeysHandling(DuplicateKeysHandling duplicateKeysHandling) {
+ delegate.duplicateKeysHandling(duplicateKeysHandling);
+ return this;
+ }
+
+ /**
+ * Configures whether file data should be persisted to persistent stores.
+ *
+ * By default, file data is not persisted ({@code shouldPersist = false}).
+ *
+ * Set this to {@code true} if you want the SDK to persist flag data to persistent stores.
+ * This isn't the recommended configuration but may be useful for testing scenarios.
+ *
+ * Example:
+ *
+ * FileData fd = FileData.initializer()
+ * .filePaths("./testData/flags.json")
+ * .shouldPersist(true);
+ *
+ *
+ * @param shouldPersist {@code true} if tile data should be persisted to persistent stores, false otherwise
+ * @return the same {@code TestData} instance
+ */
+ public FileInitializerBuilder shouldPersist(boolean shouldPersist) {
+ delegate.shouldPersist(shouldPersist);
+ return this;
+ }
+
+
+ @Override
+ public Initializer build(DataSourceBuildInputs context) {
+ return delegate.buildInitializer(context);
+ }
+}
+
+ /**
+ * Builder for creating an FDv2 {@link Synchronizer} that loads and watches file data.
+ *
+ * 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 static final class FileSynchronizerBuilder implements DataSourceBuilder {
+ private final FileDataSourceBuilder delegate = new FileDataSourceBuilder();
+
+ FileSynchronizerBuilder() {
+ delegate.shouldPersist(false);
+ }
+
+ /**
+ * Configures whether file data should be persisted to persistent stores.
+ *
+ * By default, file data is not persisted ({@code shouldPersist = false}).
+ *
+ * Set this to {@code true} if you want the SDK to persist flag data to persistent stores.
+ * This isn't the recommended configuration but may be useful for testing scenarios.
+ *
+ * Example:
+ *
+ * FileData fd = FileData.synchronizer()
+ * .filePaths("./testData/flags.json")
+ * .shouldPersist(true);
+ *
+ *
+ * @param shouldPersist {@code true} if file data should be persisted to persistent stores, false otherwise
+ * @return the same {@code TestData} instance
+ */
+ public FileSynchronizerBuilder shouldPersist(boolean shouldPersist) {
+ delegate.shouldPersist(shouldPersist);
+ return this;
+ }
+
+ /**
+ * Adds any number of source files for loading flag data, specifying each file path as a string.
+ *
+ * @param filePaths path(s) to the source file(s); may be absolute or relative to the current working directory
+ * @return the same builder
+ * @throws InvalidPathException if one of the parameters is not a valid file path
+ * @see FileDataSourceBuilder#filePaths(String...)
+ */
+ public FileSynchronizerBuilder filePaths(String... filePaths) throws InvalidPathException {
+ delegate.filePaths(filePaths);
+ return this;
+ }
+
+ /**
+ * Adds any number of source files for loading flag data, specifying each file path as a Path.
+ *
+ * @param filePaths path(s) to the source file(s); may be absolute or relative to the current working directory
+ * @return the same builder
+ * @see FileDataSourceBuilder#filePaths(Path...)
+ */
+ public FileSynchronizerBuilder filePaths(Path... filePaths) {
+ delegate.filePaths(filePaths);
+ return this;
+ }
+
+ /**
+ * Adds any number of classpath resources for loading flag data.
+ *
+ * @param resourceLocations resource location(s) in the format used by {@code ClassLoader.getResource()}
+ * @return the same builder
+ * @see FileDataSourceBuilder#classpathResources(String...)
+ */
+ public FileSynchronizerBuilder classpathResources(String... resourceLocations) {
+ delegate.classpathResources(resourceLocations);
+ return this;
+ }
+
+ /**
+ * Specifies whether the data source should watch for changes to the source file(s) and reload flags
+ * whenever there is a change.
+ *
+ * @param autoUpdate true if flags should be reloaded whenever a source file changes
+ * @return the same builder
+ * @see FileDataSourceBuilder#autoUpdate(boolean)
+ */
+ public FileSynchronizerBuilder autoUpdate(boolean autoUpdate) {
+ delegate.autoUpdate(autoUpdate);
+ return this;
+ }
+
+ /**
+ * Specifies how to handle keys that are duplicated across files.
+ *
+ * @param duplicateKeysHandling specifies how to handle duplicate keys
+ * @return the same builder
+ * @see FileDataSourceBuilder#duplicateKeysHandling(DuplicateKeysHandling)
+ */
+ public FileSynchronizerBuilder duplicateKeysHandling(DuplicateKeysHandling duplicateKeysHandling) {
+ delegate.duplicateKeysHandling(duplicateKeysHandling);
+ return this;
+ }
+
+ @Override
+ public Synchronizer build(DataSourceBuildInputs context) {
+ return delegate.buildSynchronizer(context);
+ }
+ }
}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBase.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBase.java
new file mode 100644
index 00000000..3c120049
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBase.java
@@ -0,0 +1,223 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.google.common.collect.ImmutableList;
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
+import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceBuilder.SourceInfo;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FileDataException;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFactory;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFileParser;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFileRep;
+import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
+import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo;
+import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSet;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSetType;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.launchdarkly.sdk.server.DataModel.FEATURES;
+import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
+
+/**
+ * Base class containing shared logic for file data source implementations.
+ */
+class FileDataSourceBase {
+ protected final List sources;
+ protected final FileData.DuplicateKeysHandling duplicateKeysHandling;
+ protected final LDLogger logger;
+ private final DataLoader dataLoader;
+
+ private final boolean persist;
+
+ protected FileDataSourceBase(
+ List sources,
+ FileData.DuplicateKeysHandling duplicateKeysHandling,
+ LDLogger logger,
+ boolean persist
+ ) {
+ this.sources = new ArrayList<>(sources);
+ this.duplicateKeysHandling = duplicateKeysHandling;
+ this.logger = logger;
+ this.dataLoader = new DataLoader(sources);
+ this.persist = persist;
+ }
+
+ /**
+ * Loads data from all configured files and returns an FDv2SourceResult.
+ *
+ * @param oneShot true if this is a one-shot load (Initializer), false for continuous (Synchronizer)
+ * @return an FDv2SourceResult containing either a ChangeSet or an error status
+ */
+ protected FDv2SourceResult loadData(boolean oneShot) {
+ DataBuilder builder = new DataBuilder(duplicateKeysHandling);
+ int version;
+ try {
+ version = dataLoader.load(builder);
+ } catch (FileDataException e) {
+ String description = getErrorDescription(e);
+ logger.error(description);
+ ErrorInfo errorInfo = new ErrorInfo(
+ ErrorKind.INVALID_DATA,
+ 0,
+ description,
+ Instant.now()
+ );
+ // For initializers, file errors are terminal. For synchronizers, they are recoverable.
+ return oneShot
+ ? FDv2SourceResult.terminalError(errorInfo, false)
+ : FDv2SourceResult.interrupted(errorInfo, false);
+ }
+
+ FullDataSet fullData = builder.build();
+ ChangeSet changeSet = buildChangeSet(fullData, version);
+ return FDv2SourceResult.changeSet(changeSet, false);
+ }
+
+ /**
+ * Builds a ChangeSet from a FullDataSet.
+ */
+ private ChangeSet buildChangeSet(FullDataSet fullData, int version) {
+ Selector selector = Selector.make(version, "file-data-" + version);
+ return new ChangeSet<>(
+ ChangeSetType.Full,
+ selector,
+ fullData.getData(),
+ null, // no environment ID for file data
+ persist
+ );
+ }
+
+ /**
+ * Returns the list of source infos for file watching.
+ */
+ Iterable getSources() {
+ return sources;
+ }
+
+ /**
+ * Safely gets an error description from a FileDataException, handling the case
+ * where the cause may be null.
+ */
+ private String getErrorDescription(FileDataException e) {
+ // FileDataException.getDescription() has a bug where it calls getCause().toString()
+ // without null checking. We work around this by building our own description.
+ StringBuilder s = new StringBuilder();
+ if (e.getMessage() != null) {
+ s.append(e.getMessage());
+ }
+ if (e.getCause() != null) {
+ if (s.length() > 0) {
+ s.append(" ");
+ }
+ s.append("[").append(e.getCause().toString()).append("]");
+ }
+ return s.toString();
+ }
+
+ /**
+ * Implements the loading of flag data from one or more files. Will throw an exception if any file can't
+ * be read or parsed, or if any flag or segment keys are duplicates.
+ */
+ static final class DataLoader {
+ private final List sources;
+ private final AtomicInteger lastVersion;
+
+ public DataLoader(List sources) {
+ this.sources = new ArrayList<>(sources);
+ this.lastVersion = new AtomicInteger(0);
+ }
+
+ public Iterable getSources() {
+ return sources;
+ }
+
+ /**
+ * Loads data from all sources into the builder.
+ *
+ * @param builder the data builder to populate
+ * @return the version number assigned to this load
+ * @throws FileDataException if any file cannot be read or parsed
+ */
+ public int load(DataBuilder builder) throws FileDataException {
+ int version = lastVersion.incrementAndGet();
+ for (SourceInfo s : sources) {
+ try {
+ byte[] data = s.readData();
+ FlagFileParser parser = FlagFileParser.selectForContent(data);
+ FlagFileRep fileContents = parser.parse(new ByteArrayInputStream(data));
+ if (fileContents.flags != null) {
+ for (Map.Entry e : fileContents.flags.entrySet()) {
+ builder.add(FEATURES, e.getKey(), FlagFactory.flagFromJson(e.getValue(), version));
+ }
+ }
+ if (fileContents.flagValues != null) {
+ for (Map.Entry e : fileContents.flagValues.entrySet()) {
+ builder.add(FEATURES, e.getKey(), FlagFactory.flagWithValue(e.getKey(), e.getValue(), version));
+ }
+ }
+ if (fileContents.segments != null) {
+ for (Map.Entry e : fileContents.segments.entrySet()) {
+ builder.add(SEGMENTS, e.getKey(), FlagFactory.segmentFromJson(e.getValue(), version));
+ }
+ }
+ } catch (FileDataException e) {
+ throw new FileDataException(e.getMessage(), e.getCause(), s);
+ } catch (IOException e) {
+ throw new FileDataException(null, e, s);
+ }
+ }
+ return version;
+ }
+ }
+
+ /**
+ * Internal data structure that organizes flag/segment data into the format that the feature store
+ * expects. Will throw an exception if we try to add the same flag or segment key more than once.
+ */
+ static final class DataBuilder {
+ private final Map> allData = new HashMap<>();
+ private final FileData.DuplicateKeysHandling duplicateKeysHandling;
+
+ public DataBuilder(FileData.DuplicateKeysHandling duplicateKeysHandling) {
+ this.duplicateKeysHandling = duplicateKeysHandling;
+ }
+
+ public FullDataSet build() {
+ ImmutableList.Builder>> allBuilder = ImmutableList.builder();
+ for (Map.Entry> e0 : allData.entrySet()) {
+ allBuilder.add(new AbstractMap.SimpleEntry<>(e0.getKey(), new KeyedItems<>(e0.getValue().entrySet())));
+ }
+ return new FullDataSet<>(allBuilder.build());
+ }
+
+ public void add(DataKind kind, String key, ItemDescriptor item) throws FileDataException {
+ Map items = allData.get(kind);
+ if (items == null) {
+ items = new HashMap();
+ allData.put(kind, items);
+ }
+ if (items.containsKey(key)) {
+ if (duplicateKeysHandling == FileData.DuplicateKeysHandling.IGNORE) {
+ return;
+ }
+ throw new FileDataException("in " + kind.getName() + ", key \"" + key + "\" was already defined", null, null);
+ }
+ items.put(key, item);
+ }
+ }
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java
index 14d607c5..a4920deb 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java
@@ -3,9 +3,12 @@
import com.google.common.io.ByteStreams;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.sdk.server.LDConfig.Builder;
+import com.launchdarkly.sdk.server.datasources.Initializer;
+import com.launchdarkly.sdk.server.datasources.Synchronizer;
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.DataSourceBuildInputs;
import java.io.IOException;
import java.io.InputStream;
@@ -30,6 +33,8 @@ public final class FileDataSourceBuilder implements ComponentConfigurer sources = new ArrayList<>(); // visible for tests
private boolean autoUpdate = false;
private FileData.DuplicateKeysHandling duplicateKeysHandling = FileData.DuplicateKeysHandling.FAIL;
+
+ private boolean shouldPersist = false;
/**
* Adds any number of source files for loading flag data, specifying each file path as a string. The files will
@@ -123,7 +128,76 @@ public DataSource build(ClientContext context) {
LDLogger logger = context.getBaseLogger().subLogger("DataSource");
return new FileDataSourceImpl(context.getDataSourceUpdateSink(), sources, autoUpdate, duplicateKeysHandling, logger);
}
-
+
+ /**
+ * Builds an {@link Initializer} for FDv2 data system integration.
+ *
+ * An initializer performs a one-shot load of the file data. If the file cannot be read,
+ * a terminal error is returned.
+ *
+ * @param context the data source build context
+ * @return an Initializer instance
+ */
+ Initializer buildInitializer(DataSourceBuildInputs context) {
+ LDLogger logger = context.getBaseLogger().subLogger("FileDataSource.Initializer");
+ return new FileInitializer(sources, duplicateKeysHandling, logger, shouldPersist);
+ }
+
+ /**
+ * Builds a {@link Synchronizer} for FDv2 data system integration.
+ *
+ * A synchronizer can watch for file changes (if autoUpdate is enabled) and emit
+ * new change sets when files are modified.
+ *
+ * @param context the data source build context
+ * @return a Synchronizer instance
+ */
+ Synchronizer buildSynchronizer(DataSourceBuildInputs context) {
+ LDLogger logger = context.getBaseLogger().subLogger("FileDataSource.Synchronizer");
+ return new FileSynchronizer(sources, autoUpdate, duplicateKeysHandling, logger, shouldPersist);
+ }
+
+ /**
+ * Returns whether auto-update is enabled. Package-private for use by FDv2 builders.
+ */
+ boolean isAutoUpdate() {
+ return autoUpdate;
+ }
+
+ /**
+ * Returns the duplicate keys handling mode. Package-private for use by FDv2 builders.
+ */
+ FileData.DuplicateKeysHandling getDuplicateKeysHandling() {
+ return duplicateKeysHandling;
+ }
+
+ /**
+ * Configures whether file data should be persisted to persistent stores.
+ *
+ * By default, file data is persisted ({@code shouldPersist = true}) to maintain consistency with
+ * previous versions' behavior. When {@code true}, the file data will be written to any configured persistent
+ * store (if the store is in READ_WRITE mode). This may be useful for integration tests that verify
+ * your persistent store configuration.
+ *
+ * FileData synchronizers and initializers to NOT persist data by default.
+ *
+ * Example:
+ *
+ * FileData fd = FileData.dataSource()
+ * .filePaths("./testData/flags.json")
+ * .shouldPersist(true);
+ *
+ *
+ * File data
+ *
+ * @param shouldPersist {@code true} if data from this source should be persisted
+ * @return an instance of this builder
+ */
+ public FileDataSourceBuilder shouldPersist(boolean shouldPersist) {
+ this.shouldPersist = shouldPersist;
+ return this;
+ }
+
static abstract class SourceInfo {
abstract byte[] readData() throws IOException;
abstract Path toFilePath();
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java
index 744b8505..cb45ec69 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java
@@ -1,303 +1,125 @@
package com.launchdarkly.sdk.server.integrations;
-import com.google.common.collect.ImmutableList;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.logging.LogValues;
-import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
import com.launchdarkly.sdk.server.integrations.FileDataSourceBuilder.SourceInfo;
-import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FileDataException;
-import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFactory;
-import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFileParser;
-import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFileRep;
-import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo;
-import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind;
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State;
import com.launchdarkly.sdk.server.subsystems.DataSource;
import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink;
-import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
-import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
-import java.nio.file.Path;
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
-import java.nio.file.Watchable;
-import java.time.Instant;
-import java.util.AbstractMap;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import static com.launchdarkly.sdk.server.DataModel.FEATURES;
-import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
/**
* Implements taking flag data from files and putting it into the data store, at startup time and
* optionally whenever files change.
+ *
+ * This is the legacy DataSource implementation for backward compatibility. It internally uses
+ * {@link FileSynchronizer} for file loading and watching, adapting the results to the DataSource API.
+ *
+ * For FDv2 integration, use {@link FileInitializer} or {@link FileSynchronizer} directly.
*/
final class FileDataSourceImpl implements DataSource {
- private final DataSourceUpdateSink dataSourceUpdates;
- private final DataLoader dataLoader;
- private final FileData.DuplicateKeysHandling duplicateKeysHandling;
- private final AtomicBoolean inited = new AtomicBoolean(false);
- private final FileWatcher fileWatcher;
- private final LDLogger logger;
-
- FileDataSourceImpl(
- DataSourceUpdateSink dataSourceUpdates,
- List sources,
- boolean autoUpdate,
- FileData.DuplicateKeysHandling duplicateKeysHandling,
- LDLogger logger
- ) {
- this.dataSourceUpdates = dataSourceUpdates;
- this.dataLoader = new DataLoader(sources);
- this.duplicateKeysHandling = duplicateKeysHandling;
- this.logger = logger;
+ private final DataSourceUpdateSink dataSourceUpdates;
+ private final FileSynchronizer synchronizer;
+ private final AtomicBoolean inited = new AtomicBoolean(false);
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final LDLogger logger;
+ private Thread updateThread;
- FileWatcher fw = null;
- if (autoUpdate) {
- try {
- fw = FileWatcher.create(dataLoader.getSources(), logger);
- } catch (IOException e) {
- // COVERAGE: there is no way to simulate this condition in a unit test
- logger.error("Unable to watch files for auto-updating: {}", e.toString());
- logger.debug(e.toString(), e);
- fw = null;
- }
+ FileDataSourceImpl(
+ DataSourceUpdateSink dataSourceUpdates,
+ List sources,
+ boolean autoUpdate,
+ FileData.DuplicateKeysHandling duplicateKeysHandling,
+ LDLogger logger
+ ) {
+ this.dataSourceUpdates = dataSourceUpdates;
+ this.logger = logger;
+ // The FDv1
+ this.synchronizer = new FileSynchronizer(sources, autoUpdate, duplicateKeysHandling, logger, true);
}
- fileWatcher = fw;
- }
-
- @Override
- public Future start() {
- final Future initFuture = CompletableFuture.completedFuture(null);
-
- reload();
-
- // Note that if reload() finds any errors, it will not set our status to "initialized". But we
- // will still do all the other startup steps, because we still might end up getting valid data
- // if we are told to reload by the file watcher.
- if (fileWatcher != null) {
- fileWatcher.start(this::reload);
- }
-
- return initFuture;
- }
+ @Override
+ public Future start() {
+ final Future initFuture = CompletableFuture.completedFuture(null);
- private boolean reload() {
- DataBuilder builder = new DataBuilder(duplicateKeysHandling);
- try {
- dataLoader.load(builder);
- } catch (FileDataException e) {
- logger.error(e.getDescription());
- dataSourceUpdates.updateStatus(State.INTERRUPTED,
- new ErrorInfo(ErrorKind.INVALID_DATA, 0, e.getDescription(), Instant.now()));
- return false;
- }
- dataSourceUpdates.init(builder.build());
- dataSourceUpdates.updateStatus(State.VALID, null);
- inited.set(true);
- return true;
- }
-
- @Override
- public boolean isInitialized() {
- return inited.get();
- }
+ // Get initial data from the synchronizer
+ FDv2SourceResult initialResult;
+ try {
+ initialResult = synchronizer.next().get();
+ } catch (Exception e) {
+ logger.error("Error getting initial file data: {}", LogValues.exceptionSummary(e));
+ dataSourceUpdates.updateStatus(State.INTERRUPTED, null);
+ return initFuture;
+ }
- @Override
- public void close() throws IOException {
- if (fileWatcher != null) {
- fileWatcher.stop();
- }
- }
-
- /**
- * If auto-updating is enabled, this component watches for file changes on a worker thread.
- */
- private static final class FileWatcher implements Runnable {
- private final WatchService watchService;
- private final Set watchedFilePaths;
- private Runnable fileModifiedAction;
- private final Thread thread;
- private final LDLogger logger;
- private volatile boolean stopped;
+ processResult(initialResult);
- private static FileWatcher create(Iterable sources, LDLogger logger) throws IOException {
- Set directoryPaths = new HashSet<>();
- Set absoluteFilePaths = new HashSet<>();
- FileSystem fs = FileSystems.getDefault();
- WatchService ws = fs.newWatchService();
-
- // In Java, you watch for filesystem changes at the directory level, not for individual files.
- for (SourceInfo s: sources) {
- Path p = s.toFilePath();
- if (p != null) {
- absoluteFilePaths.add(p);
- directoryPaths.add(p.getParent());
- }
- }
- for (Path d: directoryPaths) {
- d.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
- }
-
- return new FileWatcher(ws, absoluteFilePaths, logger);
- }
-
- private FileWatcher(WatchService watchService, Set watchedFilePaths, LDLogger logger) {
- this.watchService = watchService;
- this.watchedFilePaths = watchedFilePaths;
- this.logger = logger;
-
- thread = new Thread(this, FileDataSourceImpl.class.getName());
- thread.setDaemon(true);
+ // Note that if the initial load finds any errors, it will not set our status to "initialized".
+ // But we will still do all the other startup steps, because we still might end up getting
+ // valid data if we are told to reload by the file watcher.
+
+ // Start a background thread to listen for file changes
+ updateThread = new Thread(this::runUpdateLoop, FileDataSourceImpl.class.getName());
+ updateThread.setDaemon(true);
+ updateThread.start();
+
+ return initFuture;
}
-
- public void run() {
- while (!stopped) {
- try {
- WatchKey key = watchService.take(); // blocks until a change is available or we are interrupted
- boolean watchedFileWasChanged = false;
- for (WatchEvent> event: key.pollEvents()) {
- Watchable w = key.watchable();
- Object context = event.context();
- if (w instanceof Path && context instanceof Path) {
- Path dirPath = (Path)w;
- Path fileNamePath = (Path)context;
- Path absolutePath = dirPath.resolve(fileNamePath);
- if (watchedFilePaths.contains(absolutePath)) {
- watchedFileWasChanged = true;
- break;
- }
- }
- }
- if (watchedFileWasChanged) {
+
+ private void runUpdateLoop() {
+ while (!closed.get()) {
try {
- fileModifiedAction.run();
+ FDv2SourceResult result = synchronizer.next().get();
+ if (closed.get()) {
+ break;
+ }
+ if (result.getResultType() == FDv2SourceResult.ResultType.STATUS &&
+ result.getStatus().getState() == FDv2SourceResult.State.SHUTDOWN) {
+ break;
+ }
+ processResult(result);
} catch (Exception e) {
- // COVERAGE: there is no way to simulate this condition in a unit test
- logger.warn("Unexpected exception when reloading file data: {}", LogValues.exceptionSummary(e));
+ if (!closed.get()) {
+ logger.warn("Unexpected exception in file data update loop: {}", LogValues.exceptionSummary(e));
+ }
}
- }
- key.reset(); // if we don't do this, the watch on this key stops working
- } catch (InterruptedException e) {
- // if we've been stopped we will drop out at the top of the while loop
}
- }
- }
-
- public void start(Runnable fileModifiedAction) {
- this.fileModifiedAction = fileModifiedAction;
- thread.start();
}
-
- public void stop() {
- stopped = true;
- thread.interrupt();
- }
- }
-
- /**
- * Implements the loading of flag data from one or more files. Will throw an exception if any file can't
- * be read or parsed, or if any flag or segment keys are duplicates.
- */
- static final class DataLoader {
- private final List sources;
- private final AtomicInteger lastVersion;
- public DataLoader(List sources) {
- this.sources = new ArrayList<>(sources);
- this.lastVersion = new AtomicInteger(0);
- }
-
- public Iterable getSources() {
- return sources;
- }
-
- public void load(DataBuilder builder) throws FileDataException
- {
- int version = lastVersion.incrementAndGet();
- for (SourceInfo s: sources) {
- try {
- byte[] data = s.readData();
- FlagFileParser parser = FlagFileParser.selectForContent(data);
- FlagFileRep fileContents = parser.parse(new ByteArrayInputStream(data));
- if (fileContents.flags != null) {
- for (Map.Entry e: fileContents.flags.entrySet()) {
- builder.add(FEATURES, e.getKey(), FlagFactory.flagFromJson(e.getValue(), version));
- }
- }
- if (fileContents.flagValues != null) {
- for (Map.Entry e: fileContents.flagValues.entrySet()) {
- builder.add(FEATURES, e.getKey(), FlagFactory.flagWithValue(e.getKey(), e.getValue(), version));
- }
- }
- if (fileContents.segments != null) {
- for (Map.Entry e: fileContents.segments.entrySet()) {
- builder.add(SEGMENTS, e.getKey(), FlagFactory.segmentFromJson(e.getValue(), version));
+ private void processResult(FDv2SourceResult result) {
+ if (result.getResultType() == FDv2SourceResult.ResultType.CHANGE_SET) {
+ // Convert ChangeSet to FullDataSet for legacy init()
+ FullDataSet fullData = new FullDataSet<>(result.getChangeSet().getData());
+ dataSourceUpdates.init(fullData);
+ dataSourceUpdates.updateStatus(State.VALID, null);
+ inited.set(true);
+ } else if (result.getResultType() == FDv2SourceResult.ResultType.STATUS) {
+ // Handle error/status results
+ if (result.getStatus().getState() != FDv2SourceResult.State.SHUTDOWN) {
+ dataSourceUpdates.updateStatus(State.INTERRUPTED, result.getStatus().getErrorInfo());
}
- }
- } catch (FileDataException e) {
- throw new FileDataException(e.getMessage(), e.getCause(), s);
- } catch (IOException e) {
- throw new FileDataException(null, e, s);
}
- }
}
- }
-
- /**
- * Internal data structure that organizes flag/segment data into the format that the feature store
- * expects. Will throw an exception if we try to add the same flag or segment key more than once.
- */
- static final class DataBuilder {
- private final Map> allData = new HashMap<>();
- private final FileData.DuplicateKeysHandling duplicateKeysHandling;
-
- public DataBuilder(FileData.DuplicateKeysHandling duplicateKeysHandling) {
- this.duplicateKeysHandling = duplicateKeysHandling;
- }
-
- public FullDataSet build() {
- ImmutableList.Builder>> allBuilder = ImmutableList.builder();
- for (Map.Entry> e0: allData.entrySet()) {
- allBuilder.add(new AbstractMap.SimpleEntry<>(e0.getKey(), new KeyedItems<>(e0.getValue().entrySet())));
- }
- // File data source data is not authoritative and should not be persisted
- return new FullDataSet<>(allBuilder.build(), false);
+
+ @Override
+ public boolean isInitialized() {
+ return inited.get();
}
-
- public void add(DataKind kind, String key, ItemDescriptor item) throws FileDataException {
- Map items = allData.get(kind);
- if (items == null) {
- items = new HashMap();
- allData.put(kind, items);
- }
- if (items.containsKey(key)) {
- if (duplicateKeysHandling == FileData.DuplicateKeysHandling.IGNORE) {
- return;
+
+ @Override
+ public void close() throws IOException {
+ closed.set(true);
+ synchronizer.close();
+ if (updateThread != null) {
+ updateThread.interrupt();
}
- throw new FileDataException("in " + kind.getName() + ", key \"" + key + "\" was already defined", null, null);
- }
- items.put(key, item);
}
- }
}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileInitializer.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileInitializer.java
new file mode 100644
index 00000000..3f8008b4
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileInitializer.java
@@ -0,0 +1,41 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
+import com.launchdarkly.sdk.server.datasources.Initializer;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceBuilder.SourceInfo;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * One-shot file loading implementation for FDv2 initialization.
+ *
+ * This implements the {@link Initializer} interface, loading files once and returning
+ * the result. If loading fails, it returns a terminal error since an initializer
+ * cannot retry.
+ */
+final class FileInitializer extends FileDataSourceBase implements Initializer {
+ private final CompletableFuture shutdownFuture = new CompletableFuture<>();
+
+ FileInitializer(
+ List sources,
+ FileData.DuplicateKeysHandling duplicateKeysHandling,
+ LDLogger logger,
+ boolean persist
+ ) {
+ super(sources, duplicateKeysHandling, logger, persist);
+ }
+
+ @Override
+ public CompletableFuture run() {
+ CompletableFuture loadResult = CompletableFuture.supplyAsync(() -> loadData(true));
+ return CompletableFuture.anyOf(shutdownFuture, loadResult)
+ .thenApply(result -> (FDv2SourceResult) result);
+ }
+
+ @Override
+ public void close() {
+ shutdownFuture.complete(FDv2SourceResult.shutdown());
+ }
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileSynchronizer.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileSynchronizer.java
new file mode 100644
index 00000000..8f94c6dd
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FileSynchronizer.java
@@ -0,0 +1,208 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.logging.LogValues;
+import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
+import com.launchdarkly.sdk.server.datasources.Synchronizer;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceBuilder.SourceInfo;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.Watchable;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+/**
+ * Streaming file updates implementation for FDv2 synchronization.
+ *
+ * This implements the {@link Synchronizer} interface, providing file watching
+ * and emitting results when files change. If autoUpdate is disabled, it only
+ * returns the initial load result.
+ */
+final class FileSynchronizer extends FileDataSourceBase implements Synchronizer {
+ private final CompletableFuture shutdownFuture = new CompletableFuture<>();
+ private final AsyncQueue resultQueue = new AsyncQueue<>();
+ private final FileWatcher fileWatcher; // null if autoUpdate=false
+ private volatile boolean started = false;
+
+ FileSynchronizer(
+ List sources,
+ boolean autoUpdate,
+ FileData.DuplicateKeysHandling duplicateKeysHandling,
+ LDLogger logger,
+ boolean persist
+ ) {
+ super(sources, duplicateKeysHandling, logger, persist);
+
+ FileWatcher fw = null;
+ if (autoUpdate) {
+ try {
+ fw = FileWatcher.create(getSources(), logger);
+ } catch (IOException e) {
+ // COVERAGE: there is no way to simulate this condition in a unit test
+ logger.error("Unable to watch files for auto-updating: {}", e.toString());
+ logger.debug(e.toString(), e);
+ fw = null;
+ }
+ }
+ this.fileWatcher = fw;
+ }
+
+ @Override
+ public CompletableFuture next() {
+ if (!started) {
+ started = true;
+ // Perform initial load
+ resultQueue.put(loadData(false));
+ // Start file watching if enabled
+ if (fileWatcher != null) {
+ fileWatcher.start(this::onFileChange);
+ }
+ }
+ return CompletableFuture.anyOf(shutdownFuture, resultQueue.take())
+ .thenApply(result -> (FDv2SourceResult) result);
+ }
+
+ private void onFileChange() {
+ resultQueue.put(loadData(false));
+ }
+
+ @Override
+ public void close() {
+ shutdownFuture.complete(FDv2SourceResult.shutdown());
+ if (fileWatcher != null) {
+ fileWatcher.stop();
+ }
+ }
+
+ /**
+ * A simple thread-safe async queue for passing results between the file watcher and the synchronizer.
+ */
+ private static final class AsyncQueue {
+ private final Object lock = new Object();
+ private final LinkedList queue = new LinkedList<>();
+ private final LinkedList> pendingFutures = new LinkedList<>();
+
+ public void put(T item) {
+ synchronized (lock) {
+ CompletableFuture nextFuture = pendingFutures.pollFirst();
+ if (nextFuture != null) {
+ nextFuture.complete(item);
+ return;
+ }
+ queue.addLast(item);
+ }
+ }
+
+ public CompletableFuture take() {
+ synchronized (lock) {
+ if (!queue.isEmpty()) {
+ return CompletableFuture.completedFuture(queue.removeFirst());
+ }
+ CompletableFuture takeFuture = new CompletableFuture<>();
+ pendingFutures.addLast(takeFuture);
+ return takeFuture;
+ }
+ }
+ }
+
+ /**
+ * If auto-updating is enabled, this component watches for file changes on a worker thread.
+ */
+ private static final class FileWatcher implements Runnable {
+ private final WatchService watchService;
+ private final Set watchedFilePaths;
+ private Runnable fileModifiedAction;
+ private final Thread thread;
+ private final LDLogger logger;
+ private volatile boolean stopped;
+
+ private static FileWatcher create(Iterable sources, LDLogger logger) throws IOException {
+ Set directoryPaths = new HashSet<>();
+ Set absoluteFilePaths = new HashSet<>();
+ FileSystem fs = FileSystems.getDefault();
+ WatchService ws = fs.newWatchService();
+
+ // In Java, you watch for filesystem changes at the directory level, not for individual files.
+ for (SourceInfo s : sources) {
+ Path p = s.toFilePath();
+ if (p != null) {
+ // Convert to absolute path to ensure we have a parent directory
+ // (relative paths like "flags.json" have null parent)
+ Path absolutePath = p.toAbsolutePath();
+ absoluteFilePaths.add(absolutePath);
+ directoryPaths.add(absolutePath.getParent());
+ }
+ }
+ for (Path d : directoryPaths) {
+ d.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
+ }
+
+ return new FileWatcher(ws, absoluteFilePaths, logger);
+ }
+
+ private FileWatcher(WatchService watchService, Set watchedFilePaths, LDLogger logger) {
+ this.watchService = watchService;
+ this.watchedFilePaths = watchedFilePaths;
+ this.logger = logger;
+
+ thread = new Thread(this, FileSynchronizer.class.getName());
+ thread.setDaemon(true);
+ }
+
+ public void run() {
+ while (!stopped) {
+ try {
+ WatchKey key = watchService.take(); // blocks until a change is available or we are interrupted
+ boolean watchedFileWasChanged = false;
+ for (WatchEvent> event : key.pollEvents()) {
+ Watchable w = key.watchable();
+ Object context = event.context();
+ if (w instanceof Path && context instanceof Path) {
+ Path dirPath = (Path) w;
+ Path fileNamePath = (Path) context;
+ Path absolutePath = dirPath.resolve(fileNamePath);
+ if (watchedFilePaths.contains(absolutePath)) {
+ watchedFileWasChanged = true;
+ break;
+ }
+ }
+ }
+ if (watchedFileWasChanged) {
+ try {
+ fileModifiedAction.run();
+ } catch (Exception e) {
+ // COVERAGE: there is no way to simulate this condition in a unit test
+ logger.warn("Unexpected exception when reloading file data: {}", LogValues.exceptionSummary(e));
+ }
+ }
+ key.reset(); // if we don't do this, the watch on this key stops working
+ } catch (InterruptedException e) {
+ // if we've been stopped we will drop out at the top of the while loop
+ }
+ }
+ }
+
+ public void start(Runnable fileModifiedAction) {
+ this.fileModifiedAction = fileModifiedAction;
+ thread.start();
+ }
+
+ public void stop() {
+ stopped = true;
+ thread.interrupt();
+ }
+ }
+}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java
index f572d861..ae068572 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java
@@ -1,8 +1,8 @@
package com.launchdarkly.sdk.server.integrations;
import com.launchdarkly.sdk.LDValue;
-import com.launchdarkly.sdk.server.integrations.FileDataSourceImpl.DataBuilder;
-import com.launchdarkly.sdk.server.integrations.FileDataSourceImpl.DataLoader;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceBase.DataBuilder;
+import com.launchdarkly.sdk.server.integrations.FileDataSourceBase.DataLoader;
import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FileDataException;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet;
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java
new file mode 100644
index 00000000..b655593d
--- /dev/null
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java
@@ -0,0 +1,135 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
+import com.launchdarkly.sdk.server.datasources.Initializer;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSetType;
+
+import org.junit.Test;
+
+import java.nio.file.Paths;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import static com.launchdarkly.sdk.server.integrations.FileDataSourceTestData.resourceFilePath;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertNotNull;
+
+@SuppressWarnings("javadoc")
+public class FileInitializerTest {
+ private static final LDLogger testLogger = LDLogger.none();
+
+ @Test
+ public void initializerReturnsChangeSetOnSuccessfulLoad() throws Exception {
+ Initializer initializer = FileData.initializer()
+ .filePaths(resourceFilePath("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = initializer.run();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ assertThat(result.getChangeSet(), notNullValue());
+ assertThat(result.getChangeSet().getType(), equalTo(ChangeSetType.Full));
+ assertNotNull(result.getChangeSet().getData());
+ } finally {
+ initializer.close();
+ }
+ }
+
+ @Test
+ public void initializerReturnsTerminalErrorOnMissingFile() throws Exception {
+ Initializer initializer = FileData.initializer()
+ .filePaths(Paths.get("no-such-file.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = initializer.run();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
+ assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.TERMINAL_ERROR));
+ assertNotNull(result.getStatus().getErrorInfo());
+ } finally {
+ initializer.close();
+ }
+ }
+
+ @Test
+ public void initializerReturnsShutdownWhenClosedBeforeRun() throws Exception {
+ Initializer initializer = FileData.initializer()
+ .filePaths(resourceFilePath("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ // Close before calling run
+ initializer.close();
+
+ CompletableFuture resultFuture = initializer.run();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
+ assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.SHUTDOWN));
+ }
+
+ @Test
+ public void initializerCanLoadFromClasspathResource() throws Exception {
+ Initializer initializer = FileData.initializer()
+ .classpathResources(FileDataSourceTestData.resourceLocation("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = initializer.run();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ assertThat(result.getChangeSet(), notNullValue());
+ } finally {
+ initializer.close();
+ }
+ }
+
+ @Test
+ public void initializerRespectsIgnoreDuplicateKeysHandling() throws Exception {
+ Initializer initializer = FileData.initializer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE)
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = initializer.run();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ // Should succeed when ignoring duplicates
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ } finally {
+ initializer.close();
+ }
+ }
+
+ @Test
+ public void initializerFailsOnDuplicateKeysByDefault() throws Exception {
+ Initializer initializer = FileData.initializer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = initializer.run();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ // Should fail with terminal error when duplicate keys are not allowed
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
+ assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.TERMINAL_ERROR));
+ } finally {
+ initializer.close();
+ }
+ }
+}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java
new file mode 100644
index 00000000..17abddb0
--- /dev/null
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java
@@ -0,0 +1,181 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
+import com.launchdarkly.sdk.server.datasources.Synchronizer;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSetType;
+import com.launchdarkly.testhelpers.TempDir;
+import com.launchdarkly.testhelpers.TempFile;
+
+import org.junit.Test;
+
+import java.nio.file.Paths;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import static com.launchdarkly.sdk.server.integrations.FileDataSourceTestData.getResourceContents;
+import static com.launchdarkly.sdk.server.integrations.FileDataSourceTestData.resourceFilePath;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertNotNull;
+
+@SuppressWarnings("javadoc")
+public class FileSynchronizerTest {
+ private static final LDLogger testLogger = LDLogger.none();
+
+ @Test
+ public void synchronizerReturnsChangeSetOnSuccessfulLoad() throws Exception {
+ Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(resourceFilePath("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = synchronizer.next();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ assertThat(result.getChangeSet(), notNullValue());
+ assertThat(result.getChangeSet().getType(), equalTo(ChangeSetType.Full));
+ assertNotNull(result.getChangeSet().getData());
+ } finally {
+ synchronizer.close();
+ }
+ }
+
+ @Test
+ public void synchronizerReturnsInterruptedOnMissingFile() throws Exception {
+ Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(Paths.get("no-such-file.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = synchronizer.next();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ // Synchronizers return INTERRUPTED for recoverable errors, not TERMINAL_ERROR
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
+ assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.INTERRUPTED));
+ assertNotNull(result.getStatus().getErrorInfo());
+ } finally {
+ synchronizer.close();
+ }
+ }
+
+ @Test
+ public void synchronizerReturnsShutdownWhenClosed() throws Exception {
+ Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(resourceFilePath("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ // Get initial result
+ CompletableFuture initialResult = synchronizer.next();
+ FDv2SourceResult result = initialResult.get(5, TimeUnit.SECONDS);
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+
+ // Start waiting for next result
+ CompletableFuture nextResult = synchronizer.next();
+
+ // Close the synchronizer
+ synchronizer.close();
+
+ // Should return shutdown
+ result = nextResult.get(5, TimeUnit.SECONDS);
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
+ assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.SHUTDOWN));
+ }
+
+ @Test
+ public void synchronizerCanLoadFromClasspathResource() throws Exception {
+ Synchronizer synchronizer = FileData.synchronizer()
+ .classpathResources(FileDataSourceTestData.resourceLocation("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = synchronizer.next();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ assertThat(result.getChangeSet(), notNullValue());
+ } finally {
+ synchronizer.close();
+ }
+ }
+
+ @Test
+ public void synchronizerRespectsIgnoreDuplicateKeysHandling() throws Exception {
+ Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE)
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = synchronizer.next();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ // Should succeed when ignoring duplicates
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ } finally {
+ synchronizer.close();
+ }
+ }
+
+ @Test
+ public void synchronizerFailsOnDuplicateKeysByDefault() throws Exception {
+ Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ CompletableFuture resultFuture = synchronizer.next();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+
+ // Should fail with interrupted error when duplicate keys are not allowed
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
+ assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.INTERRUPTED));
+ } finally {
+ synchronizer.close();
+ }
+ }
+
+ @Test
+ public void synchronizerAutoUpdateEmitsNewResultOnFileChange() throws Exception {
+ try (TempDir dir = TempDir.create()) {
+ try (TempFile file = dir.tempFile(".json")) {
+ file.setContents(getResourceContents("flag-only.json"));
+
+ Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(file.getPath())
+ .autoUpdate(true)
+ .build(TestDataSourceBuildInputs.create(testLogger));
+
+ try {
+ // Get initial result
+ CompletableFuture resultFuture = synchronizer.next();
+ FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+
+ // Start waiting for next result
+ CompletableFuture nextResultFuture = synchronizer.next();
+
+ // Modify the file
+ Thread.sleep(200); // Small delay to ensure file watcher is ready
+ file.setContents(getResourceContents("segment-only.json"));
+
+ // Should get a new result with the updated data
+ // Note: File watching on MacOS can take up to 10 seconds
+ result = nextResultFuture.get(15, TimeUnit.SECONDS);
+ assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
+ } finally {
+ synchronizer.close();
+ }
+ }
+ }
+ }
+}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataSourceBuildInputs.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataSourceBuildInputs.java
new file mode 100644
index 00000000..ad80c633
--- /dev/null
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataSourceBuildInputs.java
@@ -0,0 +1,28 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
+import com.launchdarkly.sdk.server.Components;
+import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints;
+import com.launchdarkly.sdk.server.subsystems.DataSourceBuildInputs;
+
+import java.util.concurrent.Executors;
+
+/**
+ * Test helper for creating DataSourceBuildInputs for FDv2 initializer and synchronizer tests.
+ */
+class TestDataSourceBuildInputs {
+ static DataSourceBuildInputs create(LDLogger logger) {
+ ServiceEndpoints endpoints = Components.serviceEndpoints().createServiceEndpoints();
+ return new DataSourceBuildInputs(
+ logger,
+ Thread.NORM_PRIORITY,
+ null, // dataSourceUpdates not needed for these tests
+ endpoints,
+ null, // http not needed for these tests
+ Executors.newSingleThreadScheduledExecutor(),
+ null, // diagnosticStore not needed
+ () -> Selector.EMPTY // SelectorSource returning empty selector
+ );
+ }
+}
From 0b18393db05b8162c7ce22fd0df75b778d5aee05 Mon Sep 17 00:00:00 2001
From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Date: Fri, 30 Jan 2026 13:15:31 -0800
Subject: [PATCH 02/15] Use try-with-resources in file initializer tests.
---
.../integrations/FileInitializerTest.java | 59 +++++++------------
1 file changed, 22 insertions(+), 37 deletions(-)
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java
index b655593d..05146c57 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileInitializerTest.java
@@ -23,11 +23,10 @@ public class FileInitializerTest {
@Test
public void initializerReturnsChangeSetOnSuccessfulLoad() throws Exception {
- Initializer initializer = FileData.initializer()
- .filePaths(resourceFilePath("all-properties.json"))
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Initializer initializer = FileData.initializer()
+ .filePaths(resourceFilePath("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = initializer.run();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
@@ -35,26 +34,21 @@ public void initializerReturnsChangeSetOnSuccessfulLoad() throws Exception {
assertThat(result.getChangeSet(), notNullValue());
assertThat(result.getChangeSet().getType(), equalTo(ChangeSetType.Full));
assertNotNull(result.getChangeSet().getData());
- } finally {
- initializer.close();
}
}
@Test
public void initializerReturnsTerminalErrorOnMissingFile() throws Exception {
- Initializer initializer = FileData.initializer()
- .filePaths(Paths.get("no-such-file.json"))
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Initializer initializer = FileData.initializer()
+ .filePaths(Paths.get("no-such-file.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = initializer.run();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.TERMINAL_ERROR));
assertNotNull(result.getStatus().getErrorInfo());
- } finally {
- initializer.close();
}
}
@@ -76,60 +70,51 @@ public void initializerReturnsShutdownWhenClosedBeforeRun() throws Exception {
@Test
public void initializerCanLoadFromClasspathResource() throws Exception {
- Initializer initializer = FileData.initializer()
- .classpathResources(FileDataSourceTestData.resourceLocation("all-properties.json"))
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Initializer initializer = FileData.initializer()
+ .classpathResources(FileDataSourceTestData.resourceLocation("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = initializer.run();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
assertThat(result.getChangeSet(), notNullValue());
- } finally {
- initializer.close();
}
}
@Test
public void initializerRespectsIgnoreDuplicateKeysHandling() throws Exception {
- Initializer initializer = FileData.initializer()
- .filePaths(
- resourceFilePath("flag-only.json"),
- resourceFilePath("flag-with-duplicate-key.json")
- )
- .duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE)
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Initializer initializer = FileData.initializer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE)
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = initializer.run();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
// Should succeed when ignoring duplicates
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
- } finally {
- initializer.close();
}
}
@Test
public void initializerFailsOnDuplicateKeysByDefault() throws Exception {
- Initializer initializer = FileData.initializer()
- .filePaths(
- resourceFilePath("flag-only.json"),
- resourceFilePath("flag-with-duplicate-key.json")
- )
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Initializer initializer = FileData.initializer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = initializer.run();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
// Should fail with terminal error when duplicate keys are not allowed
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.TERMINAL_ERROR));
- } finally {
- initializer.close();
}
}
}
From 1018c51445ef1d898eb252d0e45d8cb12567bfac Mon Sep 17 00:00:00 2001
From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Date: Fri, 30 Jan 2026 13:18:54 -0800
Subject: [PATCH 03/15] Use try-with-resources for synchronizer tests.
---
.../integrations/FileSynchronizerTest.java | 71 +-
sandbox/server-sandbox/.gitignore | 1 +
.../.gradle/7.6/checksums/checksums.lock | Bin 0 -> 17 bytes
.../.gradle/7.6/checksums/md5-checksums.bin | Bin 0 -> 18747 bytes
.../.gradle/7.6/checksums/sha1-checksums.bin | Bin 0 -> 19091 bytes
.../dependencies-accessors.lock | Bin 0 -> 17 bytes
.../7.6/dependencies-accessors/gc.properties | 0
.../7.6/executionHistory/executionHistory.bin | Bin 0 -> 394847 bytes
.../executionHistory/executionHistory.lock | Bin 0 -> 17 bytes
.../.gradle/7.6/fileChanges/last-build.bin | Bin 0 -> 1 bytes
.../.gradle/7.6/fileHashes/fileHashes.bin | Bin 0 -> 18797 bytes
.../.gradle/7.6/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes
.../7.6/fileHashes/resourceHashesCache.bin | Bin 0 -> 19687 bytes
.../server-sandbox/.gradle/7.6/gc.properties | 0
.../buildOutputCleanup.lock | Bin 0 -> 17 bytes
.../buildOutputCleanup/cache.properties | 2 +
.../buildOutputCleanup/outputFiles.bin | Bin 0 -> 18965 bytes
.../server-sandbox/.gradle/file-system.probe | Bin 0 -> 8 bytes
.../.gradle/vcs-1/gc.properties | 0
sandbox/server-sandbox/README.md | 59 ++
sandbox/server-sandbox/build.gradle | 29 +
.../build/classes/java/main/Hello$1.class | Bin 0 -> 726 bytes
.../build/classes/java/main/Hello.class | Bin 0 -> 6769 bytes
.../stash-dir/Hello$1.class.uniqueId0 | Bin 0 -> 726 bytes
.../stash-dir/Hello.class.uniqueId1 | Bin 0 -> 6763 bytes
.../compileJava/previous-compilation-data.bin | Bin 0 -> 18360 bytes
sandbox/server-sandbox/flagdata.json | 849 ++++++++++++++++++
.../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes
.../gradle/wrapper/gradle-wrapper.properties | 5 +
sandbox/server-sandbox/gradlew | 234 +++++
sandbox/server-sandbox/gradlew.bat | 89 ++
sandbox/server-sandbox/settings.gradle | 1 +
.../server-sandbox/src/main/java/Hello.java | 121 +++
33 files changed, 1416 insertions(+), 45 deletions(-)
create mode 100644 sandbox/server-sandbox/.gitignore
create mode 100644 sandbox/server-sandbox/.gradle/7.6/checksums/checksums.lock
create mode 100644 sandbox/server-sandbox/.gradle/7.6/checksums/md5-checksums.bin
create mode 100644 sandbox/server-sandbox/.gradle/7.6/checksums/sha1-checksums.bin
create mode 100644 sandbox/server-sandbox/.gradle/7.6/dependencies-accessors/dependencies-accessors.lock
create mode 100644 sandbox/server-sandbox/.gradle/7.6/dependencies-accessors/gc.properties
create mode 100644 sandbox/server-sandbox/.gradle/7.6/executionHistory/executionHistory.bin
create mode 100644 sandbox/server-sandbox/.gradle/7.6/executionHistory/executionHistory.lock
create mode 100644 sandbox/server-sandbox/.gradle/7.6/fileChanges/last-build.bin
create mode 100644 sandbox/server-sandbox/.gradle/7.6/fileHashes/fileHashes.bin
create mode 100644 sandbox/server-sandbox/.gradle/7.6/fileHashes/fileHashes.lock
create mode 100644 sandbox/server-sandbox/.gradle/7.6/fileHashes/resourceHashesCache.bin
create mode 100644 sandbox/server-sandbox/.gradle/7.6/gc.properties
create mode 100644 sandbox/server-sandbox/.gradle/buildOutputCleanup/buildOutputCleanup.lock
create mode 100644 sandbox/server-sandbox/.gradle/buildOutputCleanup/cache.properties
create mode 100644 sandbox/server-sandbox/.gradle/buildOutputCleanup/outputFiles.bin
create mode 100644 sandbox/server-sandbox/.gradle/file-system.probe
create mode 100644 sandbox/server-sandbox/.gradle/vcs-1/gc.properties
create mode 100644 sandbox/server-sandbox/README.md
create mode 100644 sandbox/server-sandbox/build.gradle
create mode 100644 sandbox/server-sandbox/build/classes/java/main/Hello$1.class
create mode 100644 sandbox/server-sandbox/build/classes/java/main/Hello.class
create mode 100644 sandbox/server-sandbox/build/tmp/compileJava/compileTransaction/stash-dir/Hello$1.class.uniqueId0
create mode 100644 sandbox/server-sandbox/build/tmp/compileJava/compileTransaction/stash-dir/Hello.class.uniqueId1
create mode 100644 sandbox/server-sandbox/build/tmp/compileJava/previous-compilation-data.bin
create mode 100644 sandbox/server-sandbox/flagdata.json
create mode 100644 sandbox/server-sandbox/gradle/wrapper/gradle-wrapper.jar
create mode 100644 sandbox/server-sandbox/gradle/wrapper/gradle-wrapper.properties
create mode 100755 sandbox/server-sandbox/gradlew
create mode 100644 sandbox/server-sandbox/gradlew.bat
create mode 100644 sandbox/server-sandbox/settings.gradle
create mode 100644 sandbox/server-sandbox/src/main/java/Hello.java
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java
index 17abddb0..172b96f6 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/FileSynchronizerTest.java
@@ -26,11 +26,10 @@ public class FileSynchronizerTest {
@Test
public void synchronizerReturnsChangeSetOnSuccessfulLoad() throws Exception {
- Synchronizer synchronizer = FileData.synchronizer()
- .filePaths(resourceFilePath("all-properties.json"))
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(resourceFilePath("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = synchronizer.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
@@ -38,18 +37,15 @@ public void synchronizerReturnsChangeSetOnSuccessfulLoad() throws Exception {
assertThat(result.getChangeSet(), notNullValue());
assertThat(result.getChangeSet().getType(), equalTo(ChangeSetType.Full));
assertNotNull(result.getChangeSet().getData());
- } finally {
- synchronizer.close();
}
}
@Test
public void synchronizerReturnsInterruptedOnMissingFile() throws Exception {
- Synchronizer synchronizer = FileData.synchronizer()
- .filePaths(Paths.get("no-such-file.json"))
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(Paths.get("no-such-file.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = synchronizer.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
@@ -57,8 +53,6 @@ public void synchronizerReturnsInterruptedOnMissingFile() throws Exception {
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.INTERRUPTED));
assertNotNull(result.getStatus().getErrorInfo());
- } finally {
- synchronizer.close();
}
}
@@ -87,60 +81,51 @@ public void synchronizerReturnsShutdownWhenClosed() throws Exception {
@Test
public void synchronizerCanLoadFromClasspathResource() throws Exception {
- Synchronizer synchronizer = FileData.synchronizer()
- .classpathResources(FileDataSourceTestData.resourceLocation("all-properties.json"))
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Synchronizer synchronizer = FileData.synchronizer()
+ .classpathResources(FileDataSourceTestData.resourceLocation("all-properties.json"))
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = synchronizer.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
assertThat(result.getChangeSet(), notNullValue());
- } finally {
- synchronizer.close();
}
}
@Test
public void synchronizerRespectsIgnoreDuplicateKeysHandling() throws Exception {
- Synchronizer synchronizer = FileData.synchronizer()
- .filePaths(
- resourceFilePath("flag-only.json"),
- resourceFilePath("flag-with-duplicate-key.json")
- )
- .duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE)
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE)
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = synchronizer.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
// Should succeed when ignoring duplicates
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
- } finally {
- synchronizer.close();
}
}
@Test
public void synchronizerFailsOnDuplicateKeysByDefault() throws Exception {
- Synchronizer synchronizer = FileData.synchronizer()
- .filePaths(
- resourceFilePath("flag-only.json"),
- resourceFilePath("flag-with-duplicate-key.json")
- )
- .build(TestDataSourceBuildInputs.create(testLogger));
- try {
+ try (Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(
+ resourceFilePath("flag-only.json"),
+ resourceFilePath("flag-with-duplicate-key.json")
+ )
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
CompletableFuture resultFuture = synchronizer.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
// Should fail with interrupted error when duplicate keys are not allowed
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.STATUS));
assertThat(result.getStatus().getState(), equalTo(FDv2SourceResult.State.INTERRUPTED));
- } finally {
- synchronizer.close();
}
}
@@ -150,12 +135,10 @@ public void synchronizerAutoUpdateEmitsNewResultOnFileChange() throws Exception
try (TempFile file = dir.tempFile(".json")) {
file.setContents(getResourceContents("flag-only.json"));
- Synchronizer synchronizer = FileData.synchronizer()
- .filePaths(file.getPath())
- .autoUpdate(true)
- .build(TestDataSourceBuildInputs.create(testLogger));
-
- try {
+ try (Synchronizer synchronizer = FileData.synchronizer()
+ .filePaths(file.getPath())
+ .autoUpdate(true)
+ .build(TestDataSourceBuildInputs.create(testLogger))) {
// Get initial result
CompletableFuture resultFuture = synchronizer.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
@@ -172,8 +155,6 @@ public void synchronizerAutoUpdateEmitsNewResultOnFileChange() throws Exception
// Note: File watching on MacOS can take up to 10 seconds
result = nextResultFuture.get(15, TimeUnit.SECONDS);
assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET));
- } finally {
- synchronizer.close();
}
}
}
diff --git a/sandbox/server-sandbox/.gitignore b/sandbox/server-sandbox/.gitignore
new file mode 100644
index 00000000..9f11b755
--- /dev/null
+++ b/sandbox/server-sandbox/.gitignore
@@ -0,0 +1 @@
+.idea/
diff --git a/sandbox/server-sandbox/.gradle/7.6/checksums/checksums.lock b/sandbox/server-sandbox/.gradle/7.6/checksums/checksums.lock
new file mode 100644
index 0000000000000000000000000000000000000000..f0b654169da283bddb5923018a7f71ed95b7a8c4
GIT binary patch
literal 17
TcmZR!x@xpaCbT7m0Rp-JDe42#
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/checksums/md5-checksums.bin b/sandbox/server-sandbox/.gradle/7.6/checksums/md5-checksums.bin
new file mode 100644
index 0000000000000000000000000000000000000000..841f12f289ecaa8d210d1dde08793dac9c746f0c
GIT binary patch
literal 18747
zcmeI(-z!659LMp;k6mmwTQa7WP|8YL7HiT$xuD$PM!6v=%KVHQrD+%P52&Fe7b~U2
zjKY-*?vz^5DAU@d-HLdQb56UM;#%*gp67h~p6Aok^SV8qZXsNbPko5oaOO%X1Q0*~
z0R#|0009ILKmY**5I_I{1Q0*~0R#~EF9L(|A{Om3uEs*OurCVXwaUU2I(*&OnZ8-j
zUia)7-w%=_by-hv=w0t8jD9-RKCpea>(=_cW}{z~ID0G~cUJ5B=ZwD3tW0`mYchJj
z%jg?V*biZ1{F
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/checksums/sha1-checksums.bin b/sandbox/server-sandbox/.gradle/7.6/checksums/sha1-checksums.bin
new file mode 100644
index 0000000000000000000000000000000000000000..13b67b088b88d1aa968317070d5d67d3c61c0b4c
GIT binary patch
literal 19091
zcmeI(TS!xJ90%}|Gsv}2LUXg3zNn4TWLnA6!D2y^A_OmuRiIK?nG16WvM?2U@nH|5
zbQyWcE-Fe6yvBMNh8B@sh^5F(E5!%3haxat|HJ?PS7ILMG30yToc;OkeD^!&v&Zci
z1wk3+XPdCTMXWEef&c^{009U<00Izz00bZa0SG_<0uX=z1Rwwb2tZ)72;`6(k<*(|
z?hg_dq2+>*tRRc+)vl%6E!SS!=-Vb0|NX!*uM9qzelmvT&v@>+`6hSk?3v3f|ITx-
zb@h`cv8jvZnF&1qHj|!x|HA_{JHM9aib8Mbr0d5d&5zIUJTmCTtKS7#4)!@`d9E=&
zuG2?k9AoEq@jN9&em9~A#
zsK2oAX!w1;p~@Mb(HdP+To;|zxp!=zI^
z{=G%6a9O%l)^w?%=c75wU~gMdy>^ceeYU=3v)B_C^8UbCQms*)Z8x2@QLDJ1DC)p@
zi>Xm(t{HgRAkUBO4yUt@9-^)0hk>)CT4Yw5bgM1KxoYiLP8!guZ@e4xd5zQUWQO=H
P+sKn8;&$XKIcWa?^>REa
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/dependencies-accessors/dependencies-accessors.lock b/sandbox/server-sandbox/.gradle/7.6/dependencies-accessors/dependencies-accessors.lock
new file mode 100644
index 0000000000000000000000000000000000000000..e96cce416be4705ac4f89dfe8a0d1b6b538826b2
GIT binary patch
literal 17
ScmZR!GETS}VY*C>0SW*mD+2!j
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/dependencies-accessors/gc.properties b/sandbox/server-sandbox/.gradle/7.6/dependencies-accessors/gc.properties
new file mode 100644
index 00000000..e69de29b
diff --git a/sandbox/server-sandbox/.gradle/7.6/executionHistory/executionHistory.bin b/sandbox/server-sandbox/.gradle/7.6/executionHistory/executionHistory.bin
new file mode 100644
index 0000000000000000000000000000000000000000..b58c7df49ee9a4c1d67f8177a2efb00ef79b150a
GIT binary patch
literal 394847
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
I+dn%0KZKS`DgXcg
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/executionHistory/executionHistory.lock b/sandbox/server-sandbox/.gradle/7.6/executionHistory/executionHistory.lock
new file mode 100644
index 0000000000000000000000000000000000000000..90f0c57bc023ed036520e92e67579deb01f4a877
GIT binary patch
literal 17
VcmZQx;=h$y-pOey0~jzC0stkz16cq7
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/fileChanges/last-build.bin b/sandbox/server-sandbox/.gradle/7.6/fileChanges/last-build.bin
new file mode 100644
index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d
GIT binary patch
literal 1
IcmZPo000310RR91
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/fileHashes/fileHashes.bin b/sandbox/server-sandbox/.gradle/7.6/fileHashes/fileHashes.bin
new file mode 100644
index 0000000000000000000000000000000000000000..15d9a7d9bc1942b8fcb8aba1e966fffbc51a627f
GIT binary patch
literal 18797
zcmeI(%PT}t0LSrb#v?{%EM$yFm9bFaRxhHmqCsFKgC
zdENc%+TKpdh>|y}xq6rLS+U0z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**
z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1bz#o$PX5IMmVRTG08u^D);EVpz`H%*kSLN
zB)4hbxT0*)&^v!9Nc`KVvejz}-E~2Y)uw@takIQBH$rw|z8l^93$6{vx#1~nEUo5f
zVsucO9v$`Fu-F{p9hwW*#`g70@k+VJZphHhauK)l5
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/7.6/fileHashes/resourceHashesCache.bin b/sandbox/server-sandbox/.gradle/7.6/fileHashes/resourceHashesCache.bin
new file mode 100644
index 0000000000000000000000000000000000000000..631ef8581709a27dceb156c37957cfa70b6ab647
GIT binary patch
literal 19687
zcmeI(c}$ac9LMo03uvL`7$Ap&fPzJi1`3YKJ;h!yK|x_-pa_V_7@`{xj)8QQ$r&~j
z7B>)>GGN0^hs?-LIvEV8P(TDkj)i;C${`ty=6QZI>tkYKvSeAlB!wq$KKZqM0e{i%
zBayUJ*6h)-Q}MSe`pApEp>EeVZdh|3A3+XNu&pWf5
zpHJm$TeJ=q;Cpk_*OY(Y&+BK`Vx6-Mb+YAQiR=8iHmtMws4KB=*Txkb#ebaKT-4Pb
z7E*80mJRXqqfpn0)izyqSFOT2n}WLT_fx)iFNiDs5@ou57lfQdmG>1L)|s{*7(z|U>&S;j-l>GZWswX;{6Eg
zykyjUqk@hmk1%L>4?sQQlln#_vilbNob!M8nIUd)b72V9xxJ{PuOyNjZi+47GYEhH
z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x
zfB*=900@8p2>crY*6W9AuisRqHx=#?{({zSkEIOH`=u;bKjwPW`{!ciE-iuWqHk{^
zIW~J?hQCByBtGg%W9=;nXilXT{bKX|EO8+=?6wmtGCNO-@~ELoBWH*UO4S~r+Gy2|
zDL>Ik=4kGA;v$VdnRmcWMA6)QzG0!5+(cYl_st62K33A8xS6upzH8B#xTvwa!K8VG
zMt##-(;%vTOZt`xnP*#MRQi6XeVFZH3;l$F@r3l91u~0*vP&&VDy+cyl7P6@#Ps9D
z#j78w4fV8b?3ZoSe4WdKzFis8*ZIrbi#N@fz2sdrP-AA?7gs+aeFePCLejA|dAPFl
z1N{eHJ9|=drSI*KSqR6Ao_943X7cX#jfK6L)d!rR>4$EG!j;
z6;|9x4gWW`?5w>y2rH&0kz~K
ziChmOiIM*Yaq-Nv!}LJ6&ZW5pM%IUd^q2hIwkU{wDsIG?=njYs<-(oYiD!I9k<2d&
zR`ZA2*{ScA7jGag^n6@Q>aIk^-wDh~I`nOD5^>QiVEM25>qK1DFwf2yo|C>ON9Om`
WS^jGCa$;9xu*&BI1_;;(065(RH~;_u
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/buildOutputCleanup/cache.properties b/sandbox/server-sandbox/.gradle/buildOutputCleanup/cache.properties
new file mode 100644
index 00000000..3b11f709
--- /dev/null
+++ b/sandbox/server-sandbox/.gradle/buildOutputCleanup/cache.properties
@@ -0,0 +1,2 @@
+#Fri Jan 16 15:53:01 PST 2026
+gradle.version=7.6
diff --git a/sandbox/server-sandbox/.gradle/buildOutputCleanup/outputFiles.bin b/sandbox/server-sandbox/.gradle/buildOutputCleanup/outputFiles.bin
new file mode 100644
index 0000000000000000000000000000000000000000..b69e8600dc27447a927eea694c6d0763d302f3a6
GIT binary patch
literal 18965
zcmeI%T}V@57{Kw>LY9rCu;_MNd_`+n&E2lck~$)|d2Led{m1E;Sv|W-d+_?}`(EGE
zOzGp=Lk@dIXnxNgnG@08+;;wQX|S)ecMRWrYExBtNq!hj)L*kd0nzUQTy}lj?Wnh
zV{2vqVeRkQ-6_t;-8*FeMeW1+&36ZH)cB;|(*CvK`|kQ!Znn(%qCFPtUgfztP%OPo
z`_FI7E6=(d=cGrqPc98~72c_xkbYbHvZ;WTg?v^nfJW-9e*sDQOqzW%l|v+>?Z%yuYLn7VP=s4
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/file-system.probe b/sandbox/server-sandbox/.gradle/file-system.probe
new file mode 100644
index 0000000000000000000000000000000000000000..299e086949fdea46f85eb1175f5081466dbcbe3c
GIT binary patch
literal 8
PcmZQzV4TAv;J66@1g-(C
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/.gradle/vcs-1/gc.properties b/sandbox/server-sandbox/.gradle/vcs-1/gc.properties
new file mode 100644
index 00000000..e69de29b
diff --git a/sandbox/server-sandbox/README.md b/sandbox/server-sandbox/README.md
new file mode 100644
index 00000000..90a4fc66
--- /dev/null
+++ b/sandbox/server-sandbox/README.md
@@ -0,0 +1,59 @@
+# Server Sandbox
+
+A Java application for testing features in the LaunchDarkly Java Server SDK.
+
+## Building
+
+This project uses Gradle for building. The Gradle wrapper is included, so you don't need to install Gradle separately.
+
+```bash
+./gradlew build
+```
+
+## Dependency Resolution
+
+The project is configured to check for SDK dependencies in the following order:
+
+1. **mavenLocal** - For local development, build and publish the SDK locally first:
+ ```bash
+ cd ../../lib/sdk/server
+ ./gradlew publishToMavenLocal
+ ```
+
+2. **mavenCentral** - Falls back to published versions from Maven Central
+
+The build.gradle dependency uses a version range (7.10.+) to pick up the latest compatible version.
+
+## Running
+
+Set your LaunchDarkly SDK key either as an environment variable or by editing the SDK_KEY in Hello.java:
+
+```bash
+export LAUNCHDARKLY_SDK_KEY=your-sdk-key-here
+./gradlew run
+```
+
+Or optionally set a custom flag key:
+
+```bash
+export LAUNCHDARKLY_SDK_KEY=your-sdk-key-here
+export LAUNCHDARKLY_FLAG_KEY=your-flag-key
+./gradlew run
+```
+
+## What it does
+
+The Hello application:
+- Initializes the LaunchDarkly SDK client
+- Evaluates a feature flag (default: "sample-feature")
+- Displays the flag value and shows a banner if the flag is on
+- Listens for real-time flag changes
+- Demonstrates proper SDK lifecycle management
+
+## Development
+
+This sandbox is useful for:
+- Testing new SDK features during development
+- Reproducing customer issues
+- Validating SDK behavior changes
+- Quick experimentation with SDK APIs
diff --git a/sandbox/server-sandbox/build.gradle b/sandbox/server-sandbox/build.gradle
new file mode 100644
index 00000000..a8a68e6f
--- /dev/null
+++ b/sandbox/server-sandbox/build.gradle
@@ -0,0 +1,29 @@
+plugins {
+ id 'java'
+ id 'application'
+}
+
+group = 'com.launchdarkly'
+version = '1.0.0'
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+repositories {
+ // Check mavenLocal first for local SDK development
+ mavenLocal()
+ // Fall back to mavenCentral for published versions
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'com.launchdarkly:launchdarkly-java-server-sdk:7.10.+'
+ implementation 'org.slf4j:slf4j-simple:1.7.36'
+}
+
+application {
+ mainClass = 'Hello'
+}
+
+run {
+ standardInput = System.in
+}
diff --git a/sandbox/server-sandbox/build/classes/java/main/Hello$1.class b/sandbox/server-sandbox/build/classes/java/main/Hello$1.class
new file mode 100644
index 0000000000000000000000000000000000000000..a35f1422783e48bbd8c0dce2ec0b7d6d7b894e3d
GIT binary patch
literal 726
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
E2hrfsCjbBd
literal 0
HcmV?d00001
diff --git a/sandbox/server-sandbox/build/tmp/compileJava/compileTransaction/stash-dir/Hello$1.class.uniqueId0 b/sandbox/server-sandbox/build/tmp/compileJava/compileTransaction/stash-dir/Hello$1.class.uniqueId0
new file mode 100644
index 0000000000000000000000000000000000000000..a35f1422783e48bbd8c0dce2ec0b7d6d7b894e3d
GIT binary patch
literal 726
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