@@ -489,6 +489,103 @@ public void ConstructorUsesProvidedClientInstance()
489489 Assert . AreSame ( mockClient , client ) ;
490490 }
491491
492+ [ Test ]
493+ public void ConcurrentRequestsForSameUserUseCacheAfterFirstNetworkCall ( )
494+ {
495+ var experiment = CreateExperiment ( TEST_RULE_ID , new List < string > { AGE_ATTRIBUTE_ID } ) ;
496+ var attributeMap = new Dictionary < string , AttributeEntity >
497+ {
498+ { AGE_ATTRIBUTE_ID , new AttributeEntity { Id = AGE_ATTRIBUTE_ID , Key = "age" } } ,
499+ } ;
500+ var projectConfig = CreateProjectConfig ( TEST_RULE_ID , experiment , attributeMap ) ;
501+ var userContext = CreateUserContext ( TEST_USER_ID ,
502+ new Dictionary < string , object > { { "age" , 25 } } ) ;
503+
504+ var clientCallCount = 0 ;
505+ var clientCallLock = new object ( ) ;
506+
507+ _mockCmabClient . Setup ( c => c . FetchDecision (
508+ TEST_RULE_ID ,
509+ TEST_USER_ID ,
510+ It . Is < IDictionary < string , object > > ( attrs =>
511+ attrs != null && attrs . Count == 1 && attrs . ContainsKey ( "age" ) &&
512+ ( int ) attrs [ "age" ] == 25 ) ,
513+ It . IsAny < string > ( ) ,
514+ It . IsAny < TimeSpan ? > ( ) ) )
515+ . Returns ( ( ) =>
516+ {
517+ lock ( clientCallLock )
518+ {
519+ clientCallCount ++ ;
520+ }
521+ System . Threading . Thread . Sleep ( 100 ) ;
522+
523+ return "varConcurrent" ;
524+ } ) ;
525+
526+ var tasks = new System . Threading . Tasks . Task < CmabDecision > [ 10 ] ;
527+
528+ for ( int i = 0 ; i < tasks . Length ; i ++ )
529+ {
530+ tasks [ i ] = System . Threading . Tasks . Task . Run ( ( ) =>
531+ _cmabService . GetDecision ( projectConfig , userContext , TEST_RULE_ID ) ) ;
532+ }
533+
534+ System . Threading . Tasks . Task . WaitAll ( tasks ) ;
535+
536+ foreach ( var task in tasks )
537+ {
538+ Assert . IsNotNull ( task . Result ) ;
539+ Assert . AreEqual ( "varConcurrent" , task . Result . VariationId ) ;
540+ }
541+
542+ Assert . AreEqual ( 1 , clientCallCount ,
543+ "Client should only be called once - subsequent requests should use cache" ) ;
544+
545+ _mockCmabClient . VerifyAll ( ) ;
546+ }
547+
548+ [ Test ]
549+ public void SameUserRuleCombinationUsesConsistentLock ( )
550+ {
551+ var userId = "test_user" ;
552+ var ruleId = "test_rule" ;
553+
554+ var index1 = _cmabService . GetLockIndex ( userId , ruleId ) ;
555+ var index2 = _cmabService . GetLockIndex ( userId , ruleId ) ;
556+ var index3 = _cmabService . GetLockIndex ( userId , ruleId ) ;
557+
558+ Assert . AreEqual ( index1 , index2 , "Same user/rule should always use same lock" ) ;
559+ Assert . AreEqual ( index2 , index3 , "Same user/rule should always use same lock" ) ;
560+ }
561+
562+ [ Test ]
563+ public void LockStripingDistribution ( )
564+ {
565+ var testCases = new [ ]
566+ {
567+ new { UserId = "user1" , RuleId = "rule1" } ,
568+ new { UserId = "user2" , RuleId = "rule1" } ,
569+ new { UserId = "user1" , RuleId = "rule2" } ,
570+ new { UserId = "user3" , RuleId = "rule3" } ,
571+ new { UserId = "user4" , RuleId = "rule4" } ,
572+ } ;
573+
574+ var lockIndices = new HashSet < int > ( ) ;
575+ foreach ( var testCase in testCases )
576+ {
577+ var index = _cmabService . GetLockIndex ( testCase . UserId , testCase . RuleId ) ;
578+
579+ Assert . GreaterOrEqual ( index , 0 , "Lock index should be non-negative" ) ;
580+ Assert . Less ( index , 1000 , "Lock index should be less than NUM_LOCK_STRIPES (1000)" ) ;
581+
582+ lockIndices . Add ( index ) ;
583+ }
584+
585+ Assert . Greater ( lockIndices . Count , 1 ,
586+ "Different user/rule combinations should generally use different locks" ) ;
587+ }
588+
492589 private static ICache < CmabCacheEntry > GetInternalCache ( DefaultCmabService service )
493590 {
494591 return Reflection . GetFieldValue < ICache < CmabCacheEntry > , DefaultCmabService > ( service ,
@@ -554,5 +651,9 @@ private static Experiment CreateExperiment(string ruleId, List<string> attribute
554651 Cmab = attributeIds == null ? null : new Entity . Cmab ( attributeIds ) ,
555652 } ;
556653 }
654+
655+
656+
657+
557658 }
558659}
0 commit comments