Repository navigation
feat: add rate limiter class - #1021
Conversation
schmidt-sebastian
left a comment
There was a problem hiding this comment.
This is pretty nice! I wonder though if we are need to deduct tokens even if they are not used. What do you think?
| * @private | ||
| */ | ||
| // Visible for testing. | ||
| _tryMakeRequest(numOperations: number, currentTime: Timestamp): boolean { |
There was a problem hiding this comment.
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.
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.
Renamed to requestTimeMillis.
| * @private | ||
| */ | ||
| private refillTokens(currentTime = Timestamp.now()): void { | ||
| if (currentTime.toMillis() > this.lastRefillTime.toMillis()) { |
There was a problem hiding this comment.
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.
Switched to using Date and getTime().
| * | ||
| * @private | ||
| */ | ||
| export class RateLimiter { |
There was a problem hiding this comment.
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.
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.
| * @private | ||
| */ | ||
| // Visible for testing. | ||
| _tryMakeRequest(numOperations: number, currentTime: Timestamp): boolean { |
There was a problem hiding this comment.
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.
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.
| * @private | ||
| */ | ||
| private refillTokens(currentTime = Timestamp.now()): void { | ||
| if (currentTime.toMillis() > this.lastRefillTime.toMillis()) { |
There was a problem hiding this comment.
Switched to using Date and getTime().
schmidt-sebastian
left a comment
There was a problem hiding this comment.
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.
| * @private | ||
| */ | ||
| private refillTokens(currentTimeMillis = Date.now()): void { | ||
| if (currentTimeMillis > this.lastRefillTimeMillis) { |
There was a problem hiding this comment.
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.
Added behavior to reject timestamps that are before the last refill time. Also added test for this.
| * @private | ||
| */ | ||
| private refillTokens(currentTimeMillis = Date.now()): void { | ||
| if (currentTimeMillis > this.lastRefillTimeMillis) { |
There was a problem hiding this comment.
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:
currentTimeparameter 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.