feat: implement concurrent API fetching engine with token bucket rate…#35
feat: implement concurrent API fetching engine with token bucket rate…#35
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the RateLimiter utility to use a Token Bucket algorithm, adding support for concurrency management and a complete method to release request slots. The fetchWithRetry function was updated to ensure concurrency slots are released via a finally block, and extractCollectionData now includes more robust type checking. Review feedback identifies that resetCounter fails to wake up requests currently waiting for tokens, the politeness delay logic does not effectively stagger concurrent requests, and a new test case for concurrency lacks necessary assertions.
| const resetCounter = (): void => { | ||
| resetTime = Date.now() + 60000; | ||
| requestCount = 0; | ||
| console.log('Rate limiter counter reset.'); | ||
| tokens = maxRequestsPerMinute; | ||
| lastRefill = Date.now(); | ||
| console.log('Rate limiter tokens refilled.'); | ||
| }; |
There was a problem hiding this comment.
The resetCounter method updates the tokens and lastRefill state but does not wake up requests currently waiting in the checkLimit loop's setTimeout (line 96). These requests will continue to wait for the full duration of their previously calculated waitTime, even though tokens are now available. This can lead to unnecessary delays after a rate limit reset. Consider using a promise-based signaling mechanism or a queue for token waiters to allow immediate resumption upon reset.
| tokens--; | ||
| // Politeness delay | ||
| if (delayBetweenRequests > 0) { | ||
| await new Promise(resolve => setTimeout(resolve, delayBetweenRequests / maxConcurrency)); |
There was a problem hiding this comment.
The politeness delay logic delayBetweenRequests / maxConcurrency does not effectively space out concurrent requests. If multiple requests are initiated simultaneously, they will all reach this setTimeout at roughly the same time and resolve after the same duration, resulting in a burst of requests rather than a staggered sequence. To truly space them out, you should track the lastRequestStartTime and ensure the next request is scheduled at least delayBetweenRequests / maxConcurrency after the previous one.
| it('should allow burst up to maxConcurrency', async () => { | ||
| const rateLimiter = createRateLimiter(60, 300, 5); | ||
|
|
||
| // First 5 requests should be instant (except for the small politeness delay if configured) | ||
| // But wait, my implementation has a delay even for the first tokens if delayBetweenRequests > 0. | ||
| // Let's check: | ||
| // if (delayBetweenRequests > 0) { | ||
| // await new Promise(resolve => setTimeout(resolve, delayBetweenRequests / maxConcurrency)); | ||
| // } | ||
| // So each request waits 60ms. | ||
|
|
||
| const startTime = Date.now(); | ||
| await rateLimiter.checkLimit(); | ||
|
|
||
| // Second request - should delay 300ms | ||
| const promise = rateLimiter.checkLimit(); | ||
| jest.advanceTimersByTime(300); | ||
| await flushPromises(); | ||
| await promise; | ||
|
|
||
| expect(jest.getTimerCount()).toBe(0); | ||
| await rateLimiter.checkLimit(); | ||
| await rateLimiter.checkLimit(); | ||
| await rateLimiter.checkLimit(); | ||
| await rateLimiter.checkLimit(); | ||
|
|
||
| // Should have taken about 5 * 60ms = 300ms total if sequential, | ||
| // but if they are called concurrently? | ||
| // The rate limiter I wrote is still somewhat sequential due to the await in checkLimit. | ||
| }); |
There was a problem hiding this comment.
This test case lacks assertions. It calls checkLimit multiple times but does not verify that the requests were allowed concurrently or that the expected timing/delays were respected. Without assertions, this test only checks that the code doesn't throw, which is insufficient for verifying rate limiting logic.
… limiter
name: Pull Request
about: Submit a pull request to contribute to Make It Rain
title: ''
labels: ''
assignees: ''
Description
Please include a summary of the changes and related context. Explain the problem you're solving and why this approach was taken.
Type of Change
Related Issues
Fixes #(issue number) or Related to #(issue number)
Changes Made
Provide a bulleted list of the specific changes:
Testing
Describe the tests you've run and how to reproduce them:
Test Instructions
Breaking Changes
Does this PR introduce any breaking changes? If yes, please describe the impact and migration path:
Checklist
Screenshots (if applicable)
If your changes affect the UI or have visual components, please include screenshots or recordings.
Performance Impact
Does this change affect performance? If yes, please describe:
Additional Context
Add any other context or considerations here.
Deployment Notes
Any special instructions for deployment or integration?