Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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