Skip to content

Commit 6be3cbd

Browse files
fix: odp issues identified by FSC (#412)
* check if opti instance is valid on odp methods * fix variable missing error * fix extrenous identify calls * change integrations to default to first with key * fix cache_size bug * add timeout to pollingconfig stop * Update python.yml * revert branch to master * fix create_user_context * remove unnecessary checks Co-authored-by: Matjaz Pirnovar <[email protected]>
1 parent f67a0cc commit 6be3cbd

File tree

6 files changed

+41
-29
lines changed

6 files changed

+41
-29
lines changed

Diff for: .github/workflows/python.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name: build
55

66
on:
77
push:
8-
branches: [ master ]
8+
branches: [ master ]
99
pull_request:
1010
branches: [ master ]
1111

Diff for: optimizely/config_manager.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,11 @@ def is_running(self) -> bool:
375375
return self._polling_thread.is_alive()
376376

377377
def stop(self) -> None:
378-
""" Stop the polling thread and wait for it to exit. """
378+
""" Stop the polling thread and briefly wait for it to exit. """
379379
if self.is_running:
380380
self.stopped.set()
381-
self._polling_thread.join()
381+
# no need to wait too long as this exists to avoid interfering with tests
382+
self._polling_thread.join(timeout=0.2)
382383

383384
def _run(self) -> None:
384385
""" Triggered as part of the thread which fetches the datafile and sleeps until next update interval. """

Diff for: optimizely/entities.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def __str__(self) -> str:
188188

189189

190190
class Integration(BaseEntity):
191-
def __init__(self, key: str, host: Optional[str] = None, publicKey: Optional[str] = None):
191+
def __init__(self, key: str, host: Optional[str] = None, publicKey: Optional[str] = None, **kwargs: Any):
192192
self.key = key
193193
self.host = host
194194
self.publicKey = publicKey

Diff for: optimizely/optimizely.py

+24-20
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,8 @@ def _get_feature_variable_for_type(
345345
source_info = {}
346346
variable_value = variable.defaultValue
347347

348-
user_context = self.create_user_context(user_id, attributes)
349-
# error is logged in create_user_context
350-
if user_context is None:
351-
return None
348+
user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False)
349+
352350
decision, _ = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_context)
353351

354352
if decision.variation:
@@ -434,10 +432,8 @@ def _get_all_feature_variables_for_type(
434432
feature_enabled = False
435433
source_info = {}
436434

437-
user_context = self.create_user_context(user_id, attributes)
438-
# error is logged in create_user_context
439-
if user_context is None:
440-
return None
435+
user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False)
436+
441437
decision, _ = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_context)
442438

443439
if decision.variation:
@@ -643,10 +639,7 @@ def get_variation(
643639
if not self._validate_user_inputs(attributes):
644640
return None
645641

646-
user_context = self.create_user_context(user_id, attributes)
647-
# error is logged in create_user_context
648-
if not user_context:
649-
return None
642+
user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False)
650643

651644
variation, _ = self.decision_service.get_variation(project_config, experiment, user_context)
652645
if variation:
@@ -705,10 +698,8 @@ def is_feature_enabled(self, feature_key: str, user_id: str, attributes: Optiona
705698

706699
feature_enabled = False
707700
source_info = {}
708-
user_context = self.create_user_context(user_id, attributes)
709-
# error is logged in create_user_context
710-
if not user_context:
711-
return False
701+
702+
user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False)
712703

713704
decision, _ = self.decision_service.get_variation_for_feature(project_config, feature, user_context)
714705
is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST
@@ -1083,7 +1074,7 @@ def create_user_context(
10831074
self.logger.error(enums.Errors.INVALID_INPUT.format('attributes'))
10841075
return None
10851076

1086-
return OptimizelyUserContext(self, self.logger, user_id, attributes)
1077+
return OptimizelyUserContext(self, self.logger, user_id, attributes, True)
10871078

10881079
def _decide(
10891080
self, user_context: Optional[OptimizelyUserContext], key: str,
@@ -1330,8 +1321,8 @@ def setup_odp(self) -> None:
13301321

13311322
if not self.sdk_settings.segments_cache:
13321323
self.sdk_settings.segments_cache = LRUCache(
1333-
self.sdk_settings.segments_cache_size or enums.OdpSegmentsCacheConfig.DEFAULT_CAPACITY,
1334-
self.sdk_settings.segments_cache_timeout_in_secs or enums.OdpSegmentsCacheConfig.DEFAULT_TIMEOUT_SECS
1324+
self.sdk_settings.segments_cache_size,
1325+
self.sdk_settings.segments_cache_timeout_in_secs
13351326
)
13361327

13371328
def _update_odp_config_on_datafile_update(self) -> None:
@@ -1354,9 +1345,17 @@ def _update_odp_config_on_datafile_update(self) -> None:
13541345
)
13551346

13561347
def identify_user(self, user_id: str) -> None:
1348+
if not self.is_valid:
1349+
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('identify_user'))
1350+
return
1351+
13571352
self.odp_manager.identify_user(user_id)
13581353

13591354
def fetch_qualified_segments(self, user_id: str, options: Optional[list[str]] = None) -> Optional[list[str]]:
1355+
if not self.is_valid:
1356+
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('fetch_qualified_segments'))
1357+
return None
1358+
13601359
return self.odp_manager.fetch_qualified_segments(user_id, options or [])
13611360

13621361
def send_odp_event(
@@ -1376,11 +1375,16 @@ def send_odp_event(
13761375
data: An optional dictionary for associated data. The default event data will be added to this data
13771376
before sending to the ODP server.
13781377
"""
1378+
if not self.is_valid:
1379+
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('send_odp_event'))
1380+
return
1381+
13791382
self.odp_manager.send_event(type, action, identifiers or {}, data or {})
13801383

13811384
def close(self) -> None:
13821385
if callable(getattr(self.event_processor, 'stop', None)):
13831386
self.event_processor.stop() # type: ignore[attr-defined]
1384-
self.odp_manager.close()
1387+
if self.is_valid:
1388+
self.odp_manager.close()
13851389
if callable(getattr(self.config_manager, 'stop', None)):
13861390
self.config_manager.stop() # type: ignore[attr-defined]

Diff for: optimizely/optimizely_config.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,11 @@ def _get_variables_map(
343343

344344
# set variation specific variable value if any
345345
if variation.get('featureEnabled'):
346+
feature_variables_map = self.feature_key_variable_id_to_variable_map[feature_flag['key']]
346347
for variable in variation.get('variables', []):
347-
feature_variable = self.feature_key_variable_id_to_variable_map[feature_flag['key']][variable['id']]
348-
variables_map[feature_variable.key].value = variable['value']
348+
feature_variable = feature_variables_map.get(variable['id'])
349+
if feature_variable:
350+
variables_map[feature_variable.key].value = variable['value']
349351

350352
return variables_map
351353

Diff for: optimizely/project_config.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
112112
self.experiment_id_map[experiment_dict['id']] = entities.Experiment(**experiment_dict)
113113

114114
if self.integrations:
115-
self.integration_key_map = self._generate_key_map(self.integrations, 'key', entities.Integration)
115+
self.integration_key_map = self._generate_key_map(
116+
self.integrations, 'key', entities.Integration, first_value=True
117+
)
116118
odp_integration = self.integration_key_map.get('odp')
117119
if odp_integration:
118120
self.public_key_for_odp = odp_integration.publicKey
@@ -191,21 +193,24 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
191193

192194
@staticmethod
193195
def _generate_key_map(
194-
entity_list: Iterable[Any], key: str, entity_class: Type[EntityClass]
196+
entity_list: Iterable[Any], key: str, entity_class: Type[EntityClass], first_value: bool = False
195197
) -> dict[str, EntityClass]:
196198
""" Helper method to generate map from key to entity object for given list of dicts.
197199
198200
Args:
199201
entity_list: List consisting of dict.
200202
key: Key in each dict which will be key in the map.
201203
entity_class: Class representing the entity.
204+
first_value: If True, only save the first value found for each key.
202205
203206
Returns:
204207
Map mapping key to entity object.
205208
"""
206209

207-
key_map = {}
210+
key_map: dict[str, EntityClass] = {}
208211
for obj in entity_list:
212+
if first_value and key_map.get(obj[key]):
213+
continue
209214
key_map[obj[key]] = entity_class(**obj)
210215

211216
return key_map

0 commit comments

Comments
 (0)