Skip to content

Commit 116d23a

Browse files
[FSSDK-11901] Fix concurrency bug in cmab service (#462)
* Update: Implement locking mechanism for CMAB service to enhance concurrency * Update: Refactor get_decision method * update: type checking fix * Update: Remove Python 3.8 from testing matrix in CI workflow
1 parent 1ea261b commit 116d23a

22 files changed

+76
-24
lines changed

.github/workflows/python.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,8 @@ jobs:
6565
fail-fast: false
6666
matrix:
6767
python-version:
68-
- "pypy-3.8"
6968
- "pypy-3.9"
7069
- "pypy-3.10"
71-
- "3.8"
7270
- "3.9"
7371
- "3.10"
7472
- "3.11"
@@ -93,10 +91,8 @@ jobs:
9391
fail-fast: false
9492
matrix:
9593
python-version:
96-
- "pypy-3.8"
9794
- "pypy-3.9"
9895
- "pypy-3.10"
99-
- "3.8"
10096
- "3.9"
10197
- "3.10"
10298
- "3.11"

optimizely/bucketer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
if version_info < (3, 8):
2323
from typing_extensions import Final
2424
else:
25-
from typing import Final # type: ignore
25+
from typing import Final
2626

2727

2828
if TYPE_CHECKING:

optimizely/cmab/cmab_service.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import uuid
1414
import json
1515
import hashlib
16+
import threading
1617

1718
from typing import Optional, List, TypedDict
1819
from optimizely.cmab.cmab_client import DefaultCmabClient
@@ -21,6 +22,8 @@
2122
from optimizely.project_config import ProjectConfig
2223
from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption
2324
from optimizely import logger as _logging
25+
from optimizely.lib import pymmh3 as mmh3
26+
NUM_LOCK_STRIPES = 1000
2427

2528

2629
class CmabDecision(TypedDict):
@@ -52,10 +55,25 @@ def __init__(self, cmab_cache: LRUCache[str, CmabCacheValue],
5255
self.cmab_cache = cmab_cache
5356
self.cmab_client = cmab_client
5457
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 = mmh3.hash(hash_input, seed=0) & 0xFFFFFFFF # Convert to unsigned
65+
return hash_value % NUM_LOCK_STRIPES
5566

5667
def get_decision(self, project_config: ProjectConfig, user_context: OptimizelyUserContext,
5768
rule_id: str, options: List[str]) -> CmabDecision:
5869

70+
lock_index = self._get_lock_index(user_context.user_id, rule_id)
71+
with self.locks[lock_index]:
72+
return self._get_decision(project_config, user_context, rule_id, options)
73+
74+
def _get_decision(self, project_config: ProjectConfig, user_context: OptimizelyUserContext,
75+
rule_id: str, options: List[str]) -> CmabDecision:
76+
5977
filtered_attributes = self._filter_attributes(project_config, user_context, rule_id)
6078

6179
if OptimizelyDecideOption.IGNORE_CMAB_CACHE in options:

optimizely/decision/optimizely_decide_option.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
if version_info < (3, 8):
1717
from typing_extensions import Final
1818
else:
19-
from typing import Final # type: ignore
19+
from typing import Final
2020

2121

2222
class OptimizelyDecideOption:

optimizely/decision/optimizely_decision_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
if version_info < (3, 8):
1717
from typing_extensions import Final
1818
else:
19-
from typing import Final # type: ignore
19+
from typing import Final
2020

2121

2222
class OptimizelyDecisionMessage:

optimizely/entities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
if version_info < (3, 8):
1818
from typing_extensions import Final
1919
else:
20-
from typing import Final # type: ignore
20+
from typing import Final
2121

2222

2323
if TYPE_CHECKING:

optimizely/event/event_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
if version_info < (3, 8):
2626
from typing_extensions import Final
2727
else:
28-
from typing import Final # type: ignore
28+
from typing import Final
2929

3030
if TYPE_CHECKING:
3131
# prevent circular dependenacy by skipping import at runtime

optimizely/event/event_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
if version_info < (3, 8):
3535
from typing_extensions import Final
3636
else:
37-
from typing import Final # type: ignore
37+
from typing import Final
3838

3939

4040
class BaseEventProcessor(ABC):

optimizely/event/log_event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
if version_info < (3, 8):
2121
from typing_extensions import Literal
2222
else:
23-
from typing import Literal # type: ignore
23+
from typing import Literal
2424

2525

2626
class LogEvent(event_builder.Event):

optimizely/event/user_event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
if version_info < (3, 8):
2323
from typing_extensions import Final
2424
else:
25-
from typing import Final # type: ignore
25+
from typing import Final
2626

2727

2828
if TYPE_CHECKING:

0 commit comments

Comments
 (0)