Skip to content

Commit 9994ca0

Browse files
committed
preliminary sliding window log implementation
1 parent c8d3eac commit 9994ca0

File tree

1 file changed

+69
-4
lines changed

1 file changed

+69
-4
lines changed

src/rateLimiters/slidingWindowLog.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Redis from 'ioredis';
2-
import { RateLimiter, RateLimiterResponse } from '../@types/rateLimit';
2+
import { RateLimiter, RateLimiterResponse, RedisBucket, RedisLog } from '../@types/rateLimit';
33

44
/**
55
* The SlidingWindowLog instance of a RateLimiter limits requests based on a unique user ID.
@@ -33,7 +33,40 @@ class SlidingWindowLog implements RateLimiter {
3333
this.capacity = capacity;
3434
this.client = client;
3535
if (windowSize <= 0 || capacity <= 0)
36-
throw SyntaxError('SlidingWindowLog windowSize and capacity must be positive');
36+
throw SyntaxError('SlidingWindowLog window size and capacity must be positive');
37+
38+
// TODO: Define lua script for server side computation
39+
// while x.timestamp + window_size < timestamp lpop
40+
// //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+
});
3770
}
3871

3972
/**
@@ -50,9 +83,41 @@ class SlidingWindowLog implements RateLimiter {
5083
): Promise<RateLimiterResponse> {
5184
// set the expiry of key-value pairs in the cache to 24 hours
5285
const keyExpiry = 86400000; // TODO: Make this a global for consistency across each algo.
53-
if (tokens > this.capacity) return { success: false, tokens: this.capacity };
86+
// if (tokens > this.capacity) return { success: false, tokens: this.capacity };
87+
88+
// Each user's log is represented by a redis list with a score = request timestamp
89+
// and a value equal to the complexity
90+
// Drop expired requests from the log. represented by a sorted set in redis
91+
92+
// Get the log from redis
93+
let requestLog: RedisLog = JSON.parse((await this.client.get(uuid)) || '[]');
94+
95+
// if (requestLog.length === 0) {
96+
// if (tokens > 0) requestLog.push({ timestamp, tokens });
97+
// // return { success: true, tokens: this.capacity - tokens };
98+
// }
99+
const cutoff = timestamp - this.windowSize;
100+
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;
106+
}
107+
// drop expired requests
108+
return false;
109+
});
54110

55-
throw new Error('SlidingWindowLog.processRequest not implemented');
111+
// allow/disallow current request
112+
if (tokensInLog + tokens <= this.capacity) {
113+
// update the log
114+
if (tokens > 0) requestLog.push({ timestamp, tokens });
115+
await this.client.setex(uuid, keyExpiry, JSON.stringify(requestLog));
116+
tokensInLog += tokens;
117+
return { success: true, tokens: this.capacity - tokensInLog };
118+
}
119+
await this.client.setex(uuid, keyExpiry, JSON.stringify(requestLog));
120+
return { success: false, tokens: this.capacity - tokensInLog };
56121
}
57122

58123
/**

0 commit comments

Comments
 (0)