-
Notifications
You must be signed in to change notification settings - Fork 35
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
[FEATURE] Ensure thread safety of sdk #90
Comments
This was my main concern (and still a concern in the Java SDK I think). As for the evaluation context: I think that a user mutating a map in multiple threads should be aware of the consequences of that. If we want to protect users from themselves, one other way could perhaps encapsulate the I'm not sure if this solution is any better than yours. It does allow it to be modified after construction, but there's more code to maintain with my proposal I think. This would of course be a breaking change. cc @beeme1mr |
i personally lean more towards @toddbaert's approach, it feels cleaner to have an |
If this is the approach, it must be a priority. To stay on schedule, this change would need to be made available tomorrow. |
I kinda hate my approach but I worry that it might be the best one. Although, I hadn't considered |
Would allowing the the map to be modified after construction violate this spec requirement?
|
That point is probably a bit misleading. We should improve the wording. The actual point here is that only before hooks should return an evaluation context (one that's merged with the ambient context). The other hooks do not return the evaluation context. Whether mutating them is possible or not is more of a language and implementation concern. |
I think you've just found another reason why we should think about thread safety though @skyerus 😅 |
If other hooks have as much control over mutating the |
I get your point, and you're right. The point should be changed to say something that indicates that only the |
I'm happy to drop what I'm currently working on and to pick this up now if we're happy with this approach. |
I would love to have @kinyoklion 's opinion, since he was the genesis of this discussion... unfortunately he is on the West Coast, I think. I suppose if you start with a basic POC of the idea it would at least give us some data on if it feels right and solves our issues. I vote you at start on that with an "experimental" mindset for now, until we get feedback from @kinyoklion and perhaps @justinabrahms . |
type EvaluationContext struct {
mx *sync.Mutex
targetingKey string
attributes map[string]interface{}
}
func (e *EvaluationContext) SetAttribute(key string, value interface{}) {
e.mx.Lock()
...
}
func (e *EvaluationContext) SetAttributes(attrs map[string]interface{}) {
e.mx.Lock()
...
}
func (e *EvaluationContext) SetTargetingKey(key string) {
e.mx.Lock()
...
}
func NewEvaluationContext(targetingKey string, attributes map[string]interface{}) EvaluationContext {
// copy attributes to new map to avoid reference being externally available
} Loosely I'm thinking of something like this, does this align with what you had in mind? |
yep! I just hope I haven't missed anything silly. |
looks good to me |
In the javasdk, I'm leaning towards immutable eval context objects. Mutable state is just a headache and I'd love to avoid the complexity (at the expense of a few extra objects) if we can. |
I think this is a valid solution. And it's basically analogous to what @skyerus proposed.... but it's a significant and breaking change, right? We would have to remove all the |
I agree with @justinabrahms, when possible immutable objects are a better solution. I think a locking solution would work, but I personally would only use it after exhausting other options. It will protect from fundamental threading issues, but not logic/consistency issues around how an application developer may expect the system to behave. It is preferable that once the context is produced it is either decoupled from the application developer entirely (as in a copy), or that it is immutable. |
Like I said before:
I'd like to see what the immutable evaluation context API would look like in Java and Dotnet. I'm in favor of it if it can be done simply! |
In go perspective we can have a constructor (as depicted above) and omit the exported setters, this would enforce immutability (albeit somewhat uncomfortably so). |
Created a POC PR with the changes we initially decided upon, I can remove the setters if we come to a decision on the immutability. |
One idea that might allow us to defer this issue for a while: At least in Java and Dotnet, we could extract the read/merge functionality of For now, it would be a simple change... we'd just create an interface (maybe called This would allow us to differ this issue while preserving flexibility. What do you guys think @kinyoklion @justinabrahms ? |
@justinabrahms @kinyoklion @skyerus @james-milligan Here's an example of what I described above. We could even do this in Go, I think. If we wanted. |
I will take a look. I also have a POC for using builders in dotnet: |
I'm leaning with @kinyoklion and @justinabrahms on making the EC immutable. Using mutexes can slow down the feature flag evaluation considerably under heavy concurrency(Think a high throughput web API). The more elegant solution would be to avoid mutex locking altogether by ensuring the EC is immutable. What's the benefit of making the EC an interface other than buying time? I think we should only have a concert implementation of the EvalutionContext that is sealed(can't be extended). Seems like an unnecessary complexity with little benefit. Thoughts? |
@benjiro It buys us time and also forwards compatibility. If we decide that immutability makes things painful or generates too much GC thrash, we can release an alternative implementation without having to break our API contracts. I know this isn't a concern in go bc go has awesome interfaces.. but some of us use java. :) |
A drawback to the interface solution is that it could be confusing to application authors using the client's API for the first time and see |
@toddbaert I've created an implementation in go similar to your java implementation |
Looking at the code samples, and how the various opinions on this have shaken out, I propose that each SDK tackle this in their preferred way. This seems like the path of least resistance, since contributors to each particular language seem to agree with a direction. The spec is intentionally not prescriptive about this and it makes sense that implementing languages might make their own stylistic choices here (thread safety isn't even a thing in some languages).
|
@skyerus my vote is to take the immutability approach, i agree that using an interface may be confusing for developers |
@toddbaert, what was the conclusion for the Go SDK? immutability through #91? |
In short, yes. If you have any additional concerns though, please don't hesitate to open a new issue. |
Requirements
Inspired by open-feature/dotnet-sdk#56
The global singletons (e.g. provider, evaluationContext, logger) all share a mutex to ensure thread safety, however a client doesn't use a mutex so there's a concern if two processes want to set an evaluation context for example.
The
EvaluationContext
type has the fieldAttributes
of typemap[string]interface{}
. In go, maps are always passed by reference, this means that in theory one could pass anEvaluationContext
to a client's flag evaluation call and continue to alter theAttributes
' map in another process.This could be avoided by making the
Attributes
field unexported with an exported constructor forEvaluationContext
, this would mean that an application author couldn't change the field once constructed. However, an author could still alter the map they passed as a parameter to the constructor, which is the same map used in struct (passed by reference). In order to avoid this the constructor would need to initialise a new map and copy the map passed as a parameter.Is this overkill? Is there a better solution we can come up with?
cc @beeme1mr @toddbaert @james-milligan
The text was updated successfully, but these errors were encountered: