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

#90 Exposing TokenBucket as a standalone component #100

Merged
merged 5 commits into from
Dec 9, 2023

Conversation

kevkevy3000
Copy link
Contributor

  • Created new class buckets.py that contains the standalone component for TokenBucket
  • Changed TokenBucketLimiter and Retry API/component to pull rate limiting functionality from TokenBucket class

Copy link
Owner

@roma-glushko roma-glushko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevkevy3000 Great start, Kev 👍
Please give comments a look to see if they make sense and don't forget about tests 👀
It will probably make sense to have tests specific to TokenBucket (even though we are partially testing it via the existing ratelimiter tests) to ensure that tokens() and empty() properties are showing actual info as time goes on.

"""
Token Bucket Logic
Replenish tokens as time passes on. If tokens are available, executions can be allowed.
Otherwise, it's going to be rejected with RateLimitExceeded
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenBucket is decoupled from the rate limiting, so it probably doesn't keep using RateLimitExceeded exception here. I would create a new exception like EmptyBucket and throw it here.

hyx/ratelimit/buckets.py Show resolved Hide resolved
def empty(self) -> bool:
return self._tokens <= 0

async def acquire(self) -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about calling it just take()?

hyx/ratelimit/buckets.py Show resolved Hide resolved
@roma-glushko roma-glushko self-requested a review December 3, 2023 11:10
Copy link
Owner

@roma-glushko roma-glushko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevkevy3000 this is great Kevin 👍 Just a few more really minor things and we are merging it

self._tokens = tokens_to_add - 1
return

def __replenish(self) -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just use one underscore there? 🙏

Suggested change
def __replenish(self) -> None:
def _replenish(self) -> None:

next_replenish = self._next_replenish_at
until_next_replenish = next_replenish - now

if until_next_replenish <= 0:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to check for until_next_replenish > 0 first and return right away if so. That will make the code more "flat" which is easier to read 👀 :

if until_next_replenish > 0:
  return

# the code to replenish the bucket


self._tokens = self._bucket_size
self._next_replenish_at = self._loop.time() + self._token_per_secs
self._token_bucket = TokenBucket(max_executions, per_time_secs, bucket_size)

@property
def tokens(self) -> float:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in this case, it's better just to expose the bucket (and it will provide the remaining information around token state).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just have one property for just token_bucket and remove the existing two properties?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just have one property for just token_bucket and remove the existing two properties?

Yes, that! I would call the property just bucket

@@ -45,7 +45,7 @@ async def test__ratelimiter__limit_exceeded() -> None:
async def calc() -> float:
return 42

with pytest.raises(RateLimitExceeded):
with pytest.raises(EmptyBucket):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please keep the RateLimitExceeded exception? It's more tied to the ratelimiter "domain" while the bucket error feels like exposing detail of the underlying implementation to ratelimiter consumers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so the tokenbucket class throws an empty bucket exception, and the token bucket rate limiter class essentially pulls that logic from the tokenbucket class. Should we just make the acquire function in tokenbucketlimiter catch this exception and throw the ratelimitexceeded exception?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just make the acquire function in tokenbucketlimiter catch this exception and throw the ratelimitexceeded exception?

@kevkevy3000 yes, that's actually what I meant. Might seem redundant, but it brings some flexibility to the rate limiter component around when to fail with rate limit error.

return self._tokens <= 0

async def take(self) -> None:
print(self.tokens)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ops, we have left some debug statements 👀

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please name it test_buckets.py to keep it consistent with other test file names?

await bucket.take()


async def test__ratelimiter__fully_replenish_after_time_period() -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding this case we were discussing in the comments 👍

await bucket.take()


async def test__ratelimiter__fully_replenish_after_time_period() -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async def test__ratelimiter__fully_replenish_after_time_period() -> None:
async def test__token_bucket__fully_replenish_after_time_period() -> None:

"""
Token Bucket Logic
Replenish tokens as time passes on. If tokens are available, executions can be allowed.
Otherwise, it's going to be rejected with EmptyBucket
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Otherwise, it's going to be rejected with EmptyBucket
Otherwise, it's going to be rejected with a EmptyBucket error

@roma-glushko roma-glushko merged commit c104a40 into roma-glushko:main Dec 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Expose ToketBucket as a standalone component
2 participants