-
Notifications
You must be signed in to change notification settings - Fork 19
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(ForcedDecisions): add forced-decisions APIs to OptimizelyUserContext #324
Conversation
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.
partial review.
@@ -28,6 +28,7 @@ import ( | |||
"github.com/optimizely/go-sdk/pkg/config" | |||
"github.com/optimizely/go-sdk/pkg/decide" | |||
"github.com/optimizely/go-sdk/pkg/decision" | |||
pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons" |
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.
any reason for this named package?
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.
This is because there are several properties named reasons
throughout the code.
@@ -52,7 +53,7 @@ type OptimizelyClient struct { | |||
// CreateUserContext creates a context of the user for which decision APIs will be called. | |||
// A user context will be created successfully even when the SDK is not fully configured yet. | |||
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext { | |||
return newOptimizelyUserContext(o, userID, attributes) | |||
return newOptimizelyUserContext(o, userID, attributes, nil) |
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.
why passing nil?
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.
Since we don't have any instance of forcedDecisionService to pass since a new userContext
is being created. In this case, newOptimizelyUserContext
will create a forcedDecisionService
internally by itself .
pkg/client/client.go
Outdated
|
||
featureDecision, reasons, err := o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions) | ||
// check forced-decisions first | ||
variation, reasons, err := userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, key, "", &allOptions) |
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.
add a comment, passing empty because checking mapping with flagKey only.
// ForcedDecisionService defines user contexts that the SDK will use to make decisions for. | ||
type ForcedDecisionService struct { | ||
UserID string | ||
forcedDecisions map[forcedDecision]string |
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 will recommend to use key instead of struct as key. Just think what is the json representation of the object structure.
@jaeopt @The-inside-man any comments 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.
Structs
can be used as keys to a map if they hold comparable values which in our case is true since we have string values inside forcedDecision
. https://go.dev/blog/maps
I have benchmarked both map of maps and the above approach. Both approaches take almost equal time to read and write.
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.
@msohailhussain Since go allows for the use of structs as keys then I think this is fine. My assumption is that the struct becomes some hash value in the background and that is what allows for quick lookups, same as if it were a single value. The only thing we would need to be careful of is if later we extend the forced decision struct to contain more data, as if we then update that object, the key will be different and take more than one place in the map if we are not careful and removing the older instance. This can also lead to a memory leak possibly if we dont clean up old objects that "get lost in the map". I am not 100% sure the Go implementation of this, but in Java that is an issue and why it should be used very cautiously. @jaeopt Do we see any extension of the forcedDecisions in the future? If so I would say avoid this method, otherwise - it should be alright for now.
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.
agree !!!
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.
OK I checked it and struct is much faster than conventional mapOfMap.
2021/09/29 11:50:43 mapOfMaps took 545.791µs
2021/09/29 11:50:43 mapOfForcedDecisions took 270.387µs
2021/09/29 11:50:43 mapOfMaps took 460.888µs
2021/09/29 11:50:43 mapOfForcedDecisions took 294.126µs
2021/09/29 11:50:43 mapOfMaps took 498.434µs
2021/09/29 11:50:43 mapOfForcedDecisions took 240.871µs
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.
Insertion is bit slower in struct keys.
2021/09/29 12:20:50 mapOfMaps took 914.17801ms
2021/09/29 12:20:53 forcedDecision took 2.856786962s
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.
It's not likely to extend FD in future more than what we have, but cannot be guaranteed. I think it'll be safe as long as we have good test cases. Performance-wise, I don't care much since this is for testing environment only :) @msohailhussain @The-inside-man
// store a copy of the provided attributes so it isn't affected by changes made afterwards. | ||
if attributes == nil { | ||
attributes = map[string]interface{}{} | ||
} | ||
if forcedDecisionService == nil { | ||
forcedDecisionService = decision.NewForcedDecisionService(userID) |
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.
do we need to pass userId?
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.
userID
is required here for logging purposes.
|
||
import ( | ||
datafileprojectconfig "github.com/optimizely/go-sdk/pkg/config/datafileprojectconfig/entities" | ||
"github.com/optimizely/go-sdk/pkg/entities" |
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.
use named pkg, quite confusing.
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.
since we are importing two entities
files here, one has not be named.
} | ||
} | ||
|
||
return flagRulesMap |
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.
don't need to return.
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.
It overall looks great. See some suggestions and clarifications.
// ForcedDecisionService defines user contexts that the SDK will use to make decisions for. | ||
type ForcedDecisionService struct { | ||
UserID string | ||
forcedDecisions map[forcedDecision]string |
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.
It's not likely to extend FD in future more than what we have, but cannot be guaranteed. I think it'll be safe as long as we have good test cases. Performance-wise, I don't care much since this is for testing environment only :) @msohailhussain @The-inside-man
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.
All changes look great!
I still see copying forcedDecision missing. Also consider optimization for creating forced-decision-service on the first setForcedDecision call. If not used for forced decision, it should be nil for normal decision flows.
I have made the suggested changes but this is leading to all forcedDecision API's returning false or empty string until |
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.
Looks good. A couple of changes suggested.
@@ -85,21 +89,21 @@ func (o *OptimizelyUserContext) SetAttribute(key string, value interface{}) { | |||
// all data required to deliver the flag or experiment. | |||
func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDecideOptions) OptimizelyDecision { | |||
// use a copy of the user context so that any changes to the original context are not reflected inside the decision | |||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.forcedDecisionService) | |||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService()) |
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.
Shouldn't we use "o.forcedDecisionService" here? We do not want to CreateCopy until SetForcedDecision is called.
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.
My bad. It looks good as is :)
@@ -147,6 +160,9 @@ func (o *OptimizelyUserContext) RemoveAllForcedDecisions() bool { | |||
o.optimizely.logger.Error("Optimizely instance is not valid, failing removeForcedDecision call.", err) | |||
return false | |||
} | |||
if o.forcedDecisionService == nil { | |||
return false |
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 also see this can be either "true" or "false". Logically I'd like to have "false" here, but can be a burden for FSC. @msohailhussain
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.
Let's change this to "true". Logically false, but it should not change behavior depending on implementation. It still can be considered removed all successfully.
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
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.
Changes look good. Can we also remove the deprecation note from the PR summary?
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
Summary
Add a set of new APIs for forced-decisions to OptimizelyUserContext:
Test plan
FSC Result