|
20 | 20 | import java.util.List; |
21 | 21 | import java.util.Map; |
22 | 22 | import java.util.TreeMap; |
| 23 | +import java.util.concurrent.locks.ReentrantLock; |
23 | 24 |
|
24 | 25 | import org.slf4j.Logger; |
25 | 26 |
|
|
33 | 34 | import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; |
34 | 35 |
|
35 | 36 | public class DefaultCmabService implements CmabService { |
| 37 | + private static final int NUM_LOCK_STRIPES = 1000; |
36 | 38 |
|
37 | 39 | private final DefaultLRUCache<CmabCacheValue> cmabCache; |
38 | 40 | private final CmabClient cmabClient; |
39 | 41 | private final Logger logger; |
| 42 | + private final ReentrantLock[] locks; |
40 | 43 |
|
41 | 44 | public DefaultCmabService(CmabServiceOptions options) { |
42 | 45 | this.cmabCache = options.getCmabCache(); |
43 | 46 | this.cmabClient = options.getCmabClient(); |
44 | 47 | this.logger = options.getLogger(); |
| 48 | + |
| 49 | + this.locks = new ReentrantLock[NUM_LOCK_STRIPES]; |
| 50 | + for (int i = 0; i < NUM_LOCK_STRIPES; i++) { |
| 51 | + this.locks[i] = new ReentrantLock(); |
| 52 | + } |
45 | 53 | } |
46 | 54 |
|
47 | 55 | @Override |
48 | 56 | public CmabDecision getDecision(ProjectConfig projectConfig, OptimizelyUserContext userContext, String ruleId, List<OptimizelyDecideOption> options) { |
49 | 57 | options = options == null ? Collections.emptyList() : options; |
50 | 58 | String userId = userContext.getUserId(); |
51 | | - Map<String, Object> filteredAttributes = filterAttributes(projectConfig, userContext, ruleId); |
52 | 59 |
|
53 | | - if (options.contains(OptimizelyDecideOption.IGNORE_CMAB_CACHE)) { |
54 | | - return fetchDecision(ruleId, userId, filteredAttributes); |
55 | | - } |
| 60 | + int lockIndex = getLockIndex(userId, ruleId); |
| 61 | + ReentrantLock lock = locks[lockIndex]; |
| 62 | + lock.lock(); |
| 63 | + try { |
| 64 | + Map<String, Object> filteredAttributes = filterAttributes(projectConfig, userContext, ruleId); |
56 | 65 |
|
57 | | - if (options.contains(OptimizelyDecideOption.RESET_CMAB_CACHE)) { |
58 | | - cmabCache.reset(); |
59 | | - } |
| 66 | + if (options.contains(OptimizelyDecideOption.IGNORE_CMAB_CACHE)) { |
| 67 | + return fetchDecision(ruleId, userId, filteredAttributes); |
| 68 | + } |
60 | 69 |
|
61 | | - String cacheKey = getCacheKey(userContext.getUserId(), ruleId); |
62 | | - if (options.contains(OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE)) { |
63 | | - cmabCache.remove(cacheKey); |
64 | | - } |
| 70 | + if (options.contains(OptimizelyDecideOption.RESET_CMAB_CACHE)) { |
| 71 | + cmabCache.reset(); |
| 72 | + } |
65 | 73 |
|
66 | | - CmabCacheValue cachedValue = cmabCache.lookup(cacheKey); |
| 74 | + String cacheKey = getCacheKey(userContext.getUserId(), ruleId); |
| 75 | + if (options.contains(OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE)) { |
| 76 | + cmabCache.remove(cacheKey); |
| 77 | + } |
67 | 78 |
|
68 | | - String attributesHash = hashAttributes(filteredAttributes); |
| 79 | + CmabCacheValue cachedValue = cmabCache.lookup(cacheKey); |
69 | 80 |
|
70 | | - if (cachedValue != null) { |
71 | | - if (cachedValue.getAttributesHash().equals(attributesHash)) { |
72 | | - return new CmabDecision(cachedValue.getVariationId(), cachedValue.getCmabUuid()); |
73 | | - } else { |
74 | | - cmabCache.remove(cacheKey); |
| 81 | + String attributesHash = hashAttributes(filteredAttributes); |
| 82 | + |
| 83 | + if (cachedValue != null) { |
| 84 | + if (cachedValue.getAttributesHash().equals(attributesHash)) { |
| 85 | + return new CmabDecision(cachedValue.getVariationId(), cachedValue.getCmabUuid()); |
| 86 | + } else { |
| 87 | + cmabCache.remove(cacheKey); |
| 88 | + } |
75 | 89 | } |
76 | | - } |
77 | 90 |
|
78 | | - CmabDecision cmabDecision = fetchDecision(ruleId, userId, filteredAttributes); |
79 | | - cmabCache.save(cacheKey, new CmabCacheValue(attributesHash, cmabDecision.getVariationId(), cmabDecision.getCmabUUID())); |
| 91 | + CmabDecision cmabDecision = fetchDecision(ruleId, userId, filteredAttributes); |
| 92 | + cmabCache.save(cacheKey, new CmabCacheValue(attributesHash, cmabDecision.getVariationId(), cmabDecision.getCmabUUID())); |
80 | 93 |
|
81 | | - return cmabDecision; |
| 94 | + return cmabDecision; |
| 95 | + } finally { |
| 96 | + lock.unlock(); |
| 97 | + } |
82 | 98 | } |
83 | 99 |
|
84 | 100 | private CmabDecision fetchDecision(String ruleId, String userId, Map<String, Object> attributes) { |
@@ -182,4 +198,11 @@ private String hashAttributes(Map<String, Object> attributes) { |
182 | 198 | // Convert to hex string to match your existing pattern |
183 | 199 | return Integer.toHexString(hash); |
184 | 200 | } |
| 201 | + |
| 202 | + private int getLockIndex(String userId, String ruleId) { |
| 203 | + // Create a hash of userId + ruleId for consistent lock selection |
| 204 | + String combined = userId + ruleId; |
| 205 | + int hash = MurmurHash3.murmurhash3_x86_32(combined, 0, combined.length(), 0); |
| 206 | + return Math.abs(hash) % NUM_LOCK_STRIPES; |
| 207 | + } |
185 | 208 | } |
0 commit comments