Skip to content

Commit 3dc4c6c

Browse files
authored
Merge pull request #72 from oslabs-beta/jd/slide
SlidingWindowCounter spec completed, some functionality copied over from token bucket
2 parents 4349a67 + 89277fd commit 3dc4c6c

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

src/@types/rateLimit.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export interface RedisBucket {
2323
timestamp: number;
2424
}
2525

26+
export interface RedisWindow {
27+
currentTokens: number;
28+
previousTokens: number;
29+
fixedWindowStart: number;
30+
}
31+
2632
export type RateLimiterSelection =
2733
| 'TOKEN_BUCKET'
2834
| 'LEAKY_BUCKET'
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Redis from 'ioredis';
2+
import { RateLimiter, RateLimiterResponse, RedisWindow } from '../@types/rateLimit';
3+
4+
/**
5+
* The SlidingWindowCounter instance of a RateLimiter limits requests based on a unique user ID.
6+
* This algorithm improves upon the FixedWindowCounter because this algorithm prevents fixed window's
7+
* flaw of allowing doubled capacity requests when hugging the window's borders with a rolling window,
8+
* allowing us to average the requests between both windows proportionately with the rolling window's
9+
* takeup in each.
10+
*
11+
* Whenever a user makes a request the following steps are performed:
12+
* 1. Fixed minute windows are defined along with redis caches if previously undefined.
13+
* 2. Rolling minute windows are defined or updated based on the timestamp of the new request.
14+
* 3. Counter of the current fixed window is updated with the new request's token usage.
15+
* 4. If a new minute interval is reached, the averaging formula is run to prevent fixed window's flaw
16+
* of flooded requests around window borders
17+
* (ex. 10 token capacity: 1m59s 10 reqs 2m2s 10 reqs)
18+
*/
19+
class SlidingWindowCounter implements RateLimiter {
20+
private windowSize: number;
21+
22+
private capacity: number;
23+
24+
private client: Redis;
25+
26+
/**
27+
* Create a new instance of a TokenBucket rate limiter that can be connected to any database store
28+
* @param windowSize size of each window in milliseconds (fixed and rolling)
29+
* @param capacity max capacity of tokens allowed per fixed window
30+
* @param client redis client where rate limiter will cache information
31+
*/
32+
constructor(windowSize: number, capacity: number, client: Redis) {
33+
this.windowSize = windowSize;
34+
this.capacity = capacity;
35+
this.client = client;
36+
if (windowSize <= 0 || capacity <= 0)
37+
throw SyntaxError('SlidingWindowCounter windowSize and capacity must be positive');
38+
}
39+
40+
/**
41+
*
42+
*
43+
* @param {string} uuid - unique identifer used to throttle requests
44+
* @param {number} timestamp - time the request was recieved
45+
* @param {number} [tokens=1] - complexity of the query for throttling requests
46+
* @return {*} {Promise<RateLimiterResponse>}
47+
* @memberof SlidingWindowCounter
48+
*/
49+
async processRequest(
50+
uuid: string,
51+
timestamp: number,
52+
tokens = 1
53+
): Promise<RateLimiterResponse> {
54+
// set the expiry of key-value pairs in the cache to 24 hours
55+
const keyExpiry = 86400000;
56+
57+
// attempt to get the value for the uuid from the redis cache
58+
const windowJSON = await this.client.get(uuid);
59+
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+
}
86+
87+
/**
88+
* Resets the rate limiter to the intial state by clearing the redis store.
89+
*/
90+
public reset(): void {
91+
this.client.flushall();
92+
}
93+
}
94+
95+
export default SlidingWindowCounter;

0 commit comments

Comments
 (0)