@@ -791,6 +791,103 @@ export class RateLimiterBuilderTest {
791791 assert . deepEqual ( results , [ 'http://api2.com' , 'http://api2.com' ] )
792792 }
793793
794+ @Test ( )
795+ public async shouldRotateTargetsSequentiallyWhenUsingRoundRobinStrategy ( { assert } : Context ) {
796+ const targets = [
797+ { id : 'api1' , metadata : { baseUrl : 'http://api1.com' } } ,
798+ { id : 'api2' , metadata : { baseUrl : 'http://api2.com' } } ,
799+ { id : 'api3' , metadata : { baseUrl : 'http://api3.com' } }
800+ ]
801+
802+ const limiter = RateLimiter . build ( )
803+ . store ( 'memory' , { windowMs : { second : 1000 } } )
804+ . key ( 'request:rr-rotation' )
805+ . targetSelectionStrategy ( 'round_robin' )
806+ . addRule ( { type : 'second' , limit : 100 } )
807+ . setTargets ( targets )
808+
809+ const results : string [ ] = [ ]
810+
811+ for ( let i = 0 ; i < 6 ; i ++ ) {
812+ const result = await limiter . schedule ( ( { target } ) => target . metadata . baseUrl )
813+ results . push ( result )
814+ }
815+
816+ const urls = [ 'http://api1.com' , 'http://api2.com' , 'http://api3.com' ]
817+ const startIdx = urls . indexOf ( results [ 0 ] )
818+ const expected = Array . from ( { length : 6 } , ( _ , i ) => urls [ ( startIdx + i ) % 3 ] )
819+
820+ assert . deepEqual ( results , expected )
821+ }
822+
823+ @Test ( )
824+ public async shouldRotateTargetsConcurrentlyWhenUsingRoundRobinStrategy ( { assert } : Context ) {
825+ const api1 = { id : 'api1' , metadata : { baseUrl : 'http://api1.com' } }
826+ const api2 = { id : 'api2' , metadata : { baseUrl : 'http://api2.com' } }
827+
828+ const limiter = RateLimiter . build ( )
829+ . maxConcurrent ( 2 )
830+ . store ( 'memory' , { windowMs : { second : 100 } } )
831+ . key ( 'request:rr-rotation-concurrent' )
832+ . targetSelectionStrategy ( 'round_robin' )
833+ . addRule ( { type : 'second' , limit : 10 } )
834+ . addTarget ( api1 )
835+ . addTarget ( api2 )
836+
837+ const used : string [ ] = [ ]
838+ const barrier = this . createBarrier ( )
839+
840+ const run = async ( { target } ) => {
841+ used . push ( target . metadata . baseUrl )
842+ await barrier . wait ( )
843+ return target . metadata . baseUrl
844+ }
845+
846+ const p1 = limiter . schedule ( run )
847+ const p2 = limiter . schedule ( run )
848+
849+ await this . waitUntil ( ( ) => used . length === 2 , 10 , 1000 )
850+
851+ used . sort ( )
852+
853+ assert . deepEqual ( used , [ 'http://api1.com' , 'http://api2.com' ] )
854+
855+ barrier . release ( )
856+
857+ const results = await Promise . all ( [ p1 , p2 ] )
858+
859+ results . sort ( )
860+
861+ assert . deepEqual ( results , [ 'http://api1.com' , 'http://api2.com' ] )
862+ }
863+
864+ @Test ( )
865+ public async shouldDistributeStartingTargetAcrossMultipleInstancesWhenUsingRoundRobinStrategy ( { assert } : Context ) {
866+ const api1 = { id : 'api1' , metadata : { baseUrl : 'http://api1.com' } }
867+ const api2 = { id : 'api2' , metadata : { baseUrl : 'http://api2.com' } }
868+
869+ const firstTargets : string [ ] = [ ]
870+
871+ for ( let run = 0 ; run < 20 ; run ++ ) {
872+ const limiter = RateLimiter . build ( )
873+ . store ( 'memory' , { windowMs : { second : 1000 } } )
874+ . key ( `request:rr-dist-${ run } ` )
875+ . targetSelectionStrategy ( 'round_robin' )
876+ . addRule ( { type : 'second' , limit : 100 } )
877+ . addTarget ( api1 )
878+ . addTarget ( api2 )
879+
880+ const result = await limiter . schedule ( ( { target } ) => target . metadata . baseUrl )
881+ firstTargets . push ( result )
882+ }
883+
884+ const api1Count = firstTargets . filter ( t => t === 'http://api1.com' ) . length
885+ const api2Count = firstTargets . filter ( t => t === 'http://api2.com' ) . length
886+
887+ assert . isAbove ( api1Count , 0 )
888+ assert . isAbove ( api2Count , 0 )
889+ }
890+
794891 @Test ( )
795892 public async shouldThrowMissingRuleExceptionIfRateLimiterRulesAndTargetRulesAreNotDefined ( { assert } : Context ) {
796893 const limiter = RateLimiter . build ( )
0 commit comments