-
Notifications
You must be signed in to change notification settings - Fork 149
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
feat: add rate limiter class #1021
Conversation
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.
This is pretty nice! I wonder though if we are need to deduct tokens even if they are not used. What do you think?
dev/src/rate-limiter.ts
Outdated
* @private | ||
*/ | ||
// Visible for testing. | ||
_tryMakeRequest(numOperations: number, currentTime: Timestamp): boolean { |
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.
Can you combine tryMakeRequest and _tryMakeRequest? You can assign a default value to currentTime
(and name it something like requestTime
to make it more suited for a public API).
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.
I had that originally, but felt uncomfortable with the idea that you could break the limiter by passing in arbitrary timestamps far in the future. If someone specified a timestamp far in the future, the limiter would not refill any tokens until that time has been reached. It seems fragile to expose a parameter that would break the limiter, but if we're the only ones using the class, is it ok?
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.
Renamed to requestTimeMillis
.
dev/src/rate-limiter.ts
Outdated
* @private | ||
*/ | ||
private refillTokens(currentTime = Timestamp.now()): void { | ||
if (currentTime.toMillis() > this.lastRefillTime.toMillis()) { |
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.
If you used Date
object or even just epoch milliseconds your code will be much more portable and can be used outside of the FIrestore client if need be.
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.
Switched to using Date
and getTime()
.
* | ||
* @private | ||
*/ | ||
export class RateLimiter { |
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.
Do we have to add some code to make tokens expire even if they are not used? I would suspect that if you issue 500 writes per second, then sit idle for a second, you can only issue 500 writes and not 1000.
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.
Although we are not making tokens expire, we are already limiting the maximum number of tokens that can be accrued. When we refill the tokens, we call Math.min()
on the capacity and the number of added tokens to prevent the scenario you mentioned above from happening.
I also added another test case to cover this as well.
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.
thanks again ⏲️ ⌚ ⌛ ⏳
I wonder though if we are need to deduct tokens even if they are not used. What do you think?
See the comment I left, but I think we are already doing that in the code by limiting the number of tokens that can be accrued.
dev/src/rate-limiter.ts
Outdated
* @private | ||
*/ | ||
// Visible for testing. | ||
_tryMakeRequest(numOperations: number, currentTime: Timestamp): boolean { |
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.
I had that originally, but felt uncomfortable with the idea that you could break the limiter by passing in arbitrary timestamps far in the future. If someone specified a timestamp far in the future, the limiter would not refill any tokens until that time has been reached. It seems fragile to expose a parameter that would break the limiter, but if we're the only ones using the class, is it ok?
* | ||
* @private | ||
*/ | ||
export class RateLimiter { |
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.
Although we are not making tokens expire, we are already limiting the maximum number of tokens that can be accrued. When we refill the tokens, we call Math.min()
on the capacity and the number of added tokens to prevent the scenario you mentioned above from happening.
I also added another test case to cover this as well.
dev/src/rate-limiter.ts
Outdated
* @private | ||
*/ | ||
private refillTokens(currentTime = Timestamp.now()): void { | ||
if (currentTime.toMillis() > this.lastRefillTime.toMillis()) { |
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.
Switched to using Date
and getTime()
.
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.
Thanks for addressing the feedback!
I would like to push you once more to rename currentTimeMillis
to requestTimeMillis
. If you do that, then suddenly your testing only code becomes part of the public API and exposing it seems much more natural. This is optional if you disagree.
dev/src/rate-limiter.ts
Outdated
* @private | ||
*/ | ||
private refillTokens(currentTimeMillis = Date.now()): void { | ||
if (currentTimeMillis > this.lastRefillTimeMillis) { |
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.
We might want to make the behavior of this class more predictable and reject timestamps that go back in time.
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.
Added behavior to reject timestamps that are before the last refill time. Also added test for this.
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.
thanks sebastian!
dev/src/rate-limiter.ts
Outdated
* @private | ||
*/ | ||
private refillTokens(currentTimeMillis = Date.now()): void { | ||
if (currentTimeMillis > this.lastRefillTimeMillis) { |
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.
Added behavior to reject timestamps that are before the last refill time. Also added test for this.
Adding a rate limiter that implements token bucket that calculates if a request containing a number of writes can be made under a rate limit as well as the time required before a request can be made while staying under the limit.
It's a little more verbose than I wanted, but having a separate rate limiter has a few benefits for BulkWriter:
Implementation notes:
currentTime
parameter to make testing the limiter easier, since it's based on Timestamps. I was hoping to get some feedback/suggestions on how to test future Timestamps without hurting readability of the class too much.