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();