-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathslidingWindowLog.ts
More file actions
157 lines (133 loc) · 6.49 KB
/
Copy pathslidingWindowLog.ts
File metadata and controls
157 lines (133 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import Redis from 'ioredis';
import { RateLimiter, RateLimiterResponse, RedisLog } from '../@types/rateLimit';
/**
* The SlidingWindowLog instance of a RateLimiter limits requests based on a unique user ID.
* With the FixedWindow algorithm, users are able to send more requests to go through at the
* edges of a window. The SlidingWindowLog algorithm addresses this issue by tracking request
* timestamps in a log then removing these requests from the log once they fall outside of the window.
* If a request is received and there are more than capacity requests in the log then the request is dropped
*
* Whenever a user makes a request the following steps are performed:
* 1. The user's log is obtained from redis.
* 2. Any requests that are older than window size are dropped from the log.
* 3. The complexity of the current request is added to the complexity of all requests in the log.
* 4. If the request exceeds the specified capacity it is dropped.
* 5. Otherwise the request is allowed and the current request is added to the end of the log (if it has a complexity > 0).
*/
class SlidingWindowLog implements RateLimiter {
private windowSize: number;
private keyExpiry: number;
private capacity: number;
private client: Redis;
/**
* Create a new instance of a SlidingWindowLog rate limiter that can be connected to any redis store
* @param windowSize size of window in milliseconds
* @param capacity max number of tokens allowed in each window
* @param client redis client where rate limiter will cache information
*/
constructor(windowSize: number, capacity: number, client: Redis, expiry: number) {
this.windowSize = windowSize;
this.capacity = capacity;
this.client = client;
this.keyExpiry = expiry;
if (!windowSize || !capacity || windowSize <= 0 || capacity <= 0 || expiry <= 0)
throw SyntaxError(
'SlidingWindowLog window size, capacity and keyExpiry must be positive'
);
// TODO: Define lua script for server side computation using either sorted sets or lists
// while x.timestamp + window_size < timestamp lpop
// //https://stackoverflow.com/questions/35677682/filtering-deleting-items-from-a-redis-set
// this.client.defineCommand('popWindow', {
// // 2 value timestamp and complexity of this request
// lua: `
// local totalComplexity = 0 -- complexity of active requests
// local expiredMembers = 0 -- number of requests to remove
// local key = keys[1] -- uuid
// local current_time = keys[2]
// for index, value in next, redis.call(key, ????) do
// -- string comparisson of timestamps
// if .... then
// else
// totalComplexity += ????
// end
// end
// redis.call(pop, ???)
// if total_complexity < window_size then
// then
// end
// return {
// }
// `,
// numberOfKeys: 3, // uuid
// readOnly: true,
// });
}
/**
* @param {string} uuid - unique identifer used to throttle requests
* @param {number} timestamp - time the request was recieved
* @param {number} [tokens=1] - complexity of the query for throttling requests
* @return {*} {Promise<RateLimiterResponse>}
* @memberof SlidingWindowLog
*/
async processRequest(
uuid: string,
timestamp: number,
tokens = 1
): Promise<RateLimiterResponse> {
// Each user's log is represented by a redis list with a score = request timestamp
// and a value equal to the complexity
// Drop expired requests from the log. represented by a sorted set in redis
// Get the log from redis
let requestLog: RedisLog = JSON.parse((await this.client.get(uuid)) || '[]');
// Iterate through the list in reverse and count active tokens
// This allows us to track the threshold for when this request would be allowed if it is blocked
// Stop at the first timestamp that's expired and cut the rest.
const cutoff = timestamp - this.windowSize;
let tokensInLog = 0; // total active tokens in the log
let cutoffIndex = 0; // index of oldest active request
let lastAllowedIndex = requestLog.length; // Index of oldest request in the log for which this request would be allowed.
for (let index = requestLog.length - 1; index >= 0; index--) {
if (cutoff >= requestLog[index].timestamp) {
// we reached the first expired request
cutoffIndex = index + 1;
break;
} else {
// the request is active
tokensInLog += requestLog[index].tokens;
if (this.capacity - tokensInLog >= tokens) {
// the log is able to accept the current request
lastAllowedIndex = index;
}
}
}
// Time (ms) after which the current request would succeed if it is blocked.
let retryAfter: number;
// Request will never be allowed
if (tokens > this.capacity) retryAfter = Infinity;
// need the request before lastAllowedIndex
else if (lastAllowedIndex > 0)
retryAfter = Math.ceil(
(this.windowSize + requestLog[lastAllowedIndex - 1].timestamp - timestamp) / 1000
);
else retryAfter = 0; // request is allowed
// Conditional check to avoid unecessary slice
if (cutoffIndex > 0) requestLog = requestLog.slice(cutoffIndex);
// allow/disallow current request
if (tokensInLog + tokens <= this.capacity) {
// update the log
if (tokens > 0) requestLog.push({ timestamp, tokens });
await this.client.setex(uuid, this.keyExpiry, JSON.stringify(requestLog));
tokensInLog += tokens;
return { success: true, tokens: this.capacity - tokensInLog };
}
await this.client.setex(uuid, this.keyExpiry, JSON.stringify(requestLog));
return { success: false, tokens: this.capacity - tokensInLog, retryAfter };
}
/**
* Resets the rate limiter to the intial state by clearing the redis store.
*/
public reset(): void {
this.client.flushall();
}
}
export default SlidingWindowLog;