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 @@ -255,6 +255,7 @@ public static class EvaluationSeriesContextParam {
LDContext context;
LDValue defaultValue;
String method;
String environmentId;
}

public static class IdentifyEventParams {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public Map<String, Object> beforeEvaluation(EvaluationSeriesContext seriesContex
seriesContextParam.context = seriesContext.context;
seriesContextParam.defaultValue = seriesContext.defaultValue;
seriesContextParam.method = seriesContext.method;
seriesContextParam.environmentId = seriesContext.environmentId;
params.evaluationSeriesContext = seriesContextParam;

params.evaluationSeriesData = data;
Expand Down Expand Up @@ -72,6 +73,7 @@ public Map<String, Object> afterEvaluation(EvaluationSeriesContext seriesContext
seriesContextParam.context = seriesContext.context;
seriesContextParam.defaultValue = seriesContext.defaultValue;
seriesContextParam.method = seriesContext.method;
seriesContextParam.environmentId = seriesContext.environmentId;
params.evaluationSeriesContext = seriesContextParam;

params.evaluationSeriesData = data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class TestService {
"event-gzip",
"event-sampling",
"filtering",
"hook-environment-id",
"inline-context-all",
"migrations",
"optional-event-gzip",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public void close() {
*/
private static class ConvertingUpdateSink implements DataSourceUpdateSink {
private final IterableAsyncQueue<FDv2SourceResult> resultQueue;
private volatile String environmentId = null;

public ConvertingUpdateSink(IterableAsyncQueue<FDv2SourceResult> resultQueue) {
this.resultQueue = resultQueue;
Expand All @@ -144,7 +145,7 @@ public boolean init(DataStoreTypes.FullDataSet<ItemDescriptor> allData) {
ChangeSetType.Full,
Selector.EMPTY,
allData.getData(),
null,
environmentId,
allData.shouldPersist()
);
resultQueue.put(FDv2SourceResult.changeSet(changeSet, false));
Expand All @@ -166,13 +167,20 @@ public boolean upsert(DataKind kind, String key, ItemDescriptor item) {
ChangeSetType.Partial,
Selector.EMPTY,
data,
null,
environmentId,
true // default to true as this adapter is used for adapting FDv1 data sources which are always persistent
);
resultQueue.put(FDv2SourceResult.changeSet(changeSet, false));
return true;
}

@Override
public void setEnvironmentId(String environmentId) {
if (environmentId != null && !environmentId.isEmpty()) {
this.environmentId = environmentId;
}
}

@Override
public DataStoreStatusProvider getDataStoreStatusProvider() {
// This adapter doesn't use a data store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ final class DataSourceUpdatesImpl implements DataSourceUpdateSink, DataSourceUpd

private volatile Status currentStatus;
private volatile boolean lastStoreUpdateFailed = false;
private volatile String environmentId = null;
volatile Consumer<String> onOutageErrorLog = null; // test instrumentation

DataSourceUpdatesImpl(
Expand Down Expand Up @@ -369,14 +370,29 @@ private static String describeErrorCount(Map.Entry<ErrorInfo, Integer> entry) {
return entry.getKey() + " (" + entry.getValue() + (entry.getValue() == 1 ? " time" : " times") + ")";
}

@Override
public void setEnvironmentId(String environmentId) {
if (environmentId != null && !environmentId.isEmpty()) {
this.environmentId = environmentId;
}
}

String getEnvironmentId() {
return environmentId;
}

@Override
public boolean apply(ChangeSet<Iterable<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>>> changeSet) {
if (store instanceof TransactionalDataStore) {
return applyToTransactionalStore((TransactionalDataStore) store, changeSet);
boolean applied = store instanceof TransactionalDataStore
? applyToTransactionalStore((TransactionalDataStore) store, changeSet)
// Legacy update path for non-transactional stores
: applyToLegacyStore(changeSet);

if (applied) {
setEnvironmentId(changeSet.getEnvironmentId());
}

// Legacy update path for non-transactional stores
return applyToLegacyStore(changeSet);

return applied;
}

private boolean applyToTransactionalStore(TransactionalDataStore transactionalDataStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ interface DataSystem {
* @return the data store status provider
*/
DataStoreStatusProvider getDataStoreStatusProvider();

/**
* Returns the ID of the LaunchDarkly environment the data came from, or null if it is not known.
*
* @return the environment ID, or null
*/
String getEnvironmentId();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private static void redactHeaderValueForLog(Request.Builder builder, Request req
@VisibleForTesting
final URI pollingUri;
private final Headers headers;
private volatile String environmentId = null;
private final Path cacheDir;
private final LDLogger logger;

Expand Down Expand Up @@ -92,6 +93,11 @@ private static void redactHeaderValueForLog(Request.Builder builder, Request req
httpClient = httpBuilder.build();
}

@Override
public String getEnvironmentId() {
return environmentId;
}

public void close() {
HttpProperties.shutdownHttpClient(httpClient);
Util.deleteDirectory(cacheDir);
Expand Down Expand Up @@ -126,6 +132,11 @@ public FullDataSet<ItemDescriptor> getAllData(boolean returnDataEvenIfCached)
throw new HttpErrorException(response.code());
}

String responseEnvironmentId = response.header(HeaderConstants.ENVIRONMENT_ID.getHeaderName());
if (responseEnvironmentId != null && !responseEnvironmentId.isEmpty()) {
environmentId = responseEnvironmentId;
}

JsonReader jr = new JsonReader(response.body().charStream());
// Polling data from LaunchDarkly should be persisted
return new FullDataSet<>(parseFullDataSet(jr), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

/**
* An {@link EvaluatorInterface} that will invoke the evaluation series methods of the provided {@link Hook} when
Expand All @@ -21,13 +22,17 @@ class EvaluatorWithHooks implements EvaluatorInterface {
private final EvaluatorInterface underlyingEvaluator;
private final List<Hook> hooks;
private final LDLogger logger;
private final Supplier<String> environmentIdSupplier;

/**
* @param underlyingEvaluator that will do the actual flag evaluation
* @param hooks that will be invoked at various stages of the evaluation series
* @param hooksLogger that will be used to log
* @param underlyingEvaluator that will do the actual flag evaluation
* @param hooks that will be invoked at various stages of the evaluation series
* @param hooksLogger that will be used to log
* @param environmentIdSupplier provides the environment ID reported by LaunchDarkly, if known
*/
EvaluatorWithHooks(EvaluatorInterface underlyingEvaluator, List<Hook> hooks, LDLogger hooksLogger) {
EvaluatorWithHooks(EvaluatorInterface underlyingEvaluator, List<Hook> hooks, LDLogger hooksLogger,
Supplier<String> environmentIdSupplier) {
this.environmentIdSupplier = environmentIdSupplier;
this.underlyingEvaluator = underlyingEvaluator;
this.hooks = hooks;
this.logger = hooksLogger;
Expand All @@ -40,7 +45,8 @@ public EvalResultAndFlag evalAndFlag(String method, String featureKey, LDContext
int size = hooks.size();
List<Map> seriesDataList = new ArrayList<>(size);

EvaluationSeriesContext seriesContext = new EvaluationSeriesContext(method, featureKey, context, defaultValue);
EvaluationSeriesContext seriesContext = new EvaluationSeriesContext(method, featureKey, context, defaultValue,
environmentIdSupplier.get());
Map<String, Object> emptyMap = Collections.emptyMap();
for (int i = 0; i < size; i++) {
Hook currentHook = hooks.get(i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class FDv1DataSystem implements DataSystem, Closeable {
private final FlagChangeNotifier flagChanged;
private final DataSourceStatusProvider dataSourceStatusProvider;
private final DataStoreStatusProvider dataStoreStatusProvider;
private final DataSourceUpdatesImpl dataSourceUpdates;
private boolean disposed = false;

/**
Expand Down Expand Up @@ -54,8 +55,10 @@ private FDv1DataSystem(
DataStoreStatusProvider dataStoreStatusProvider,
DataSourceStatusProvider dataSourceStatusProvider,
DataSource dataSource,
FlagChangeNotifier flagChanged
FlagChangeNotifier flagChanged,
DataSourceUpdatesImpl dataSourceUpdates
) {
this.dataSourceUpdates = dataSourceUpdates;
this.dataStoreStatusProvider = dataStoreStatusProvider;
this.dataSourceStatusProvider = dataSourceStatusProvider;
this.store = new ReadonlyStoreFacade(store);
Expand Down Expand Up @@ -121,7 +124,8 @@ static FDv1DataSystem create(
dataStoreStatusProvider,
dataSourceStatusProvider,
dataSource,
flagChanged
flagChanged,
dataSourceUpdates
);
}

Expand Down Expand Up @@ -155,6 +159,11 @@ public DataStoreStatusProvider getDataStoreStatusProvider() {
return dataStoreStatusProvider;
}

@Override
public String getEnvironmentId() {
return dataSourceUpdates.getEnvironmentId();
}

@Override
public void close() throws IOException {
if (disposed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,18 @@ final class FDv2DataSystem implements DataSystem, Closeable {
private final FlagChangeNotifier flagChanged;
private final DataSourceStatusProvider dataSourceStatusProvider;
private final DataStoreStatusProvider dataStoreStatusProvider;
private final DataSourceUpdatesImpl dataSourceUpdates;
private boolean disposed = false;

private FDv2DataSystem(
DataStore store,
DataSource dataSource,
DataSourceStatusProvider dataSourceStatusProvider,
DataStoreStatusProvider dataStoreStatusProvider,
FlagChangeNotifier flagChanged
FlagChangeNotifier flagChanged,
DataSourceUpdatesImpl dataSourceUpdates
) {
this.dataSourceUpdates = dataSourceUpdates;
this.store = store;
this.dataSource = dataSource;
this.dataStoreStatusProvider = dataStoreStatusProvider;
Expand Down Expand Up @@ -192,7 +195,8 @@ static FDv2DataSystem create(
dataSource,
dataSourceStatusProvider,
dataStoreStatusProvider,
flagChanged
flagChanged,
dataSourceUpdates
);
}

Expand Down Expand Up @@ -226,6 +230,11 @@ public DataStoreStatusProvider getDataStoreStatusProvider() {
return dataStoreStatusProvider;
}

@Override
public String getEnvironmentId() {
return dataSourceUpdates.getEnvironmentId();
}

@Override
public void close() throws IOException {
if (disposed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,14 @@ interface FeatureRequestor extends Closeable {
* @throws HttpErrorException for HTTP error responses
*/
FullDataSet<ItemDescriptor> getAllData(boolean returnDataEvenIfCached) throws IOException, HttpErrorException;

/**
* Returns the environment ID reported by LaunchDarkly in the most recent successful response, or
* null if none has been reported.
*
* @return the environment ID, or null
*/
default String getEnvironmentId() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,10 @@ public LDClient(String sdkKey, LDConfig config) {
this.evaluator = evaluator;
this.migrationEvaluator = new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger);
} else {
this.evaluator = new EvaluatorWithHooks(evaluator, allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
this.evaluator = new EvaluatorWithHooks(evaluator, allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME),
this.dataSystem::getEnvironmentId);
this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), allHooks,
this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME), this.dataSystem::getEnvironmentId);
}

// Create FlagTracker using the dataSystem's flag change notifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private void poll() {
// storing any data, then we would still want to parse and try to store it even if it's cached.
boolean alreadyInited = initialized.get();
FullDataSet<ItemDescriptor> allData = requestor.getAllData(!alreadyInited);
dataSourceUpdates.setEnvironmentId(requestor.getEnvironmentId());
if (allData == null) {
// This means it was cached, and alreadyInited was true
dataSourceUpdates.updateStatus(State.VALID, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,12 @@ private boolean handleEvent(StreamEvent event, CompletableFuture<Void> initFutur
}
logger.debug("Received StreamEvent: {}", event);
if (event instanceof MessageEvent) {
handleMessage((MessageEvent)event, initFuture);
MessageEvent messageEvent = (MessageEvent)event;
if (messageEvent.getHeaders() != null) {
dataSourceUpdates.setEnvironmentId(
messageEvent.getHeaders().value(HeaderConstants.ENVIRONMENT_ID.getHeaderName()));
}
handleMessage(messageEvent, initFuture);
} else if (event instanceof FaultEvent) {
return handleError(((FaultEvent)event).getCause(), initFuture);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,36 @@ public class EvaluationSeriesContext {
*/
public final LDValue defaultValue;

/**
* The ID of the LaunchDarkly environment the evaluated data came from, or null if it is not
* known. It is not known before the SDK has received a successful response from LaunchDarkly,
* or when the data came from a source other than LaunchDarkly.
*/
public final String environmentId;

/**
* @param method the variation method that was used to invoke the evaluation.
* @param key the key of the feature flag being evaluated.
* @param context the context the evaluation was for.
* @param defaultValue the user-provided default value for the evaluation.
*/
public EvaluationSeriesContext(String method, String key, LDContext context, LDValue defaultValue) {
this(method, key, context, defaultValue, null);
}

/**
* @param method the variation method that was used to invoke the evaluation.
* @param key the key of the feature flag being evaluated.
* @param context the context the evaluation was for.
* @param defaultValue the user-provided default value for the evaluation.
* @param environmentId the ID of the LaunchDarkly environment, or null if it is not known.
*/
public EvaluationSeriesContext(String method, String key, LDContext context, LDValue defaultValue,
String environmentId) {
this.flagKey = key;
this.context = context;
this.defaultValue = defaultValue;
this.method = method;
this.environmentId = environmentId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,14 @@ public interface DataSourceUpdateSink {
* @see DataSourceStatusProvider
*/
void updateStatus(State newState, ErrorInfo newError);

/**
* Reports the ID of the LaunchDarkly environment that the data came from, as reported by
* LaunchDarkly in a successful response. The SDK makes this available to hook implementations.
* <p>
* Null or empty values are ignored and do not clear a previously reported ID.
*
* @param environmentId the environment ID reported by LaunchDarkly
*/
default void setEnvironmentId(String environmentId) {}
}
Loading
Loading