Skip to content

feat: POC of an approach to adding async #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pkgs/sdk/server/src/Interfaces/ILdClient.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -96,6 +98,27 @@ public interface ILdClient
/// <seealso cref="BoolVariation(string, Context, bool)"/>
EvaluationDetail<bool> BoolVariationDetail(string key, Context context, bool defaultValue);

/// <summary>
/// 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
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="EvaluationDetail{T}.Reason"/> property in the result will also be included
/// in analytics events, if you are capturing detailed event data for this flag.
/// </para>
/// <para>
/// The behavior is otherwise identical to <see cref="BoolVariation(string, Context, bool)"/>.
/// </para>
/// </remarks>
/// <param name="key">the unique feature key for the feature flag</param>
/// <param name="context">the evaluation context</param>
/// <param name="defaultValue">the default value of the flag</param>
/// <param name="cancelationToken"> optional cancelation token</param>
/// <returns>an <see cref="EvaluationDetail{T}"/> object</returns>
/// <seealso cref="BoolVariation(string, Context, bool)"/>
ValueTask<EvaluationDetail<bool>> BoolVariationDetailAsync(string key, Context context, bool defaultValue, CancellationToken cancelationToken = default);

/// <summary>
/// Calculates the integer value of a feature flag for a given context.
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions pkgs/sdk/server/src/Internal/DataStores/InMemoryDataStore.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -60,6 +62,23 @@ public void Init(FullDataSet<ItemDescriptor> data)
return item;
}

public ValueTask<ItemDescriptor?> GetAsync(DataKind kind, string key, CancellationToken cancelationToken = default)
{
if (!Items.TryGetValue(kind, out var itemsOfKind))
{
return new ValueTask<ItemDescriptor?>();
}
if (!itemsOfKind.TryGetValue(key, out var item))
{
return new ValueTask<ItemDescriptor?>();
}
#if NET31_OR_GREATER
return ValueTask.FromResult<ItemDescriptor?>(item);
#else
return new ValueTask<ItemDescriptor?>(Task.FromResult<ItemDescriptor?>(item));
#endif
Comment on lines +75 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#if NET31_OR_GREATER
return ValueTask.FromResult<ItemDescriptor?>(item);
#else
return new ValueTask<ItemDescriptor?>(Task.FromResult<ItemDescriptor?>(item));
#endif
return new ValueTask<ItemDescriptor?>(item);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's not a thing in net 46

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add conditional reference to https://www.nuget.org/packages/System.Threading.Tasks.Extensions for net462?

}

public KeyedItems<ItemDescriptor> GetAll(DataKind kind)
{
if (Items.TryGetValue(kind, out var itemsOfKind))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ public void Init(FullDataSet<SerializedItemDescriptor> allData)
{
return WaitSafely(() => _coreAsync.GetAsync(kind, key));
}


public ValueTask<SerializedItemDescriptor?> GetAsync(DataKind kind, string key,CancellationToken cancellationToken = default)
{
return _coreAsync.GetAsync(kind, key,cancellationToken);
}

public KeyedItems<SerializedItemDescriptor> GetAll(DataKind kind)
{
return WaitSafely(() => _coreAsync.GetAllAsync(kind));
Expand Down Expand Up @@ -82,5 +87,14 @@ private T WaitSafely<T>(Func<Task<T>> taskFn)
.GetAwaiter()
.GetResult();
}

private T WaitSafely<T>(Func<ValueTask<T>> taskFn)
{
return _taskFactory.StartNew(taskFn)
.GetAwaiter()
.GetResult()
.GetAwaiter()
.GetResult();
Comment on lines +93 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're trying to avoid allocations, maybe something like...

Suggested change
return _taskFactory.StartNew(taskFn)
.GetAwaiter()
.GetResult()
.GetAwaiter()
.GetResult();
var vt = taskFn();
return vt.IsCompleted ? vt.Result : WaitSafely(() => vt.AsTask());

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -185,6 +187,22 @@ public void Init(FullDataSet<ItemDescriptor> items)
}
}

