diff --git a/pkgs/sdk/server/src/Hooks/EvaluationSeriesContext.cs b/pkgs/sdk/server/src/Hooks/EvaluationSeriesContext.cs
index d49d5126..70714de8 100644
--- a/pkgs/sdk/server/src/Hooks/EvaluationSeriesContext.cs
+++ b/pkgs/sdk/server/src/Hooks/EvaluationSeriesContext.cs
@@ -25,6 +25,11 @@ public sealed class EvaluationSeriesContext {
///
public string Method { get; }
+ ///
+ /// The environment ID for the evaluation, or null if not available.
+ ///
+ public string EnvironmentId { get; }
+
///
/// Constructs a new EvaluationSeriesContext.
///
@@ -32,11 +37,14 @@ public sealed class EvaluationSeriesContext {
/// the context
/// the default value
/// the variation method
- public EvaluationSeriesContext(string flagKey, Context context, LdValue defaultValue, string method) {
+ /// the environment ID
+ public EvaluationSeriesContext(string flagKey, Context context, LdValue defaultValue, string method,
+ string environmentId = null) {
FlagKey = flagKey;
Context = context;
DefaultValue = defaultValue;
Method = method;
+ EnvironmentId = environmentId;
}
}
}
diff --git a/pkgs/sdk/server/src/Internal/DataSources/DataSourceUpdatesImpl.cs b/pkgs/sdk/server/src/Internal/DataSources/DataSourceUpdatesImpl.cs
index b3015ede..13364f16 100644
--- a/pkgs/sdk/server/src/Internal/DataSources/DataSourceUpdatesImpl.cs
+++ b/pkgs/sdk/server/src/Internal/DataSources/DataSourceUpdatesImpl.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
@@ -22,7 +23,7 @@ namespace LaunchDarkly.Sdk.Server.Internal.DataSources
/// This component is also responsible for receiving updates to the data source status, broadcasting
/// them to any status listeners, and tracking the length of any period of sustained failure.
///
- internal sealed class DataSourceUpdatesImpl : IDataSourceUpdates
+ internal sealed class DataSourceUpdatesImpl : IDataSourceUpdates, IDataSourceUpdatesHeaders
{
#region Private fields
@@ -84,7 +85,7 @@ internal DataSourceUpdatesImpl(
StateSince = DateTime.Now,
LastError = null
};
- _status = new StateMonitor(initialStatus, MaybeUpdateStatus, _log);
+ _status = new StateMonitor(initialStatus, MaybeUpdateStatus, _log);
}
#endregion
@@ -93,47 +94,7 @@ internal DataSourceUpdatesImpl(
public bool Init(FullDataSet allData)
{
- ImmutableDictionary> oldData = null;
-
- try
- {
- if (HasFlagChangeListeners())
- {
- // Query the existing data if any, so that after the update we can send events for
- // whatever was changed
- var oldDataBuilder = ImmutableDictionary.CreateBuilder>();
- foreach (var kind in DataModel.AllDataKinds)
- {
- var items = _store.GetAll(kind);
- oldDataBuilder.Add(kind, items.Items.ToImmutableDictionary());
- }
- oldData = oldDataBuilder.ToImmutable();
- }
- _store.Init(DataStoreSorter.SortAllCollections(allData));
- _lastStoreUpdateFailed = false;
- }
- catch (Exception e)
- {
- ReportStoreFailure(e);
- return false;
- }
-
- // Calling Init implies that the data source is now in a valid state.
- UpdateStatus(DataSourceState.Valid, null);
-
- // We must always update the dependency graph even if we don't currently have any event listeners, because if
- // listeners are added later, we don't want to have to reread the whole data store to compute the graph
- UpdateDependencyTrackerFromFullDataSet(allData);
-
- // Now, if we previously queried the old data because someone is listening for flag change events, compare
- // the versions of all items and generate events for those (and any other items that depend on them)
- if (oldData != null)
- {
- SendChangeEvents(ComputeChangedItemsForFullDataSet(oldData, FullDataSetToMap(allData)));
- }
-
- return true;
+ return InitWithHeaders(allData, null);
}
public bool Upsert(DataKind kind, string key, ItemDescriptor item)
@@ -326,5 +287,67 @@ private void ReportStoreFailure(Exception e)
}
#endregion
+
+ #region IDataSourceUpdatesHeaders methods
+ public bool InitWithHeaders(FullDataSet allData, IEnumerable>> headers)
+ {
+ ImmutableDictionary> oldData = null;
+
+ try
+ {
+ if (HasFlagChangeListeners())
+ {
+ // Query the existing data if any, so that after the update we can send events for
+ // whatever was changed
+ var oldDataBuilder = ImmutableDictionary.CreateBuilder>();
+ foreach (var kind in DataModel.AllDataKinds)
+ {
+ var items = _store.GetAll(kind);
+ oldDataBuilder.Add(kind, items.Items.ToImmutableDictionary());
+ }
+ oldData = oldDataBuilder.ToImmutable();
+ }
+
+ var sortedCollections = DataStoreSorter.SortAllCollections(allData);
+
+ if (_store is IDataStoreMetadata storeMetadata)
+ {
+ var environmentId = headers?.FirstOrDefault((item) =>
+ item.Key.ToLower() == HeaderConstants.EnvironmentId).Value
+ ?.FirstOrDefault();
+ storeMetadata.InitWithMetadata(sortedCollections, new InitMetadata(environmentId));
+ }
+ else
+ {
+ _store.Init(sortedCollections);
+ }
+
+ _lastStoreUpdateFailed = false;
+ }
+ catch (Exception e)
+ {
+ ReportStoreFailure(e);
+ return false;
+ }
+
+ // Calling Init implies that the data source is now in a valid state.
+ UpdateStatus(DataSourceState.Valid, null);
+
+ // We must always update the dependency graph even if we don't currently have any event listeners, because if
+ // listeners are added later, we don't want to have to reread the whole data store to compute the graph
+ UpdateDependencyTrackerFromFullDataSet(allData);
+
+ // Now, if we previously queried the old data because someone is listening for flag change events, compare
+ // the versions of all items and generate events for those (and any other items that depend on them)
+ if (oldData != null)
+ {
+ SendChangeEvents(ComputeChangedItemsForFullDataSet(oldData, FullDataSetToMap(allData)));
+ }
+
+ return true;
+ }
+ #endregion
+
}
}
diff --git a/pkgs/sdk/server/src/Internal/DataSources/FeatureRequestor.cs b/pkgs/sdk/server/src/Internal/DataSources/FeatureRequestor.cs
index 9d8506dd..f6b08645 100644
--- a/pkgs/sdk/server/src/Internal/DataSources/FeatureRequestor.cs
+++ b/pkgs/sdk/server/src/Internal/DataSources/FeatureRequestor.cs
@@ -17,6 +17,8 @@
namespace LaunchDarkly.Sdk.Server.Internal.DataSources
{
+ using BytesWithHeaders = Tuple>>>;
+
internal class FeatureRequestor : IFeatureRequestor
{
private readonly Uri _allUri;
@@ -51,19 +53,19 @@ private void Dispose(bool disposing)
// Returns a data set of the latest flags and segments, or null if they have not been modified. Throws an
// exception if there was a problem getting data.
- public async Task?> GetAllDataAsync()
+ public async Task GetAllDataAsync()
{
- var json = await GetAsync(_allUri);
- if (json is null)
+ var res = await GetAsync(_allUri);
+ if (res is null)
{
return null;
}
- var data = ParseAllData(json);
+ var data = ParseAllData(res.Item1);
Func countItems = kind =>
data.Data.FirstOrDefault(kv => kv.Key == kind).Value.Items?.Count() ?? 0;
_log.Debug("Get all returned {0} feature flags and {1} segments",
countItems(DataModel.Features), countItems(DataModel.Segments));
- return data;
+ return new DataSetWithHeaders(data, res.Item2);
}
private FullDataSet ParseAllData(byte[] json)
@@ -72,7 +74,7 @@ private FullDataSet ParseAllData(byte[] json)
return StreamProcessorEvents.ParseFullDataset(ref r);
}
- private async Task GetAsync(Uri path)
+ private async Task GetAsync(Uri path)
{
_log.Debug("Getting flags with uri: {0}", path.AbsoluteUri);
var request = new HttpRequestMessage(HttpMethod.Get, path);
@@ -113,7 +115,7 @@ private async Task GetAsync(Uri path)
}
}
var content = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
- return content.Length == 0 ? null : content;
+ return new BytesWithHeaders(content.Length == 0 ? null : content, response.Headers);
}
}
catch (TaskCanceledException tce)
diff --git a/pkgs/sdk/server/src/Internal/DataSources/IFeatureRequestor.cs b/pkgs/sdk/server/src/Internal/DataSources/IFeatureRequestor.cs
index 573c0dca..adb82f0f 100644
--- a/pkgs/sdk/server/src/Internal/DataSources/IFeatureRequestor.cs
+++ b/pkgs/sdk/server/src/Internal/DataSources/IFeatureRequestor.cs
@@ -1,12 +1,26 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes;
namespace LaunchDarkly.Sdk.Server.Internal.DataSources
{
+ internal class DataSetWithHeaders
+ {
+ public readonly FullDataSet? DataSet;
+ public readonly IEnumerable>> Headers;
+
+ public DataSetWithHeaders(FullDataSet? dataSet,
+ IEnumerable>> headers)
+ {
+ DataSet = dataSet;
+ Headers = headers;
+ }
+ }
+
internal interface IFeatureRequestor : IDisposable
{
- Task?> GetAllDataAsync();
+ Task GetAllDataAsync();
}
}
diff --git a/pkgs/sdk/server/src/Internal/DataSources/PollingDataSource.cs b/pkgs/sdk/server/src/Internal/DataSources/PollingDataSource.cs
index 3eb79aca..2540a763 100644
--- a/pkgs/sdk/server/src/Internal/DataSources/PollingDataSource.cs
+++ b/pkgs/sdk/server/src/Internal/DataSources/PollingDataSource.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -60,15 +61,15 @@ private async Task UpdateTaskAsync()
_log.Info("Polling LaunchDarkly for feature flag updates");
try
{
- var allData = await _featureRequestor.GetAllDataAsync();
- if (allData is null)
+ var dataAndHeaders = await _featureRequestor.GetAllDataAsync();
+ if (dataAndHeaders.DataSet is null)
{
// This means it was cached, and alreadyInited was true
_dataSourceUpdates.UpdateStatus(DataSourceState.Valid, null);
}
else
{
- if (_dataSourceUpdates.Init(allData.Value)) // this also automatically sets the state to Valid
+ if (InitWithHeaders(dataAndHeaders.DataSet.Value, dataAndHeaders.Headers)) // this also automatically sets the state to Valid
{
if (!_initialized.GetAndSet(true))
{
@@ -137,5 +138,16 @@ private void Dispose(bool disposing)
_featureRequestor.Dispose();
}
}
+
+ private bool InitWithHeaders(DataStoreTypes.FullDataSet allData,
+ IEnumerable>> headers)
+ {
+ if (_dataSourceUpdates is IDataSourceUpdatesHeaders dataSourceUpdatesHeaders)
+ {
+ return dataSourceUpdatesHeaders.InitWithHeaders(allData, headers);
+ }
+
+ return _dataSourceUpdates.Init(allData);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/pkgs/sdk/server/src/Internal/DataSources/StreamingDataSource.cs b/pkgs/sdk/server/src/Internal/DataSources/StreamingDataSource.cs
index 527c23f1..6adb575c 100644
--- a/pkgs/sdk/server/src/Internal/DataSources/StreamingDataSource.cs
+++ b/pkgs/sdk/server/src/Internal/DataSources/StreamingDataSource.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
@@ -43,6 +44,8 @@ internal class StreamingDataSource : IDataSource
private volatile bool _lastStoreUpdateFailed = false;
internal DateTime _esStarted; // exposed for testing
+ private IEnumerable>> _headers;
+
internal delegate IEventSource EventSourceCreator(Uri streamUri,
HttpConfiguration httpConfig);
@@ -139,6 +142,7 @@ private void RecordStreamInit(bool failed)
private void OnOpen(object sender, EventSource.StateChangedEventArgs e)
{
+ _headers = e.Headers;
_log.Debug("EventSource Opened");
RecordStreamInit(false);
}
@@ -238,13 +242,24 @@ private void OnError(object sender, EventSource.ExceptionEventArgs e)
}
}
+ private bool InitWithHeaders(FullDataSet allData,
+ IEnumerable>> headers)
+ {
+ if (_dataSourceUpdates is IDataSourceUpdatesHeaders dataSourceUpdatesHeaders)
+ {
+ return dataSourceUpdatesHeaders.InitWithHeaders(allData, headers);
+ }
+
+ return _dataSourceUpdates.Init(allData);
+ }
+
private void HandleMessage(string messageType, byte[] messageData)
{
switch (messageType)
{
case PUT:
var putData = ParsePutData(messageData);
- if (!_dataSourceUpdates.Init(putData.Data)) // this also automatically sets the state to Valid
+ if (!InitWithHeaders(putData.Data, _headers)) // this also automatically sets the state to Valid
{
throw new StreamStoreException("failed to write full data set to data store");
}
diff --git a/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs b/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs
index ebc86be0..fdbf68f3 100644
--- a/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs
+++ b/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs
@@ -12,39 +12,22 @@ namespace LaunchDarkly.Sdk.Server.Internal.DataStores
/// Application code cannot see this implementation class and uses
/// instead.
///
- internal class InMemoryDataStore : IDataStore
+ internal class InMemoryDataStore : IDataStore, IDataStoreMetadata
{
private readonly object WriterLock = new object();
private volatile ImmutableDictionary> Items =
ImmutableDictionary>.Empty;
private volatile bool _initialized = false;
+ private volatile InitMetadata _metadata;
+
internal InMemoryDataStore() { }
public bool StatusMonitoringEnabled => false;
public void Init(FullDataSet data)
{
- var itemsBuilder = ImmutableDictionary.CreateBuilder>();
-
- foreach (var kindEntry in data.Data)
- {
- var kindItemsBuilder = ImmutableDictionary.CreateBuilder();
- foreach (var e1 in kindEntry.Value.Items)
- {
- kindItemsBuilder.Add(e1.Key, e1.Value);
- }
-
- itemsBuilder.Add(kindEntry.Key, kindItemsBuilder.ToImmutable());
- }
-
- var newItems = itemsBuilder.ToImmutable();
-
- lock (WriterLock)
- {
- Items = newItems;
- _initialized = true;
- }
+ InitWithMetadata(data, new InitMetadata());
}
public ItemDescriptor? Get(DataKind kind, string key)
@@ -68,7 +51,7 @@ public KeyedItems GetAll(DataKind kind)
}
return KeyedItems.Empty();
}
-
+
public bool Upsert(DataKind kind, string key, ItemDescriptor item)
{
lock (WriterLock)
@@ -93,5 +76,35 @@ public bool Initialized()
}
public void Dispose() { }
+
+ public void InitWithMetadata(FullDataSet data, InitMetadata metadata)
+ {
+ var itemsBuilder = ImmutableDictionary.CreateBuilder>();
+
+ foreach (var kindEntry in data.Data)
+ {
+ var kindItemsBuilder = ImmutableDictionary.CreateBuilder();
+ foreach (var e1 in kindEntry.Value.Items)
+ {
+ kindItemsBuilder.Add(e1.Key, e1.Value);
+ }
+
+ itemsBuilder.Add(kindEntry.Key, kindItemsBuilder.ToImmutable());
+ }
+
+ var newItems = itemsBuilder.ToImmutable();
+
+ lock (WriterLock)
+ {
+ Items = newItems;
+ _metadata = metadata;
+ _initialized = true;
+ }
+ }
+
+ public InitMetadata GetMetadata()
+ {
+ return _metadata;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/pkgs/sdk/server/src/Internal/HeaderConstants.cs b/pkgs/sdk/server/src/Internal/HeaderConstants.cs
new file mode 100644
index 00000000..b6dfca8a
--- /dev/null
+++ b/pkgs/sdk/server/src/Internal/HeaderConstants.cs
@@ -0,0 +1,16 @@
+namespace LaunchDarkly.Sdk.Server.Internal
+{
+ ///
+ /// Constants for headers returned from, or sent to, LaunchDarkly.
+ ///
+ /// All header keys should be lowercase.
+ ///
+ ///
+ public static class HeaderConstants
+ {
+ ///
+ /// The LaunchDarkly environment ID. This is included in responses from streaming and polling endpoints.
+ ///
+ public static string EnvironmentId = "x-ld-envid";
+ }
+}
diff --git a/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj b/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj
index 278ed64a..2b98d6c1 100644
--- a/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj
+++ b/pkgs/sdk/server/src/LaunchDarkly.ServerSdk.csproj
@@ -40,7 +40,7 @@
-
+
diff --git a/pkgs/sdk/server/src/LdClient.cs b/pkgs/sdk/server/src/LdClient.cs
index ecc32f28..46f783df 100644
--- a/pkgs/sdk/server/src/LdClient.cs
+++ b/pkgs/sdk/server/src/LdClient.cs
@@ -428,7 +428,7 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[
private (EvaluationDetail, FeatureFlag) EvaluateWithHooks(string method, string key, Context context, LdValue defaultValue, LdValue.Converter converter,
bool checkType, EventFactory eventFactory)
{
- var evalSeriesContext = new EvaluationSeriesContext(key, context, defaultValue, method);
+ var evalSeriesContext = new EvaluationSeriesContext(key, context, defaultValue, method, GetEnvironmentId());
return _hookExecutor.EvaluationSeries(
evalSeriesContext,
converter,
@@ -681,6 +681,19 @@ private void Dispose(bool disposing)
}
}
+ ///
+ /// Get the environment ID.
+ ///
+ /// The environment ID, or null if one is not available
+ private string GetEnvironmentId()
+ {
+ if (_dataStore is IDataStoreMetadata dataStoreMetadata)
+ {
+ return dataStoreMetadata.GetMetadata()?.EnvironmentId;
+ }
+ return null;
+ }
+
#endregion
}
}
diff --git a/pkgs/sdk/server/src/Subsystems/DataStoreTypes.cs b/pkgs/sdk/server/src/Subsystems/DataStoreTypes.cs
index b1a9e35d..def684d3 100644
--- a/pkgs/sdk/server/src/Subsystems/DataStoreTypes.cs
+++ b/pkgs/sdk/server/src/Subsystems/DataStoreTypes.cs
@@ -155,7 +155,7 @@ public override string ToString()
return "DataKind(" + _name + ")";
}
}
-
+
///
/// A versioned item (or placeholder) storeable in an .
///
@@ -303,7 +303,7 @@ public FullDataSet(IEnumerable>>
/// Shortcut for constructing an empty data set.
///
/// an instance containing no data
- public static FullDataSet Empty() => new FullDataSet(null);
+ public static FullDataSet Empty() => new FullDataSet(null);
}
///
@@ -341,5 +341,22 @@ public KeyedItems(IEnumerable> items)
/// an instance containing no data
public static KeyedItems Empty() => new KeyedItems(null);
}
+
+ ///
+ /// Meta-data associated with feature store initialization.
+ ///
+ public sealed class InitMetadata
+ {
+ internal InitMetadata() {}
+ internal InitMetadata(string environmentId)
+ {
+ EnvironmentId = environmentId;
+ }
+
+ ///
+ /// The environment ID for the associated payload or null if the environment ID is not available.
+ ///
+ public string EnvironmentId { get; }
+ }
}
}
diff --git a/pkgs/sdk/server/src/Subsystems/IDataStoreMetadata.cs b/pkgs/sdk/server/src/Subsystems/IDataStoreMetadata.cs
new file mode 100644
index 00000000..1bddad74
--- /dev/null
+++ b/pkgs/sdk/server/src/Subsystems/IDataStoreMetadata.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+
+namespace LaunchDarkly.Sdk.Server.Subsystems
+{
+ ///
+ /// This interface is to allow extending init without a major version.
+ /// This interface should be removed in the next major version of the SDK and headers
+ /// should be added in the IDataStore interface.
+ ///
+ public interface IDataStoreMetadata
+ {
+ ///
+ /// Overwrites the store's contents with a set of items for each collection.
+ ///
+ ///
+ ///
+ /// All previous data should be discarded, regardless of versioning.
+ ///
+ ///
+ /// The update should be done atomically. If it cannot be done atomically, then the store
+ /// must first add or update each item in the same order that they are given in the input
+ /// data, and then delete any previously stored items that were not in the input data.
+ ///
+ ///
+ /// a list of instances and their
+ /// corresponding data sets
+ /// metadata assciated with the payload
+ void InitWithMetadata(DataStoreTypes.FullDataSet allData, DataStoreTypes.InitMetadata metadata);
+
+ ///
+ /// Metadata associated with the data store content.
+ ///
+ /// metadata associated with the store content
+ DataStoreTypes.InitMetadata GetMetadata();
+ }
+}
diff --git a/pkgs/sdk/server/src/Subsystems/IDataStoreUpdatesHeaders.cs b/pkgs/sdk/server/src/Subsystems/IDataStoreUpdatesHeaders.cs
new file mode 100644
index 00000000..3cde0bd4
--- /dev/null
+++ b/pkgs/sdk/server/src/Subsystems/IDataStoreUpdatesHeaders.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace LaunchDarkly.Sdk.Server.Subsystems
+{
+ ///
+ /// This interface is to allow extending init without a major version.
+ /// This interface should be removed in the next major version of the SDK and headers
+ /// should be added in the IDataStore interface.
+ ///
+ public interface IDataSourceUpdatesHeaders
+ {
+ ///
+ /// Completely overwrites the current contents of the data store with a set of items for each collection.
+ ///
+ /// a list of instances and their
+ /// corresponding data sets
+ /// response headers for the connection
+ /// true if the update succeeded, false if it failed
+ bool InitWithHeaders(DataStoreTypes.FullDataSet allData,
+ IEnumerable>> headers);
+ }
+}
diff --git a/pkgs/sdk/server/test/Internal/DataSources/PollingDataSourceTest.cs b/pkgs/sdk/server/test/Internal/DataSources/PollingDataSourceTest.cs
index a5e7f69d..422c4ce8 100644
--- a/pkgs/sdk/server/test/Internal/DataSources/PollingDataSourceTest.cs
+++ b/pkgs/sdk/server/test/Internal/DataSources/PollingDataSourceTest.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using LaunchDarkly.Sdk.Server.Internal.Model;
using LaunchDarkly.Sdk.Server.Subsystems;
using LaunchDarkly.TestHelpers.HttpTest;
@@ -74,6 +75,35 @@ public void SuccessfulRequestCausesDataToBeStoredAndDataSourceInitialized()
}
}
+ [Fact]
+ public void SuccessfulRequestCausesDataToBeStoredAndDataSourceInitializedMetadata()
+ {
+ CapturingDataSourceUpdatesWithHeaders updateSink = new CapturingDataSourceUpdatesWithHeaders();
+ using (var server = HttpServer.Start(PollingResponse(AllData)))
+ {
+ var builder = BasicConfig()
+ .DataSource(Components.PollingDataSource())
+ .ServiceEndpoints(Components.ServiceEndpoints().Polling(server.Uri));
+ var config = builder.Build();
+
+ using (var dataSource = config.DataSource.Build(ContextFrom(config).WithDataSourceUpdates(updateSink)))
+ {
+ var initTask = dataSource.Start();
+
+ var receivedData = updateSink.Inits.ExpectValue();
+ AssertHelpers.DataSetsEqual(AllData, receivedData.Item1);
+ // There should be some headers from polling, but we don't want to depend on exact values from
+ // the http server as it isn't implemented in this package.
+ Assert.NotEmpty(receivedData.Item2);
+
+ Assert.True(dataSource.Initialized);
+
+ Assert.True(initTask.IsCompleted);
+ Assert.False(initTask.IsFaulted);
+ }
+ }
+ }
+
[Theory]
[InlineData(401)]
[InlineData(403)]
diff --git a/pkgs/sdk/server/test/LdClientHooksTest.cs b/pkgs/sdk/server/test/LdClientHooksTest.cs
index 9600bced..d142d5c6 100644
--- a/pkgs/sdk/server/test/LdClientHooksTest.cs
+++ b/pkgs/sdk/server/test/LdClientHooksTest.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Linq;
using LaunchDarkly.Sdk.Server.Hooks;
+using LaunchDarkly.Sdk.Server.Subsystems;
using Xunit;
using Xunit.Abstractions;
@@ -14,8 +15,6 @@ namespace LaunchDarkly.Sdk.Server
public class LdClientHooksTest : BaseTest
{
-
-
private class TestHook : Hook
{
private readonly Callbacks _befores;
@@ -62,10 +61,7 @@ private Dictionary GenerateVariationMethods(Context context, strin
{
Method.BoolVariation, new Call()
{
- Variation = (client, value) =>
- {
- client.BoolVariation(flagKey, context, value.AsBool);
- },
+ Variation = (client, value) => { client.BoolVariation(flagKey, context, value.AsBool); },
Value = LdValue.Of(true)
}
},
@@ -73,20 +69,14 @@ private Dictionary GenerateVariationMethods(Context context, strin
{
Method.BoolVariationDetail, new Call()
{
- Variation = (client, value) =>
- {
- client.BoolVariationDetail(flagKey, context, value.AsBool);
- },
+ Variation = (client, value) => { client.BoolVariationDetail(flagKey, context, value.AsBool); },
Value = LdValue.Of(true)
}
},
{
Method.StringVariation, new Call()
{
- Variation = (client, value) =>
- {
- client.StringVariation(flagKey, context, value.AsString);
- },
+ Variation = (client, value) => { client.StringVariation(flagKey, context, value.AsString); },
Value = LdValue.Of("default")
}
},
@@ -103,30 +93,21 @@ private Dictionary GenerateVariationMethods(Context context, strin
{
Method.IntVariation, new Call()
{
- Variation = (client, value) =>
- {
- client.IntVariation(flagKey, context, value.AsInt);
- },
+ Variation = (client, value) => { client.IntVariation(flagKey, context, value.AsInt); },
Value = LdValue.Of(3)
}
},
{
Method.IntVariationDetail, new Call()
{
- Variation = (client, value) =>
- {
- client.IntVariationDetail(flagKey, context, value.AsInt);
- },
+ Variation = (client, value) => { client.IntVariationDetail(flagKey, context, value.AsInt); },
Value = LdValue.Of(3)
}
},
{
Method.DoubleVariation, new Call()
{
- Variation = (client, value) =>
- {
- client.DoubleVariation(flagKey, context, value.AsDouble);
- },
+ Variation = (client, value) => { client.DoubleVariation(flagKey, context, value.AsDouble); },
Value = LdValue.Of(3.14)
}
},
@@ -143,10 +124,7 @@ private Dictionary GenerateVariationMethods(Context context, strin
{
Method.FloatVariation, new Call()
{
- Variation = (client, value) =>
- {
- client.FloatVariation(flagKey, context, value.AsFloat);
- },
+ Variation = (client, value) => { client.FloatVariation(flagKey, context, value.AsFloat); },
Value = LdValue.Of(3.14f)
}
},
@@ -163,20 +141,14 @@ private Dictionary GenerateVariationMethods(Context context, strin
{
Method.JsonVariation, new Call()
{
- Variation = (client, value) =>
- {
- client.JsonVariation(flagKey, context, value);
- },
+ Variation = (client, value) => { client.JsonVariation(flagKey, context, value); },
Value = LdValue.ArrayFrom(new List() { LdValue.Of("foo"), LdValue.Of("bar") })
}
},
{
Method.JsonVariationDetail, new Call()
{
- Variation = (client, value) =>
- {
- client.JsonVariationDetail(flagKey, context, value);
- },
+ Variation = (client, value) => { client.JsonVariationDetail(flagKey, context, value); },
Value = LdValue.ArrayFrom(new List() { LdValue.Of("foo"), LdValue.Of("bar") })
}
}
@@ -228,7 +200,8 @@ public void ClientExecutesConfiguredHooks()
}
}
- foreach (var series in new Dictionary{{"beforeEvaluation", befores}, {"afterEvaluation", afters}})
+ foreach (var series in new Dictionary
+ { { "beforeEvaluation", befores }, { "afterEvaluation", afters } })
{
foreach (var kvp in series.Value)
{
@@ -251,6 +224,7 @@ public void ClientExecutesConfiguredHooks()
// The hook framework passes empty SeriesData into the beforeEvaluation stage.
Assert.True(seriesData.All(s => s.Equals(SeriesData.Empty)));
}
+
if (series.Key == "afterEvaluation")
{
// The test hook's beforeEvaluation stage returns a new SeriesData, check that it is correct.
@@ -259,5 +233,95 @@ public void ClientExecutesConfiguredHooks()
}
}
}
+
+
+ private class TestHookWithEnvironmentId : Hook
+ {
+ public string BeforeEnvironmentId { get; set; }
+ public string AfterEnvironmentId { get; set; }
+
+ public TestHookWithEnvironmentId(string name) : base(name)
+ {
+ }
+
+ public override SeriesData BeforeEvaluation(EvaluationSeriesContext context, SeriesData data)
+ {
+ BeforeEnvironmentId = context.EnvironmentId;
+ return data;
+ }
+
+ public override SeriesData AfterEvaluation(EvaluationSeriesContext context, SeriesData data,
+ EvaluationDetail detail)
+ {
+ AfterEnvironmentId = context.EnvironmentId;
+ return data;
+ }
+ }
+
+ private class MetadataDataStore : IDataStore, IDataStoreMetadata
+ {
+ public void Dispose()
+ {
+ }
+
+ public bool StatusMonitoringEnabled { get; }
+
+ public void Init(DataStoreTypes.FullDataSet allData)
+ {
+ }
+
+ public DataStoreTypes.ItemDescriptor? Get(DataStoreTypes.DataKind kind, string key)
+ {
+ return null;
+ }
+
+ public DataStoreTypes.KeyedItems GetAll(DataStoreTypes.DataKind kind)
+ {
+ return DataStoreTypes.KeyedItems.Empty();
+ }
+
+ public bool Upsert(DataStoreTypes.DataKind kind, string key, DataStoreTypes.ItemDescriptor item)
+ {
+ return true;
+ }
+
+ public bool Initialized()
+ {
+ return true;
+ }
+
+ public void InitWithMetadata(DataStoreTypes.FullDataSet allData,
+ DataStoreTypes.InitMetadata metadata)
+ {
+ }
+
+ public DataStoreTypes.InitMetadata GetMetadata()
+ {
+ return new DataStoreTypes.InitMetadata("environment-id");
+ }
+ }
+
+ private class MetadataDataStoreConfigurer : IComponentConfigurer
+ {
+ public IDataStore Build(LdClientContext context)
+ {
+ return new MetadataDataStore();
+ }
+ }
+
+ [Fact]
+ public void EvaluationContextIncludesEnvironmentIdWhenAvailable()
+ {
+ var testHook = new TestHookWithEnvironmentId("test");
+ var config = BasicConfig().DataStore(new MetadataDataStoreConfigurer())
+ .Hooks(Components.Hooks().Add(testHook)).Build();
+ using (var client = new LdClient(config))
+ {
+ client.BoolVariation("toaster", Context.New("user-key"));
+ }
+
+ Assert.Equal("environment-id", testHook.BeforeEnvironmentId);
+ Assert.Equal("environment-id", testHook.AfterEnvironmentId);
+ }
}
}
diff --git a/pkgs/sdk/server/test/MockComponents.cs b/pkgs/sdk/server/test/MockComponents.cs
index a581a552..ba60f95b 100644
--- a/pkgs/sdk/server/test/MockComponents.cs
+++ b/pkgs/sdk/server/test/MockComponents.cs
@@ -81,6 +81,56 @@ public bool Upsert(DataKind kind, string key, ItemDescriptor item)
}
}
+ public class CapturingDataSourceUpdatesWithHeaders : IDataSourceUpdates, IDataSourceUpdatesHeaders
+ {
+ internal readonly
+ EventSink, IEnumerable>>>>
+ Inits =
+ new EventSink,
+ IEnumerable>>>>();
+
+ internal readonly EventSink Upserts = new EventSink();
+ internal readonly EventSink StatusUpdates = new EventSink();
+
+ public struct UpsertParams
+ {
+ public DataKind Kind;
+ public string Key;
+ public ItemDescriptor Item;
+ }
+
+ internal MockDataStoreStatusProvider MockDataStoreStatusProvider = new MockDataStoreStatusProvider();
+
+ internal int InitsShouldFail = 0;
+
+ internal int UpsertsShouldFail = 0;
+
+ public IDataStoreStatusProvider DataStoreStatusProvider => MockDataStoreStatusProvider;
+
+ public bool Init(FullDataSet allData)
+ {
+ return InitWithHeaders(allData, null);
+ }
+
+ public void UpdateStatus(DataSourceState newState, DataSourceStatus.ErrorInfo? newError) =>
+ StatusUpdates.Enqueue(new DataSourceStatus() { State = newState, LastError = newError });
+
+ public bool Upsert(DataKind kind, string key, ItemDescriptor item)
+ {
+ Upserts.Enqueue(new UpsertParams { Kind = kind, Key = key, Item = item });
+ return UpsertsShouldFail <= 0 || (--UpsertsShouldFail < 0);
+ }
+
+ public bool InitWithHeaders(FullDataSet allData,
+ IEnumerable>> headers)
+ {
+ Inits.Enqueue(
+ new Tuple, IEnumerable>>>(allData,
+ headers));
+ return InitsShouldFail <= 0 || (--InitsShouldFail < 0);
+ }
+ }
+
public class CapturingDataStoreFactory : IComponentConfigurer
{
private readonly IComponentConfigurer _factory;