-
Notifications
You must be signed in to change notification settings - Fork 21
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
Conversation
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>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
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. |
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.
@kinyoklion awesome work, I left a few comments, but everything looks solid. 🏆
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 Report
@@ 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
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
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 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. |
Looks great! |
public StructureBuilder Remove(string key) | ||
{ | ||
this._attributes.Remove(key); | ||
return this; | ||
} |
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.
Is this method needed now? Is is realistic that a builder would add and then remove something before having build()
called on it?
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.
I could do without the remove methods. I think it makes sense to just put stuff in that you want.
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.
Removed the Removes.
public EvaluationContextBuilder Remove(string key) | ||
{ | ||
this._attributes.Remove(key); | ||
return this; |
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.
@@ -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>(); |
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.
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)
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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>
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. |
@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>
Works for me! I don't mind tackling them either if you need to move onto other things. Let me know. |
I re-ran the build and it was appeased. |
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. |
@benjiro any objections to merging this? |
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.
LGTM
🤖 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>
Proof of concept implementing builders for
EvaluationContext
andStructure
types to address context threading concerns.This allows for the context provided to the client to be immutable, and for immutability for the duration of a hook.
See: #56