-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Exposing TokenBucket as a standalone component * Renamed tokenbucket.py to buckets.py and changed the retry component to use the newly exposed TokenBucket component * Added in new method replenish to ensure token consistency and added in more token bucket tests * Fixed styling of test_tokenbucket * Changed variable/function names and reformatted code for better structure --------- Co-authored-by: Kevin Yang <[email protected]>
- Loading branch information
1 parent
513caa3
commit c104a40
Showing
7 changed files
with
142 additions
and
54 deletions.
There are no files selected for viewing
This file contains 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 |
---|---|---|
@@ -1,8 +1,10 @@ | ||
from hyx.ratelimit.api import ratelimiter, tokenbucket | ||
from hyx.ratelimit.buckets import TokenBucket | ||
from hyx.ratelimit.managers import TokenBucketLimiter | ||
|
||
__all__ = ( | ||
"ratelimiter", | ||
"tokenbucket", | ||
"TokenBucketLimiter", | ||
"TokenBucket", | ||
) |
This file contains 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,84 @@ | ||
import asyncio | ||
from typing import Optional | ||
|
||
from hyx.ratelimit.exceptions import EmptyBucket | ||
|
||
|
||
class TokenBucket: | ||
""" | ||
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 an EmptyBucket error | ||
""" | ||
|
||
__slots__ = ( | ||
"_max_executions", | ||
"_per_time_secs", | ||
"_bucket_size", | ||
"_loop", | ||
"_token_per_secs", | ||
"_tokens", | ||
"_next_replenish_at", | ||
) | ||
|
||
def __init__(self, max_executions: float, per_time_secs: float, bucket_size: Optional[float] = None) -> None: | ||
self._max_executions = max_executions | ||
self._per_time_secs = per_time_secs | ||
|
||
self._bucket_size = bucket_size if bucket_size else max_executions | ||
|
||
self._loop = asyncio.get_running_loop() | ||
self._token_per_secs = self._per_time_secs / self._max_executions | ||
|
||
self._tokens = self._bucket_size | ||
self._next_replenish_at = self._loop.time() + self._token_per_secs | ||
|
||
@property | ||
def tokens(self) -> float: | ||
self._replenish() | ||
return self._tokens | ||
|
||
@property | ||
def empty(self) -> bool: | ||
self._replenish() | ||
return self._tokens <= 0 | ||
|
||
async def take(self) -> None: | ||
if not self.empty: | ||
self._tokens -= 1 | ||
return | ||
|
||
now = self._loop.time() | ||
|
||
next_replenish = self._next_replenish_at | ||
until_next_replenish = next_replenish - now | ||
|
||
if until_next_replenish > 0: | ||
raise EmptyBucket | ||
|
||
tokens_to_add = min(self._bucket_size, 1 + abs(until_next_replenish / self._token_per_secs)) | ||
|
||
self._next_replenish_at = max( | ||
next_replenish + tokens_to_add * self._token_per_secs, | ||
now + self._token_per_secs, | ||
) | ||
|
||
self._tokens = tokens_to_add - 1 | ||
return | ||
|
||
def _replenish(self) -> None: | ||
now = self._loop.time() | ||
|
||
next_replenish = self._next_replenish_at | ||
until_next_replenish = next_replenish - now | ||
|
||
if until_next_replenish > 0: | ||
return | ||
|
||
tokens_to_add = min(self._bucket_size, 1 + abs(until_next_replenish / self._token_per_secs)) | ||
self._next_replenish_at = max( | ||
next_replenish + tokens_to_add * self._token_per_secs, | ||
now + self._token_per_secs, | ||
) | ||
self._tokens = tokens_to_add | ||
return |
This file contains 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 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 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 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 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,35 @@ | ||
import asyncio | ||
|
||
import pytest | ||
|
||
from hyx.ratelimit.buckets import TokenBucket | ||
from hyx.ratelimit.exceptions import EmptyBucket | ||
|
||
|
||
async def test__token_bucket_success() -> None: | ||
bucket = TokenBucket(3, 1, 3) | ||
|
||
for i in range(3): | ||
assert bucket.tokens == (3 - i) | ||
await bucket.take() | ||
assert bucket.empty is True | ||
|
||
|
||
async def test__token_bucket_limit_exceeded() -> None: | ||
bucket = TokenBucket(3, 1, 3) | ||
|
||
with pytest.raises(EmptyBucket): | ||
for _ in range(4): | ||
await bucket.take() | ||
|
||
|
||
async def test__token_bucket__fully_replenish_after_time_period() -> None: | ||
bucket = TokenBucket(3, 1, 3) | ||
|
||
for _ in range(3): | ||
await bucket.take() | ||
|
||
await asyncio.sleep(3) | ||
|
||
assert bucket.tokens == 3 | ||
assert bucket.empty is False |