Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ public int compare(DataKind o1, DataKind o2) {
/**
* Maintains a bidirectional dependency graph that can be updated whenever an item has changed.
*/
/**
* Dependency edges that live outside the LaunchDarkly data, such as those of the override
* layer's entries. Given an item, adds it and everything that depends on it through those edges.
*/
interface ExternalDependencies {
void addAffectedItems(Set<KindAndKey> itemsOut, KindAndKey item);
}

static final class DependencyTracker {
private final Map<KindAndKey, Set<KindAndKey>> dependenciesFrom = new HashMap<>();
private final Map<KindAndKey, Set<KindAndKey>> dependenciesTo = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Deque;
import java.util.Arrays;
import java.util.ArrayDeque;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
Expand Down Expand Up @@ -56,6 +59,10 @@ final class DataSourceUpdatesImpl implements DataSourceUpdateSink, DataSourceUpd
private final EventBroadcasterImpl<FlagChangeListener, FlagChangeEvent> flagChangeEventNotifier;
private final EventBroadcasterImpl<StatusListener, Status> dataSourceStatusNotifier;
private final DataModelDependencies.DependencyTracker dependencyTracker = new DataModelDependencies.DependencyTracker();
// Dependencies of definitions the data system does not hold. The override layer supplies its
// entries' dependencies, so that a change to a LaunchDarkly item also notifies the overridden
// flags that reference it. Null when no override source is configured.
private volatile DataModelDependencies.ExternalDependencies externalDependencies = null;
private final DataStoreStatusProvider dataStoreStatusProvider;
private final OutageTracker outageTracker;
private final Object stateLock = new Object();
Expand Down Expand Up @@ -84,6 +91,47 @@ final class DataSourceUpdatesImpl implements DataSourceUpdateSink, DataSourceUpd
currentStatus = new Status(State.INITIALIZING, Instant.now(), null);
}

/**
* Registers dependency edges that live outside the LaunchDarkly data, such as the override
* layer's. Called once during construction of the data system.
*/
void setExternalDependencies(DataModelDependencies.ExternalDependencies externalDependencies) {
this.externalDependencies = externalDependencies;
}

/**
* Adds the modified item and everything that depends on it to the set. The LaunchDarkly data and
* the external dependencies each know their own edges, and a chain can cross between them in
* either direction: an overridden flag can reference a LaunchDarkly segment, and a LaunchDarkly
* flag can have an overridden prerequisite. Each newly reached item is therefore expanded through
* both until nothing new is found.
*/
private void addAffectedItems(Set<KindAndKey> itemsOut, KindAndKey item) {
DataModelDependencies.ExternalDependencies external = externalDependencies;
if (external == null) {
dependencyTracker.addAffectedItems(itemsOut, item);
return;
}
Deque<KindAndKey> pending = new ArrayDeque<>();
pending.push(item);
while (!pending.isEmpty()) {
KindAndKey next = pending.pop();
// Each tracker gets its own set: a tracker skips an item that is already in the set it is
// given, so sharing one set would stop the second tracker at the starting item.
Set<KindAndKey> viaData = new HashSet<>();
dependencyTracker.addAffectedItems(viaData, next);
Set<KindAndKey> viaExternal = new HashSet<>();
external.addAffectedItems(viaExternal, next);
for (Set<KindAndKey> reached : Arrays.asList(viaData, viaExternal)) {
for (KindAndKey reachedItem : reached) {
if (itemsOut.add(reachedItem)) {
pending.push(reachedItem);
}
}
}
}
}

@Override
public boolean init(FullDataSet<ItemDescriptor> allData) {
Map<DataKind, Map<String, ItemDescriptor>> oldData = null;
Expand Down Expand Up @@ -132,7 +180,7 @@ public boolean upsert(DataKind kind, String key, ItemDescriptor item) {
dependencyTracker.updateDependenciesFrom(kind, key, item);
if (hasFlagChangeEventListeners()) {
Set<KindAndKey> affectedItems = new HashSet<>();
dependencyTracker.addAffectedItems(affectedItems, new KindAndKey(kind, key));
addAffectedItems(affectedItems, new KindAndKey(kind, key));
sendChangeEvents(affectedItems);
}
}
Expand Down Expand Up @@ -270,7 +318,7 @@ private Set<KindAndKey> computeChangedItemsForFullDataSet(Map<DataKind, Map<Stri
continue;
}
if (oldItem == null || newItem == null || oldItem.getVersion() < newItem.getVersion()) {
dependencyTracker.addAffectedItems(affectedItems, new KindAndKey(kind, key));
addAffectedItems(affectedItems, new KindAndKey(kind, key));
}
// Note that comparing the version numbers is sufficient; we don't have to compare every detail of the
// flag or segment configuration, because it's a basic underlying assumption of the entire LD data model
Expand Down Expand Up @@ -535,7 +583,7 @@ private Set<KindAndKey> handlePartialChangeset(
for (Map.Entry<String, ItemDescriptor> itemEntry: kindEntry.getValue().getItems()) {
String key = itemEntry.getKey();
dependencyTracker.updateDependenciesFrom(kind, key, itemEntry.getValue());
dependencyTracker.addAffectedItems(affectedItems, new KindAndKey(kind, key));
addAffectedItems(affectedItems, new KindAndKey(kind, key));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ interface DataSystem {
* @return the environment ID, or null
*/
String getEnvironmentId();

/**
* Returns the override layer, or null if no override source is configured. The layer holds the
* entries that take precedence over LaunchDarkly data; {@link #getStore()} already applies them.
* The client consults the layer directly only for the not-initialized short-circuit.
*
* @return the override layer, or null
*/
OverrideLayer getOverrideLayer();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ public String getEnvironmentId() {
return dataStore.getEnvironmentId();
}

@Override
public OverrideLayer getOverrideLayer() {
return null;
}

@Override
public void close() throws IOException {
if (disposed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.common.collect.ImmutableList;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.logging.LogValues;
import com.launchdarkly.sdk.server.datasources.Initializer;
import com.launchdarkly.sdk.server.datasources.SelectorSource;
import com.launchdarkly.sdk.server.datasources.Synchronizer;
Expand All @@ -15,6 +16,7 @@
import com.launchdarkly.sdk.server.subsystems.DataStore;
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
import com.launchdarkly.sdk.server.subsystems.DataSystemConfiguration;
import com.launchdarkly.sdk.server.subsystems.OverrideSource;

import java.io.Closeable;
import java.io.IOException;
Expand All @@ -34,21 +36,44 @@ final class FDv2DataSystem implements DataSystem, Closeable {
private final FlagChangeNotifier flagChanged;
private final DataSourceStatusProvider dataSourceStatusProvider;
private final DataStoreStatusProvider dataStoreStatusProvider;
private final LDLogger logger;
// The following are non-null only when an override source is configured. The layer holds the
// override entries. The overlay serves them in preference to the store's data at the store read
// boundary. The source populates the layer at runtime. None of them take part in
// initialization or data source status.
private final OverrideLayer overrideLayer;
private final OverrideSource overrideSource;
private final OverrideSinkImpl overrideSink;
private boolean disposed = false;

private FDv2DataSystem(
DataStore store,
DataSource dataSource,
DataSourceStatusProvider dataSourceStatusProvider,
DataStoreStatusProvider dataStoreStatusProvider,
FlagChangeNotifier flagChanged
FlagChangeNotifier flagChanged,
EventBroadcasterImpl<FlagChangeListener, FlagChangeEvent> flagChangeBroadcaster,
OverrideSource overrideSource,
LDLogger logger
) {
this.store = store;
this.dataSource = dataSource;
this.dataStoreStatusProvider = dataStoreStatusProvider;
this.dataSourceStatusProvider = dataSourceStatusProvider;
this.flagChanged = flagChanged;
this.readOnlyStore = new ReadonlyStoreFacade(store);
this.logger = logger;
ReadOnlyStore baseStore = new ReadonlyStoreFacade(store);
this.overrideSource = overrideSource;
if (overrideSource == null) {
this.overrideLayer = null;
this.overrideSink = null;
this.readOnlyStore = baseStore;
} else {
this.overrideLayer = new OverrideLayer();
this.overrideSink = new OverrideSinkImpl(overrideLayer, baseStore, flagChangeBroadcaster,
logger.subLogger(Loggers.DATA_SOURCE_LOGGER_NAME));
this.readOnlyStore = new OverrideOverlayStore(baseStore, overrideLayer);
Comment thread
kinyoklion marked this conversation as resolved.
}
}

private static class FactoryWrapper<TDataSource> implements FDv2DataSource.DataSourceFactory<TDataSource> {
Expand Down Expand Up @@ -187,13 +212,29 @@ static FDv2DataSystem create(

FlagChangeNotifier flagChanged = new FlagChangedFacade(dataSourceUpdates);

return new FDv2DataSystem(
// The override source is built like any other component. Invalid configuration fails here, the
// same way an invalid data source or data store configuration fails. An offline client starts
// no data sources and no override source.
OverrideSource overrideSource = null;
if (dataSystemConfiguration.getOverrideSource() != null && !config.offline) {
overrideSource = dataSystemConfiguration.getOverrideSource().build(clientContext);
}

FDv2DataSystem system = new FDv2DataSystem(
store,
dataSource,
dataSourceStatusProvider,
dataStoreStatusProvider,
flagChanged
flagChanged,
flagChangeBroadcaster,
overrideSource,
logger
);
if (system.overrideLayer != null) {
// A LaunchDarkly change must also notify the flags whose overrides depend on it.
dataSourceUpdates.setExternalDependencies(system.overrideLayer);
}
return system;
}

@Override
Expand All @@ -203,6 +244,12 @@ public ReadOnlyStore getStore() {

@Override
public Future<Void> start() {
if (overrideSource != null) {
// The source starts before the data source, so a source that loads synchronously has its
// overrides in place before the client begins evaluating. Its initial load is part of
// starting the client.
overrideSource.start(overrideSink);
}
return dataSource.start();
}

Expand Down Expand Up @@ -231,12 +278,25 @@ public String getEnvironmentId() {
return store.getEnvironmentId();
}

@Override
public OverrideLayer getOverrideLayer() {
return overrideLayer;
}

@Override
public void close() throws IOException {
if (disposed) {
return;
}
try {
if (overrideSource != null) {
try {
overrideSource.close();
} catch (IOException | RuntimeException e) {
logger.warn("Error closing override source: {}", LogValues.exceptionSummary(e));
logger.debug(LogValues.exceptionTrace(e));
}
}
dataSource.close();
store.close();
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,19 @@ public Builder add(
}

Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) {
// A consumer of this state sends individual events according to the tracking fields. An
// override-affected evaluation produces no individual events, so the state turns them off
// for this flag. The flag, its value, its version, and its reason stay.
boolean overrideAffected = eval.isOverrideAffected();
return add(
flag.getKey(),
eval.getValue(),
eval.isNoVariation() ? null : eval.getVariationIndex(),
eval.getReason(),
flag.getVersion(),
flag.isTrackEvents() || eval.isForceReasonTracking(),
eval.isForceReasonTracking(),
flag.getDebugEventsUntilDate(),
!overrideAffected && (flag.isTrackEvents() || eval.isForceReasonTracking()),
!overrideAffected && eval.isForceReasonTracking(),
overrideAffected ? null : flag.getDebugEventsUntilDate(),
eval.getPrerequisiteEvalRecords().stream()
.filter(record -> record.prereqOfFlag.getKey() == flag.getKey()) // only include top level prereqs
.map(record -> record.flag.getKey()) // map from prereq record to prereq key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
Expand All @@ -28,7 +29,11 @@ class InputValidatingEvaluator implements EvaluatorInterface {

private final Evaluator evaluator;
private final ReadOnlyStore store;
// Null when no override source is configured. The store already applies the layer. The layer is
// consulted directly only for the not-initialized short-circuit.
private final OverrideLayer overrideLayer;
private final LDLogger logger;
private final AtomicBoolean allFlagsOverridesOnlyWarningLogged = new AtomicBoolean(false);

// these are created at construction to avoid recreation during each evaluation
private final EvaluationRecorder evaluationEventRecorderWithDetails;
Expand All @@ -40,11 +45,14 @@ class InputValidatingEvaluator implements EvaluatorInterface {
* Creates an {@link InputValidatingEvaluator}
*
* @param store will be used to get flag data
* @param overrideLayer the override layer, or null if no override source is configured
* @param segmentStore will be used to get segment data
* @param eventProcessor will be used to record events during evaluations as necessary
* @param logger for logging messages and errors during evaluations
*/
InputValidatingEvaluator(ReadOnlyStore store, BigSegmentStoreWrapper segmentStore, @Nonnull EventProcessor eventProcessor, LDLogger logger) {
InputValidatingEvaluator(ReadOnlyStore store, @Nullable OverrideLayer overrideLayer, BigSegmentStoreWrapper segmentStore,
@Nonnull EventProcessor eventProcessor, LDLogger logger) {
this.overrideLayer = overrideLayer;
this.evaluator = new Evaluator(new Evaluator.Getters() {
public DataModel.FeatureFlag getFlag(String key) {
return InputValidatingEvaluator.getFlag(store, key);
Expand Down Expand Up @@ -101,7 +109,9 @@ public EvalResultAndFlag evalAndFlag(String method, String flagKey, LDContext co
*/
EvalResultAndFlag evaluate(String flagKey, LDContext context, LDValue defaultValue,
@Nullable LDValueType requireType, EvaluationRecorder recorder) {
if (!store.isInitialized()) {
// The override layer is consulted before the not-initialized short-circuit. A flag that the
// layer holds is served from it. Any other flag returns the not-ready default as before.
if (!store.isInitialized() && !hasOverrideFor(flagKey)) {
logger.warn("Evaluation called before client initialized for feature flag \"{}\"; data store unavailable, returning default value", flagKey);
recorder.recordEvaluationUnknownFlagError(flagKey, context, defaultValue, ErrorKind.CLIENT_NOT_READY);
return new EvalResultAndFlag(EvalResult.error(ErrorKind.CLIENT_NOT_READY, defaultValue), null);
Expand Down Expand Up @@ -163,8 +173,16 @@ public FeatureFlagsState allFlagsState(LDContext context, FlagsStateOption... op
FeatureFlagsState.Builder builder = FeatureFlagsState.builder(options);

if (!store.isInitialized()) {
logger.warn("allFlagsState() was called before client initialized; data store unavailable, returning no data");
return builder.valid(false).build();
// With no LaunchDarkly data, the store read below returns only the entries that the override
// layer holds. The resulting state contains only those flags.
if (overrideLayer == null || overrideLayer.isEmpty()) {
logger.warn("allFlagsState() was called before client initialized; data store unavailable, returning no data");
return builder.valid(false).build();
}
if (allFlagsOverridesOnlyWarningLogged.compareAndSet(false, true)) {
logger.warn("allFlagsState() was called before client initialized; returning only flags from the override layer."
+ " This message is logged once.");
}
}

if (context == null) {
Expand Down Expand Up @@ -214,6 +232,14 @@ private static DataModel.FeatureFlag getFlag(ReadOnlyStore store, String key) {
return item == null ? null : (DataModel.FeatureFlag) item.getItem();
}

private boolean hasOverrideFor(String flagKey) {
if (overrideLayer == null) {
return false;
}
DataStoreTypes.ItemDescriptor item = overrideLayer.get(FEATURES, flagKey);
return item != null && item.getItem() != null;
}

private static DataModel.Segment getSegment(ReadOnlyStore store, String key) {
DataStoreTypes.ItemDescriptor item = store.get(SEGMENTS, key);
return item == null ? null : (DataModel.Segment) item.getItem();
Expand Down
Loading
Loading