-
-
Notifications
You must be signed in to change notification settings - Fork 2
SlidingWindowCounter spec completed, some functionality copied over from token bucket #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import Redis from 'ioredis'; | ||
| import { RateLimiter, RateLimiterResponse, RedisWindow } from '../@types/rateLimit'; | ||
|
|
||
| /** | ||
| * The SlidingWindowCounter instance of a RateLimiter limits requests based on a unique user ID. | ||
| * This algorithm improves upon the FixedWindowCounter because this algorithm prevents fixed window's | ||
| * flaw of allowing doubled capacity requests when hugging the window's borders with a rolling window, | ||
| * allowing us to average the requests between both windows proportionately with the rolling window's | ||
| * takeup in each. | ||
| * | ||
| * Whenever a user makes a request the following steps are performed: | ||
| * 1. Fixed minute windows are defined along with redis caches if previously undefined. | ||
jondeweydev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * 2. Rolling minute windows are defined or updated based on the timestamp of the new request. | ||
| * 3. Counter of the current fixed window is updated with the new request's token usage. | ||
| * 4. If a new minute interval is reached, the averaging formula is run to prevent fixed window's flaw | ||
| * of flooded requests around window borders | ||
| * (ex. 10 token capacity: 1m59s 10 reqs 2m2s 10 reqs) | ||
| */ | ||
| class SlidingWindowCounter implements RateLimiter { | ||
| private windowSize: number; | ||
|
|
||
| private capacity: number; | ||
|
|
||
| private client: Redis; | ||
|
|
||
| /** | ||
| * Create a new instance of a TokenBucket rate limiter that can be connected to any database store | ||
jondeweydev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * @param windowSize size of each window in milliseconds (fixed and rolling) | ||
| * @param capacity max capacity of tokens allowed per fixed window | ||
| * @param client redis client where rate limiter will cache information | ||
| */ | ||
| constructor(windowSize: number, capacity: number, client: Redis) { | ||
| this.windowSize = windowSize; | ||
| this.capacity = capacity; | ||
| this.client = client; | ||
| if (windowSize <= 0 || capacity <= 0) | ||
| throw SyntaxError('SlidingWindowCounter windowSize and capacity must be positive'); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * | ||
| * @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 SlidingWindowCounter | ||
| */ | ||
| async processRequest( | ||
| uuid: string, | ||
| timestamp: number, | ||
| tokens = 1 | ||
| ): Promise<RateLimiterResponse> { | ||
| // set the expiry of key-value pairs in the cache to 24 hours | ||
| const keyExpiry = 86400000; | ||
|
|
||
| // attempt to get the value for the uuid from the redis cache | ||
| const windowJSON = await this.client.get(uuid); | ||
|
|
||
| // // if the response is null, we need to create a window for the user | ||
| // if (windowJSON === null) { | ||
| // // rolling window is 1 minute long | ||
| // const rollingWindowEnd = timestamp + 60000; | ||
|
|
||
| // // grabs the actual minute from the timestamp to create fixed window | ||
| // const fixedWindowStart = timestamp - (timestamp % 10000); | ||
| // const fixedWindowEnd = fixedWindowStart + 60000; | ||
|
|
||
| // const newUserWindow: RedisWindow = { | ||
| // // conditionally set tokens depending on how many are requested compared to the capacity | ||
| // tokens: tokens > this.capacity ? this.capacity : this.capacity - tokens, | ||
| // timestamp, | ||
| // }; | ||
|
|
||
| // // reject the request, not enough tokens could even be in the bucket | ||
| // if (tokens > this.capacity) { | ||
| // await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow)); | ||
| // return { success: false, tokens: this.capacity }; | ||
| // } | ||
| // await this.client.setex(uuid, keyExpiry, JSON.stringify(newUserWindow)); | ||
| // return { success: true, tokens: newUserWindow.tokens }; | ||
| // } | ||
|
|
||
| return { success: true, tokens: 0 }; | ||
| } | ||
|
|
||
| /** | ||
| * Resets the rate limiter to the intial state by clearing the redis store. | ||
| */ | ||
| public reset(): void { | ||
| this.client.flushall(); | ||
| } | ||
| } | ||
|
|
||
| export default SlidingWindowCounter; | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You created interface
RedisWindowwhich looks the same as the interfaceRedisBucket. Any reason you can't useRedisBucket?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or rename
RedisBucketto a name that works for both buckets and windows?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
going to add more values in the future to track the rolling and fixed window timestamps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case the new type should extend
RedisBucketto keep things DRY.At a higher level, what data does Redis need to track? My understanding is the complexity of all requests in the previous window along with the complexity of all requests in the current window. In each case the timestamp could just represent the beginning of the window. I don't think we need to track the timestamp of each request in Redis.
If we don't need anything else then for each user we just need to store two
RedisBucketseither under to separate keys or both as an array. In the latter case then we would just need to type something as[RedisBucket, RedisBucket]Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline