2020import java .util .List ;
2121import java .util .Map ;
2222import java .util .TreeMap ;
23+ import java .util .concurrent .locks .ReentrantLock ;
2324
2425import org .slf4j .Logger ;
2526import org .slf4j .LoggerFactory ;
3738public class DefaultCmabService implements CmabService {
3839 public static final int DEFAULT_CMAB_CACHE_SIZE = 10000 ;
3940 public static final int DEFAULT_CMAB_CACHE_TIMEOUT_SECS = 30 *60 ; // 30 minutes
41+ private static final int NUM_LOCK_STRIPES = 1000 ;
4042
4143 private final Cache <CmabCacheValue > cmabCache ;
4244 private final CmabClient cmabClient ;
4345 private final Logger logger ;
46+ private final ReentrantLock [] locks ;
4447
4548 public DefaultCmabService (CmabClient cmabClient , Cache <CmabCacheValue > cmabCache ) {
4649 this (cmabClient , cmabCache , null );
@@ -50,52 +53,64 @@ public DefaultCmabService(CmabClient cmabClient, Cache<CmabCacheValue> cmabCache
5053 this .cmabCache = cmabCache ;
5154 this .cmabClient = cmabClient ;
5255 this .logger = logger != null ? logger : LoggerFactory .getLogger (DefaultCmabService .class );
56+ this .locks = new ReentrantLock [NUM_LOCK_STRIPES ];
57+ for (int i = 0 ; i < NUM_LOCK_STRIPES ; i ++) {
58+ this .locks [i ] = new ReentrantLock ();
59+ }
5360 }
5461
5562 @ Override
5663 public CmabDecision getDecision (ProjectConfig projectConfig , OptimizelyUserContext userContext , String ruleId , List <OptimizelyDecideOption > options ) {
5764 options = options == null ? Collections .emptyList () : options ;
5865 String userId = userContext .getUserId ();
59- Map <String , Object > filteredAttributes = filterAttributes (projectConfig , userContext , ruleId );
6066
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- }
67+ int lockIndex = getLockIndex (userId , ruleId );
68+ ReentrantLock lock = locks [lockIndex ];
69+ lock .lock ();
70+ try {
71+ Map <String , Object > filteredAttributes = filterAttributes (projectConfig , userContext , ruleId );
6572
66- if (options .contains (OptimizelyDecideOption .RESET_CMAB_CACHE )) {
67- logger .debug ("Resetting CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
68- cmabCache . reset ( );
69- }
73+ if (options .contains (OptimizelyDecideOption .IGNORE_CMAB_CACHE )) {
74+ logger .debug ("Ignoring CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
75+ return fetchDecision ( ruleId , userId , filteredAttributes );
76+ }
7077
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- }
78+ if (options .contains (OptimizelyDecideOption .RESET_CMAB_CACHE )) {
79+ logger .debug ("Resetting CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
80+ cmabCache .reset ();
81+ }
82+
83+ String cacheKey = getCacheKey (userContext .getUserId (), ruleId );
84+ if (options .contains (OptimizelyDecideOption .INVALIDATE_USER_CMAB_CACHE )) {
85+ logger .debug ("Invalidating CMAB cache for user '{}' and rule '{}'" , userId , ruleId );
86+ cmabCache .remove (cacheKey );
87+ }
7688
77- CmabCacheValue cachedValue = cmabCache .lookup (cacheKey );
89+ CmabCacheValue cachedValue = cmabCache .lookup (cacheKey );
7890
79- String attributesHash = hashAttributes (filteredAttributes );
91+ String attributesHash = hashAttributes (filteredAttributes );
8092
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 ());
93+ if (cachedValue != null ) {
94+ if (cachedValue .getAttributesHash ().equals (attributesHash )) {
95+ logger .debug ("CMAB cache hit for user '{}' and rule '{}'" , userId , ruleId );
96+ return new CmabDecision (cachedValue .getVariationId (), cachedValue .getCmabUuid ());
97+ } else {
98+ logger .debug ("CMAB cache attributes mismatch for user '{}' and rule '{}', fetching new decision" , userId , ruleId );
99+ cmabCache .remove (cacheKey );
100+ }
85101 } else {
86- logger .debug ("CMAB cache attributes mismatch for user '{}' and rule '{}', fetching new decision" , userId , ruleId );
87- cmabCache .remove (cacheKey );
102+ logger .debug ("CMAB cache miss for user '{}' and rule '{}'" , userId , ruleId );
88103 }
89- } else {
90- logger .debug ("CMAB cache miss for user '{}' and rule '{}'" , userId , ruleId );
91- }
92104
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 ()));
105+ CmabDecision cmabDecision = fetchDecision (ruleId , userId , filteredAttributes );
106+ logger .debug ("CMAB decision is {}" , cmabDecision );
97107
98- return cmabDecision ;
108+ cmabCache .save (cacheKey , new CmabCacheValue (attributesHash , cmabDecision .getVariationId (), cmabDecision .getCmabUUID ()));
109+
110+ return cmabDecision ;
111+ } finally {
112+ lock .unlock ();
113+ }
99114 }
100115
101116 private CmabDecision fetchDecision (String ruleId , String userId , Map <String , Object > attributes ) {
@@ -192,6 +207,13 @@ private String hashAttributes(Map<String, Object> attributes) {
192207 return Integer .toHexString (hash );
193208 }
194209
210+ private int getLockIndex (String userId , String ruleId ) {
211+ // Create a hash of userId + ruleId for consistent lock selection
212+ String combined = userId + ruleId ;
213+ int hash = MurmurHash3 .murmurhash3_x86_32 (combined , 0 , combined .length (), 0 );
214+ return Math .abs (hash ) % NUM_LOCK_STRIPES ;
215+ }
216+
195217 public static Builder builder () {
196218 return new Builder ();
197219 }
0 commit comments