Skip to content

Commit 7687390

Browse files
committed
merge conflicts
1 parent 46b6d1a commit 7687390

File tree

3 files changed

+577
-26
lines changed

3 files changed

+577
-26
lines changed

src/@types/rateLimit.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export interface RedisBucket {
2525

2626
export interface RedisWindow {
2727
currentTokens: number;
28-
previousTokens: number;
28+
// null if limiter is currently on the initial fixed window
29+
previousTokens: number | null;
2930
fixedWindowStart: number;
3031
}
3132

src/rateLimiters/slidingWindowCounter.ts

Lines changed: 99 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,37 @@ class SlidingWindowCounter implements RateLimiter {
3838
}
3939

4040
/**
41+
* @function processRequest - Sliding window counter algorithm to allow or block
42+
* based on the depth/complexity (in amount of tokens) of incoming requests.
4143
*
44+
* First, checks if a window exists in the redis cache.
45+
*
46+
* If not, then `fixedWindowStart` is set as the current timestamp, and `currentTokens`
47+
* is checked against `capacity`. If enough room exists for the request, returns
48+
* success as true and tokens as how many tokens remain in the current fixed window.
49+
*
50+
* If a window does exist in the cache, we first check if the timestamp is greater than
51+
* the fixedWindowStart + windowSize.
52+
*
53+
* If it isn't then we check the number of tokens in the arguments as well as in the cache
54+
* against the capacity and return success or failure from there while updating the cache.
55+
*
56+
* If the timestamp is over the windowSize beyond the fixedWindowStart, then we update fixedWindowStart
57+
* to be fixedWindowStart + windowSize (to create a new fixed window) and
58+
* make previousTokens = currentTokens, and currentTokens equal to the number of tokens in args, if
59+
* not over capacity.
60+
*
61+
* Once previousTokens is not null, we then run functionality using the rolling window to compute
62+
* the formula this entire limiting algorithm is distinguished by:
63+
*
64+
* currentTokens + previousTokens * overlap % of rolling window over previous fixed window
4265
*
4366
* @param {string} uuid - unique identifer used to throttle requests
4467
* @param {number} timestamp - time the request was recieved
4568
* @param {number} [tokens=1] - complexity of the query for throttling requests
4669
* @return {*} {Promise<RateLimiterResponse>}
70+
* RateLimiterResponse: {success: boolean, tokens: number}
71+
* (tokens represents the remaining available capacity of the window)
4772
* @memberof SlidingWindowCounter
4873
*/
4974
async processRequest(
@@ -57,31 +82,80 @@ class SlidingWindowCounter implements RateLimiter {
5782
// attempt to get the value for the uuid from the redis cache
5883
const windowJSON = await this.client.get(uuid);
5984

60-
// // if the response is null, we need to create a window for the user
61-
// if (windowJSON === null) {
62-
// // rolling window is 1 minute long
63-
// const rollingWindowEnd = timestamp + 60000;
64-
65-
// // grabs the actual minute from the timestamp to create fixed window
66-
// const fixedWindowStart = timestamp - (timestamp % 10000);
67-
// const fixedWindowEnd = fixedWindowStart + 60000;
68-
69-
// const newUserWindow: RedisWindow = {
70-
// // conditionally set tokens depending on how many are requested compared to the capacity
71-
// tokens: tokens > this.capacity ? this.capacity : this.capacity - tokens,
72-
// timestamp,
73-
// };
74-
75-
// // reject the request, not enough tokens could even be in the bucket
76-
// if (tokens > this.capacity) {
77-
// await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
78-
// return { success: false, tokens: this.capacity };
79-
// }
80-
// await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
81-
// return { success: true, tokens: newUserWindow.tokens };
82-
// }
83-
84-
return { success: true, tokens: 0 };
85+
// if the response is null, we need to create a window for the user
86+
if (windowJSON === null) {
87+
const newUserWindow: RedisWindow = {
88+
// current and previous tokens represent how many tokens are in each window
89+
currentTokens: tokens <= this.capacity ? tokens : 0,
90+
previousTokens: null,
91+
fixedWindowStart: timestamp,
92+
};
93+
94+
if (tokens > this.capacity) {
95+
await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
96+
// tokens property represents how much capacity remains
97+
return { success: false, tokens: this.capacity };
98+
}
99+
100+
await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
101+
return { success: true, tokens: this.capacity - newUserWindow.currentTokens };
102+
}
103+
104+
// if the cache is populated
105+
106+
const window: RedisWindow = await JSON.parse(windowJSON);
107+
108+
let updatedUserWindow: RedisWindow = {
109+
currentTokens: window.currentTokens,
110+
previousTokens: window.previousTokens,
111+
fixedWindowStart: window.fixedWindowStart,
112+
};
113+
114+
// if request time is in a new window
115+
if (timestamp > window.fixedWindowStart + this.windowSize + 1) {
116+
updatedUserWindow.previousTokens = updatedUserWindow.currentTokens;
117+
updatedUserWindow.currentTokens = 0;
118+
updatedUserWindow.fixedWindowStart = window.fixedWindowStart + this.windowSize;
119+
}
120+
121+
// assigned to avoid TS error, this var will never be used as 0
122+
// var is declared here so that below can be inside a conditional for efficiency's sake
123+
let rollingWindowProportion: number = 0;
124+
125+
if (updatedUserWindow.previousTokens) {
126+
// subtract window size by current time less fixed window's start
127+
// current time less fixed window start is the amount that rolling window is in current window
128+
// to get amount that rolling window is in previous window, we subtract this difference by window size
129+
// then we divide this amount by window size to get the proportion of rolling window in previous window
130+
// ex. 60000 - (1million+5 - 1million) = 59995 / 60000 = 0.9999
131+
rollingWindowProportion =
132+
(this.windowSize - (timestamp - updatedUserWindow.fixedWindowStart)) /
133+
this.windowSize;
134+
135+
// remove unecessary decimals, 0.xx is enough
136+
rollingWindowProportion = rollingWindowProportion - (rollingWindowProportion % 0.01);
137+
}
138+
139+
// the sliding window counter formula
140+
// ex. tokens(1) + currentTokens(2) + previousTokens(4) * RWP(.75) = 6 < capacity(10)
141+
// adjusts formula if previousTokens is null
142+
const rollingWindowAllowal = updatedUserWindow.previousTokens
143+
? tokens +
144+
updatedUserWindow.currentTokens +
145+
updatedUserWindow.previousTokens * rollingWindowProportion <=
146+
this.capacity
147+
: tokens + updatedUserWindow.currentTokens <= this.capacity;
148+
149+
// if request is allowed
150+
if (rollingWindowAllowal) {
151+
updatedUserWindow.currentTokens += tokens;
152+
await this.client.setex(uuid, keyExpiry, JSON.stringify(updatedUserWindow));
153+
return { success: true, tokens: this.capacity - updatedUserWindow.currentTokens };
154+
}
155+
156+
// if request is blocked
157+
await this.client.setex(uuid, keyExpiry, JSON.stringify(updatedUserWindow));
158+
return { success: false, tokens: this.capacity - updatedUserWindow.currentTokens };
85159
}
86160

87161
/**

0 commit comments

Comments
 (0)