Skip to content

Commit a5aa9d3

Browse files
committed
updated tests and tweaked sliding window to pass all tests
1 parent 9d1d224 commit a5aa9d3

File tree

2 files changed

+33
-33
lines changed

2 files changed

+33
-33
lines changed

src/rateLimiters/slidingWindowCounter.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ class SlidingWindowCounter implements RateLimiter {
9191
fixedWindowStart: timestamp,
9292
};
9393

94-
if (tokens > this.capacity) {
94+
if (tokens <= this.capacity) {
9595
await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
96-
// tokens property represents how much capacity remains
97-
return { success: false, tokens: this.capacity };
96+
return { success: true, tokens: this.capacity - newUserWindow.currentTokens };
9897
}
9998

10099
await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow));
101-
return { success: true, tokens: this.capacity - newUserWindow.currentTokens };
100+
// tokens property represents how much capacity remains
101+
return { success: false, tokens: this.capacity };
102102
}
103103

104104
// if the cache is populated
@@ -112,45 +112,44 @@ class SlidingWindowCounter implements RateLimiter {
112112
};
113113

114114
// if request time is in a new window
115-
if (timestamp > window.fixedWindowStart + this.windowSize + 1) {
115+
if (timestamp >= window.fixedWindowStart + this.windowSize + 1) {
116116
updatedUserWindow.previousTokens = updatedUserWindow.currentTokens;
117117
updatedUserWindow.currentTokens = 0;
118-
updatedUserWindow.fixedWindowStart = window.fixedWindowStart + this.windowSize;
118+
updatedUserWindow.fixedWindowStart = window.fixedWindowStart + this.windowSize + 1;
119119
}
120120

121121
// assigned to avoid TS error, this var will never be used as 0
122122
// var is declared here so that below can be inside a conditional for efficiency's sake
123123
let rollingWindowProportion = 0;
124+
let previousRollingTokens = 0;
124125

125126
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
127+
// proportion of rolling window present in previous window
131128
rollingWindowProportion =
132129
(this.windowSize - (timestamp - updatedUserWindow.fixedWindowStart)) /
133130
this.windowSize;
134131

135132
// remove unecessary decimals, 0.xx is enough
136133
rollingWindowProportion -= rollingWindowProportion % 0.01;
134+
135+
// # of tokens present in rolling & previous window
136+
previousRollingTokens = Math.floor(
137+
updatedUserWindow.previousTokens * rollingWindowProportion
138+
);
137139
}
138140

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;
141+
// # of tokens present in rolling and/or current window
142+
// if previous tokens is null, previousRollingTokens will be 0
143+
const rollingTokens = updatedUserWindow.currentTokens + previousRollingTokens;
148144

149145
// if request is allowed
150-
if (rollingWindowAllowal) {
146+
if (tokens + rollingTokens <= this.capacity) {
151147
updatedUserWindow.currentTokens += tokens;
152148
await this.client.setex(uuid, keyExpiry, JSON.stringify(updatedUserWindow));
153-
return { success: true, tokens: this.capacity - updatedUserWindow.currentTokens };
149+
return {
150+
success: true,
151+
tokens: this.capacity - (updatedUserWindow.currentTokens + previousRollingTokens),
152+
};
154153
}
155154

156155
// if request is blocked

test/rateLimiters/slidingWindowCounter.test.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ describe('Test TokenBucket Rate Limiter', () => {
9191
await setTokenCountInClient(client, user4, 10, null, timestamp);
9292
// tokens returned in processRequest is equal to the capacity
9393
// still available in the fixed window
94+
9495
expect(
9596
(await limiter.processRequest(user4, timestamp + WINDOW_SIZE + 1, 1)).tokens
9697
).toBe(0); // here, we expect the rolling window to only allow 1 token, b/c
@@ -199,8 +200,8 @@ describe('Test TokenBucket Rate Limiter', () => {
199200
(await limiter.processRequest(user2, timestamp + WINDOW_SIZE, 5)).tokens
200201
).toBe(CAPACITY - initRequest);
201202

202-
// expect current tokens in the window to still be 0
203-
expect((await getWindowFromClient(client, user2)).currentTokens).toBe(0);
203+
// expect current tokens in the window to still be 6
204+
expect((await getWindowFromClient(client, user2)).currentTokens).toBe(6);
204205
});
205206

206207
// 3 rolling window tests with different proportions (.25, .5, .75)
@@ -276,7 +277,7 @@ describe('Test TokenBucket Rate Limiter', () => {
276277
// currentTokens (in current fixed window): 0
277278
// previousTokens (in previous fixed window): 8
278279
const count = await getWindowFromClient(client, user4);
279-
expect(count.currentTokens).toBe(4);
280+
expect(count.currentTokens).toBe(0);
280281
expect(count.previousTokens).toBe(initRequest);
281282
});
282283
});
@@ -321,20 +322,19 @@ describe('Test TokenBucket Rate Limiter', () => {
321322
});
322323

323324
test('fixed window and current/previous tokens update as expected', async () => {
324-
await setTokenCountInClient(client, user1, 0, null, timestamp);
325-
// fills first window with 4 tokens
325+
// fills first window with 5 tokens
326326
await limiter.processRequest(user1, timestamp, 5);
327-
// fills second window with 5 tokens
327+
// fills second window with 4 tokens
328328
expect(
329329
await (
330330
await limiter.processRequest(user1, timestamp + WINDOW_SIZE + 1, 4)
331331
).tokens
332-
).toBe(6);
332+
).toBe(2);
333333
// currentTokens (in current fixed window): 0
334334
// previousTokens (in previous fixed window): 8
335-
const count = await getWindowFromClient(client, user4);
335+
const count = await getWindowFromClient(client, user1);
336336
// ensures that fixed window is updated when a request goes over
337-
expect(count.fixedWindowStart).toBe(timestamp + WINDOW_SIZE);
337+
expect(count.fixedWindowStart).toBe(timestamp + WINDOW_SIZE + 1);
338338
// ensures that previous tokens property updates on fixed window change
339339
expect(count.previousTokens).toBe(5);
340340
// ensures that current tokens only represents tokens from current window requests
@@ -348,10 +348,11 @@ describe('Test TokenBucket Rate Limiter', () => {
348348

349349
await newLimiter.processRequest(user1, timestamp, 8);
350350

351-
// expect that a new window is entered, leaving 9 tokens available after a 1 token request
351+
// expect that a new window is entered, leaving 2 tokens available after both requests
352+
// 8 * .99 -> 7 (floored) + 1 = 8
352353
expect(
353354
(await newLimiter.processRequest(user1, timestamp + newWindowSize + 1, 1)).tokens
354-
).toBe(9);
355+
).toBe(2);
355356
});
356357

357358
test('sliding window allows custom capacities', async () => {

0 commit comments

Comments
 (0)