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 @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LD updates miss override dependents

Medium Severity

OverrideSinkImpl fans out change notifications over the merged view, but LaunchDarkly store updates still use only base-store dependencies. An override that references a LaunchDarkly segment or prerequisite will not notify that flag when the underlying item changes, so listeners can miss a real evaluation change.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2d12762. Configure here.

}
}

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

FlagChangeNotifier flagChanged = new FlagChangedFacade(dataSourceUpdates);

// 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);
}

return new FDv2DataSystem(
store,
dataSource,
dataSourceStatusProvider,
dataStoreStatusProvider,
flagChanged
flagChanged,
flagChangeBroadcaster,
overrideSource,
logger
);
}

Expand All @@ -203,6 +239,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 +273,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
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ public LDClient(String sdkKey, LDConfig config) {
this.dataSystem = FDv1DataSystem.create(baseLogger, config, context, context.getLogging());
}

EvaluatorInterface evaluator = new InputValidatingEvaluator(this.dataSystem.getStore(), bigSegmentStoreWrapper, eventProcessor, evaluationLogger);
EvaluatorInterface evaluator = new InputValidatingEvaluator(this.dataSystem.getStore(),
this.dataSystem.getOverrideLayer(), bigSegmentStoreWrapper, eventProcessor, evaluationLogger);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isFlagKnown ignores uninitialized overrides

Medium Severity

isFlagKnown still returns false as soon as the store is uninitialized, so an override-only flag that evaluate and allFlagsState already serve in that state is reported as unknown. Callers that gate on isFlagKnown will skip the override during the incident path this feature is meant to cover.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2d12762. Configure here.


// build environment metadata for plugins
SdkMetadata sdkMetadata;
Expand Down
Loading
Loading