feat: add thread-safe token bucket rate limiter utility#55
Conversation
There was a problem hiding this comment.
Review: feat: add thread-safe token bucket rate limiter utility
Thanks for this addition — the token bucket algorithm is a solid choice and the overall structure is clean. However, there are a few issues that need to be addressed before merging, including a missing input validation that can silently corrupt state, a flawed thread-safety test, and some convention alignment.
Summary of findings
| Severity | Count |
|---|---|
| 🔴 Critical | 1 |
| 🟠 Major | 2 |
| 🟡 Minor | 3 |
| 💅 Nit | 1 |
See inline comments for details.
| bool | ||
| True if tokens were acquired, False otherwise. | ||
| """ | ||
| with self._lock: |
There was a problem hiding this comment.
🔴 Critical: Negative tokens values corrupt the rate limiter state.
Neither try_acquire() nor acquire() validates the tokens parameter. Calling try_acquire(tokens=-1) will always succeed (since self._tokens >= -1 is always true) and then adds a token to the bucket (self._tokens -= -1), silently inflating the token count beyond burst.
Fix: Add validation at the top of both methods:
if tokens <= 0:
raise ValueError("tokens must be positive")| True if tokens were acquired, False otherwise. | ||
| """ | ||
| with self._lock: | ||
| self._refill() |
There was a problem hiding this comment.
🟠 Major: try_acquire(tokens=N) where N > burst will always return False silently.
If a caller requests more tokens than the bucket can ever hold, this is almost certainly a programming error — but the caller gets no feedback, just an eternal False (or an infinite spin in acquire() without timeout).
Fix: Validate in both methods:
if tokens > self._burst:
raise ValueError(f"tokens ({tokens}) exceeds burst capacity ({self._burst})")| def acquire(self, tokens: int = 1, timeout: float = None) -> bool: | ||
| """ | ||
| Block until tokens are available or timeout is reached. | ||
|
|
There was a problem hiding this comment.
🟡 Minor: timeout: float = None should be timeout: Optional[float] = None.
The rest of the samcli codebase (e.g., boto_utils.py, async_utils.py) consistently uses Optional from typing. This also makes the type hint technically correct since None is not a float.
| @@ -0,0 +1,102 @@ | |||
| """ | |||
There was a problem hiding this comment.
🟡 Minor: Missing module-level logger.
Every other non-trivial utility in samcli/lib/utils/ defines LOG = logging.getLogger(__name__). The acquire() method's busy-wait loop is a great candidate for LOG.debug() calls to aid diagnostics (e.g., logging when waiting for token refill).
| def __init__(self, rate: float, burst: int): | ||
| if rate <= 0: | ||
| raise ValueError("rate must be positive") | ||
| if burst <= 0: |
There was a problem hiding this comment.
💅 Nit: burst is typed as int but the <= 0 guard doesn't reject float values like 1.5. A caller passing burst=1.5 silently gets a float bucket capacity. Consider adding if not isinstance(burst, int): raise TypeError(...) to enforce the type hint.
| threads = [threading.Thread(target=worker) for _ in range(10)] | ||
| for t in threads: | ||
| t.start() | ||
| for t in threads: |
There was a problem hiding this comment.
🟠 Major: Thread-safety test assertion is flawed — will produce false negatives.
With rate=1000 tokens/sec, the bucket refills significantly during test execution (10 threads × 20 calls takes measurable wall time). The total acquired tokens can legitimately exceed the initial burst of 100, causing the assertion sum(results) <= 100 to fail sporadically.
Fix: Use a near-zero refill rate so the bucket doesn't meaningfully refill during the test:
limiter = RateLimiter(rate=0.001, burst=100)This makes the assertion reliable while still testing thread safety of concurrent try_acquire() calls.
| t.join() | ||
|
|
||
| # Total acquired should not exceed burst (100) | ||
| self.assertLessEqual(sum(results), 100) |
There was a problem hiding this comment.
🟡 Minor: Missing test coverage for edge cases.
Consider adding tests for:
try_acquire(tokens=-1)/try_acquire(tokens=0)— should raiseValueErrorafter the fix abovetry_acquire(tokens=N)where N > burst — should raiseValueErroracquire()withtokens > burst— verify it doesn't spin forever- Concurrent
acquire()calls (onlytry_acquire()is tested under concurrency)
|
I've completed the review of PR #55. Here's a summary of what I found: Verdict: Request Changes — 7 inline comments submitted. Key findings:
|
Summary
Adds a thread-safe rate limiter to
samcli.lib.utilsusing the token bucket algorithm. This is useful for throttling API calls to AWS services to stay within service quotas.Changes
samcli/lib/utils/rate_limiter.py—RateLimiterclass with:try_acquire()— non-blocking token acquisitionacquire(timeout=)— blocking with optional timeoutthreading.Locktests/unit/lib/utils/test_rate_limiter.py— 8 unit tests covering:Usage