diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java index 99112355..cd7593e7 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java @@ -255,6 +255,7 @@ public static class EvaluationSeriesContextParam { LDContext context; LDValue defaultValue; String method; + String environmentId; } public static class IdentifyEventParams { diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestHook.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestHook.java index ac979d07..bb16db72 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestHook.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestHook.java @@ -44,6 +44,7 @@ public Map 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; @@ -72,6 +73,7 @@ public Map 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; diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java index faf35246..7db5cc3e 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java @@ -34,6 +34,7 @@ public class TestService { "event-gzip", "event-sampling", "filtering", + "hook-environment-id", "inline-context-all", "migrations", "optional-event-gzip", diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapter.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapter.java index 3281088e..7b73e1a0 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapter.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapter.java @@ -131,6 +131,7 @@ public void close() { */ private static class ConvertingUpdateSink implements DataSourceUpdateSink { private final IterableAsyncQueue resultQueue; + private volatile String environmentId = null; public ConvertingUpdateSink(IterableAsyncQueue resultQueue) { this.resultQueue = resultQueue; @@ -144,7 +145,7 @@ public boolean init(DataStoreTypes.FullDataSet allData) { ChangeSetType.Full, Selector.EMPTY, allData.getData(), - null, + environmentId, allData.shouldPersist() ); resultQueue.put(FDv2SourceResult.changeSet(changeSet, false)); @@ -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 diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java index 5b8e5095..02cf81ac 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java @@ -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 onOutageErrorLog = null; // test instrumentation DataSourceUpdatesImpl( @@ -369,14 +370,29 @@ private static String describeErrorCount(Map.Entry 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>>> 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, diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java index 7b5df387..6558cc45 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystem.java @@ -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(); } /** diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java index 6f01f596..85b24b07 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java @@ -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; @@ -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); @@ -126,6 +132,11 @@ public FullDataSet 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); diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorWithHooks.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorWithHooks.java index aada825e..8e366aae 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorWithHooks.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorWithHooks.java @@ -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 @@ -21,13 +22,17 @@ class EvaluatorWithHooks implements EvaluatorInterface { private final EvaluatorInterface underlyingEvaluator; private final List hooks; private final LDLogger logger; + private final Supplier 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 hooks, LDLogger hooksLogger) { + EvaluatorWithHooks(EvaluatorInterface underlyingEvaluator, List hooks, LDLogger hooksLogger, + Supplier environmentIdSupplier) { + this.environmentIdSupplier = environmentIdSupplier; this.underlyingEvaluator = underlyingEvaluator; this.hooks = hooks; this.logger = hooksLogger; @@ -40,7 +45,8 @@ public EvalResultAndFlag evalAndFlag(String method, String featureKey, LDContext int size = hooks.size(); List seriesDataList = new ArrayList<>(size); - EvaluationSeriesContext seriesContext = new EvaluationSeriesContext(method, featureKey, context, defaultValue); + EvaluationSeriesContext seriesContext = new EvaluationSeriesContext(method, featureKey, context, defaultValue, + environmentIdSupplier.get()); Map emptyMap = Collections.emptyMap(); for (int i = 0; i < size; i++) { Hook currentHook = hooks.get(i); diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java index ef1d340c..e0e553a0 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv1DataSystem.java @@ -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; /** @@ -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); @@ -121,7 +124,8 @@ static FDv1DataSystem create( dataStoreStatusProvider, dataSourceStatusProvider, dataSource, - flagChanged + flagChanged, + dataSourceUpdates ); } @@ -155,6 +159,11 @@ public DataStoreStatusProvider getDataStoreStatusProvider() { return dataStoreStatusProvider; } + @Override + public String getEnvironmentId() { + return dataSourceUpdates.getEnvironmentId(); + } + @Override public void close() throws IOException { if (disposed) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java index 1fd870ba..65b67cf4 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSystem.java @@ -34,6 +34,7 @@ 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( @@ -41,8 +42,10 @@ private FDv2DataSystem( DataSource dataSource, DataSourceStatusProvider dataSourceStatusProvider, DataStoreStatusProvider dataStoreStatusProvider, - FlagChangeNotifier flagChanged + FlagChangeNotifier flagChanged, + DataSourceUpdatesImpl dataSourceUpdates ) { + this.dataSourceUpdates = dataSourceUpdates; this.store = store; this.dataSource = dataSource; this.dataStoreStatusProvider = dataStoreStatusProvider; @@ -192,7 +195,8 @@ static FDv2DataSystem create( dataSource, dataSourceStatusProvider, dataStoreStatusProvider, - flagChanged + flagChanged, + dataSourceUpdates ); } @@ -226,6 +230,11 @@ public DataStoreStatusProvider getDataStoreStatusProvider() { return dataStoreStatusProvider; } + @Override + public String getEnvironmentId() { + return dataSourceUpdates.getEnvironmentId(); + } + @Override public void close() throws IOException { if (disposed) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java index 71c79c3b..d369151a 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java @@ -23,4 +23,14 @@ interface FeatureRequestor extends Closeable { * @throws HttpErrorException for HTTP error responses */ FullDataSet 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; + } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java index ba0e4c93..54bbd6df 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -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 diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java index 99d63b55..73535aa0 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java @@ -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 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); diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java index 773ae7e8..a25a30bf 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java @@ -258,7 +258,12 @@ private boolean handleEvent(StreamEvent event, CompletableFuture 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); } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EvaluationSeriesContext.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EvaluationSeriesContext.java index 7961f28a..2e90d503 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EvaluationSeriesContext.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EvaluationSeriesContext.java @@ -32,6 +32,13 @@ 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. @@ -39,9 +46,22 @@ public class EvaluationSeriesContext { * @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; } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java index 622031d4..150fe944 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java @@ -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. + *

+ * 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) {} } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapterTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapterTest.java index 7d88f621..a2bb8f24 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapterTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceSynchronizerAdapterTest.java @@ -4,6 +4,7 @@ import com.launchdarkly.sdk.fdv2.SourceSignal; import com.launchdarkly.sdk.server.datasources.FDv2SourceResult; import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; import org.junit.After; @@ -17,6 +18,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; @@ -295,4 +297,30 @@ public boolean isDone() { return delegate.isDone(); } } + + /** + * Test that an environment ID reported through the FDv1 update sink is carried on the + * change sets produced for the FDv2 data system. + */ + @Test + public void environmentIdIsForwardedToChangeSets() throws Exception { + AtomicReference capturedSink = new AtomicReference<>(); + + DataSourceSynchronizerAdapter adapter = new DataSourceSynchronizerAdapter(sink -> { + capturedSink.set(sink); + return new MockDataSource(new CountDownLatch(1), null); + }); + resourcesToClose.add(adapter); + + CompletableFuture nextFuture = adapter.next(); + + capturedSink.get().setEnvironmentId("env-from-fdv1"); + capturedSink.get().init(DataBuilder.forStandardTypes().build()); + + FDv2SourceResult result = nextFuture.get(2, TimeUnit.SECONDS); + assertEquals(SourceResultType.CHANGE_SET, result.getResultType()); + assertEquals("env-from-fdv1", result.getChangeSet().getEnvironmentId()); + + adapter.close(); + } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java index 5df085f7..dbb7abe7 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java @@ -57,6 +57,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; @SuppressWarnings("javadoc") public class DataSourceUpdatesImplTest { @@ -1034,9 +1035,12 @@ public void applyFullChangeSetToLegacyStoreWithEnvironmentId() throws Exception ); updates.apply(changeSet); - // Note: Java SDK doesn't have InitMetadata/EnvironmentId support in the same way as C#, - // so this test just verifies the changeset is applied without error ItemDescriptor retrievedFlag1 = legacyStore.get(FEATURES, flag1.getKey()); assertThat(retrievedFlag1, is(org.hamcrest.Matchers.notNullValue())); + assertEquals("test-env-id", updates.getEnvironmentId()); + + // A later change set without an environment ID does not clear the retained value. + updates.apply(new ChangeSet<>(ChangeSetType.Full, Selector.make(2, "state2"), data, null, true)); + assertEquals("test-env-id", updates.getEnvironmentId()); } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorWithHookTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorWithHookTest.java index e88625f5..69d90c34 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorWithHookTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorWithHookTest.java @@ -6,6 +6,7 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import com.launchdarkly.sdk.server.integrations.EvaluationSeriesContext; import com.launchdarkly.sdk.server.integrations.Hook; import com.launchdarkly.sdk.server.integrations.HookMetadata; import org.junit.Test; @@ -47,7 +48,7 @@ public void beforeIsExecutedBeforeAfter() { assertTrue(beforeCalled.get()); return Collections.emptyMap(); }); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); } @@ -61,7 +62,7 @@ public void evaluationResultIsPassedToAfter() { when(mockHook.beforeEvaluation(any(), any())).thenReturn(Collections.emptyMap()); when(mockHook.afterEvaluation(any(), any(), any())).thenReturn(Collections.emptyMap()); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); verify(mockHook).afterEvaluation(any(), any(), eq(EvaluationDetail.fromValue(LDValue.of("aValue"), 0, EvaluationReason.fallthrough()))); @@ -95,7 +96,7 @@ public void afterExecutesInReverseOrder() { return Collections.emptyMap(); }); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Arrays.asList(mockHookA, mockHookB), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Arrays.asList(mockHookA, mockHookB), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); assertEquals(calls, Arrays.asList("hookABefore", "hookBBefore", "hookBAfter", "hookAAfter")); } @@ -110,7 +111,7 @@ public void beforeIsGivenEmptySeriesData() { when(mockHook.beforeEvaluation(any(), any())).thenReturn(Collections.emptyMap()); when(mockHook.afterEvaluation(any(), any(), eq(evalResult.getResult().getAnyType()))).thenReturn(Collections.emptyMap()); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); verify(mockHook).beforeEvaluation(any(), eq(Collections.emptyMap())); @@ -128,7 +129,7 @@ public void seriesDataFromBeforeIsPassedToAfter() { when(mockHook.beforeEvaluation(any(), any())).thenReturn(mockData); when(mockHook.afterEvaluation(any(), any(), any())).thenReturn(Collections.emptyMap()); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); verify(mockHook).afterEvaluation(any(), eq(mockData), any()); @@ -147,7 +148,7 @@ public void beforeThrowingErrorLeadsToEmptySeriesDataPassedToAfter() { when(mockHook.getMetadata()).thenReturn(new HookMetadata("mockHookName") {}); when(mockHook.afterEvaluation(any(), any(), any())).thenReturn(Collections.emptyMap()); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); verify(mockHook, times(1)).getMetadata(); @@ -186,11 +187,34 @@ public void oneHookThrowingErrorDoesNotAffectOtherHooks() { return Collections.emptyMap(); }); - EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Arrays.asList(mockHookA, mockHookB), LDLogger.none()); + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Arrays.asList(mockHookA, mockHookB), LDLogger.none(), () -> null); evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); assertEquals(calls, Arrays.asList("hookABefore", "hookBBefore", "hookBAfter", "hookAAfter")); verify(mockHookA).afterEvaluation(any(), eq(Collections.emptyMap()), any()); verify(mockHookB).afterEvaluation(any(), eq(mockData), any()); } + @Test + public void environmentIdIsProvidedToSeriesContext() { + EvalResultAndFlag evalResult = new EvalResultAndFlag(EvalResult.of(LDValue.of("aValue"), 0, EvaluationReason.fallthrough()), null); + EvaluatorInterface mockEvaluator = mock(EvaluatorInterface.class); + when(mockEvaluator.evalAndFlag(any(), any(), any(), any(), any(), any())).thenReturn(evalResult); + + Hook mockHook = mock(Hook.class); + List environmentIds = new ArrayList<>(); + when(mockHook.beforeEvaluation(any(), any())).thenAnswer((Answer>) invocation -> { + environmentIds.add(((EvaluationSeriesContext)invocation.getArgument(0)).environmentId); + return Collections.emptyMap(); + }); + when(mockHook.afterEvaluation(any(), any(), any())).thenAnswer((Answer>) invocation -> { + environmentIds.add(((EvaluationSeriesContext)invocation.getArgument(0)).environmentId); + return Collections.emptyMap(); + }); + + EvaluatorWithHooks evaluatorUnderTest = new EvaluatorWithHooks(mockEvaluator, Collections.singletonList(mockHook), + LDLogger.none(), () -> "the-environment-id"); + evaluatorUnderTest.evalAndFlag("aMethod", "aKey", LDContext.create("aKey"), LDValue.of("aDefault"), LDValueType.STRING, EvaluationOptions.NO_EVENTS); + + assertEquals(Arrays.asList("the-environment-id", "the-environment-id"), environmentIds); + } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java index 5d73e2ae..37e172ed 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java @@ -50,6 +50,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -410,4 +411,58 @@ private void withStatusQueue(ActionCanThrowAnyException