diff --git a/pkgs/sdk/server/src/Interfaces/ILdClient.cs b/pkgs/sdk/server/src/Interfaces/ILdClient.cs index 928fb7ca..f29f13c3 100644 --- a/pkgs/sdk/server/src/Interfaces/ILdClient.cs +++ b/pkgs/sdk/server/src/Interfaces/ILdClient.cs @@ -1,6 +1,8 @@ using System; using LaunchDarkly.Sdk.Server.Migrations; using LaunchDarkly.Logging; +using System.Threading.Tasks; +using System.Threading; namespace LaunchDarkly.Sdk.Server.Interfaces { @@ -96,6 +98,27 @@ public interface ILdClient /// EvaluationDetail BoolVariationDetail(string key, Context context, bool defaultValue); + /// + /// Calculates the boolean value of a feature flag for a given context, and returns an object that + /// describes the way the value was determined. Async + /// + /// + /// + /// The property in the result will also be included + /// in analytics events, if you are capturing detailed event data for this flag. + /// + /// + /// The behavior is otherwise identical to . + /// + /// + /// the unique feature key for the feature flag + /// the evaluation context + /// the default value of the flag + /// optional cancelation token + /// an object + /// + ValueTask> BoolVariationDetailAsync(string key, Context context, bool defaultValue, CancellationToken cancelationToken = default); + /// /// Calculates the integer value of a feature flag for a given context. /// diff --git a/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs b/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs index ebc86be0..0f6d4a63 100644 --- a/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs +++ b/pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs @@ -1,4 +1,6 @@ using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Sdk.Server.Subsystems; using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes; @@ -60,6 +62,23 @@ public void Init(FullDataSet data) return item; } + public ValueTask GetAsync(DataKind kind, string key, CancellationToken cancelationToken = default) + { + if (!Items.TryGetValue(kind, out var itemsOfKind)) + { + return new ValueTask(); + } + if (!itemsOfKind.TryGetValue(key, out var item)) + { + return new ValueTask(); + } +#if NET31_OR_GREATER + return ValueTask.FromResult(item); +#else + return new ValueTask(Task.FromResult(item)); +#endif + } + public KeyedItems GetAll(DataKind kind) { if (Items.TryGetValue(kind, out var itemsOfKind)) diff --git a/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreAsyncAdapter.cs b/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreAsyncAdapter.cs index 915fdb1f..ed98c567 100644 --- a/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreAsyncAdapter.cs +++ b/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreAsyncAdapter.cs @@ -33,7 +33,12 @@ public void Init(FullDataSet allData) { return WaitSafely(() => _coreAsync.GetAsync(kind, key)); } - + + public ValueTask GetAsync(DataKind kind, string key,CancellationToken cancellationToken = default) + { + return _coreAsync.GetAsync(kind, key,cancellationToken); + } + public KeyedItems GetAll(DataKind kind) { return WaitSafely(() => _coreAsync.GetAllAsync(kind)); @@ -82,5 +87,14 @@ private T WaitSafely(Func> taskFn) .GetAwaiter() .GetResult(); } + + private T WaitSafely(Func> taskFn) + { + return _taskFactory.StartNew(taskFn) + .GetAwaiter() + .GetResult() + .GetAwaiter() + .GetResult(); + } } } diff --git a/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreWrapper.cs b/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreWrapper.cs index 2af8189d..fddeaed8 100644 --- a/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreWrapper.cs +++ b/pkgs/sdk/server/src/Internal/DataStores/PersistentStoreWrapper.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Cache; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; @@ -185,6 +187,22 @@ public void Init(FullDataSet items) } } + public async ValueTask GetAsync(DataKind kind, string key, CancellationToken cancellationToken = default) + { + try + { + var ret = _itemCache is null ? await GetAndDeserializeItemAsync(kind, key, cancellationToken).ConfigureAwait(false) : + _itemCache.Get(new CacheKey(kind, key)); + ProcessError(null); + return ret; + } + catch (Exception e) + { + ProcessError(e); + throw; + } + } + public KeyedItems GetAll(DataKind kind) { try @@ -319,7 +337,17 @@ private void Dispose(bool disposing) } return Deserialize(kind, maybeSerializedItem.Value); } - + + private async ValueTask GetAndDeserializeItemAsync(DataKind kind, string key, CancellationToken cancellationToken = default) + { + var maybeSerializedItem = await _core.GetAsync(kind, key, cancellationToken).ConfigureAwait(false); + if (!maybeSerializedItem.HasValue) + { + return null; + } + return Deserialize(kind, maybeSerializedItem.Value); + } + private ImmutableDictionary GetAllAndDeserialize(DataKind kind) { return _core.GetAll(kind).Items.ToImmutableDictionary( diff --git a/pkgs/sdk/server/src/Internal/Hooks/Executor/Executor.cs b/pkgs/sdk/server/src/Internal/Hooks/Executor/Executor.cs index 7dfc6500..9cc9470f 100644 --- a/pkgs/sdk/server/src/Internal/Hooks/Executor/Executor.cs +++ b/pkgs/sdk/server/src/Internal/Hooks/Executor/Executor.cs @@ -8,6 +8,8 @@ using LaunchDarkly.Sdk.Server.Internal.Hooks.Series; using LaunchDarkly.Sdk.Server.Internal.Hooks.Interfaces; using LaunchDarkly.Sdk.Server.Internal.Model; +using System.Threading.Tasks; +using System.Threading; namespace LaunchDarkly.Sdk.Server.Internal.Hooks.Executor { @@ -36,6 +38,16 @@ public Executor(Logger logger, IEnumerable hooks) return (detail, flag); } + public async ValueTask<(EvaluationDetail, FeatureFlag)> EvaluationSeriesAsync(EvaluationSeriesContext context, LdValue.Converter converter, Func, FeatureFlag)>> evaluateAsync,CancellationToken cancellationToken = default) + { + var seriesData = _beforeEvaluation.Execute(context, default); + + var (detail, flag) = await evaluateAsync().ConfigureAwait(false); + + _afterEvaluation.Execute(context, new EvaluationDetail(converter.FromType(detail.Value), detail.VariationIndex, detail.Reason), seriesData); + return (detail, flag); + } + public void Dispose() { foreach (var hook in _hooks) diff --git a/pkgs/sdk/server/src/Internal/Hooks/Executor/NoopExecutor.cs b/pkgs/sdk/server/src/Internal/Hooks/Executor/NoopExecutor.cs index 4d65a720..9126c0b2 100644 --- a/pkgs/sdk/server/src/Internal/Hooks/Executor/NoopExecutor.cs +++ b/pkgs/sdk/server/src/Internal/Hooks/Executor/NoopExecutor.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Sdk.Server.Hooks; using LaunchDarkly.Sdk.Server.Internal.Hooks.Interfaces; using LaunchDarkly.Sdk.Server.Internal.Model; @@ -14,6 +16,9 @@ internal sealed class NoopExecutor: IHookExecutor public (EvaluationDetail, FeatureFlag) EvaluationSeries(EvaluationSeriesContext context, LdValue.Converter converter, Func<(EvaluationDetail, FeatureFlag)> evaluate) => evaluate(); + public ValueTask<(EvaluationDetail, FeatureFlag)> EvaluationSeriesAsync(EvaluationSeriesContext context, + LdValue.Converter converter, Func, FeatureFlag)>> evaluateAsync,CancellationToken cancellationToken = default) => evaluateAsync(); + public void Dispose() { } diff --git a/pkgs/sdk/server/src/Internal/Hooks/Interfaces/IHookExecutor.cs b/pkgs/sdk/server/src/Internal/Hooks/Interfaces/IHookExecutor.cs index 83913c7f..3deedbfc 100644 --- a/pkgs/sdk/server/src/Internal/Hooks/Interfaces/IHookExecutor.cs +++ b/pkgs/sdk/server/src/Internal/Hooks/Interfaces/IHookExecutor.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Sdk.Server.Hooks; using LaunchDarkly.Sdk.Server.Internal.Model; @@ -25,5 +27,8 @@ internal interface IHookExecutor: IDisposable /// the EvaluationDetail returned from the evaluator (EvaluationDetail, FeatureFlag) EvaluationSeries(EvaluationSeriesContext context, LdValue.Converter converter, Func<(EvaluationDetail, FeatureFlag)> evaluate); + + ValueTask<(EvaluationDetail, FeatureFlag)> EvaluationSeriesAsync(EvaluationSeriesContext context, LdValue.Converter converter, + Func, FeatureFlag)>> evaluateAsync,CancellationToken cancellationToken = default); } } diff --git a/pkgs/sdk/server/src/LdClient.cs b/pkgs/sdk/server/src/LdClient.cs index ecc32f28..4dab76b1 100644 --- a/pkgs/sdk/server/src/LdClient.cs +++ b/pkgs/sdk/server/src/LdClient.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Hooks; @@ -296,6 +298,10 @@ public LdValue JsonVariation(string key, Context context, LdValue defaultValue) public EvaluationDetail BoolVariationDetail(string key, Context context, bool defaultValue) => Evaluate(Method.BoolVariationDetail, key, context, LdValue.Of(defaultValue), LdValue.Convert.Bool, true, EventFactory.DefaultWithReasons); + /// + public ValueTask> BoolVariationDetailAsync(string key, Context context, bool defaultValue, CancellationToken cancellationToken = default) => + EvaluateAsync(Method.BoolVariationDetail, key, context, LdValue.Of(defaultValue), LdValue.Convert.Bool, true, EventFactory.DefaultWithReasons, cancellationToken); + /// public EvaluationDetail IntVariationDetail(string key, Context context, int defaultValue) => Evaluate(Method.IntVariationDetail, key, context, LdValue.Of(defaultValue), LdValue.Convert.Int, true, EventFactory.DefaultWithReasons); @@ -435,6 +441,16 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[ () => EvaluationAndFlag(key, context, defaultValue, converter, checkType, eventFactory)); } + private async ValueTask<(EvaluationDetail, FeatureFlag)> EvaluateWithHooksAsync(string method, string key, Context context, LdValue defaultValue, LdValue.Converter converter, + bool checkType, EventFactory eventFactory,CancellationToken cancellationToken = default) + { + var evalSeriesContext = new EvaluationSeriesContext(key, context, defaultValue, method); + return await _hookExecutor.EvaluationSeriesAsync( + evalSeriesContext, + converter, + () => EvaluationAndFlagAsync(key, context, defaultValue, converter, checkType, eventFactory,cancellationToken),cancellationToken).ConfigureAwait(false); + } + private (EvaluationDetail, FeatureFlag) EvaluationAndFlag(string featureKey, Context context, LdValue defaultValue, LdValue.Converter converter, bool checkType, EventFactory eventFactory) @@ -533,12 +549,117 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[ } } + private async ValueTask<(EvaluationDetail, FeatureFlag)> EvaluationAndFlagAsync(string featureKey, Context context, + LdValue defaultValue, LdValue.Converter converter, + bool checkType, EventFactory eventFactory,CancellationToken cancellationToken = default) + { + T defaultValueOfType = converter.ToType(defaultValue); + if (!Initialized) + { + // TODO should this also be async? + if (_dataStore.Initialized()) + { + _evalLog.Warn("Flag evaluation before client initialized; using last known values from data store"); + } + else + { + _evalLog.Warn("Flag evaluation before client initialized; data store unavailable, returning default value"); + return (new EvaluationDetail(defaultValueOfType, null, + EvaluationReason.ErrorReason(EvaluationErrorKind.ClientNotReady)), null); + } + } + + if (!context.Valid) + { + _evalLog.Warn("Invalid evaluation context when evaluating flag \"{0}\" ({1}); returning default value", featureKey, + context.Error); + return (new EvaluationDetail(defaultValueOfType, null, + EvaluationReason.ErrorReason(EvaluationErrorKind.UserNotSpecified)), null); + } + + FeatureFlag featureFlag = null; + try + { + featureFlag = await GetFlagAsync(featureKey, cancellationToken).ConfigureAwait(false); + if (featureFlag == null) + { + _evalLog.Info("Unknown feature flag \"{0}\"; returning default value", + featureKey); + _eventProcessor.RecordEvaluationEvent(eventFactory.NewUnknownFlagEvaluationEvent( + featureKey, context, defaultValue, EvaluationErrorKind.FlagNotFound)); + return (new EvaluationDetail(defaultValueOfType, null, + EvaluationReason.ErrorReason(EvaluationErrorKind.FlagNotFound)), null); + } + + EvaluatorTypes.EvalResult evalResult = _evaluator.Evaluate(featureFlag, context); + if (!IsOffline()) + { + foreach (var prereqEvent in evalResult.PrerequisiteEvals) + { + _eventProcessor.RecordEvaluationEvent(eventFactory.NewPrerequisiteEvaluationEvent( + prereqEvent.PrerequisiteFlag, context, prereqEvent.Result, prereqEvent.FlagKey)); + } + } + var evalDetail = evalResult.Result; + EvaluationDetail returnDetail; + if (evalDetail.VariationIndex == null) + { + returnDetail = new EvaluationDetail(defaultValueOfType, null, evalDetail.Reason); + evalDetail = new EvaluationDetail(defaultValue, null, evalDetail.Reason); + } + else + { + if (checkType && !defaultValue.IsNull && evalDetail.Value.Type != defaultValue.Type) + { + _evalLog.Error("Expected type {0} but got {1} when evaluating feature flag \"{2}\"; returning default value", + defaultValue.Type, + evalDetail.Value.Type, + featureKey); + + _eventProcessor.RecordEvaluationEvent(eventFactory.NewDefaultValueEvaluationEvent( + featureFlag, context, defaultValue, EvaluationErrorKind.WrongType)); + return (new EvaluationDetail(defaultValueOfType, null, + EvaluationReason.ErrorReason(EvaluationErrorKind.WrongType)), featureFlag); + } + returnDetail = new EvaluationDetail(converter.ToType(evalDetail.Value), + evalDetail.VariationIndex, evalDetail.Reason); + } + _eventProcessor.RecordEvaluationEvent(eventFactory.NewEvaluationEvent( + featureFlag, context, evalDetail, defaultValue)); + return (returnDetail, featureFlag); + } + catch (Exception e) + { + LogHelpers.LogException(_evalLog, + string.Format("Exception when evaluating feature flag \"{0}\"", featureKey), + e); + var reason = EvaluationReason.ErrorReason(EvaluationErrorKind.Exception); + if (featureFlag == null) + { + _eventProcessor.RecordEvaluationEvent(eventFactory.NewUnknownFlagEvaluationEvent( + featureKey, context, defaultValue, EvaluationErrorKind.Exception)); + } + else + { + _eventProcessor.RecordEvaluationEvent(eventFactory.NewEvaluationEvent( + featureFlag, context, new EvaluationDetail(defaultValue, null, reason), defaultValue)); + } + return (new EvaluationDetail(defaultValueOfType, null, reason), null); + } + } + private EvaluationDetail Evaluate(string method, string featureKey, Context context, LdValue defaultValue, LdValue.Converter converter, bool checkType, EventFactory eventFactory) { return EvaluateWithHooks(method, featureKey, context, defaultValue, converter, checkType, eventFactory).Item1; } + private async ValueTask> EvaluateAsync(string method, string featureKey, Context context, LdValue defaultValue, LdValue.Converter converter, + bool checkType, EventFactory eventFactory,CancellationToken cancellationToken = default) + { + return (await EvaluateWithHooksAsync(method, featureKey, context, defaultValue, converter, checkType, eventFactory,cancellationToken).ConfigureAwait(false)).Item1; + } + /// public string SecureModeHash(Context context) { @@ -658,6 +779,16 @@ private FeatureFlag GetFlag(string key) return null; } + private async ValueTask GetFlagAsync(string key, CancellationToken cancellationToken = default) + { + var maybeItem = await _dataStore.GetAsync(DataModel.Features, key,cancellationToken).ConfigureAwait(false); + if (maybeItem.HasValue && maybeItem.Value.Item != null && maybeItem.Value.Item is FeatureFlag f) + { + return f; + } + return null; + } + private Segment GetSegment(string key) { var maybeItem = _dataStore.Get(DataModel.Segments, key); diff --git a/pkgs/sdk/server/src/Subsystems/IDataStore.cs b/pkgs/sdk/server/src/Subsystems/IDataStore.cs index 884292c4..65d91a1b 100644 --- a/pkgs/sdk/server/src/Subsystems/IDataStore.cs +++ b/pkgs/sdk/server/src/Subsystems/IDataStore.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes; namespace LaunchDarkly.Sdk.Server.Subsystems @@ -72,6 +74,11 @@ public interface IDataStore : IDisposable /// deleted data); null if the key is unknown ItemDescriptor? Get(DataKind kind, string key); + /// + /// Retrieves an item from the specified collection, if available. + /// + ValueTask GetAsync(DataKind kind, string key,CancellationToken cancellation=default); + /// /// Retrieves all items from the specified collection. /// diff --git a/pkgs/sdk/server/src/Subsystems/IPersistentDataStore.cs b/pkgs/sdk/server/src/Subsystems/IPersistentDataStore.cs index 26bc7f28..81308a77 100644 --- a/pkgs/sdk/server/src/Subsystems/IPersistentDataStore.cs +++ b/pkgs/sdk/server/src/Subsystems/IPersistentDataStore.cs @@ -1,5 +1,6 @@ using System; - +using System.Threading; +using System.Threading.Tasks; using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes; namespace LaunchDarkly.Sdk.Server.Subsystems @@ -109,6 +110,11 @@ public interface IPersistentDataStore : IDisposable /// deleted data); null if the key is unknown SerializedItemDescriptor? Get(DataKind kind, string key); + /// + /// Retrieves an item from the specified collection, if available. + /// + ValueTask GetAsync(DataKind kind, string key, CancellationToken cancellationToken = default); + /// /// Retrieves all items from the specified collection. /// diff --git a/pkgs/sdk/server/src/Subsystems/IPersistentDataStoreAsync.cs b/pkgs/sdk/server/src/Subsystems/IPersistentDataStoreAsync.cs index 4fc3b55c..ec40919f 100644 --- a/pkgs/sdk/server/src/Subsystems/IPersistentDataStoreAsync.cs +++ b/pkgs/sdk/server/src/Subsystems/IPersistentDataStoreAsync.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes; @@ -14,7 +15,7 @@ namespace LaunchDarkly.Sdk.Server.Subsystems /// implementations that use a task-based asynchronous pattern. /// public interface IPersistentDataStoreAsync : IDisposable - { + { /// /// Equivalent to . /// @@ -27,9 +28,10 @@ public interface IPersistentDataStoreAsync : IDisposable /// /// specifies which collection to use /// the unique key of the item within that collection + /// optional cancellation token /// a versioned item that contains the stored data (or placeholder for /// deleted data); null if the key is unknown - Task GetAsync(DataKind kind, string key); + ValueTask GetAsync(DataKind kind, string key,CancellationToken cancellationToken = default); /// /// Equivalent to . diff --git a/pkgs/sdk/server/test/Internal/DataStores/DataStoreStatusProviderImplTest.cs b/pkgs/sdk/server/test/Internal/DataStores/DataStoreStatusProviderImplTest.cs index a1b518ec..47017735 100644 --- a/pkgs/sdk/server/test/Internal/DataStores/DataStoreStatusProviderImplTest.cs +++ b/pkgs/sdk/server/test/Internal/DataStores/DataStoreStatusProviderImplTest.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Subsystems; using LaunchDarkly.TestHelpers; @@ -72,6 +74,9 @@ public void Dispose() { } public DataStoreTypes.ItemDescriptor? Get(DataStoreTypes.DataKind kind, string key) => throw new NotImplementedException(); + public ValueTask GetAsync(DataStoreTypes.DataKind kind, string key,CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + public DataStoreTypes.KeyedItems GetAll(DataStoreTypes.DataKind kind) => throw new NotImplementedException(); diff --git a/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestAsync.cs b/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestAsync.cs index 67f0e1a4..ed68210b 100644 --- a/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestAsync.cs +++ b/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestAsync.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using LaunchDarkly.Sdk.Server.Subsystems; using Xunit.Abstractions; @@ -30,7 +31,7 @@ private async Task ArbitraryTask() await Task.Delay(TimeSpan.FromTicks(1)); } - public async Task GetAsync(DataKind kind, string key) + public new async ValueTask GetAsync(DataKind kind, string key,CancellationToken cancellationToken = default) { await ArbitraryTask(); return Get(kind, key); diff --git a/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestBase.cs b/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestBase.cs index dbaa87aa..f3873626 100644 --- a/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestBase.cs +++ b/pkgs/sdk/server/test/Internal/DataStores/PersistentStoreWrapperTestBase.cs @@ -11,6 +11,7 @@ using static LaunchDarkly.Sdk.Server.Subsystems.DataStoreTypes; using static LaunchDarkly.Sdk.Server.Internal.DataStores.DataStoreTestTypes; +using System.Threading.Tasks; namespace LaunchDarkly.Sdk.Server.Internal.DataStores { @@ -690,6 +691,24 @@ public void Dispose() { } return null; } + public ValueTask GetAsync(DataKind kind, string key,CancellationToken cancellationToken = default) + { + MaybeThrowError(); + if (Data.TryGetValue(kind, out var items)) + { + if (items.TryGetValue(key, out var item)) + { + if (PersistOnlyAsString) + { + // This simulates the kind of store implementation that can't track metadata separately + return new ValueTask(Task.FromResult(new SerializedItemDescriptor(0, false, item.SerializedItem))); + } + return new ValueTask(Task.FromResult(item)); + } + } + return new ValueTask(); + } + public KeyedItems GetAll(DataKind kind) { MaybeThrowError();