Reject token-bucket requests that exceed the remaining tokens - #156
Open
eeshsaxena wants to merge 1 commit into
Open
Reject token-bucket requests that exceed the remaining tokens#156eeshsaxena wants to merge 1 commit into
eeshsaxena wants to merge 1 commit into
Conversation
The token bucket rejected only when the stored token count was exactly 0 (`tokens == 0 and incrementBy > 0`). That covers the default rate of 1, but a larger rate consuming more than is available fell through: with 2 tokens left a `rate: 5` request set tokens to -3, wrote it back, and reported failure. So a rejected request still consumed tokens, and a single large rate drove the count far negative, locking the identifier out long after the rejected call (the PEXPIRE is derived from the deficit, e.g. a rate of 1e6 sets a ~115-day TTL). Reject when incrementBy > 0 and tokens < incrementBy, before the decrement, so the bucket is left untouched. For incrementBy == 1 this is identical to the old tokens == 0 check (tokens are integers), and exact-fit requests still pass. The script hash is recomputed to match, and a regression test covers a rate larger than the remaining tokens.
Contributor
|
@eeshsaxena is attempting to deploy a commit to the Upstash Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The token bucket only rejects a request when the stored token count is exactly
0:That is correct for the default rate of 1, but
.limit(id, { rate })lets a single call consume more than one token. When the remaining tokens are between 1 andrate - 1, the guard does not fire and the code falls through to:So:
rate: 5returnssuccess: falsebut leaves the bucket at-3instead of2. The two tokens the caller was entitled to are gone.rate: 1_000_000against a near-empty bucket stores~ -1e6.expireAt = ceil((limit - remaining) / refillRate) * intervalis derived from that deficit, so the key is pinned with a very long TTL (a10s/refill-1 bucket ends up with a ~115-day PEXPIRE), and the identifier stays blocked long after the rejected call because the bucket has to climb back from deep negative.The existing
refund when over limittest only exercises thetokens == 0path (bucket exactly empty), so the partial-bucket case slipped through.The fix
Reject before the decrement whenever a consuming request asks for more than is available:
incrementBy == 1,tokens < 1is exactlytokens == 0(tokens are integers), so the default path is unchanged.tokens == incrementBy) still succeed.incrementBy < 0) are unaffected.The
EVALSHAhash for the script inhash.tsis recomputed to match the new source, and a regression test covers a rate larger than the remaining tokens (rejected, bucket left intact, remaining tokens still spendable).Verifying
I ported the script's arithmetic to check the before/after (no Redis needed):
rate 5-3, reported failure (tokens consumed)2rate 1e6-999998, ~115-day TTL2rate 100(unchanged)rate 1rate 1000(exact fit still passes)CJS/ESM builds pass; the added test follows the existing
negative-rate.test.tslayout. (The repo's DTS build trips on a pre-existinganalytics.ts@ts-expect-errorunrelated to this change.)