Skip to content

Commit f058405

Browse files
committed
Merge branch 'master' into mpirnovar-expose-prediction-endpoint-fssdk-12010-py
Resolved conflict in optimizely/optimizely.py: - Kept master's new cmab_service parameter support - Added prediction endpoint support inside the else block (when using default CMAB service) - All CMAB tests passing (22/22)
2 parents ac65f4b + caba597 commit f058405

File tree

10 files changed

+403
-40
lines changed

10 files changed

+403
-40
lines changed

optimizely/cmab/cmab_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from optimizely.exceptions import CmabFetchError, CmabInvalidResponseError
2121

2222
# Default constants for CMAB requests
23-
DEFAULT_MAX_RETRIES = 3
23+
DEFAULT_MAX_RETRIES = 1
2424
DEFAULT_INITIAL_BACKOFF = 0.1 # in seconds (100 ms)
2525
DEFAULT_MAX_BACKOFF = 10 # in seconds
2626
DEFAULT_BACKOFF_MULTIPLIER = 2.0

optimizely/cmab/cmab_service.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@
1515
import hashlib
1616
import threading
1717

18-
from typing import Optional, List, TypedDict
18+
from typing import Optional, List, TypedDict, Tuple
1919
from optimizely.cmab.cmab_client import DefaultCmabClient
2020
from optimizely.odp.lru_cache import LRUCache
2121
from optimizely.optimizely_user_context import OptimizelyUserContext, UserAttributes
2222
from optimizely.project_config import ProjectConfig
2323
from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption
2424
from optimizely import logger as _logging
2525
from optimizely.lib import pymmh3 as mmh3
26+
2627
NUM_LOCK_STRIPES = 1000
28+
DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 # 30 minutes
29+
DEFAULT_CMAB_CACHE_SIZE = 10000
2730

2831

2932
class CmabDecision(TypedDict):
@@ -65,26 +68,40 @@ def _get_lock_index(self, user_id: str, rule_id: str) -> int:
6568
return hash_value % NUM_LOCK_STRIPES
6669

6770
def get_decision(self, project_config: ProjectConfig, user_context: OptimizelyUserContext,
68-
rule_id: str, options: List[str]) -> CmabDecision:
71+
rule_id: str, options: List[str]) -> Tuple[CmabDecision, List[str]]:
6972

7073
lock_index = self._get_lock_index(user_context.user_id, rule_id)
7174
with self.locks[lock_index]:
7275
return self._get_decision(project_config, user_context, rule_id, options)
7376

7477
def _get_decision(self, project_config: ProjectConfig, user_context: OptimizelyUserContext,
75-
rule_id: str, options: List[str]) -> CmabDecision:
78+
rule_id: str, options: List[str]) -> Tuple[CmabDecision, List[str]]:
7679

7780
filtered_attributes = self._filter_attributes(project_config, user_context, rule_id)
81+
reasons = []
7882

7983
if OptimizelyDecideOption.IGNORE_CMAB_CACHE in options:
80-
return self._fetch_decision(rule_id, user_context.user_id, filtered_attributes)
84+
reason = f"Ignoring CMAB cache for user '{user_context.user_id}' and rule '{rule_id}'"
85+
if self.logger:
86+
self.logger.debug(reason)
87+
reasons.append(reason)
88+
cmab_decision = self._fetch_decision(rule_id, user_context.user_id, filtered_attributes)
89+
return cmab_decision, reasons
8190

8291
if OptimizelyDecideOption.RESET_CMAB_CACHE in options:
92+
reason = f"Resetting CMAB cache for user '{user_context.user_id}' and rule '{rule_id}'"
93+
if self.logger:
94+
self.logger.debug(reason)
95+
reasons.append(reason)
8396
self.cmab_cache.reset()
8497

8598
cache_key = self._get_cache_key(user_context.user_id, rule_id)
8699

87100
if OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE in options:
101+
reason = f"Invalidating CMAB cache for user '{user_context.user_id}' and rule '{rule_id}'"
102+
if self.logger:
103+
self.logger.debug(reason)
104+
reasons.append(reason)
88105
self.cmab_cache.remove(cache_key)
89106

90107
cached_value = self.cmab_cache.lookup(cache_key)
@@ -93,17 +110,39 @@ def _get_decision(self, project_config: ProjectConfig, user_context: OptimizelyU
93110

94111
if cached_value:
95112
if cached_value['attributes_hash'] == attributes_hash:
96-
return CmabDecision(variation_id=cached_value['variation_id'], cmab_uuid=cached_value['cmab_uuid'])
113+
reason = f"CMAB cache hit for user '{user_context.user_id}' and rule '{rule_id}'"
114+
if self.logger:
115+
self.logger.debug(reason)
116+
reasons.append(reason)
117+
return CmabDecision(variation_id=cached_value['variation_id'],
118+
cmab_uuid=cached_value['cmab_uuid']), reasons
97119
else:
120+
reason = (
121+
f"CMAB cache attributes mismatch for user '{user_context.user_id}' "
122+
f"and rule '{rule_id}', fetching new decision."
123+
)
124+
if self.logger:
125+
self.logger.debug(reason)
126+
reasons.append(reason)
98127
self.cmab_cache.remove(cache_key)
128+
else:
129+
reason = f"CMAB cache miss for user '{user_context.user_id}' and rule '{rule_id}'"
130+
if self.logger:
131+
self.logger.debug(reason)
132+
reasons.append(reason)
99133

100134
cmab_decision = self._fetch_decision(rule_id, user_context.user_id, filtered_attributes)
135+
reason = f"CMAB decision is {cmab_decision}"
136+
if self.logger:
137+
self.logger.debug(reason)
138+
reasons.append(reason)
139+
101140
self.cmab_cache.save(cache_key, {
102141
'attributes_hash': attributes_hash,
103142
'variation_id': cmab_decision['variation_id'],
104143
'cmab_uuid': cmab_decision['cmab_uuid'],
105144
})
106-
return cmab_decision
145+
return cmab_decision, reasons
107146

108147
def _fetch_decision(self, rule_id: str, user_id: str, attributes: UserAttributes) -> CmabDecision:
109148
cmab_uuid = str(uuid.uuid4())

optimizely/decision_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,10 @@ def _get_decision_for_cmab_experiment(
175175
# User is in CMAB allocation, proceed to CMAB decision
176176
try:
177177
options_list = list(options) if options is not None else []
178-
cmab_decision = self.cmab_service.get_decision(
178+
cmab_decision, cmab_reasons = self.cmab_service.get_decision(
179179
project_config, user_context, experiment.id, options_list
180180
)
181+
decide_reasons.extend(cmab_reasons)
181182
return {
182183
"error": False,
183184
"result": cmab_decision,

optimizely/event/event_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class Signal:
7272

7373
def __init__(
7474
self,
75-
event_dispatcher: Optional[type[EventDispatcher] | CustomEventDispatcher] = None,
75+
event_dispatcher: Optional[EventDispatcher | CustomEventDispatcher] = None,
7676
logger: Optional[_logging.Logger] = None,
7777
start_on_init: bool = False,
7878
event_queue: Optional[queue.Queue[UserEvent | Signal]] = None,

optimizely/optimizely.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,13 @@
4646
from .optimizely_user_context import OptimizelyUserContext, UserAttributes
4747
from .project_config import ProjectConfig
4848
from .cmab.cmab_client import DefaultCmabClient, CmabRetryConfig
49-
from .cmab.cmab_service import DefaultCmabService, CmabCacheValue
49+
from .cmab.cmab_service import DefaultCmabService, CmabCacheValue, DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT
5050

5151
if TYPE_CHECKING:
5252
# prevent circular dependency by skipping import at runtime
5353
from .user_profile import UserProfileService
5454
from .helpers.event_tag_utils import EventTags
5555

56-
# Default constants for CMAB cache
57-
DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 * 1000 # 30 minutes in milliseconds
58-
DEFAULT_CMAB_CACHE_SIZE = 1000
59-
6056

6157
class Optimizely:
6258
""" Class encapsulating all SDK functionality. """
@@ -77,6 +73,7 @@ def __init__(
7773
default_decide_options: Optional[list[str]] = None,
7874
event_processor_options: Optional[dict[str, Any]] = None,
7975
settings: Optional[OptimizelySdkSettings] = None,
76+
cmab_service: Optional[DefaultCmabService] = None,
8077
) -> None:
8178
""" Optimizely init method for managing Custom projects.
8279
@@ -178,21 +175,26 @@ def __init__(
178175
self.event_builder = event_builder.EventBuilder()
179176

180177
# Initialize CMAB components
181-
cmab_prediction_endpoint = None
182-
if self.sdk_settings and self.sdk_settings.cmab_prediction_endpoint:
183-
cmab_prediction_endpoint = self.sdk_settings.cmab_prediction_endpoint
184-
185-
self.cmab_client = DefaultCmabClient(
186-
retry_config=CmabRetryConfig(),
187-
logger=self.logger,
188-
prediction_endpoint=cmab_prediction_endpoint
189-
)
190-
self.cmab_cache: LRUCache[str, CmabCacheValue] = LRUCache(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT)
191-
self.cmab_service = DefaultCmabService(
192-
cmab_cache=self.cmab_cache,
193-
cmab_client=self.cmab_client,
194-
logger=self.logger
195-
)
178+
if cmab_service:
179+
self.cmab_service = cmab_service
180+
else:
181+
# Get custom prediction endpoint from settings if provided
182+
cmab_prediction_endpoint = None
183+
if self.sdk_settings and self.sdk_settings.cmab_prediction_endpoint:
184+
cmab_prediction_endpoint = self.sdk_settings.cmab_prediction_endpoint
185+
186+
self.cmab_client = DefaultCmabClient(
187+
retry_config=CmabRetryConfig(),
188+
logger=self.logger,
189+
prediction_endpoint=cmab_prediction_endpoint
190+
)
191+
self.cmab_cache: LRUCache[str, CmabCacheValue] = LRUCache(DEFAULT_CMAB_CACHE_SIZE,
192+
DEFAULT_CMAB_CACHE_TIMEOUT)
193+
self.cmab_service = DefaultCmabService(
194+
cmab_cache=self.cmab_cache,
195+
cmab_client=self.cmab_client,
196+
logger=self.logger
197+
)
196198
self.decision_service = decision_service.DecisionService(self.logger, user_profile_service, self.cmab_service)
197199
self.user_profile_service = user_profile_service
198200

optimizely/optimizely_factory.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
from .event_dispatcher import EventDispatcher, CustomEventDispatcher
2323
from .notification_center import NotificationCenter
2424
from .optimizely import Optimizely
25+
from .odp.lru_cache import LRUCache
26+
from .cmab.cmab_client import DefaultCmabClient, CmabRetryConfig
27+
from .cmab.cmab_service import DefaultCmabService, CmabCacheValue, DEFAULT_CMAB_CACHE_TIMEOUT, DEFAULT_CMAB_CACHE_SIZE
2528

2629
if TYPE_CHECKING:
2730
# prevent circular dependenacy by skipping import at runtime
@@ -36,6 +39,9 @@ class OptimizelyFactory:
3639
max_event_flush_interval: Optional[int] = None
3740
polling_interval: Optional[float] = None
3841
blocking_timeout: Optional[int] = None
42+
cmab_cache_size: Optional[int] = None
43+
cmab_cache_ttl: Optional[int] = None
44+
cmab_custom_cache: Optional[LRUCache[str, CmabCacheValue]] = None
3945

4046
@staticmethod
4147
def set_batch_size(batch_size: int) -> int:
@@ -75,6 +81,51 @@ def set_blocking_timeout(blocking_timeout: int) -> int:
7581
OptimizelyFactory.blocking_timeout = blocking_timeout
7682
return OptimizelyFactory.blocking_timeout
7783

84+
@staticmethod
85+
def set_cmab_cache_size(cache_size: int, logger: Optional[optimizely_logger.Logger] = None) -> Optional[int]:
86+
""" Convenience method for setting the maximum number of items in CMAB cache.
87+
Args:
88+
cache_size: Maximum number of items in CMAB cache.
89+
logger: Optional logger for logging messages.
90+
"""
91+
logger = logger or optimizely_logger.NoOpLogger()
92+
93+
if not isinstance(cache_size, int) or cache_size <= 0:
94+
logger.error(
95+
f"CMAB cache size is invalid, setting to default size {DEFAULT_CMAB_CACHE_SIZE}."
96+
)
97+
return None
98+
99+
OptimizelyFactory.cmab_cache_size = cache_size
100+
return OptimizelyFactory.cmab_cache_size
101+
102+
@staticmethod
103+
def set_cmab_cache_ttl(cache_ttl: int, logger: Optional[optimizely_logger.Logger] = None) -> Optional[int]:
104+
""" Convenience method for setting CMAB cache TTL.
105+
Args:
106+
cache_ttl: Time in seconds for cache entries to live.
107+
logger: Optional logger for logging messages.
108+
"""
109+
logger = logger or optimizely_logger.NoOpLogger()
110+
111+
if not isinstance(cache_ttl, (int, float)) or cache_ttl <= 0:
112+
logger.error(
113+
f"CMAB cache TTL is invalid, setting to default TTL {DEFAULT_CMAB_CACHE_TIMEOUT}."
114+
)
115+
return None
116+
117+
OptimizelyFactory.cmab_cache_ttl = int(cache_ttl)
118+
return OptimizelyFactory.cmab_cache_ttl
119+
120+
@staticmethod
121+
def set_cmab_custom_cache(custom_cache: LRUCache[str, CmabCacheValue]) -> LRUCache[str, CmabCacheValue]:
122+
""" Convenience method for setting custom CMAB cache.
123+
Args:
124+
custom_cache: Cache implementation with lookup, save, remove, and reset methods.
125+
"""
126+
OptimizelyFactory.cmab_custom_cache = custom_cache
127+
return OptimizelyFactory.cmab_custom_cache
128+
78129
@staticmethod
79130
def default_instance(sdk_key: str, datafile: Optional[str] = None) -> Optimizely:
80131
""" Returns a new optimizely instance..
@@ -104,9 +155,17 @@ def default_instance(sdk_key: str, datafile: Optional[str] = None) -> Optimizely
104155
notification_center=notification_center,
105156
)
106157

158+
# Initialize CMAB components
159+
cmab_client = DefaultCmabClient(retry_config=CmabRetryConfig(), logger=logger)
160+
cmab_cache = OptimizelyFactory.cmab_custom_cache or LRUCache(
161+
OptimizelyFactory.cmab_cache_size or DEFAULT_CMAB_CACHE_SIZE,
162+
OptimizelyFactory.cmab_cache_ttl or DEFAULT_CMAB_CACHE_TIMEOUT
163+
)
164+
cmab_service = DefaultCmabService(cmab_cache, cmab_client, logger)
165+
107166
optimizely = Optimizely(
108167
datafile, None, logger, error_handler, None, None, sdk_key, config_manager, notification_center,
109-
event_processor
168+
event_processor, cmab_service=cmab_service
110169
)
111170
return optimizely
112171

@@ -174,7 +233,16 @@ def custom_instance(
174233
notification_center=notification_center,
175234
)
176235

236+
# Initialize CMAB components
237+
cmab_client = DefaultCmabClient(retry_config=CmabRetryConfig(), logger=logger)
238+
cmab_cache = OptimizelyFactory.cmab_custom_cache or LRUCache(
239+
OptimizelyFactory.cmab_cache_size or DEFAULT_CMAB_CACHE_SIZE,
240+
OptimizelyFactory.cmab_cache_ttl or DEFAULT_CMAB_CACHE_TIMEOUT
241+
)
242+
cmab_service = DefaultCmabService(cmab_cache, cmab_client, logger)
243+
177244
return Optimizely(
178245
datafile, event_dispatcher, logger, error_handler, skip_json_validation, user_profile_service,
179-
sdk_key, config_manager, notification_center, event_processor, settings=settings
246+
sdk_key, config_manager, notification_center, event_processor, settings=settings,
247+
cmab_service=cmab_service
180248
)

0 commit comments

Comments
 (0)