From cc24d67d605ea664beadc2b2c074579f71a28dcd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:43:46 +0000 Subject: [PATCH 1/3] feat: Add environment ID support for hooks Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../main/java/sdktest/Representations.java | 1 + .../src/main/java/sdktest/TestHook.java | 2 + .../src/main/java/sdktest/TestService.java | 1 + .../server/DataSourceSynchronizerAdapter.java | 12 +++- .../sdk/server/DataSourceUpdatesImpl.java | 26 +++++++-- .../launchdarkly/sdk/server/DataSystem.java | 7 +++ .../sdk/server/DefaultFeatureRequestor.java | 11 ++++ .../sdk/server/EvaluatorWithHooks.java | 16 ++++-- .../sdk/server/FDv1DataSystem.java | 13 ++++- .../sdk/server/FDv2DataSystem.java | 13 ++++- .../sdk/server/FeatureRequestor.java | 10 ++++ .../com/launchdarkly/sdk/server/LDClient.java | 6 +- .../sdk/server/PollingProcessor.java | 1 + .../sdk/server/StreamProcessor.java | 7 ++- .../integrations/EvaluationSeriesContext.java | 20 +++++++ .../subsystems/DataSourceUpdateSink.java | 10 ++++ .../DataSourceSynchronizerAdapterTest.java | 28 ++++++++++ .../sdk/server/DataSourceUpdatesImplTest.java | 8 ++- .../sdk/server/EvaluatorWithHookTest.java | 38 ++++++++++--- .../sdk/server/PollingProcessorTest.java | 55 +++++++++++++++++++ .../sdk/server/StreamProcessorTest.java | 34 ++++++++++++ .../sdk/server/TestComponents.java | 9 +++ 22 files changed, 300 insertions(+), 28 deletions(-) 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 Date: Tue, 18 Aug 2026 19:45:10 +0000 Subject: [PATCH 2/3] refactor: Carry the environment ID with the flag data into the store Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../sdk/server/DataModelDependencies.java | 2 +- .../server/DataSourceSynchronizerAdapter.java | 11 +++---- .../sdk/server/DataSourceUpdatesImpl.java | 29 +++++------------ .../sdk/server/DefaultFeatureRequestor.java | 14 ++------- .../sdk/server/FDv1DataSystem.java | 10 ++---- .../sdk/server/FDv2DataSystem.java | 10 ++---- .../sdk/server/FeatureRequestor.java | 10 ------ .../sdk/server/InMemoryDataStore.java | 23 +++++++++++--- .../server/PersistentDataStoreConverter.java | 2 +- .../server/PersistentDataStoreWrapper.java | 13 +++++++- .../sdk/server/PollingProcessor.java | 1 - .../sdk/server/StreamProcessor.java | 21 +++++++------ .../sdk/server/WriteThroughStore.java | 8 ++++- .../subsystems/DataSourceUpdateSink.java | 10 ------ .../sdk/server/subsystems/DataStore.java | 12 +++++++ .../sdk/server/subsystems/DataStoreTypes.java | 31 ++++++++++++++++--- .../DataSourceSynchronizerAdapterTest.java | 5 +-- .../sdk/server/DataSourceUpdatesImplTest.java | 13 ++++++-- .../sdk/server/InMemoryDataStoreTest.java | 18 +++++++++++ .../sdk/server/PollingProcessorTest.java | 15 ++++----- .../sdk/server/StreamProcessorTest.java | 6 ++-- .../sdk/server/TestComponents.java | 9 ------ 22 files changed, 153 insertions(+), 120 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java index 57a4a16c..17ca660e 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java @@ -131,7 +131,7 @@ public static FullDataSet sortAllCollections(FullDataSet(builder.build().entrySet(), allData.shouldPersist()); + return new FullDataSet<>(builder.build().entrySet(), allData.shouldPersist(), allData.getEnvironmentId()); } /** 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 7b73e1a0..ab98fde5 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; + // Only a full data set carries an environment ID; it is retained for subsequent partial updates. private volatile String environmentId = null; public ConvertingUpdateSink(IterableAsyncQueue resultQueue) { @@ -139,6 +140,9 @@ public ConvertingUpdateSink(IterableAsyncQueue resultQueue) { @Override public boolean init(DataStoreTypes.FullDataSet allData) { + if (allData.getEnvironmentId() != null && !allData.getEnvironmentId().isEmpty()) { + environmentId = allData.getEnvironmentId(); + } // Convert the full data set into a ChangeSet and emit it ChangeSet>>> changeSet = new ChangeSet<>( @@ -174,13 +178,6 @@ public boolean upsert(DataKind kind, String key, ItemDescriptor item) { 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 02cf81ac..2f945f64 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,7 +63,6 @@ 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( @@ -370,29 +369,14 @@ 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) { - boolean applied = store instanceof TransactionalDataStore - ? applyToTransactionalStore((TransactionalDataStore) store, changeSet) - // Legacy update path for non-transactional stores - : applyToLegacyStore(changeSet); - - if (applied) { - setEnvironmentId(changeSet.getEnvironmentId()); + if (store instanceof TransactionalDataStore) { + return applyToTransactionalStore((TransactionalDataStore) store, changeSet); } - - return applied; + + // Legacy update path for non-transactional stores + return applyToLegacyStore(changeSet); } private boolean applyToTransactionalStore(TransactionalDataStore transactionalDataStore, @@ -445,7 +429,8 @@ private boolean applyToLegacyStore(ChangeSet>>> unsortedChangeset) { // Convert ChangeSet to FullDataSet for legacy init path, preserving shouldPersist flag - return init(new FullDataSet<>(unsortedChangeset.getData(), unsortedChangeset.shouldPersist())); + return init(new FullDataSet<>(unsortedChangeset.getData(), unsortedChangeset.shouldPersist(), + unsortedChangeset.getEnvironmentId())); } private boolean applyPartialChangeSetToLegacyStore(ChangeSet>>> changeSet) { 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 85b24b07..abe46048 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,7 +53,6 @@ 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; @@ -93,11 +92,6 @@ 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); @@ -132,14 +126,10 @@ 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); + return new FullDataSet<>(parseFullDataSet(jr), true, + response.header(HeaderConstants.ENVIRONMENT_ID.getHeaderName())); } } } 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 e0e553a0..a03da864 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,7 +26,6 @@ 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; /** @@ -55,10 +54,8 @@ private FDv1DataSystem( DataStoreStatusProvider dataStoreStatusProvider, DataSourceStatusProvider dataSourceStatusProvider, DataSource dataSource, - FlagChangeNotifier flagChanged, - DataSourceUpdatesImpl dataSourceUpdates + FlagChangeNotifier flagChanged ) { - this.dataSourceUpdates = dataSourceUpdates; this.dataStoreStatusProvider = dataStoreStatusProvider; this.dataSourceStatusProvider = dataSourceStatusProvider; this.store = new ReadonlyStoreFacade(store); @@ -124,8 +121,7 @@ static FDv1DataSystem create( dataStoreStatusProvider, dataSourceStatusProvider, dataSource, - flagChanged, - dataSourceUpdates + flagChanged ); } @@ -161,7 +157,7 @@ public DataStoreStatusProvider getDataStoreStatusProvider() { @Override public String getEnvironmentId() { - return dataSourceUpdates.getEnvironmentId(); + return dataStore.getEnvironmentId(); } @Override 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 65b67cf4..5053625b 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,7 +34,6 @@ 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( @@ -42,10 +41,8 @@ private FDv2DataSystem( DataSource dataSource, DataSourceStatusProvider dataSourceStatusProvider, DataStoreStatusProvider dataStoreStatusProvider, - FlagChangeNotifier flagChanged, - DataSourceUpdatesImpl dataSourceUpdates + FlagChangeNotifier flagChanged ) { - this.dataSourceUpdates = dataSourceUpdates; this.store = store; this.dataSource = dataSource; this.dataStoreStatusProvider = dataStoreStatusProvider; @@ -195,8 +192,7 @@ static FDv2DataSystem create( dataSource, dataSourceStatusProvider, dataStoreStatusProvider, - flagChanged, - dataSourceUpdates + flagChanged ); } @@ -232,7 +228,7 @@ public DataStoreStatusProvider getDataStoreStatusProvider() { @Override public String getEnvironmentId() { - return dataSourceUpdates.getEnvironmentId(); + return store.getEnvironmentId(); } @Override 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 d369151a..71c79c3b 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,14 +23,4 @@ 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/InMemoryDataStore.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java index 2e394990..97d52c95 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java @@ -33,10 +33,11 @@ class InMemoryDataStore implements DataStore, TransactionalDataStore, CacheExpor private final Object selectorLock = new Object(); private volatile Selector selector = Selector.EMPTY; private volatile boolean shouldPersist = false; + private volatile String environmentId = null; @Override public void init(FullDataSet allData) { - applyFullPayload(allData.getData(), null, Selector.EMPTY, allData.shouldPersist()); + applyFullPayload(allData.getData(), allData.getEnvironmentId(), Selector.EMPTY, allData.shouldPersist()); } @Override @@ -128,7 +129,8 @@ public void apply(ChangeSet>> data, - Selector selector, boolean shouldPersist) { + String environmentId, Selector selector, boolean shouldPersist) { synchronized (this.writeLock) { // Build the complete updated dictionary before assigning to Items for transactional update ImmutableMap.Builder> itemsBuilder = ImmutableMap.builder(); @@ -192,6 +205,7 @@ private void applyPartialData(Iterable exportAll() { } // Preserve the shouldPersist value that was set when data was provided to this store - return new FullDataSet<>(builder.build(), this.shouldPersist); + return new FullDataSet<>(builder.build(), this.shouldPersist, this.environmentId); } } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreConverter.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreConverter.java index a47bfba3..54d47d62 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreConverter.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreConverter.java @@ -40,7 +40,7 @@ static FullDataSet toSerializedFormat( } // Preserve shouldPersist flag when converting formats - return new FullDataSet<>(builder.build(), inMemoryData.shouldPersist()); + return new FullDataSet<>(builder.build(), inMemoryData.shouldPersist(), inMemoryData.getEnvironmentId()); } /** diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java index ef0e5e8f..f080ee27 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java @@ -63,6 +63,7 @@ final class PersistentDataStoreWrapper implements DataStore, SettableCache, Disa // themselves remain alive until GC reclaims them; the LoadingCache loaders // are short-circuited because every touch site checks this flag first. private volatile boolean cacheDisabled; + private volatile String environmentId = null; PersistentDataStoreWrapper( final PersistentDataStore core, @@ -204,7 +205,12 @@ public void init(FullDataSet allData) { KeyedItems items = PersistentDataStoreConverter.serializeAll(kind, e0.getValue()); allBuilder.add(new AbstractMap.SimpleEntry<>(kind, items)); } - RuntimeException failure = initCore(new FullDataSet<>(allBuilder.build(), allData.shouldPersist())); + String newEnvironmentId = allData.getEnvironmentId(); + if (newEnvironmentId != null && !newEnvironmentId.isEmpty()) { + environmentId = newEnvironmentId; + } + RuntimeException failure = initCore(new FullDataSet<>(allBuilder.build(), allData.shouldPersist(), + allData.getEnvironmentId())); if (itemCache != null && allCache != null && !cacheDisabled) { itemCache.invalidateAll(); allCache.invalidateAll(); @@ -372,6 +378,11 @@ public CacheStats getCacheStats() { itemStats.evictionCount() + allStats.evictionCount()); } + @Override + public String getEnvironmentId() { + return environmentId; + } + private ItemDescriptor getAndDeserializeItem(DataKind kind, String key) { SerializedItemDescriptor maybeSerializedItem = core.get(kind, key); return maybeSerializedItem == null ? null : PersistentDataStoreConverter.deserialize(kind, maybeSerializedItem); 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 73535aa0..99d63b55 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,7 +94,6 @@ 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 a25a30bf..f4158d43 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 @@ -31,6 +31,7 @@ import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; import com.launchdarkly.sdk.server.subsystems.DataSource; import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import com.launchdarkly.sdk.server.subsystems.SerializationException; @@ -258,12 +259,7 @@ private boolean handleEvent(StreamEvent event, CompletableFuture initFutur } logger.debug("Received StreamEvent: {}", event); if (event instanceof MessageEvent) { - MessageEvent messageEvent = (MessageEvent)event; - if (messageEvent.getHeaders() != null) { - dataSourceUpdates.setEnvironmentId( - messageEvent.getHeaders().value(HeaderConstants.ENVIRONMENT_ID.getHeaderName())); - } - handleMessage(messageEvent, initFuture); + handleMessage((MessageEvent)event, initFuture); } else if (event instanceof FaultEvent) { return handleError(((FaultEvent)event).getCause(), initFuture); } @@ -274,7 +270,7 @@ private void handleMessage(MessageEvent event, CompletableFuture initFutur try { switch (event.getEventName()) { case PUT: - handlePut(event.getDataReader(), initFuture); + handlePut(event.getDataReader(), environmentIdOf(event), initFuture); break; case PATCH: @@ -336,12 +332,19 @@ private static boolean exceptionHasCause(Throwable e, Class c) { return e.getCause() != null && exceptionHasCause(e.getCause(), c); } - private void handlePut(Reader eventData, CompletableFuture initFuture) + private static String environmentIdOf(MessageEvent event) { + return event.getHeaders() == null ? null + : event.getHeaders().value(HeaderConstants.ENVIRONMENT_ID.getHeaderName()); + } + + private void handlePut(Reader eventData, String environmentId, CompletableFuture initFuture) throws StreamInputException, StreamStoreException { recordStreamInit(false); esStarted = 0; PutData putData = parseStreamJson(StreamProcessorEvents::parsePutData, eventData); - if (!dataSourceUpdates.init(putData.data)) { + FullDataSet data = new FullDataSet<>(putData.data.getData(), putData.data.shouldPersist(), + environmentId); + if (!dataSourceUpdates.init(data)) { throw new StreamStoreException(); } if (!initialized.getAndSet(true)) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/WriteThroughStore.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/WriteThroughStore.java index aa895228..4ef80ef3 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/WriteThroughStore.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/WriteThroughStore.java @@ -131,6 +131,11 @@ public Selector getSelector() { return txMemoryStore.getSelector(); } + @Override + public String getEnvironmentId() { + return memoryStore.getEnvironmentId(); + } + @Override public void close() throws IOException { memoryStore.close(); @@ -182,7 +187,8 @@ private boolean applyToLegacyPersistence(ChangeSet>>> sortedChangeSet) { // Preserve shouldPersist flag when converting ChangeSet to FullDataSet - persistentStore.init(new FullDataSet<>(sortedChangeSet.getData(), sortedChangeSet.shouldPersist())); + persistentStore.init(new FullDataSet<>(sortedChangeSet.getData(), sortedChangeSet.shouldPersist(), + sortedChangeSet.getEnvironmentId())); } /** 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 150fe944..622031d4 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,14 +84,4 @@ 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/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java index e2cd3632..83a707e6 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java @@ -104,4 +104,16 @@ public interface DataStore extends Closeable { * @return a cache statistics object, or null if not applicable */ CacheStats getCacheStats(); + + /** + * Returns the ID of the LaunchDarkly environment that the currently stored data came from. + *

+ * This is the environment ID that was provided with the data, if any. The SDK makes it available + * to hook implementations. + * + * @return the environment ID, or null if it is unknown + */ + default String getEnvironmentId() { + return null; + } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java index 0667bf4e..0978a883 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java @@ -263,6 +263,7 @@ public String toString() { public static final class FullDataSet { private final Iterable>> data; private final boolean shouldPersist; + private final String environmentId; /** * Returns the wrapped data set. @@ -286,14 +287,22 @@ public boolean shouldPersist() { return shouldPersist; } + /** + * Returns the ID of the LaunchDarkly environment that this data came from, if known. + * + * @return the environment ID, or null if it was not reported with the data + */ + public String getEnvironmentId() { + return environmentId; + } + /** * Constructs a new instance. Will default to shouldPersist = true and can be stored in a persistent store. * * @param data the data set */ public FullDataSet(Iterable>> data) { - this.data = data == null ? ImmutableList.of(): data; - this.shouldPersist = true; // default to true if not specified for backwards compatibility + this(data, true); // default to true if not specified for backwards compatibility } /** @@ -303,22 +312,36 @@ public FullDataSet(Iterable>> data) * @param shouldPersist true if the data should be persisted to persistent stores, false otherwise */ public FullDataSet(Iterable>> data, boolean shouldPersist) { + this(data, shouldPersist, null); + } + + /** + * Constructs a new instance. + * + * @param data the data set + * @param shouldPersist true if the data should be persisted to persistent stores, false otherwise + * @param environmentId the ID of the LaunchDarkly environment the data came from, or null if unknown + */ + public FullDataSet(Iterable>> data, boolean shouldPersist, + String environmentId) { this.data = data == null ? ImmutableList.of(): data; this.shouldPersist = shouldPersist; + this.environmentId = environmentId; } @Override public boolean equals(Object o) { if (o instanceof FullDataSet) { FullDataSet other = (FullDataSet)o; - return data.equals(other.data) && shouldPersist == other.shouldPersist; + return data.equals(other.data) && shouldPersist == other.shouldPersist + && Objects.equals(environmentId, other.environmentId); } return false; } @Override public int hashCode() { - return Objects.hash(data, shouldPersist); + return Objects.hash(data, shouldPersist, 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 a2bb8f24..d7e47173 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 @@ -6,6 +6,7 @@ import com.launchdarkly.sdk.server.subsystems.DataSource; import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; import org.junit.After; import org.junit.Test; @@ -314,8 +315,8 @@ public void environmentIdIsForwardedToChangeSets() throws Exception { CompletableFuture nextFuture = adapter.next(); - capturedSink.get().setEnvironmentId("env-from-fdv1"); - capturedSink.get().init(DataBuilder.forStandardTypes().build()); + capturedSink.get().init(new FullDataSet<>(DataBuilder.forStandardTypes().build().getData(), true, + "env-from-fdv1")); FDv2SourceResult result = nextFuture.get(2, TimeUnit.SECONDS); assertEquals(SourceResultType.CHANGE_SET, result.getResultType()); 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 dbb7abe7..1b448afe 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 @@ -164,10 +164,14 @@ private static ChangeSet private static class LegacyDataStore implements DataStore { private final Map> data = new HashMap<>(); + private String environmentId; @Override public void init(FullDataSet allData) { data.clear(); + if (allData.getEnvironmentId() != null) { + environmentId = allData.getEnvironmentId(); + } for (Map.Entry> kindEntry : allData.getData()) { DataKind kind = kindEntry.getKey(); Map items = new HashMap<>(); @@ -178,6 +182,11 @@ public void init(FullDataSet allData) { } } + @Override + public String getEnvironmentId() { + return environmentId; + } + @Override public boolean upsert(DataKind kind, String key, ItemDescriptor item) { Map items = data.get(kind); @@ -1037,10 +1046,10 @@ public void applyFullChangeSetToLegacyStoreWithEnvironmentId() throws Exception ItemDescriptor retrievedFlag1 = legacyStore.get(FEATURES, flag1.getKey()); assertThat(retrievedFlag1, is(org.hamcrest.Matchers.notNullValue())); - assertEquals("test-env-id", updates.getEnvironmentId()); + assertEquals("test-env-id", legacyStore.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()); + assertEquals("test-env-id", legacyStore.getEnvironmentId()); } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java index 68b9dd95..17aa5ae0 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java @@ -104,6 +104,24 @@ public void applyWithFullChangeSetReplacesAllData() { assertEquals(item3, result.getItem()); } + @Test + public void environmentIdIsRetainedFromDataAndNotClearedByLaterData() { + assertNull(typedStore().getEnvironmentId()); + + typedStore().init(new FullDataSet<>(ImmutableList.of(), true, "env-id")); + assertEquals("env-id", typedStore().getEnvironmentId()); + + typedStore().init(new FullDataSet<>(ImmutableList.of(), true, null)); + assertEquals("env-id", typedStore().getEnvironmentId()); + + typedStore().apply(new ChangeSet<>(ChangeSetType.Full, Selector.make(1, "state1"), ImmutableList.of(), "", true)); + assertEquals("env-id", typedStore().getEnvironmentId()); + + typedStore().apply(new ChangeSet<>(ChangeSetType.Full, Selector.make(2, "state2"), ImmutableList.of(), + "other-env-id", true)); + assertEquals("other-env-id", typedStore().getEnvironmentId()); + } + @Test public void applyWithFullChangeSetSetsSelector() { Selector selector = Selector.make(42, "test-state"); 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 37e172ed..2989775b 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 @@ -61,11 +61,12 @@ public class PollingProcessorTest extends BaseTest { private static final Duration BRIEF_INTERVAL = Duration.ofMillis(20); private MockDataSourceUpdates dataSourceUpdates; + private InMemoryDataStore dataStore; @Before public void setup() { - DataStore store = new InMemoryDataStore(); - dataSourceUpdates = TestComponents.dataSourceUpdates(store, new MockDataStoreStatusProvider()); + dataStore = new InMemoryDataStore(); + dataSourceUpdates = TestComponents.dataSourceUpdates(dataStore, new MockDataStoreStatusProvider()); } private PollingProcessor makeProcessor(URI baseUri, Duration pollInterval) { @@ -420,11 +421,11 @@ public void environmentIdIsCapturedFromPollResponseHeader() throws Exception { try (HttpServer server = HttpServer.start(pollingHandler)) { try (PollingProcessor pollingProcessor = makeProcessor(server.getUri(), LENGTHY_INTERVAL)) { - assertNull(dataSourceUpdates.getEnvironmentId()); + assertNull(dataStore.getEnvironmentId()); assertFutureIsCompleted(pollingProcessor.start(), 1, TimeUnit.SECONDS); - assertEquals("env-from-poll", dataSourceUpdates.getEnvironmentId()); + assertEquals("env-from-poll", dataStore.getEnvironmentId()); } } } @@ -440,7 +441,7 @@ public void environmentIdIsNullIfHeaderIsEmpty() throws Exception { try (PollingProcessor pollingProcessor = makeProcessor(server.getUri(), LENGTHY_INTERVAL)) { assertFutureIsCompleted(pollingProcessor.start(), 1, TimeUnit.SECONDS); - assertNull(dataSourceUpdates.getEnvironmentId()); + assertNull(dataStore.getEnvironmentId()); } } } @@ -456,12 +457,12 @@ public void environmentIdIsRetainedAfterPollFailure() throws Exception { try (HttpServer server = HttpServer.start(pollingHandler)) { try (PollingProcessor pollingProcessor = makeProcessor(server.getUri(), BRIEF_INTERVAL)) { assertFutureIsCompleted(pollingProcessor.start(), 1, TimeUnit.SECONDS); - assertEquals("env-from-poll", dataSourceUpdates.getEnvironmentId()); + assertEquals("env-from-poll", dataStore.getEnvironmentId()); handler.setError(503); Thread.sleep(100); - assertEquals("env-from-poll", dataSourceUpdates.getEnvironmentId()); + assertEquals("env-from-poll", dataStore.getEnvironmentId()); } } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java index 3e4ac949..0e5ab6f8 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java @@ -890,12 +890,12 @@ public void environmentIdIsCapturedFromStreamResponseHeader() throws Exception { try (HttpServer server = HttpServer.start(streamHandler)) { try (StreamProcessor sp = createStreamProcessor(null, server.getUri())) { - assertNull(dataSourceUpdates.getEnvironmentId()); + assertNull(dataStore.getEnvironmentId()); sp.start(); dataSourceUpdates.awaitInit(); - assertEquals("env-from-stream", dataSourceUpdates.getEnvironmentId()); + assertEquals("env-from-stream", dataStore.getEnvironmentId()); } } } @@ -907,7 +907,7 @@ public void environmentIdIsNullWhenStreamResponseHasNoHeader() throws Exception sp.start(); dataSourceUpdates.awaitInit(); - assertNull(dataSourceUpdates.getEnvironmentId()); + assertNull(dataStore.getEnvironmentId()); } } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java index d6c9535e..77b4d699 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java @@ -238,15 +238,6 @@ public boolean upsert(DataKind kind, String key, ItemDescriptor item) { return result; } - @Override - public void setEnvironmentId(String environmentId) { - wrappedInstance.setEnvironmentId(environmentId); - } - - public String getEnvironmentId() { - return wrappedInstance.getEnvironmentId(); - } - @Override public DataStoreStatusProvider getDataStoreStatusProvider() { return dataStoreStatusProvider; From fc78a70fea2007801c026d2f3e5282108a12456f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:50:09 +0000 Subject: [PATCH 3/3] fix: Only retain the environment ID after a successful persistent store init Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../sdk/server/PersistentDataStoreWrapper.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java index f080ee27..0851cf6e 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java @@ -205,10 +205,6 @@ public void init(FullDataSet allData) { KeyedItems items = PersistentDataStoreConverter.serializeAll(kind, e0.getValue()); allBuilder.add(new AbstractMap.SimpleEntry<>(kind, items)); } - String newEnvironmentId = allData.getEnvironmentId(); - if (newEnvironmentId != null && !newEnvironmentId.isEmpty()) { - environmentId = newEnvironmentId; - } RuntimeException failure = initCore(new FullDataSet<>(allBuilder.build(), allData.shouldPersist(), allData.getEnvironmentId())); if (itemCache != null && allCache != null && !cacheDisabled) { @@ -232,6 +228,10 @@ public void init(FullDataSet allData) { } if (failure == null || cacheIndefinitely) { inited.set(true); + String newEnvironmentId = allData.getEnvironmentId(); + if (newEnvironmentId != null && !newEnvironmentId.isEmpty()) { + environmentId = newEnvironmentId; + } } if (failure != null) { throw failure;