com.launchdarkly.sdk.server.DataModelDependencies Maven / Gradle / Ivy
Show all versions of launchdarkly-java-server-sdk Show documentation
package com.launchdarkly.sdk.server;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.server.DataModel.Operator;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.common.collect.Iterables.transform;
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
/**
* Implements a dependency graph ordering for data to be stored in a data store.
*
* We use this to order the data that we pass to {@link com.launchdarkly.sdk.server.interfaces.DataStore#init(FullDataSet)},
* and also to determine which flags are affected by a change if the application is listening for flag change events.
*
* Dependencies are defined as follows: there is a dependency from flag F to flag G if F is a prerequisite flag for
* G, or transitively for any of G's prerequisites; there is a dependency from flag F to segment S if F contains a
* rule with a segmentMatch clause that uses S. Therefore, if G or S is modified or deleted then F may be affected,
* and if we must populate the store non-atomically then G and S should be added before F.
*
* @since 4.6.1
*/
abstract class DataModelDependencies {
private DataModelDependencies() {}
static class KindAndKey {
final DataKind kind;
final String key;
public KindAndKey(DataKind kind, String key) {
this.kind = kind;
this.key = key;
}
@Override
public boolean equals(Object other) {
if (other instanceof KindAndKey) {
KindAndKey o = (KindAndKey)other;
return kind == o.kind && key.equals(o.key);
}
return false;
}
@Override
public int hashCode() {
return kind.hashCode() * 31 + key.hashCode();
}
}
/**
* Returns the immediate dependencies from the given item.
*
* @param fromKind the item's kind
* @param fromItem the item descriptor
* @return the flags and/or segments that this item depends on
*/
public static Set computeDependenciesFrom(DataKind fromKind, ItemDescriptor fromItem) {
if (fromItem == null || fromItem.getItem() == null) {
return emptySet();
}
if (fromKind == FEATURES) {
DataModel.FeatureFlag flag = (DataModel.FeatureFlag)fromItem.getItem();
Iterable prereqFlagKeys = transform(flag.getPrerequisites(), p -> p.getKey());
Iterable segmentKeys = concat(
transform(
flag.getRules(),
rule -> concat(
Iterables.>transform(
rule.getClauses(),
clause -> clause.getOp() == Operator.segmentMatch ?
transform(clause.getValues(), LDValue::stringValue) :
emptyList()
)
)
)
);
return ImmutableSet.copyOf(
concat(
transform(prereqFlagKeys, key -> new KindAndKey(FEATURES, key)),
transform(segmentKeys, key -> new KindAndKey(SEGMENTS, key))
)
);
}
return emptySet();
}
/**
* Returns a copy of the input data set that guarantees that if you iterate through it the outer list and
* the inner list in the order provided, any object that depends on another object will be updated after it.
*
* @param allData the unordered data set
* @return a map with a defined ordering
*/
public static FullDataSet sortAllCollections(FullDataSet allData) {
ImmutableSortedMap.Builder> builder =
ImmutableSortedMap.orderedBy(dataKindPriorityOrder);
for (Map.Entry> entry: allData.getData()) {
DataKind kind = entry.getKey();
builder.put(kind, sortCollection(kind, entry.getValue()));
}
return new FullDataSet<>(builder.build().entrySet());
}
private static KeyedItems sortCollection(DataKind kind, KeyedItems input) {
if (!isDependencyOrdered(kind) || isEmpty(input.getItems())) {
return input;
}
Map remainingItems = new HashMap<>();
for (Map.Entry e: input.getItems()) {
remainingItems.put(e.getKey(), e.getValue());
}
ImmutableMap.Builder builder = ImmutableMap.builder();
// Note, ImmutableMap guarantees that the iteration order will be the same as the builder insertion order
while (!remainingItems.isEmpty()) {
// pick a random item that hasn't been updated yet
for (Map.Entry entry: remainingItems.entrySet()) {
addWithDependenciesFirst(kind, entry.getKey(), entry.getValue(), remainingItems, builder);
break;
}
}
return new KeyedItems<>(builder.build().entrySet());
}
private static void addWithDependenciesFirst(DataKind kind,
String key,
ItemDescriptor item,
Map remainingItems,
ImmutableMap.Builder builder) {
remainingItems.remove(key); // we won't need to visit this item again
for (KindAndKey dependency: computeDependenciesFrom(kind, item)) {
if (dependency.kind == kind) {
ItemDescriptor prereqItem = remainingItems.get(dependency.key);
if (prereqItem != null) {
addWithDependenciesFirst(kind, dependency.key, prereqItem, remainingItems, builder);
}
}
}
builder.put(key, item);
}
private static boolean isDependencyOrdered(DataKind kind) {
return kind == FEATURES;
}
private static int getPriority(DataKind kind) {
if (kind == FEATURES) {
return 1;
} else if (kind == SEGMENTS) {
return 0;
} else {
return kind.getName().length() + 2;
}
}
private static Comparator dataKindPriorityOrder = new Comparator() {
@Override
public int compare(DataKind o1, DataKind o2) {
return getPriority(o1) - getPriority(o2);
}
};
/**
* Maintains a bidirectional dependency graph that can be updated whenever an item has changed.
*/
static final class DependencyTracker {
private final Map> dependenciesFrom = new HashMap<>();
private final Map> dependenciesTo = new HashMap<>();
/**
* Updates the dependency graph when an item has changed.
*
* @param fromKind the changed item's kind
* @param fromKey the changed item's key
* @param fromItem the changed item
*/
public void updateDependenciesFrom(DataKind fromKind, String fromKey, ItemDescriptor fromItem) {
KindAndKey fromWhat = new KindAndKey(fromKind, fromKey);
Set updatedDependencies = computeDependenciesFrom(fromKind, fromItem); // never null
Set oldDependencySet = dependenciesFrom.get(fromWhat);
if (oldDependencySet != null) {
for (KindAndKey oldDep: oldDependencySet) {
Set depsToThisOldDep = dependenciesTo.get(oldDep);
if (depsToThisOldDep != null) {
// COVERAGE: cannot cause this condition in unit tests, it should never be null
depsToThisOldDep.remove(fromWhat);
}
}
}
dependenciesFrom.put(fromWhat, updatedDependencies);
for (KindAndKey newDep: updatedDependencies) {
Set depsToThisNewDep = dependenciesTo.get(newDep);
if (depsToThisNewDep == null) {
depsToThisNewDep = new HashSet<>();
dependenciesTo.put(newDep, depsToThisNewDep);
}
depsToThisNewDep.add(fromWhat);
}
}
public void reset() {
dependenciesFrom.clear();
dependenciesTo.clear();
}
/**
* Populates the given set with the union of the initial item and all items that directly or indirectly
* depend on it (based on the current state of the dependency graph).
*
* @param itemsOut an existing set to be updated
* @param initialModifiedItem an item that has been modified
*/
public void addAffectedItems(Set itemsOut, KindAndKey initialModifiedItem) {
if (!itemsOut.contains(initialModifiedItem)) {
itemsOut.add(initialModifiedItem);
Set affectedItems = dependenciesTo.get(initialModifiedItem);
if (affectedItems != null) {
for (KindAndKey affectedItem: affectedItems) {
addAffectedItems(itemsOut, affectedItem);
}
}
}
}
}
}