Skip to content

Commit 152b3f2

Browse files
update FSSDK-11899: implement lock striping for improved concurrency in DefaultCmabService
1 parent 59b90d7 commit 152b3f2

File tree

2 files changed

+108
-22
lines changed

2 files changed

+108
-22
lines changed

core-api/src/main/java/com/optimizely/ab/cmab/service/DefaultCmabService.java

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121
import java.util.Map;
2222
import java.util.TreeMap;
23+
import java.util.concurrent.locks.ReentrantLock;
2324

2425
import org.slf4j.Logger;
2526

@@ -33,52 +34,67 @@
3334
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
3435

3536
public class DefaultCmabService implements CmabService {
37+
private static final int NUM_LOCK_STRIPES = 1000;
3638

3739
private final DefaultLRUCache<CmabCacheValue> cmabCache;
3840
private final CmabClient cmabClient;
3941
private final Logger logger;
42+
private final ReentrantLock[] locks;
4043

4144
public DefaultCmabService(CmabServiceOptions options) {
4245
this.cmabCache = options.getCmabCache();
4346
this.cmabClient = options.getCmabClient();
4447
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+
}
4553
}
4654

4755
@Override
4856
public CmabDecision getDecision(ProjectConfig projectConfig, OptimizelyUserContext userContext, String ruleId, List<OptimizelyDecideOption> options) {
4957
options = options == null ? Collections.emptyList() : options;
5058
String userId = userContext.getUserId();
51-
Map<String, Object> filteredAttributes = filterAttributes(projectConfig, userContext, ruleId);
5259

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);
5665

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+
}
6069

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+
}
6573

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+
}
6778

68-
String attributesHash = hashAttributes(filteredAttributes);
79+
CmabCacheValue cachedValue = cmabCache.lookup(cacheKey);
6980

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+
}
7589
}
76-
}
7790

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()));
8093

81-
return cmabDecision;
94+
return cmabDecision;
95+
} finally {
96+
lock.unlock();
97+
}
8298
}
8399

84100
private CmabDecision fetchDecision(String ruleId, String userId, Map<String, Object> attributes) {
@@ -182,4 +198,11 @@ private String hashAttributes(Map<String, Object> attributes) {
182198
// Convert to hex string to match your existing pattern
183199
return Integer.toHexString(hash);
184200
}
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+
}
185208
}

core-api/src/test/java/com/optimizely/ab/cmab/DefaultCmabServiceTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,20 @@
1515
*/
1616
package com.optimizely.ab.cmab;
1717

18+
import java.lang.reflect.Method;
1819
import java.util.Arrays;
1920
import java.util.Collections;
2021
import java.util.HashMap;
22+
import java.util.HashSet;
2123
import java.util.LinkedHashMap;
2224
import java.util.List;
2325
import java.util.Map;
26+
import java.util.Set;
2427

2528
import static org.junit.Assert.assertEquals;
2629
import static org.junit.Assert.assertNotNull;
30+
import static org.junit.Assert.assertTrue;
31+
import static org.junit.Assert.fail;
2732
import org.junit.Before;
2833
import org.junit.Test;
2934
import org.mockito.ArgumentCaptor;
@@ -378,4 +383,62 @@ public void testAttributeOrderDoesNotMatterForCaching() {
378383
assertNotNull(decision.getCmabUUID());
379384
verify(mockCmabCache).save(eq(cacheKey), any(CmabCacheValue.class));
380385
}
386+
387+
@Test
388+
public void testLockStripingDistribution() {
389+
// Test different combinations to ensure they get different lock indices
390+
String[][] testCases = {
391+
{"user1", "rule1"},
392+
{"user2", "rule1"},
393+
{"user1", "rule2"},
394+
{"user3", "rule3"},
395+
{"user4", "rule4"}
396+
};
397+
398+
Set<Integer> lockIndices = new HashSet<>();
399+
for (String[] testCase : testCases) {
400+
String userId = testCase[0];
401+
String ruleId = testCase[1];
402+
403+
// Use reflection to access the private getLockIndex method
404+
try {
405+
Method getLockIndexMethod = DefaultCmabService.class.getDeclaredMethod("getLockIndex", String.class, String.class);
406+
getLockIndexMethod.setAccessible(true);
407+
408+
int index = (Integer) getLockIndexMethod.invoke(cmabService, userId, ruleId);
409+
410+
// Verify index is within expected range
411+
assertTrue("Lock index should be non-negative", index >= 0);
412+
assertTrue("Lock index should be less than NUM_LOCK_STRIPES", index < 1000);
413+
414+
lockIndices.add(index);
415+
} catch (Exception e) {
416+
fail("Failed to invoke getLockIndex method: " + e.getMessage());
417+
}
418+
}
419+
420+
assertTrue("Different user/rule combinations should generally use different locks", lockIndices.size() > 1);
421+
}
422+
423+
@Test
424+
public void testSameUserRuleCombinationUsesConsistentLock() {
425+
String userId = "test_user";
426+
String ruleId = "test_rule";
427+
428+
try {
429+
Method getLockIndexMethod = DefaultCmabService.class.getDeclaredMethod("getLockIndex", String.class, String.class);
430+
getLockIndexMethod.setAccessible(true);
431+
432+
// Get lock index multiple times
433+
int index1 = (Integer) getLockIndexMethod.invoke(cmabService, userId, ruleId);
434+
int index2 = (Integer) getLockIndexMethod.invoke(cmabService, userId, ruleId);
435+
int index3 = (Integer) getLockIndexMethod.invoke(cmabService, userId, ruleId);
436+
437+
// All should be the same
438+
assertEquals("Same user/rule should always use same lock", index1, index2);
439+
assertEquals("Same user/rule should always use same lock", index2, index3);
440+
} catch (Exception e) {
441+
fail("Failed to invoke getLockIndex method: " + e.getMessage());
442+
}
443+
}
381444
}

0 commit comments

Comments
 (0)