@@ -38,35 +38,35 @@ class SlidingWindowLog implements RateLimiter {
3838 // TODO: Define lua script for server side computation
3939 // while x.timestamp + window_size < timestamp lpop
4040 // //https://stackoverflow.com/questions/35677682/filtering-deleting-items-from-a-redis-set
41- this . client . defineCommand ( 'popWindow' , {
42- // 2 value timestamp and complexity of this request
43- lua : `
44- local totalComplexity = 0 -- complexity of active requests
45- local expiredMembers = 0 -- number of requests to remove
46- local key = keys[1] -- uuid
47- local current_time = keys[2]
48-
49- for index, value in next, redis.call(key, ????) do
50- -- string comparisson of timestamps
51- if .... then
52-
53- else
54- totalComplexity += ????
55- end
56- end
57-
58- redis.call(pop, ???)
59-
60- if total_complexity < window_size then
61- then
62- end
63- return {
64-
65- }
66- ` ,
67- numberOfKeys : 3 , // uuid
68- readOnly : true ,
69- } ) ;
41+ // this.client.defineCommand('popWindow', {
42+ // // 2 value timestamp and complexity of this request
43+ // lua: `
44+ // local totalComplexity = 0 -- complexity of active requests
45+ // local expiredMembers = 0 -- number of requests to remove
46+ // local key = keys[1] -- uuid
47+ // local current_time = keys[2]
48+
49+ // for index, value in next, redis.call(key, ????) do
50+ // -- string comparisson of timestamps
51+ // if .... then
52+
53+ // else
54+ // totalComplexity += ????
55+ // end
56+ // end
57+
58+ // redis.call(pop, ???)
59+
60+ // if total_complexity < window_size then
61+ // then
62+ // end
63+ // return {
64+
65+ // }
66+ // `,
67+ // numberOfKeys: 3, // uuid
68+ // readOnly: true,
69+ // });
7070 }
7171
7272 /**
@@ -83,7 +83,6 @@ class SlidingWindowLog implements RateLimiter {
8383 ) : Promise < RateLimiterResponse > {
8484 // set the expiry of key-value pairs in the cache to 24 hours
8585 const keyExpiry = 86400000 ; // TODO: Make this a global for consistency across each algo.
86- // if (tokens > this.capacity) return { success: false, tokens: this.capacity };
8786
8887 // Each user's log is represented by a redis list with a score = request timestamp
8988 // and a value equal to the complexity
@@ -92,21 +91,41 @@ class SlidingWindowLog implements RateLimiter {
9291 // Get the log from redis
9392 let requestLog : RedisLog = JSON . parse ( ( await this . client . get ( uuid ) ) || '[]' ) ;
9493
95- // if (requestLog.length === 0) {
96- // if (tokens > 0) requestLog.push({ timestamp, tokens });
97- // // return { success: true, tokens: this.capacity - tokens };
98- // }
94+ // Iterate through the list in reverse and count active tokens
95+ // This allows us to track the threshold for when this request would be allowed if it is blocked
96+ // Stop at the first timestamp that's expired and cut the rest.
97+
9998 const cutoff = timestamp - this . windowSize ;
10099 let tokensInLog = 0 ;
101- requestLog = requestLog . filter ( ( bucket : RedisBucket ) => {
102- if ( bucket . timestamp > cutoff ) {
103- // get complexity sum of active requests
104- tokensInLog += bucket . tokens ;
105- return true ;
100+ let cutoffIndex = 0 ; // index of oldest active request
101+ // TODO: Provide a timestamp for when the request will succeeed.
102+ // Compute time between response and this timestamp later on
103+ // FIXME: What should this be if the complexity is too big?
104+ let retryIndex = requestLog . length ; // time the user must wait before a request can be allowed.
105+
106+ for ( let index = requestLog . length - 1 ; index >= 0 ; index -- ) {
107+ if ( cutoff >= requestLog [ index ] . timestamp ) {
108+ cutoffIndex = index + 1 ;
109+ break ;
110+ } else {
111+ // the request is active
112+ tokensInLog += requestLog [ index ] . tokens ;
113+ if ( this . capacity - tokensInLog >= tokens ) {
114+ // the log is able to accept this request
115+ retryIndex = index ;
116+ }
106117 }
107- // drop expired requests
108- return false ;
109- } ) ;
118+ }
119+
120+ let retryAfter : number ;
121+ if ( tokens > this . capacity ) retryAfter = Infinity ;
122+ // need the request before retryIndex
123+ else if ( retryIndex > 0 )
124+ retryAfter = this . windowSize + requestLog [ retryIndex - 1 ] . timestamp ;
125+ else retryAfter = 0 ; // request is allowed
126+
127+ // Conditional check to avoid unecessary slice
128+ if ( cutoffIndex > 0 ) requestLog = requestLog . slice ( cutoffIndex ) ;
110129
111130 // allow/disallow current request
112131 if ( tokensInLog + tokens <= this . capacity ) {
@@ -116,8 +135,10 @@ class SlidingWindowLog implements RateLimiter {
116135 tokensInLog += tokens ;
117136 return { success : true , tokens : this . capacity - tokensInLog } ;
118137 }
138+
119139 await this . client . setex ( uuid , keyExpiry , JSON . stringify ( requestLog ) ) ;
120- return { success : false , tokens : this . capacity - tokensInLog } ;
140+
141+ return { success : false , tokens : this . capacity - tokensInLog , retryAfter } ;
121142 }
122143
123144 /**
0 commit comments