All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.sdk.server.DataModelDependencies Maven / Gradle / Ivy

There is a newer version: 7.5.0
Show newest version
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); } } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy