Skip to content
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

feat!: Implement builders and immutable contexts. #77

Merged
merged 10 commits into from
Oct 6, 2022

Conversation

kinyoklion
Copy link
Member

@kinyoklion kinyoklion commented Oct 4, 2022

Proof of concept implementing builders for EvaluationContext and Structure types to address context threading concerns.

var sample = Structure.Builder().Set("key", "val").Build();
var context = EvalContext.Builder().Set("my-struct", sample).Build();

This allows for the context provided to the client to be immutable, and for immutability for the duration of a hook.

See: #56

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion kinyoklion changed the title POC: Implement builders and immutable contexts. feat!: Implement builders and immutable contexts. Oct 4, 2022
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion kinyoklion changed the title feat!: Implement builders and immutable contexts. chore: poc, Implement builders and immutable contexts. Oct 4, 2022
@kinyoklion
Copy link
Member Author

I also have a branch where I implemented interfaces for the builders, structure, and eval context: https://github.com/open-feature/dotnet-sdk/pull/new/rlamb/immutable-with-builders-interfaces

Working through this I wasn't as sure about implementing interfaces. Because if they are interfaces, then they are effectively unconstrained with their threading guarantees. Though possibly that is fine. It is even easier to document that if you implement the interface directly, then all bets are off.

The nice thing about concrete types though is you can seal them be and be sure within reason they are your types.

@kinyoklion kinyoklion closed this Oct 4, 2022
@kinyoklion kinyoklion reopened this Oct 4, 2022
Copy link
Member

@benjiro benjiro left a comment

Choose a reason for hiding this comment

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

@kinyoklion awesome work, I left a few comments, but everything looks solid. 🏆

src/OpenFeatureSDK/Model/EvaluationContext.cs Show resolved Hide resolved
src/OpenFeatureSDK/Model/EvaluationContext.cs Outdated Show resolved Hide resolved
src/OpenFeatureSDK/Model/HookContext.cs Outdated Show resolved Hide resolved
src/OpenFeatureSDK/Model/Structure.cs Outdated Show resolved Hide resolved
src/OpenFeatureSDK/Model/Value.cs Show resolved Hide resolved
src/OpenFeatureSDK/OpenFeatureSDK.csproj Outdated Show resolved Hide resolved
kinyoklion and others added 3 commits October 4, 2022 17:03
Co-authored-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Co-authored-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
…ies for AsDictionary.

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@codecov-commenter
Copy link

codecov-commenter commented Oct 5, 2022

Codecov Report

Merging #77 (7d35490) into main (b59750b) will decrease coverage by 0.78%.
The diff coverage is 90.96%.

@@            Coverage Diff             @@
##             main      #77      +/-   ##
==========================================
- Coverage   94.13%   93.34%   -0.79%     
==========================================
  Files          18       20       +2     
  Lines         426      481      +55     
  Branches       36       36              
==========================================
+ Hits          401      449      +48     
- Misses         13       20       +7     
  Partials       12       12              
Impacted Files Coverage Δ
...c/OpenFeatureSDK/Model/EvaluationContextBuilder.cs 83.33% <83.33%> (ø)
src/OpenFeatureSDK/Model/Value.cs 75.60% <85.71%> (-0.07%) ⬇️
src/OpenFeatureSDK/Model/StructureBuilder.cs 91.48% <91.48%> (ø)
src/OpenFeatureSDK/Model/EvaluationContext.cs 77.27% <91.66%> (+0.48%) ⬆️
src/OpenFeatureSDK/Hook.cs 100.00% <100.00%> (ø)
src/OpenFeatureSDK/Model/HookContext.cs 100.00% <100.00%> (ø)
src/OpenFeatureSDK/Model/Structure.cs 93.10% <100.00%> (-6.90%) ⬇️
src/OpenFeatureSDK/OpenFeature.cs 100.00% <100.00%> (ø)
src/OpenFeatureSDK/OpenFeatureClient.cs 99.25% <100.00%> (+0.03%) ⬆️
... and 2 more

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@kinyoklion
Copy link
Member Author

GC thrashing was brought up as a possibility. This could potentially be susceptible to that.

One way to reduce that would be to change the types of Value, and possibly Struct to be structs. Then they would be value types and mostly inhabit the stack, or be inline in other objects. Because there are collections, those are still inherently references, but you would only encounter the cost if using them.

That would be more code than this currently is though. It would also use a bit more memory per value, but the dozens of bytes range. Because each of the types would exist in the struct. and only be used if the type was that type. So you have an enum and a number of extra fields.

A lot of this could change internally, and code would still compile. A breaking change at the ABI level though, for people just dropping in assemblies.

@toddbaert
Copy link
Member

Looks great!

@kinyoklion kinyoklion changed the title chore: poc, Implement builders and immutable contexts. feat!: Implement builders and immutable contexts. Oct 5, 2022
@toddbaert toddbaert self-requested a review October 5, 2022 18:13
@kinyoklion kinyoklion marked this pull request as ready for review October 5, 2022 18:18
Comment on lines 141 to 145
public StructureBuilder Remove(string key)
{
this._attributes.Remove(key);
return this;
}
Copy link
Member

@toddbaert toddbaert Oct 5, 2022

Choose a reason for hiding this comment

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

Is this method needed now? Is is realistic that a builder would add and then remove something before having build() called on it?

Copy link
Member Author

Choose a reason for hiding this comment

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

I could do without the remove methods. I think it makes sense to just put stuff in that you want.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed the Removes.

Comment on lines 123 to 126
public EvaluationContextBuilder Remove(string key)
{
this._attributes.Remove(key);
return this;
Copy link
Member

Choose a reason for hiding this comment

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

@@ -11,7 +11,7 @@ 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 = new EvaluationContext();
private EvaluationContext _evaluationContext = EvaluationContext.Empty;
private FeatureProvider _featureProvider = new NoOpFeatureProvider();
private readonly List<Hook> _hooks = new List<Hook>();
Copy link
Member

@toddbaert toddbaert Oct 5, 2022

Choose a reason for hiding this comment

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

While we're addressing thread safety, do you think we need to worry about the hook list here being mutated in multiple threads?

(Same applies to hooks in client)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that is true. I can take a look at that as well. Likely, to finish up thread safety, we may need some changes more than just the immutable builder.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, the hooks are a problem.

Currently I think there is a problem with the global context as well. A threading issue, but also I think right now your client will have a global context based on when you get the client. So if you update the global context it doesn't affect clients you have already gotten from the SDK.

I suspect that is supposed to be like hooks, where adding a hook affect subsequent evaluations from existing clients.

Copy link
Member

Choose a reason for hiding this comment

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

I think right now your client will have a global context based on when you get the client.

I don't think this is explicitly spelled out in the spec, but it's implied. I think I should add something about it. It doesn't necessarily need to be addressed in this PR.

The same should be true with regards to the configured provider - the currently registered one should be used for all clients, regardless of when they were created.

cc @beeme1mr

Copy link
Member Author

Choose a reason for hiding this comment

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

Another question, are hooks supposed to be executed in some specific order based on the order they were registered? There is an order of the sources of hooks, but multiple hooks registered at the API level for instance.

It affects the type of concurrency I would provide here. The C# ConcurrentStack has all the correct functionality, but the contents are reversed. (I can enumerable reverse this, so the cost shouldn't be that much.)

ConcurrentQueue puts things in the right order, but doesn't have a clear method. So you cannot atomically remove everything.

Or just making a synchronized collection, I prefer the concurrent solutions as they are generally lockless.

Copy link
Member

@toddbaert toddbaert Oct 6, 2022

Choose a reason for hiding this comment

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

Personally I didn't want to provide guarantees of hook ordering within a stage. The spec never dictates any behavior regarding that, and in a few discussions it seemed like it would only cause problems.

ConcurrentQueue puts things in the right order, but doesn't have a clear method. So you cannot atomically remove everything.

A means for removing hooks is also not specified. Most implementations provide one b/c it helps with testing, but you could make this a package private method if you want. I don't think there's a real compelling use case for allowing authors to remove hooks, though I could be wrong about that.

@kinyoklion
Copy link
Member Author

I could look at value types some if we are interested in tackling that now. It may take me a little bit though. I am not as certain how much of a problem that will be.

@toddbaert
Copy link
Member

I could look at value types some if we are interested in tackling that now. It may take me a little bit though. I am not as certain how much of a problem that will be.

If it's not a breaking change, my gut feeling is to defer it.

@kinyoklion
Copy link
Member Author

I could look at value types some if we are interested in tackling that now. It may take me a little bit though. I am not as certain how much of a problem that will be.

If it's not a breaking change, my gut feeling is to defer it.

I think that the Value type would be the place with the most possible benefit, because those are what you have a lot of. It might be a small break. Specifically you cannot have explicit parameter-less constructors for for structs. So the current parameterless constructor for null wouldn't work.

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@toddbaert
Copy link
Member

toddbaert commented Oct 6, 2022

Another point: in Go, Java, and JS, the provider is not a member field in the client. Instead, the currently registered provider is retrieved from the API. From a developer experience perspective, this means that existing clients are "updated" when a new provider is set, which I think is a better experience...?

Again, this isn't in the spec, but perhaps it should be (interested in feedback here). The challenge is that this does introduce more need for locking. In Java I would probably use a read/write lock for this, so n threads could read the provider without contention, and the lock is only unavailable when the provider is updated.

We can also continue to leave this particular case undefined in the spec, with each SDK handling this edge case differently. At a minimum we should document this behavior in each SDK though.

EDIT:

Another reason we probably want to change this behavior, is that, ala OpenTelemetry, one of the long-term goals was to have 3rd party libraries integrate the SDK. They could create clients in their code that first party developers could use. This works optimally if clients created in those third part libraries can use the globally registered provider after their instantiation.

@kinyoklion
Copy link
Member Author

@benjiro @toddbaert How about I do another PR to address the hooks and a couple other thread safety issues? Leaving this one just the builders.

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@toddbaert
Copy link
Member

@benjiro @toddbaert How about I do another PR to address the hooks and a couple other thread safety issues? Leaving this one just the builders.

Works for me! I don't mind tackling them either if you need to move onto other things. Let me know.

@toddbaert toddbaert self-requested a review October 6, 2022 15:37
@kinyoklion
Copy link
Member Author

kinyoklion commented Oct 6, 2022

It seems that GitHub is unhappy with me somehow. Code coverage report not uploading.

I re-ran the build and it was appeased.

@kinyoklion
Copy link
Member Author

@benjiro @toddbaert How about I do another PR to address the hooks and a couple other thread safety issues? Leaving this one just the builders.

Works for me! I don't mind tackling them either if you need to move onto other things. Let me know.

I have some of this in a branch, but if it looks like I won't get to it today, then I will let you know.

@toddbaert
Copy link
Member

@benjiro any objections to merging this?

@toddbaert toddbaert requested a review from benjiro October 6, 2022 17:42
Copy link
Member

@benjiro benjiro left a comment

Choose a reason for hiding this comment

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

LGTM

@toddbaert toddbaert merged commit d980a94 into main Oct 6, 2022
benjiro pushed a commit that referenced this pull request Oct 12, 2022
🤖 I have created a release *beep* *boop*
---


##
[0.4.0](v0.3.0...v0.4.0)
(2022-10-12)


### ⚠ BREAKING CHANGES

* Thread safe hooks, provider, and context (#79)
* Implement builders and immutable contexts. (#77)

### Features

* Implement builders and immutable contexts.
([#77](#77))
([d980a94](d980a94))
* Thread safe hooks, provider, and context
([#79](#79))
([609016f](609016f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants