|
13 | 13 | import uuid
|
14 | 14 | import json
|
15 | 15 | import hashlib
|
| 16 | +import threading |
16 | 17 |
|
17 | 18 | from typing import Optional, List, TypedDict
|
18 | 19 | from optimizely.cmab.cmab_client import DefaultCmabClient
|
|
22 | 23 | from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption
|
23 | 24 | from optimizely import logger as _logging
|
24 | 25 |
|
| 26 | +NUM_LOCK_STRIPES = 1000 |
| 27 | + |
25 | 28 |
|
26 | 29 | class CmabDecision(TypedDict):
|
27 | 30 | variation_id: str
|
@@ -52,40 +55,50 @@ def __init__(self, cmab_cache: LRUCache[str, CmabCacheValue],
|
52 | 55 | self.cmab_cache = cmab_cache
|
53 | 56 | self.cmab_client = cmab_client
|
54 | 57 | self.logger = logger
|
| 58 | + self.locks = [threading.Lock() for _ in range(NUM_LOCK_STRIPES)] |
| 59 | + |
| 60 | + def _get_lock_index(self, user_id: str, rule_id: str) -> int: |
| 61 | + """Calculate the lock index for a given user and rule combination.""" |
| 62 | + # Create a hash of user_id + rule_id for consistent lock selection |
| 63 | + hash_input = f"{user_id}{rule_id}" |
| 64 | + hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16) % NUM_LOCK_STRIPES |
| 65 | + return hash_value |
55 | 66 |
|
56 | 67 | def get_decision(self, project_config: ProjectConfig, user_context: OptimizelyUserContext,
|
57 | 68 | rule_id: str, options: List[str]) -> CmabDecision:
|
58 | 69 |
|
59 |
| - filtered_attributes = self._filter_attributes(project_config, user_context, rule_id) |
| 70 | + lock_index = self._get_lock_index(user_context.user_id, rule_id) |
| 71 | + with self.locks[lock_index]: |
| 72 | + filtered_attributes = self._filter_attributes(project_config, user_context, rule_id) |
60 | 73 |
|
61 |
| - if OptimizelyDecideOption.IGNORE_CMAB_CACHE in options: |
62 |
| - return self._fetch_decision(rule_id, user_context.user_id, filtered_attributes) |
| 74 | + if OptimizelyDecideOption.IGNORE_CMAB_CACHE in options: |
| 75 | + return self._fetch_decision(rule_id, user_context.user_id, filtered_attributes) |
63 | 76 |
|
64 |
| - if OptimizelyDecideOption.RESET_CMAB_CACHE in options: |
65 |
| - self.cmab_cache.reset() |
| 77 | + if OptimizelyDecideOption.RESET_CMAB_CACHE in options: |
| 78 | + self.cmab_cache.reset() |
66 | 79 |
|
67 |
| - cache_key = self._get_cache_key(user_context.user_id, rule_id) |
| 80 | + cache_key = self._get_cache_key(user_context.user_id, rule_id) |
68 | 81 |
|
69 |
| - if OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE in options: |
70 |
| - self.cmab_cache.remove(cache_key) |
| 82 | + if OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE in options: |
| 83 | + self.cmab_cache.remove(cache_key) |
71 | 84 |
|
72 |
| - cached_value = self.cmab_cache.lookup(cache_key) |
| 85 | + cached_value = self.cmab_cache.lookup(cache_key) |
73 | 86 |
|
74 |
| - attributes_hash = self._hash_attributes(filtered_attributes) |
| 87 | + attributes_hash = self._hash_attributes(filtered_attributes) |
75 | 88 |
|
76 |
| - if cached_value: |
77 |
| - if cached_value['attributes_hash'] == attributes_hash: |
78 |
| - return CmabDecision(variation_id=cached_value['variation_id'], cmab_uuid=cached_value['cmab_uuid']) |
79 |
| - else: |
80 |
| - self.cmab_cache.remove(cache_key) |
| 89 | + if cached_value: |
| 90 | + if cached_value['attributes_hash'] == attributes_hash: |
| 91 | + return CmabDecision(variation_id=cached_value['variation_id'], cmab_uuid=cached_value['cmab_uuid']) |
| 92 | + else: |
| 93 | + self.cmab_cache.remove(cache_key) |
81 | 94 |
|
82 |
| - cmab_decision = self._fetch_decision(rule_id, user_context.user_id, filtered_attributes) |
83 |
| - self.cmab_cache.save(cache_key, { |
84 |
| - 'attributes_hash': attributes_hash, |
85 |
| - 'variation_id': cmab_decision['variation_id'], |
86 |
| - 'cmab_uuid': cmab_decision['cmab_uuid'], |
87 |
| - }) |
88 |
| - return cmab_decision |
| 95 | + cmab_decision = self._fetch_decision(rule_id, user_context.user_id, filtered_attributes) |
| 96 | + self.cmab_cache.save(cache_key, { |
| 97 | + 'attributes_hash': attributes_hash, |
| 98 | + 'variation_id': cmab_decision['variation_id'], |
| 99 | + 'cmab_uuid': cmab_decision['cmab_uuid'], |
| 100 | + }) |
| 101 | + return cmab_decision |
89 | 102 |
|
90 | 103 | def _fetch_decision(self, rule_id: str, user_id: str, attributes: UserAttributes) -> CmabDecision:
|
91 | 104 | cmab_uuid = str(uuid.uuid4())
|
|
0 commit comments