Skip to content
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

Merged
merged 4 commits into from
Apr 16, 2020
Merged

feat: add rate limiter class #1021

merged 4 commits into from
Apr 16, 2020

Conversation

thebrianchen
Copy link

@thebrianchen thebrianchen commented Apr 15, 2020

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:

  • We would not have to log sendTimes for each batch
  • We would not have to manually calculate the number of writes made in the past second each time we want to determine whether a batch can be sent.
  • Simplifies calculation for the required delay before the next batch can be sent.

Implementation notes:

  • I used a 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.
  • It is possible to factor in the next multiplier in calculating the required delay (ex: a request that would currently exceed the maximum capacity would be possible after the next capacity increase), but I don't think it's worth adding.

@thebrianchen thebrianchen self-assigned this Apr 15, 2020
@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Apr 15, 2020
@thebrianchen thebrianchen changed the title Add RateLimiter feat: add rate limiter class Apr 15, 2020
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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 Show resolved Hide resolved
* @private
*/
// Visible for testing.
_tryMakeRequest(numOperations: number, currentTime: Timestamp): boolean {
Copy link
Contributor

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).

Copy link
Author

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?

Copy link
Author

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 Show resolved Hide resolved
* @private
*/
private refillTokens(currentTime = Timestamp.now()): void {
if (currentTime.toMillis() > this.lastRefillTime.toMillis()) {
Copy link
Contributor

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.

Copy link
Author

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 {
Copy link
Contributor

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.

Copy link
Author

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 Show resolved Hide resolved
dev/test/rate-limiter.ts Outdated Show resolved Hide resolved
dev/test/rate-limiter.ts Show resolved Hide resolved
Copy link
Author

@thebrianchen thebrianchen left a 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.

* @private
*/
// Visible for testing.
_tryMakeRequest(numOperations: number, currentTime: Timestamp): boolean {
Copy link
Author

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?

dev/src/rate-limiter.ts Show resolved Hide resolved
dev/test/rate-limiter.ts Show resolved Hide resolved
dev/src/rate-limiter.ts Outdated Show resolved Hide resolved
dev/test/rate-limiter.ts Outdated Show resolved Hide resolved
*
* @private
*/
export class RateLimiter {
Copy link
Author

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.

* @private
*/
private refillTokens(currentTime = Timestamp.now()): void {
if (currentTime.toMillis() > this.lastRefillTime.toMillis()) {
Copy link
Author

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().

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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 Show resolved Hide resolved
dev/src/rate-limiter.ts Outdated Show resolved Hide resolved
* @private
*/
private refillTokens(currentTimeMillis = Date.now()): void {
if (currentTimeMillis > this.lastRefillTimeMillis) {
Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Author

@thebrianchen thebrianchen left a 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 Show resolved Hide resolved
* @private
*/
private refillTokens(currentTimeMillis = Date.now()): void {
if (currentTimeMillis > this.lastRefillTimeMillis) {
Copy link
Author

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.

@thebrianchen thebrianchen merged commit 05773ba into bc/bulk-master Apr 16, 2020
@thebrianchen thebrianchen deleted the bc/rate-limiter branch April 16, 2020 21:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants