Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/sdk/server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ ext.versions = [
"guava": "32.0.1-jre",
"jackson": "2.11.2",
"launchdarklyJavaSdkCommon": "2.1.2",
"launchdarklyJavaSdkInternal": "1.5.1",
"launchdarklyJavaSdkInternal": "1.6.1",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reviewers: I come from the future, and 1.6.1 solves all of humanities problems.

"launchdarklyLogging": "1.1.0",
"okhttp": "4.12.0", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
"okhttpEventsource": "4.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.launchdarkly.sdk.server;

import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;

/**
* Optional interface for data stores that can export their entire contents.
* <p>
* This interface is used to enable recovery scenarios where a persistent store
* needs to be re-synchronized from an in-memory cache. Not all data stores need
* to implement this interface.
* <p>
* This is currently only for internal implementations.
*/
interface CacheExporter {
/**
* Exports all data from the cache across all known DataKinds.
*
* @return A FullDataSet containing all items in the cache. The data is a snapshot
* taken at the time of the call and may be stale immediately after return.
*/
FullDataSet<ItemDescriptor> exportAll();

/**
* Indicates if the cache has been populated with a full data set.
*
* @return true when the cache has been populated
*/
boolean isInitialized();
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats;
import com.launchdarkly.sdk.server.subsystems.DataStore;
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 com.launchdarkly.sdk.server.subsystems.TransactionalDataStore;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
* A thread-safe, versioned store for feature flags and related data based on a
Expand All @@ -20,21 +26,16 @@
* As of version 5.0.0, this is package-private; applications must use the factory method
* {@link Components#inMemoryDataStore()}.
*/
class InMemoryDataStore implements DataStore {
class InMemoryDataStore implements DataStore, TransactionalDataStore, CacheExporter {
private volatile ImmutableMap<DataKind, Map<String, ItemDescriptor>> allData = ImmutableMap.of();
private volatile boolean initialized = false;
private Object writeLock = new Object();
private final Object selectorLock = new Object();
private volatile Selector selector = Selector.EMPTY;

@Override
public void init(FullDataSet<ItemDescriptor> allData) {
synchronized (writeLock) {
ImmutableMap.Builder<DataKind, Map<String, ItemDescriptor>> newData = ImmutableMap.builder();
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> entry: allData.getData()) {
newData.put(entry.getKey(), ImmutableMap.copyOf(entry.getValue().getItems()));
}
this.allData = newData.build(); // replaces the entire map atomically
this.initialized = true;
}
applyFullPayload(allData.getData(), null, Selector.EMPTY);
}

@Override
Expand Down Expand Up @@ -118,4 +119,115 @@ public CacheStats getCacheStats() {
public void close() throws IOException {
return;
}

@Override
public void apply(ChangeSet<ItemDescriptor> changeSet) {
switch (changeSet.getType()) {
case Full:
applyFullPayload(changeSet.getData(), changeSet.getEnvironmentId(), changeSet.getSelector());
break;
case Partial:
applyPartialData(changeSet.getData(), changeSet.getSelector());
break;
case None:
break;
default:
// This represents an implementation error. The ChangeSetType was extended, but handling was not
// added.
throw new IllegalArgumentException("Unknown ChangeSetType: " + changeSet.getType());
}
}

@Override
public Selector getSelector() {
synchronized (selectorLock) {
return selector;
}
}

private void setSelector(Selector newSelector) {
synchronized (selectorLock) {
selector = newSelector;
}
}

private void applyPartialData(Iterable<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> data,
Selector selector) {
synchronized (writeLock) {
// Build the complete updated dictionary before assigning to Items for transactional update
ImmutableMap.Builder<DataKind, Map<String, ItemDescriptor>> itemsBuilder = ImmutableMap.builder();

// First, collect all kinds that will be updated
java.util.Set<DataKind> updatedKinds = new java.util.HashSet<>();
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> kindItemsPair : data) {
updatedKinds.add(kindItemsPair.getKey());
}

// Add all existing kinds that are NOT being updated
for (Map.Entry<DataKind, Map<String, ItemDescriptor>> existingEntry : allData.entrySet()) {
if (!updatedKinds.contains(existingEntry.getKey())) {
itemsBuilder.put(existingEntry.getKey(), existingEntry.getValue());
}
}

// Now process the updated kinds
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> kindItemsPair : data) {
DataKind kind = kindItemsPair.getKey();
// Use HashMap to allow overwriting, then convert to ImmutableMap
Map<String, ItemDescriptor> kindMap = new HashMap<>();

Map<String, ItemDescriptor> itemsOfKind = allData.get(kind);
if (itemsOfKind != null) {
kindMap.putAll(itemsOfKind);
}

// Overwrite/add items from the change set (HashMap.put overwrites existing keys)
for (Map.Entry<String, ItemDescriptor> keyValuePair : kindItemsPair.getValue().getItems()) {
kindMap.put(keyValuePair.getKey(), keyValuePair.getValue());
}

itemsBuilder.put(kind, ImmutableMap.copyOf(kindMap));
}

allData = itemsBuilder.build();
setSelector(selector);
}
}

private void applyFullPayload(Iterable<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> data,
String environmentId, Selector selector) {
ImmutableMap.Builder<DataKind, Map<String, ItemDescriptor>> itemsBuilder = ImmutableMap.builder();

for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> kindEntry : data) {
ImmutableMap.Builder<String, ItemDescriptor> kindItemsBuilder = ImmutableMap.builder();
for (Map.Entry<String, ItemDescriptor> e1 : kindEntry.getValue().getItems()) {
kindItemsBuilder.put(e1.getKey(), e1.getValue());
}
itemsBuilder.put(kindEntry.getKey(), kindItemsBuilder.build());
}

ImmutableMap<DataKind, Map<String, ItemDescriptor>> newItems = itemsBuilder.build();

synchronized (writeLock) {
allData = newItems;
initialized = true;
setSelector(selector);
}
}

@Override
public FullDataSet<ItemDescriptor> exportAll() {
synchronized (writeLock) {
ImmutableList.Builder<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> builder = ImmutableList.builder();

for (Map.Entry<DataKind, Map<String, ItemDescriptor>> kindEntry : allData.entrySet()) {
builder.add(new AbstractMap.SimpleEntry<>(
kindEntry.getKey(),
new KeyedItems<>(ImmutableList.copyOf(kindEntry.getValue().entrySet()))
));
}

return new FullDataSet<>(builder.build());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.launchdarkly.sdk.server.subsystems;

import com.google.common.collect.ImmutableList;
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;

import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -328,4 +329,117 @@ public int hashCode() {
return items.hashCode();
}
}

/**
* Enumeration that indicates if this change is a full or partial change.
* <p>
* 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
* </p>
*/
public enum ChangeSetType {
/**
* Represents a full store configuration which replaces all data currently in the store.
*/
Full,

/**
* Represents an incremental set of changes to be applied to the existing data in the store.
*/
Partial,

/**
* Indicates that there are no store changes.
*/
None
}

/**
* Represents a set of changes to apply to a store.
* <p>
* 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
* </p>
*
* @param <TItemDescriptor> will be {@link ItemDescriptor} or {@link SerializedItemDescriptor}
*/
public static final class ChangeSet<TItemDescriptor> {
private final ChangeSetType type;
private final Selector selector;
private final String environmentId;
private final Iterable<Map.Entry<DataKind, KeyedItems<TItemDescriptor>>> data;

/**
* Returns the type of the changeset.
*
* @return the changeset type
*/
public ChangeSetType getType() {
return type;
}

/**
* Returns the selector for this change. This selector will not be null, but it can be an empty selector.
*
* @return the selector
*/
public Selector getSelector() {
return selector;
}

/**
* Returns the environment ID associated with the change. This may not always be available, and when it is not,
* the value will be null.
*
* @return the environment ID, or null if not available
*/
public String getEnvironmentId() {
return environmentId;
}

/**
* Returns a list of changes.
*
* @return an enumeration of key-value pairs; may be empty, but will not be null
*/
public Iterable<Map.Entry<DataKind, KeyedItems<TItemDescriptor>>> getData() {
return data;
}

/**
* Constructs a new ChangeSet instance.
*
* @param type the type of the changeset
* @param selector the selector for this change
* @param data the list of changes
* @param environmentId the environment ID, or null if not available
*/
public ChangeSet(ChangeSetType type, Selector selector,
Iterable<Map.Entry<DataKind, KeyedItems<TItemDescriptor>>> data, String environmentId) {
this.type = type;
this.selector = selector;
this.data = data == null ? ImmutableList.of() : data;
this.environmentId = environmentId;
}

@Override
public boolean equals(Object o) {
if (o instanceof ChangeSet<?>) {
ChangeSet<?> other = (ChangeSet<?>)o;
return type == other.type && Objects.equals(selector, other.selector) &&
Objects.equals(environmentId, other.environmentId) && Objects.equals(data, other.data);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(type, selector, environmentId, data);
}

@Override
public String toString() {
return "ChangeSet(" + type + "," + selector + "," + environmentId + "," + data + ")";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.launchdarkly.sdk.server.subsystems;

import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSet;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;

/**
* Interface for a data store that holds feature flags and related data received by the SDK.
* This interface supports updating the store transactionally using ChangeSets.
* <p>
* Ordinarily, the only implementation of this interface is the default in-memory
* implementation, which holds references to actual SDK data model objects. Any data store
* implementation that uses an external store, such as a database, should instead use
* {@link PersistentDataStore}.
* <p>
* Implementations must be thread-safe.
* <p>
* This interface 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
*
* @see PersistentDataStore
*/
public interface TransactionalDataStore {
/**
* Apply the given change set to the store. This should be done atomically if possible.
*
* @param changeSet the changeset to apply
*/
void apply(ChangeSet<ItemDescriptor> changeSet);

/**
* Returns the selector for the currently stored data. The selector will be non-null but may be empty.
*
* @return the selector for the currently stored data
*/
Selector getSelector();
}
Loading