Skip to content

Commit 4b2b788

Browse files
krithika369Krithika Sundararajan
authored and
Krithika Sundararajan
committed
Bugfix: Turing API should process experiment engine passkey only if client selection enabled (#196)
* Bugfix: Passkey should not be processed if client selection disabled * Update hardcoded sample plugin to use experiment variables, consistent with the runner * Update RPC plugin example and docs * Correct numbering in doc and plugin name change * Add debug message * Update Deployment controller to consider if client selection enabled * Add another unit test case for TestIsClientSelectionEnabled Co-authored-by: Krithika Sundararajan <[email protected]> (cherry picked from commit 83a0c6c)
1 parent 7caf52e commit 4b2b788

13 files changed

+218
-24
lines changed

api/e2e/test/helpers_api_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func getRouter(
4141

4242
err = json.Unmarshal(respBytes, &router)
4343
if err != nil {
44-
return nil, err
44+
return nil, fmt.Errorf("Could not unmarshal: %v\n %s", err, string(respBytes))
4545
}
4646

4747
return &router, err

api/turing/api/deployment_controller.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,11 @@ func (c RouterDeploymentController) deployRouterVersion(
192192
expSvc := c.BaseController.AppContext.ExperimentsService
193193
if routerVersion.ExperimentEngine.Type != models.ExperimentEngineTypeNop {
194194
experimentConfig = routerVersion.ExperimentEngine.Config
195-
if expSvc.IsStandardExperimentManager(routerVersion.ExperimentEngine.Type) {
195+
isClientSelectionEnabled, err := expSvc.IsClientSelectionEnabled(routerVersion.ExperimentEngine.Type)
196+
if err != nil {
197+
return "", c.updateRouterVersionStatusToFailed(err, routerVersion)
198+
}
199+
if isClientSelectionEnabled {
196200
// Convert the config to the standard type
197201
standardExperimentConfig, err := manager.ParseStandardExperimentConfig(experimentConfig)
198202
if err != nil {

api/turing/api/deployment_controller_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ func TestDeployVersionSuccess(t *testing.T) {
143143
exps := &mocks.ExperimentsService{}
144144
exps.
145145
On(
146-
"IsStandardExperimentManager",
146+
"IsClientSelectionEnabled",
147147
data.routerVersion.ExperimentEngine.Type,
148-
).Return(true)
148+
).Return(true, nil)
149149
exps.
150150
On(
151151
"GetExperimentRunnerConfig",
@@ -278,7 +278,7 @@ func TestRollbackVersionSuccess(t *testing.T) {
278278
es.On("Save", mock.Anything).Return(nil)
279279

280280
exps := &mocks.ExperimentsService{}
281-
exps.On("IsStandardExperimentManager", "nop").Return(false)
281+
exps.On("IsClientSelectionEnabled", "nop").Return(false, nil)
282282

283283
// Create test controller
284284
ctrl := RouterDeploymentController{

api/turing/api/request/request.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,12 @@ func (r RouterConfig) BuildExperimentEngineConfig(
206206
expSvc service.ExperimentsService,
207207
) (json.RawMessage, error) {
208208
rawExpConfig := r.ExperimentEngine.Config
209-
210-
// Handle missing passkey / encrypt it in Standard experiment config
211-
if expSvc.IsStandardExperimentManager(r.ExperimentEngine.Type) {
209+
// Handle missing passkey / encrypt it, if Standard experiment config using client selection
210+
isClientSelectionEnabled, err := expSvc.IsClientSelectionEnabled(r.ExperimentEngine.Type)
211+
if err != nil {
212+
return nil, err
213+
}
214+
if isClientSelectionEnabled {
212215
// Convert the new config to the standard type
213216
expConfig, err := manager.ParseStandardExperimentConfig(rawExpConfig)
214217
if err != nil {

api/turing/api/request/request_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ func TestRequestBuildRouterVersionWithDefaults(t *testing.T) {
411411

412412
// Set up mock Experiment service
413413
expSvc := &mocks.ExperimentsService{}
414-
expSvc.On("IsStandardExperimentManager", mock.Anything).Return(true)
414+
expSvc.On("IsClientSelectionEnabled", mock.Anything).Return(true, nil)
415415

416416
// Set up mock Ensembler service
417417
ensemblerSvc := &mocks.EnsemblersService{}
@@ -468,8 +468,8 @@ func TestBuildExperimentEngineConfig(t *testing.T) {
468468
Return("passkey-enc", nil)
469469

470470
es := &mocks.ExperimentsService{}
471-
es.On("IsStandardExperimentManager", "standard-manager").Return(true)
472-
es.On("IsStandardExperimentManager", "custom-manager").Return(false)
471+
es.On("IsClientSelectionEnabled", "standard-manager").Return(true, nil)
472+
es.On("IsClientSelectionEnabled", "custom-manager").Return(false, nil)
473473

474474
// Define tests
475475
tests := map[string]struct {

api/turing/service/experiment_service.go

+17
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const (
2424
type ExperimentsService interface {
2525
// IsStandardExperimentManager checks if the experiment manager is of the standard type
2626
IsStandardExperimentManager(engine string) bool
27+
// IsClientSelectionEnabled checks if the experiment manager is of the standard type and
28+
// has clients
29+
IsClientSelectionEnabled(engine string) (bool, error)
2730
// ListEngines returns a list of the experiment engines available
2831
ListEngines() []manager.Engine
2932
// ListClients returns a list of the clients registered on the given experiment engine
@@ -118,6 +121,20 @@ func (es *experimentsService) IsStandardExperimentManager(engine string) bool {
118121
return manager.IsStandardExperimentManager(expManager)
119122
}
120123

124+
func (es *experimentsService) IsClientSelectionEnabled(engine string) (bool, error) {
125+
expManager, err := es.getExperimentManager(engine)
126+
if err != nil {
127+
return false, err
128+
}
129+
engineInfo, err := expManager.GetEngineInfo()
130+
if err != nil {
131+
return false, err
132+
}
133+
return manager.IsStandardExperimentManager(expManager) &&
134+
engineInfo.StandardExperimentManagerConfig != nil &&
135+
engineInfo.StandardExperimentManagerConfig.ClientSelectionEnabled, nil
136+
}
137+
121138
func (es *experimentsService) ListEngines() []manager.Engine {
122139
engines := []manager.Engine{}
123140

api/turing/service/experiment_service_test.go

+132-3
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,147 @@ import (
88
"testing"
99
"time"
1010

11-
"github.com/gojek/turing/engines/experiment/manager"
12-
"github.com/gojek/turing/engines/experiment/manager/mocks"
13-
"github.com/gojek/turing/engines/experiment/pkg/request"
1411
"github.com/patrickmn/go-cache"
1512
"github.com/stretchr/testify/assert"
1613
"github.com/stretchr/testify/mock"
1714
"github.com/stretchr/testify/require"
15+
16+
"github.com/gojek/turing/engines/experiment/manager"
17+
"github.com/gojek/turing/engines/experiment/manager/mocks"
18+
"github.com/gojek/turing/engines/experiment/pkg/request"
1819
)
1920

2021
var standardExperimentManagerConfig = manager.Engine{Type: manager.StandardExperimentManagerType}
2122
var customExperimentManagerConfig = manager.Engine{Type: manager.CustomExperimentManagerType}
2223

24+
func TestIsStandardExperimentManager(t *testing.T) {
25+
tests := map[string]struct {
26+
engineInfo manager.Engine
27+
engineInfoErr string
28+
expected bool
29+
}{
30+
"standard": {
31+
engineInfo: manager.Engine{
32+
Name: "standard-engine-1",
33+
Type: manager.StandardExperimentManagerType,
34+
},
35+
expected: true,
36+
},
37+
"error": {
38+
engineInfo: manager.Engine{
39+
Name: "standard-engine-2",
40+
Type: manager.StandardExperimentManagerType,
41+
},
42+
engineInfoErr: "test error",
43+
},
44+
"custom": {
45+
engineInfo: manager.Engine{
46+
Name: "custom-engine",
47+
Type: manager.CustomExperimentManagerType,
48+
},
49+
},
50+
}
51+
52+
for name, data := range tests {
53+
t.Run(name, func(t *testing.T) {
54+
// Set up mock API call
55+
expMgr := &mocks.ExperimentManager{}
56+
var err error
57+
if data.engineInfoErr != "" {
58+
err = errors.New(data.engineInfoErr)
59+
}
60+
expMgr.On("GetEngineInfo").Return(data.engineInfo, err)
61+
// Set up experiment service
62+
svc := &experimentsService{
63+
experimentManagers: map[string]manager.ExperimentManager{
64+
data.engineInfo.Name: expMgr,
65+
},
66+
cache: cache.New(time.Second, time.Second),
67+
}
68+
// Validate
69+
isStdEngine := svc.IsStandardExperimentManager(data.engineInfo.Name)
70+
assert.Equal(t, data.expected, isStdEngine)
71+
})
72+
}
73+
}
74+
75+
func TestIsClientSelectionEnabled(t *testing.T) {
76+
// Set up mock experiment managers
77+
tests := map[string]struct {
78+
engineInfo manager.Engine
79+
engineInfoErr string
80+
expectedErr string
81+
expected bool
82+
}{
83+
"standard | no client selection": {
84+
engineInfo: manager.Engine{
85+
Name: "standard-engine-1",
86+
Type: manager.StandardExperimentManagerType,
87+
StandardExperimentManagerConfig: &manager.StandardExperimentManagerConfig{
88+
ClientSelectionEnabled: false,
89+
},
90+
},
91+
},
92+
"standard | no standard config": {
93+
engineInfo: manager.Engine{
94+
Name: "standard-engine-2",
95+
Type: manager.StandardExperimentManagerType,
96+
},
97+
},
98+
"standard | with client selection": {
99+
engineInfo: manager.Engine{
100+
Name: "standard-engine-3",
101+
Type: manager.StandardExperimentManagerType,
102+
StandardExperimentManagerConfig: &manager.StandardExperimentManagerConfig{
103+
ClientSelectionEnabled: true,
104+
},
105+
},
106+
expected: true,
107+
},
108+
"custom": {
109+
engineInfo: manager.Engine{
110+
Name: "custom-engine-1",
111+
Type: manager.CustomExperimentManagerType,
112+
},
113+
},
114+
"error": {
115+
engineInfo: manager.Engine{
116+
Name: "custom-engine-2",
117+
Type: manager.CustomExperimentManagerType,
118+
},
119+
engineInfoErr: "test error",
120+
expectedErr: "test error",
121+
},
122+
}
123+
124+
for name, data := range tests {
125+
t.Run(name, func(t *testing.T) {
126+
// Set up mock API call
127+
expMgr := &mocks.ExperimentManager{}
128+
var err error
129+
if data.engineInfoErr != "" {
130+
err = errors.New(data.engineInfoErr)
131+
}
132+
expMgr.On("GetEngineInfo").Return(data.engineInfo, err)
133+
// Set up experiment service
134+
svc := &experimentsService{
135+
experimentManagers: map[string]manager.ExperimentManager{
136+
data.engineInfo.Name: expMgr,
137+
},
138+
cache: cache.New(time.Second, time.Second),
139+
}
140+
// Validate
141+
isClientSelectionEnabled, err := svc.IsClientSelectionEnabled(data.engineInfo.Name)
142+
if data.expectedErr != "" {
143+
assert.EqualError(t, err, data.expectedErr)
144+
} else {
145+
assert.NoError(t, err)
146+
assert.Equal(t, data.expected, isClientSelectionEnabled)
147+
}
148+
})
149+
}
150+
}
151+
23152
func TestListEngines(t *testing.T) {
24153
// Set up mock Experiment Managers
25154
standardEngineInfo := manager.Engine{

api/turing/service/mocks/experiments_service.go

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Loading

engines/experiment/docs/rpc_plugins.md

+8-6
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,18 @@ func (*FooBarExperimentRunner) Configure(cfg json.RawMessage) error {
254254
ExperimentRunner's configuration is stored in the Turing database together with the rest of the Router configuration. I.e:
255255

256256
**Router Configuration Stage:**
257-
1. Turing Server calls `ExperimentEngine plugin`::`ExperimentManager`::`GetExperimentRunnerConfig` to retrieve
258-
ExperimentRunner's configuration
257+
1. Based on the type of the experiment manager (standard or custom) and the engine properties, the router's experiment data may be configured.
259258
2. Turing Server saves this data into the database as part of the Router configuration
260-
(`router_versions.experiment_engine`)
259+
(`router_versions.experiment_engine`).
260+
The screenshot below shows the configuration UI for the standard experiment engine defined in [`configs/plugin_config_example.yaml`](../examples/plugins/hardcoded/configs/plugin_config_example.yaml)
261+
262+
![example_experiment_manager_ui](./assets/example_experiment_manager_router_ui.png)
261263

262264
**Router (Re-)Deployment Stage:**
263265
1. Turing Server retrieves Router's configuration from the Database
264-
2. Turing Server creates required resources in the k8s cluster (deployments, services, config maps and secrets)
265-
3. Turing Router is being deployed and during the initialization, it establishes the connection with the
266-
ExperimentEngine plugin and passes the configuration into `ExperimentRunner`'s `Configure` method.
266+
2. Turing Server calls `ExperimentEngine plugin`::`ExperimentManager`::`GetExperimentRunnerConfig` with the saved configuration from the DB, to create the configuration used by the `ExperimentRunner`
267+
3. Turing Server creates required resources in the k8s cluster (deployments, services, config maps and secrets)
268+
4. Turing Router is being deployed and during the initialization, it establishes the connection with the ExperimentEngine plugin and passes the configuration from step 2 into `ExperimentRunner`'s `Configure` method.
267269

268270
### Logging
269271
In order for Turing Server/Router to include log messages from the Experiment Engine plugin, you can use

engines/experiment/examples/plugins/hardcoded/config.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ type Experiment struct {
2525
}
2626

2727
type ManagerConfig struct {
28-
Engine manager.Engine `json:"engine"`
29-
Experiments []Experiment `json:"experiments"`
30-
Variables []manager.Variables `json:"variables"`
28+
Engine manager.Engine `json:"engine"`
29+
Experiments []Experiment `json:"experiments"`
30+
Variables map[string][]manager.Variable `json:"variables"`
3131
}
3232

3333
type RunnerConfig struct {

engines/experiment/examples/plugins/hardcoded/configs/plugin_config_example.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ engine:
55
type: standard
66
standard_experiment_manager_config:
77
client_selection_enabled: false
8-
experiment_selection_enabled: false
8+
experiment_selection_enabled: true
99
experiments:
1010
- id: '001'
1111
name: exp_1
@@ -21,3 +21,8 @@ experiments:
2121
traffic: 0.15
2222
treatment_configuration:
2323
bar: baz
24+
variables:
25+
'001':
26+
- name: client
27+
required: true
28+
type: unit

engines/experiment/examples/plugins/hardcoded/manager.go

+13
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package hardcoded
22

33
import (
44
"encoding/json"
5+
56
"github.com/gojek/turing/engines/experiment/manager"
67
)
78

89
type ExperimentManager struct {
910
*manager.BaseStandardExperimentManager
1011
experiments map[string]Experiment
12+
variables map[string][]manager.Variable
1113
}
1214

1315
func (e *ExperimentManager) Configure(cfg json.RawMessage) error {
@@ -23,6 +25,7 @@ func (e *ExperimentManager) Configure(cfg json.RawMessage) error {
2325
for _, exp := range config.Experiments {
2426
e.experiments[exp.Name] = exp
2527
}
28+
e.variables = config.Variables
2629
return nil
2730
}
2831

@@ -39,6 +42,16 @@ func (e *ExperimentManager) ListExperimentsForClient(manager.Client) ([]manager.
3942
return e.ListExperiments()
4043
}
4144

45+
func (e *ExperimentManager) ListVariablesForExperiments(exps []manager.Experiment) (map[string][]manager.Variable, error) {
46+
variableMap := map[string][]manager.Variable{}
47+
for _, exp := range exps {
48+
if variables, ok := e.variables[exp.ID]; ok {
49+
variableMap[exp.ID] = variables
50+
}
51+
}
52+
return variableMap, nil
53+
}
54+
4255
func (e ExperimentManager) GetExperimentRunnerConfig(cfg json.RawMessage) (json.RawMessage, error) {
4356
standardExpCfg, err := manager.ParseStandardExperimentConfig(cfg)
4457
if err != nil {

0 commit comments

Comments
 (0)