public async ValueTask<ItemDescriptor?> 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));
Comment on lines +194 to +195
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was going to say that since the _itemCache branch here is all sync there would probably be a perf benefit to splitting into paths with and without async/await.

But that made me realize that we don't want an all sync path. The cache loader is async over sync, so we haven't actually avoided any blocking unless we disable the cache. 😭 We'd need to update LaunchDarkly.Cache with IAsyncCache/IAsyncSingleValueCache and wire it through.

ProcessError(null);
return ret;
}
catch (Exception e)
{
ProcessError(e);
throw;
}
}

public KeyedItems<ItemDescriptor> GetAll(DataKind kind)
{
try
Expand Down Expand Up @@ -319,7 +337,17 @@ private void Dispose(bool disposing)
}
return Deserialize(kind, maybeSerializedItem.Value);
}


private async ValueTask<ItemDescriptor?> 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<string, ItemDescriptor> GetAllAndDeserialize(DataKind kind)
{
return _core.GetAll(kind).Items.ToImmutableDictionary(
Expand Down
12 changes: 12 additions & 0 deletions pkgs/sdk/server/src/Internal/Hooks/Executor/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -36,6 +38,16 @@ public Executor(Logger logger, IEnumerable<Hook> hooks)
return (detail, flag);
}

public async ValueTask<(EvaluationDetail<T>, FeatureFlag)> EvaluationSeriesAsync<T>(EvaluationSeriesContext context, LdValue.Converter<T> converter, Func<ValueTask<(EvaluationDetail<T>, FeatureFlag)>> evaluateAsync,CancellationToken cancellationToken = default)
{
var seriesData = _beforeEvaluation.Execute(context, default);

var (detail, flag) = await evaluateAsync().ConfigureAwait(false);

_afterEvaluation.Execute(context, new EvaluationDetail<LdValue>(converter.FromType(detail.Value), detail.VariationIndex, detail.Reason), seriesData);
return (detail, flag);
}

public void Dispose()
{
foreach (var hook in _hooks)
Expand Down
5 changes: 5 additions & 0 deletions pkgs/sdk/server/src/Internal/Hooks/Executor/NoopExecutor.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +16,9 @@ internal sealed class NoopExecutor: IHookExecutor
public (EvaluationDetail<T>, FeatureFlag) EvaluationSeries<T>(EvaluationSeriesContext context,
LdValue.Converter<T> converter, Func<(EvaluationDetail<T>, FeatureFlag)> evaluate) => evaluate();

public ValueTask<(EvaluationDetail<T>, FeatureFlag)> EvaluationSeriesAsync<T>(EvaluationSeriesContext context,
LdValue.Converter<T> converter, Func<ValueTask<(EvaluationDetail<T>, FeatureFlag)>> evaluateAsync,CancellationToken cancellationToken = default) => evaluateAsync();

public void Dispose()
{
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using LaunchDarkly.Sdk.Server.Hooks;
using LaunchDarkly.Sdk.Server.Internal.Model;

Expand All @@ -25,5 +27,8 @@ internal interface IHookExecutor: IDisposable
/// <returns>the EvaluationDetail returned from the evaluator</returns>
(EvaluationDetail<T>, FeatureFlag) EvaluationSeries<T>(EvaluationSeriesContext context, LdValue.Converter<T> converter,
Func<(EvaluationDetail<T>, FeatureFlag)> evaluate);

ValueTask<(EvaluationDetail<T>, FeatureFlag)> EvaluationSeriesAsync<T>(EvaluationSeriesContext context, LdValue.Converter<T> converter,
Func<ValueTask<(EvaluationDetail<T>, FeatureFlag)>> evaluateAsync,CancellationToken cancellationToken = default);
}
}
131 changes: 131 additions & 0 deletions pkgs/sdk/server/src/LdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -296,6 +298,10 @@ public LdValue JsonVariation(string key, Context context, LdValue defaultValue)
public EvaluationDetail<bool> BoolVariationDetail(string key, Context context, bool defaultValue) =>
Evaluate(Method.BoolVariationDetail, key, context, LdValue.Of(defaultValue), LdValue.Convert.Bool, true, EventFactory.DefaultWithReasons);

/// <inheritdoc/>
public ValueTask<EvaluationDetail<bool>> 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);

