A token-bucket rate limiter
Implement the limiter each call asks first, answering now, later or not in time, so two automations on one key stay under the vendor's limit.
On this page
A vendor's rate limit is a bucket on their side, and the cheapest place to honour it is a copy of that bucket on yours, asked before every call, where why we write these five by hand puts it on the path. The answer it gives is a time, not a yes or no. Rate limits, retries and backoff counts what an unpaced connector costs, and rate limiting compares the token bucket with the other counting algorithms from the server's side.
The situation
The 40-person wholesaler this module uses in place of the agency supplies 900 cafés, and its accounting API gives each key a bucket of 10 requests, refilled at one every 2 seconds, 30 a minute, and answers 429 when the bucket is empty. Two automations share the key: the invoice sender, about 240 calls across a working day, and the Friday statement run, which sends 80 account statements at 16:00.
Last Friday the statement run fired all 80 calls in the same second. Ten succeeded and 70 got 429s. Those retried three times at a fixed 1-second interval into a bucket that refills half a token a second, so one more got through and 69 failed for good; two invoice sends in the same seconds failed with them. The bookkeeper resent the 69 statements by hand on Monday at 2 minutes each: 2.3 hours at an assumed $30 an hour, $69.
Both automations run on a platform that stops any execution after 5 minutes, so a call that waits past that point never goes.
The question
Implement the limiter so the examples below hold. A caller that would wait longer than max_wait is refused at once, so it can be rescheduled instead of dying mid-wait.
def solve(requests, capacity, every, max_wait):
"""requests: the times callers ask to send, in seconds, non-decreasing, one call each.
capacity: tokens the bucket holds when full; it starts full. every: seconds to refill
one token. max_wait: the longest a caller will wait, in seconds.
Each granted call reserves its token, so a later caller sees the bucket after every
earlier reservation. Returns one entry per request: the time it may send, or None when
the wait would exceed max_wait; a refused request reserves nothing."""
solve([0, 0, 0, 0], 2, 2, 60) -> [0, 0, 2, 4]
solve([0, 0, 0, 10], 2, 2, 60) -> [0, 0, 2, 10]
solve([0, 0, 0, 1], 2, 2, 1) -> [0, 0, None, 2]
solve([0] * 80, 10, 2, 300) -> ten 0s, then 2, 4, 6, ..., 140
A strong answer · Rubric · Where answers go wrong · Solution
Write your answer first, then open