-
Notifications
You must be signed in to change notification settings - Fork 20
feat!: Thread safe hooks, provider, and context #79
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
Changes from 4 commits
9e4ee6d
08e68ba
bd7ba04
dda0f28
a86fb83
4a6736f
392a005
b75e3e3
c7f2cc3
f70d391
51cf8f2
b6658b6
094f3d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Extensions.Logging; | ||
using OpenFeatureSDK.Model; | ||
|
||
|
@@ -11,9 +13,15 @@ namespace OpenFeatureSDK | |
/// <seealso href="https://github.com/open-feature/spec/blob/main/specification/flag-evaluation.md#flag-evaluation-api"/> | ||
public sealed class OpenFeature | ||
{ | ||
private EvaluationContext _evaluationContext = EvaluationContext.Empty; | ||
private FeatureProvider _featureProvider = new NoOpFeatureProvider(); | ||
private readonly List<Hook> _hooks = new List<Hook>(); | ||
/// References are atomic in C#, but it is possible to get an old value due to register caching. | ||
/// Making it volatile will result in reads bypassing that cache. It doesn't provide any ordering | ||
/// guarantee. Considering this is just a method to replace the value, regardless of the current value, | ||
/// this should be sufficient. | ||
/// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables#96-atomicity-of-variable-references | ||
/// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile | ||
private volatile EvaluationContext _evaluationContext = EvaluationContext.Empty; | ||
private volatile FeatureProvider _featureProvider = new NoOpFeatureProvider(); | ||
private readonly ConcurrentStack<Hook> _hooks = new ConcurrentStack<Hook>(); | ||
|
||
/// <summary> | ||
/// Singleton instance of OpenFeature | ||
|
@@ -53,25 +61,30 @@ private OpenFeature() { } | |
/// <param name="context">Context given to this client</param> | ||
/// <returns><see cref="FeatureClient"/></returns> | ||
public FeatureClient GetClient(string name = null, string version = null, ILogger logger = null, EvaluationContext context = null) => | ||
new FeatureClient(this._featureProvider, name, version, logger, context); | ||
new FeatureClient(name, version, logger, context); | ||
|
||
/// <summary> | ||
/// Appends list of hooks to global hooks list | ||
/// </summary> | ||
/// <param name="hooks">A list of <see cref="Hook"/></param> | ||
public void AddHooks(IEnumerable<Hook> hooks) => this._hooks.AddRange(hooks); | ||
public void AddHooks(IEnumerable<Hook> hooks) => this._hooks.PushRange(hooks.ToArray()); | ||
|
||
/// <summary> | ||
/// Adds a hook to global hooks list | ||
/// </summary> | ||
/// <param name="hook">A list of <see cref="Hook"/></param> | ||
public void AddHooks(Hook hook) => this._hooks.Add(hook); | ||
public void AddHooks(Hook hook) => this._hooks.Push(hook); | ||
|
||
/// <summary> | ||
/// Returns the global immutable hooks list | ||
/// Enumerates the global hooks. | ||
/// <para> | ||
/// The items enumerated will reflect the registered hooks | ||
/// at the start of enumeration. Hooks added during enumeration | ||
/// will not be included. | ||
/// </para> | ||
/// </summary> | ||
/// <returns>A immutable list of <see cref="Hook"/></returns> | ||
public IReadOnlyList<Hook> GetHooks() => this._hooks.AsReadOnly(); | ||
/// <returns>Enumeration of <see cref="Hook"/></returns> | ||
public IEnumerable<Hook> GetHooks() => this._hooks.Reverse(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the open feature API and Client, the SDK controls the implementation type and can promise the use of a concurrent collection. In the provider this is not the case, so that is constrained to an immutable collection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I am reversing these for now. I wasn't completely comfortable with all the test changes required for them to be in the opposite order. |
||
|
||
/// <summary> | ||
/// Removes all hooks from global hooks list | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
|
@@ -17,8 +18,7 @@ namespace OpenFeatureSDK | |
public sealed class FeatureClient : IFeatureClient | ||
{ | ||
private readonly ClientMetadata _metadata; | ||
private readonly FeatureProvider _featureProvider; | ||
private readonly List<Hook> _hooks = new List<Hook>(); | ||
private readonly ConcurrentStack<Hook> _hooks = new ConcurrentStack<Hook>(); | ||
private readonly ILogger _logger; | ||
private EvaluationContext _evaluationContext; | ||
|
||
|
@@ -36,15 +36,13 @@ public sealed class FeatureClient : IFeatureClient | |
/// <summary> | ||
/// Initializes a new instance of the <see cref="FeatureClient"/> class. | ||
/// </summary> | ||
/// <param name="featureProvider">Feature provider used by client <see cref="FeatureProvider"/></param> | ||
/// <param name="name">Name of client <see cref="ClientMetadata"/></param> | ||
/// <param name="version">Version of client <see cref="ClientMetadata"/></param> | ||
/// <param name="logger">Logger used by client</param> | ||
/// <param name="context">Context given to this client</param> | ||
/// <exception cref="ArgumentNullException">Throws if any of the required parameters are null</exception> | ||
public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null, EvaluationContext context = null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I lifted the behavior the Java SDK has. Which is to log that it was not set, and then use the NoOp provider. |
||
public FeatureClient(string name, string version, ILogger logger = null, EvaluationContext context = null) | ||
{ | ||
this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider)); | ||
this._metadata = new ClientMetadata(name, version); | ||
this._logger = logger ?? new Logger<OpenFeature>(new NullLoggerFactory()); | ||
this._evaluationContext = context ?? EvaluationContext.Empty; | ||
|
@@ -60,19 +58,24 @@ public FeatureClient(FeatureProvider featureProvider, string name, string versio | |
/// Add hook to client | ||
/// </summary> | ||
/// <param name="hook">Hook that implements the <see cref="Hook"/> interface</param> | ||
public void AddHooks(Hook hook) => this._hooks.Add(hook); | ||
public void AddHooks(Hook hook) => this._hooks.Push(hook); | ||
|
||
/// <summary> | ||
/// Appends hooks to client | ||
/// </summary> | ||
/// <param name="hooks">A list of Hooks that implement the <see cref="Hook"/> interface</param> | ||
public void AddHooks(IEnumerable<Hook> hooks) => this._hooks.AddRange(hooks); | ||
public void AddHooks(IEnumerable<Hook> hooks) => this._hooks.PushRange(hooks.ToArray()); | ||
|
||
/// <summary> | ||
/// Return a immutable list of hooks that are registered against the client | ||
/// Enumerates the global hooks. | ||
/// <para> | ||
/// The items enumerated will reflect the registered hooks | ||
/// at the start of enumeration. Hooks added during enumeration | ||
/// will not be included. | ||
/// </para> | ||
/// </summary> | ||
/// <returns>A list of immutable hooks</returns> | ||
public IReadOnlyList<Hook> GetHooks() => this._hooks.ToList().AsReadOnly(); | ||
/// <returns>Enumeration of <see cref="Hook"/></returns> | ||
public IEnumerable<Hook> GetHooks() => this._hooks.Reverse(); | ||
|
||
/// <summary> | ||
/// Removes all hooks from the client | ||
|
@@ -101,7 +104,7 @@ public async Task<bool> GetBooleanValue(string flagKey, bool defaultValue, Evalu | |
/// <returns>Resolved flag details <see cref="FlagEvaluationDetails{T}"/></returns> | ||
public async Task<FlagEvaluationDetails<bool>> GetBooleanDetails(string flagKey, bool defaultValue, | ||
EvaluationContext context = null, FlagEvaluationOptions config = null) => | ||
await this.EvaluateFlag(this._featureProvider.ResolveBooleanValue, FlagValueType.Boolean, flagKey, | ||
await this.EvaluateFlag(OpenFeature.Instance.GetProvider().ResolveBooleanValue, FlagValueType.Boolean, flagKey, | ||
defaultValue, context, config); | ||
|
||
/// <summary> | ||
|
@@ -126,7 +129,7 @@ public async Task<string> GetStringValue(string flagKey, string defaultValue, Ev | |
/// <returns>Resolved flag details <see cref="FlagEvaluationDetails{T}"/></returns> | ||
public async Task<FlagEvaluationDetails<string>> GetStringDetails(string flagKey, string defaultValue, | ||
EvaluationContext context = null, FlagEvaluationOptions config = null) => | ||
await this.EvaluateFlag(this._featureProvider.ResolveStringValue, FlagValueType.String, flagKey, | ||
await this.EvaluateFlag(OpenFeature.Instance.GetProvider().ResolveStringValue, FlagValueType.String, flagKey, | ||
defaultValue, context, config); | ||
|
||
/// <summary> | ||
|
@@ -151,7 +154,7 @@ public async Task<int> GetIntegerValue(string flagKey, int defaultValue, Evaluat | |
/// <returns>Resolved flag details <see cref="FlagEvaluationDetails{T}"/></returns> | ||
public async Task<FlagEvaluationDetails<int>> GetIntegerDetails(string flagKey, int defaultValue, | ||
EvaluationContext context = null, FlagEvaluationOptions config = null) => | ||
await this.EvaluateFlag(this._featureProvider.ResolveIntegerValue, FlagValueType.Number, flagKey, | ||
await this.EvaluateFlag(OpenFeature.Instance.GetProvider().ResolveIntegerValue, FlagValueType.Number, flagKey, | ||
defaultValue, context, config); | ||
|
||
/// <summary> | ||
|
@@ -177,7 +180,7 @@ public async Task<double> GetDoubleValue(string flagKey, double defaultValue, | |
/// <returns>Resolved flag details <see cref="FlagEvaluationDetails{T}"/></returns> | ||
public async Task<FlagEvaluationDetails<double>> GetDoubleDetails(string flagKey, double defaultValue, | ||
EvaluationContext context = null, FlagEvaluationOptions config = null) => | ||
await this.EvaluateFlag(this._featureProvider.ResolveDoubleValue, FlagValueType.Number, flagKey, | ||
await this.EvaluateFlag(OpenFeature.Instance.GetProvider().ResolveDoubleValue, FlagValueType.Number, flagKey, | ||
defaultValue, context, config); | ||
|
||
/// <summary> | ||
|
@@ -202,7 +205,7 @@ public async Task<Value> GetObjectValue(string flagKey, Value defaultValue, Eval | |
/// <returns>Resolved flag details <see cref="FlagEvaluationDetails{T}"/></returns> | ||
public async Task<FlagEvaluationDetails<Value>> GetObjectDetails(string flagKey, Value defaultValue, | ||
EvaluationContext context = null, FlagEvaluationOptions config = null) => | ||
await this.EvaluateFlag(this._featureProvider.ResolveStructureValue, FlagValueType.Object, flagKey, | ||
await this.EvaluateFlag(OpenFeature.Instance.GetProvider().ResolveStructureValue, FlagValueType.Object, flagKey, | ||
defaultValue, context, config); | ||
|
||
private async Task<FlagEvaluationDetails<T>> EvaluateFlag<T>( | ||
|
@@ -225,9 +228,9 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlag<T>( | |
|
||
var allHooks = new List<Hook>() | ||
.Concat(OpenFeature.Instance.GetHooks()) | ||
.Concat(this._hooks) | ||
.Concat(this.GetHooks()) | ||
.Concat(options?.Hooks ?? Enumerable.Empty<Hook>()) | ||
.Concat(this._featureProvider.GetProviderHooks()) | ||
.Concat(OpenFeature.Instance.GetProvider().GetProviderHooks()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. This isn't correct. You would't want the hooks from a different provider than the evaluation method. Those two things need to be resolved at the same time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we need to get the reference, then use that reference to get the method and the hooks. We would not just want to get them in the call, because they could still be separated in time. |
||
.ToList() | ||
.AsReadOnly(); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively this could be enumerable like other places, with documentation that the provider must use a thread-safe implementation. Technically a readonlylist, with the same constraint, would also works. I think immutable directs people in a way that the easiest solution is also the correct solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.