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(ForcedDecisions): add forced-decisions APIs to OptimizelyUserContext #324

Merged
merged 20 commits into from
Oct 20, 2021

Conversation

yasirfolio3
Copy link
Contributor

@yasirfolio3 yasirfolio3 commented Sep 28, 2021

Summary

Add a set of new APIs for forced-decisions to OptimizelyUserContext:

  • SetForcedDecision
  • GetForcedDecision
  • RemoveForcedDecision
  • RemoveAllForcedDecisions

Test plan

  • unit tests for the new APIs
  • FSC tests with new test cases

FSC Result

Copy link
Contributor

@msohailhussain msohailhussain left a 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"
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

why passing nil?

Copy link
Contributor Author

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 .


featureDecision, reasons, err := o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions)
// check forced-decisions first
variation, reasons, err := userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, key, "", &allOptions)
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

agree !!!

Copy link
Contributor

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

Copy link
Contributor

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

Copy link

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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"
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

@msohailhussain msohailhussain marked this pull request as ready for review September 29, 2021 18:53
@msohailhussain msohailhussain requested a review from a team as a code owner September 29, 2021 18:53
Copy link

@jaeopt jaeopt left a 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.

pkg/client/client.go Outdated Show resolved Hide resolved
pkg/client/optimizely_user_context.go Show resolved Hide resolved
pkg/decision/rollout_service.go Outdated Show resolved Hide resolved
// ForcedDecisionService defines user contexts that the SDK will use to make decisions for.
type ForcedDecisionService struct {
UserID string
forcedDecisions map[forcedDecision]string
Copy link

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

pkg/decision/forced_decision_service.go Show resolved Hide resolved
pkg/client/optimizely_user_context_test.go Show resolved Hide resolved
pkg/decision/forced_decision_service_test.go Show resolved Hide resolved
@yasirfolio3 yasirfolio3 deleted the yasir/forced-decision branch September 30, 2021 07:01
@yasirfolio3 yasirfolio3 restored the yasir/forced-decision branch September 30, 2021 07:04
@yasirfolio3 yasirfolio3 reopened this Sep 30, 2021
Copy link

@jaeopt jaeopt left a 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.

@yasirfolio3 yasirfolio3 removed their assignment Oct 1, 2021
@yasirfolio3
Copy link
Contributor Author

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 setForcedDecision is called once. We might have to update FSC tests accordingly too as currently, one of the tests expects remove_all_forced_decisions to return true in every case.

Copy link

@jaeopt jaeopt left a 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())
Copy link

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.

Copy link

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
Copy link

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

Copy link

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.

Copy link

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link

@jaeopt jaeopt left a 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?

Copy link

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

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

LGTM

@msohailhussain msohailhussain merged commit 5cbe8e7 into master Oct 20, 2021
@msohailhussain msohailhussain deleted the yasir/forced-decision branch October 20, 2021 19:31
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