2020import java .util .List ;
2121import java .util .Map ;
2222import java .util .TreeMap ;
23+ import java .util .concurrent .locks .ReentrantLock ;
2324
25+ import com .optimizely .ab .event .internal .ClientEngineInfo ;
2426import org .slf4j .Logger ;
2527import org .slf4j .LoggerFactory ;
2628
3739public class DefaultCmabService implements CmabService {
3840 public static final int DEFAULT_CMAB_CACHE_SIZE = 10000 ;
3941 public static final int DEFAULT_CMAB_CACHE_TIMEOUT_SECS = 30 *60 ; // 30 minutes
42+ private static final boolean IS_ANDROID = ClientEngineInfo .getClientEngineName ().toLowerCase ().contains ("android" );
43+ private static final int NUM_LOCK_STRIPES = IS_ANDROID ? 100 : 1000 ;
4044
4145 private final Cache <CmabCacheValue > cmabCache ;
4246 private final CmabClient cmabClient ;
4347 private final Logger logger ;
48+ private final ReentrantLock [] locks ;
4449
4550 public DefaultCmabService (CmabClient cmabClient , Cache <CmabCacheValue > cmabCache ) {
4651 this (cmabClient , cmabCache , null );
@@ -50,52 +55,64 @@ public DefaultCmabService(CmabClient cmabClient, Cache<CmabCacheValue> cmabCache
5055 this .cmabCache = cmabCache ;
5156 this .cmabClient = cmabClient ;
5257 this .logger = logger != null ? logger : LoggerFactory .getLogger (DefaultCmabService .class );
58+ this .locks = new ReentrantLock [NUM_LOCK_STRIPES ];
59+ for (int i = 0 ; i < NUM_LOCK_STRIPES ; i ++) {
60+ this .locks [i ] = new ReentrantLock ();
61+ }
5362 }
5463
5564 @ Override
5665 public CmabDecision getDecision (ProjectConfig projectConfig , OptimizelyUserContext userContext , String ruleId , List <OptimizelyDecideOption > options ) {
5766 options = options == null ? Collections .emptyList () : options ;
5867 String userId = userContext .getUserId ();
59- Map <String , Object > filteredAttributes = filterAttributes (projectConfig , userContext , ruleId );
6068
61- if (options .contains (OptimizelyDecideOption .IGNORE_CMAB_CACHE )) {
62- logger .debug ("Ignoring CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
63- return fetchDecision (ruleId , userId , filteredAttributes );
64- }
69+ int lockIndex = getLockIndex (userId , ruleId );
70+ ReentrantLock lock = locks [lockIndex ];
71+ lock .lock ();
72+ try {
73+ Map <String , Object > filteredAttributes = filterAttributes (projectConfig , userContext , ruleId );
6574
66- if (options .contains (OptimizelyDecideOption .RESET_CMAB_CACHE )) {
67- logger .debug ("Resetting CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
68- cmabCache . reset ( );
69- }
75+ if (options .contains (OptimizelyDecideOption .IGNORE_CMAB_CACHE )) {
76+ logger .debug ("Ignoring CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
77+ return fetchDecision ( ruleId , userId , filteredAttributes );
78+ }
7079
71- String cacheKey = getCacheKey (userContext .getUserId (), ruleId );
72- if (options .contains (OptimizelyDecideOption .INVALIDATE_USER_CMAB_CACHE )) {
73- logger .debug ("Invalidating CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
74- cmabCache .remove (cacheKey );
75- }
80+ if (options .contains (OptimizelyDecideOption .RESET_CMAB_CACHE )) {
81+ logger .debug ("Resetting CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
82+ cmabCache .reset ();
83+ }
84+
85+ String cacheKey = getCacheKey (userContext .getUserId (), ruleId );
86+ if (options .contains (OptimizelyDecideOption .INVALIDATE_USER_CMAB_CACHE )) {
87+ logger .debug ("Invalidating CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
88+ cmabCache .remove (cacheKey );
89+ }
7690
77- CmabCacheValue cachedValue = cmabCache .lookup (cacheKey );
91+ CmabCacheValue cachedValue = cmabCache .lookup (cacheKey );
7892
79- String attributesHash = hashAttributes (filteredAttributes );
93+ String attributesHash = hashAttributes (filteredAttributes );
8094
81- if (cachedValue != null ) {
82- if (cachedValue .getAttributesHash ().equals (attributesHash )) {
83- logger .debug ("CMAB cache hit for user '{}' and rule '{}'" , userId , ruleId );
84- return new CmabDecision (cachedValue .getVariationId (), cachedValue .getCmabUuid ());
95+ if (cachedValue != null ) {
96+ if (cachedValue .getAttributesHash ().equals (attributesHash )) {
97+ logger .debug ("CMAB cache hit for user '{}' and rule '{}'" , userId , ruleId );
98+ return new CmabDecision (cachedValue .getVariationId (), cachedValue .getCmabUuid ());
99+ } else {
100+ logger .debug ("CMAB cache attributes mismatch for user '{}' and rule '{}', fetching new decision" , userId , ruleId );
101+ cmabCache .remove (cacheKey );
102+ }
85103 } else {
86- logger .debug ("CMAB cache attributes mismatch for user '{}' and rule '{}', fetching new decision" , userId , ruleId );
87- cmabCache .remove (cacheKey );
104+ logger .debug ("CMAB cache miss for user '{}' and rule '{}'" , userId , ruleId );
88105 }
89- } else {
90- logger .debug ("CMAB cache miss for user '{}' and rule '{}'" , userId , ruleId );
91- }
92106
93- CmabDecision cmabDecision = fetchDecision (ruleId , userId , filteredAttributes );
94- logger .debug ("CMAB decision is {}" , cmabDecision );
95-
96- cmabCache .save (cacheKey , new CmabCacheValue (attributesHash , cmabDecision .getVariationId (), cmabDecision .getCmabUUID ()));
107+ CmabDecision cmabDecision = fetchDecision (ruleId , userId , filteredAttributes );
108+ logger .debug ("CMAB decision is {}" , cmabDecision );
97109
98- return cmabDecision ;
110+ cmabCache .save (cacheKey , new CmabCacheValue (attributesHash , cmabDecision .getVariationId (), cmabDecision .getCmabUUID ()));
111+
112+ return cmabDecision ;
113+ } finally {
114+ lock .unlock ();
115+ }
99116 }
100117
101118 private CmabDecision fetchDecision (String ruleId , String userId , Map <String , Object > attributes ) {
@@ -192,6 +209,13 @@ private String hashAttributes(Map<String, Object> attributes) {
192209 return Integer .toHexString (hash );
193210 }
194211
212+ private int getLockIndex (String userId , String ruleId ) {
213+ // Create a hash of userId + ruleId for consistent lock selection
214+ String combined = userId + ruleId ;
215+ int hash = MurmurHash3 .murmurhash3_x86_32 (combined , 0 , combined .length (), 0 );
216+ return Math .abs (hash ) % NUM_LOCK_STRIPES ;
217+ }
218+
195219 public static Builder builder () {
196220 return new Builder ();
197221 }
0 commit comments