/// <inheritdoc/>
public EvaluationDetail<int> IntVariationDetail(string key, Context context, int defaultValue) =>
Evaluate(Method.IntVariationDetail, key, context, LdValue.Of(defaultValue), LdValue.Convert.Int, true, EventFactory.DefaultWithReasons);
Expand Down Expand Up @@ -435,6 +441,16 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[
() => EvaluationAndFlag(key, context, defaultValue, converter, checkType, eventFactory));
}

private async ValueTask<(EvaluationDetail<T>, FeatureFlag)> EvaluateWithHooksAsync<T>(string method, string key, Context context, LdValue defaultValue, LdValue.Converter<T> 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<T>, FeatureFlag) EvaluationAndFlag<T>(string featureKey, Context context,
LdValue defaultValue, LdValue.Converter<T> converter,
bool checkType, EventFactory eventFactory)
Expand Down Expand Up @@ -533,12 +549,117 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[
}
}

private async ValueTask<(EvaluationDetail<T>, FeatureFlag)> EvaluationAndFlagAsync<T>(string featureKey, Context context,
LdValue defaultValue, LdValue.Converter<T> 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<T>(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<T>(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<T>(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<T> returnDetail;
if (evalDetail.VariationIndex == null)
{
returnDetail = new EvaluationDetail<T>(defaultValueOfType, null, evalDetail.Reason);
evalDetail = new EvaluationDetail<LdValue>(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<T>(defaultValueOfType, null,
EvaluationReason.ErrorReason(EvaluationErrorKind.WrongType)), featureFlag);
}
returnDetail = new EvaluationDetail<T>(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<LdValue>(defaultValue, null, reason), defaultValue));
}
return (new EvaluationDetail<T>(defaultValueOfType, null, reason), null);
}
}

private EvaluationDetail<T> Evaluate<T>(string method, string featureKey, Context context, LdValue defaultValue, LdValue.Converter<T> converter,
bool checkType, EventFactory eventFactory)
{
return EvaluateWithHooks(method, featureKey, context, defaultValue, converter, checkType, eventFactory).Item1;
}

private async ValueTask<EvaluationDetail<T>> EvaluateAsync<T>(string method, string featureKey, Context context, LdValue defaultValue, LdValue.Converter<T> converter,
bool checkType, EventFactory eventFactory,CancellationToken cancellationToken = default)
{
return (await EvaluateWithHooksAsync(method, featureKey, context, defaultValue, converter, checkType, eventFactory,cancellationToken).ConfigureAwait(false)).Item1;
}

/// <inheritdoc/>
public string SecureModeHash(Context context)
{
Expand Down Expand Up @@ -658,6 +779,16 @@ private FeatureFlag GetFlag(string key)
return null;
}

private async ValueTask<FeatureFlag> 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);
Expand Down
7 changes: 7 additions & 0 deletions pkgs/sdk/server/src/Subsystems/IDataStore.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -72,6 +74,11 @@ public interface IDataStore : IDisposable
/// deleted data); null if the key is unknown</returns>
ItemDescriptor? Get(DataKind kind, string key);

/// <summary>
/// Retrieves an item from the specified collection, if available.
/// </summary>
ValueTask<ItemDescriptor?> GetAsync(DataKind kind, string key,CancellationToken cancellation=default);

/// <summary>
/// Retrieves all items from the specified collection.
/// </summary>
Expand Down
Loading