Skip to content

Commit

Permalink
Feat/custom rates support (#79)
Browse files Browse the repository at this point in the history
* feat: added custom rates(WIP)

* feat: custom rate limiting in fixed window algorithm

* feat: added custom rates

* fix: implement only one custom rate

* fix: revert example

* ref: refactor custom rate

* feat: usage doc

* ref: refactor custom rate

* ref: refactored variable and payload names

* ref: refactored variable and payload names

* ref: refactored variable and payload names

* feat: add payload limit for sliding window

* fix: script variable and comment

* feat: create lua script files

* fix: missing imports and exports

* fix: removed payload limit from the interface

* fix: changed variable name

* chore: cleanup

* feat: add custom rate to the tests

* chore: updated doc

* chore: formatted

* fix: rm redundant params

* feat: added rate param and usage with example

* chore: formatted

* feat: add custom rate to cached fixed window algorithm

* feat: add cached fixed window test case

* feat: add custom rate

* test: add custom rate test

* ref: param names and variable names

* chore: add log

* feat: added new cmd

* feat: custom rate support for multi region

* test: multi region test updated

* chore: cleanup
  • Loading branch information
sourabpramanik authored Mar 8, 2024
1 parent 248d82d commit f901874
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 189 deletions.
23 changes: 16 additions & 7 deletions examples/with-vercel-kv/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Ratelimit } from "../../../src";
import kv from "@vercel/kv";
import { Inter } from "next/font/google";
import { headers } from "next/headers";
Expand All @@ -7,12 +7,17 @@ import Link from "next/link";

const ratelimit = new Ratelimit({
redis: kv,
limiter: Ratelimit.fixedWindow(10, "60s"),
limiter: Ratelimit.fixedWindow(10, "30s"),
});

export default async function Home() {
const ip = headers().get("x-forwarded-for");
const { success, limit, remaining, reset } = await ratelimit.limit(ip ?? "anonymous");
const {
success,
limit,
remaining,
reset,
} = await ratelimit.limit(ip ?? "anonymous011");

return (
<main className="flex flex-col items-center justify-between min-h-screen p-24">
Expand Down Expand Up @@ -48,22 +53,26 @@ export default async function Home() {
<div className="grid mb-32 text-center lg:mb-0 lg:grid-cols-4 lg:text-left">
<div className="px-5 py-4 transition-colors border border-transparent rounded-lg group hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30">
<h2 className={"mb-3 text-2xl font-semibold"}>Success</h2>
<p className={"m-0 max-w-[30ch] text-sm opacity-50"}>{success.toString()}</p>
<p className={"m-0 max-w-[30ch] text-sm opacity-50"}>
{success.toString()}
</p>
</div>

<div className="px-5 py-4 transition-colors border border-transparent rounded-lg group hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800 hover:dark:bg-opacity-30">
<h2 className={"mb-3 text-2xl font-semibold"}>Limit </h2>
<p className={"m-0 max-w-[30ch] text-sm opacity-50"}>{limit}</p>
<p className={"m-0 max-w-[30ch] text-sm"}>{limit}</p>
</div>

<div className="px-5 py-4 transition-colors border border-transparent rounded-lg group hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30">
<h2 className={"mb-3 text-2xl font-semibold"}>Remaining</h2>
<h2 className={"mb-3 text-2xl font-semibold"}>Remaining </h2>
<p className={"m-0 max-w-[30ch] text-sm opacity-50"}>{remaining}</p>
</div>

<div className="px-5 py-4 transition-colors border border-transparent rounded-lg group hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30">
<h2 className={"mb-3 text-2xl font-semibold"}>Reset</h2>
<p className={"m-0 max-w-[30ch] text-sm opacity-50"}>{new Date(reset).toUTCString()}</p>
<p className={"m-0 max-w-[30ch] text-sm opacity-50"}>
{new Date(reset).toUTCString()}
</p>
</div>
</div>
</main>
Expand Down
101 changes: 101 additions & 0 deletions src/lua-scripts/single.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
export const fixedWindowScript = `
local key = KEYS[1]
local window = ARGV[1]
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
local r = redis.call("INCRBY", key, incrementBy)
if r == incrementBy then
-- The first time this key is set, the value will be equal to incrementBy.
-- So we only need the expire command once
redis.call("PEXPIRE", key, window)
end
return r`;

export const slidingWindowScript = `
local currentKey = KEYS[1] -- identifier including prefixes
local previousKey = KEYS[2] -- key of the previous bucket
local tokens = tonumber(ARGV[1]) -- tokens per window
local now = ARGV[2] -- current timestamp in milliseconds
local window = ARGV[3] -- interval in milliseconds
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1
local requestsInCurrentWindow = redis.call("GET", currentKey)
if requestsInCurrentWindow == false then
requestsInCurrentWindow = 0
end
local requestsInPreviousWindow = redis.call("GET", previousKey)
if requestsInPreviousWindow == false then
requestsInPreviousWindow = 0
end
local percentageInCurrent = ( now % window ) / window
-- weighted requests to consider from the previous window
requestsInPreviousWindow = math.floor(( incrementBy - percentageInCurrent ) * requestsInPreviousWindow)
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then
return -1
end
local newValue = redis.call("INCRBY", currentKey, incrementBy)
if newValue == incrementBy then
-- The first time this key is set, the value will be equal to incrementBy.
-- So we only need the expire command once
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second
end
return tokens - ( newValue + requestsInPreviousWindow )
`;

export const tokenBucketScript = `
local key = KEYS[1] -- identifier including prefixes
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1
local bucket = redis.call("HMGET", key, "refilledAt", "tokens")
local refilledAt
local tokens
if bucket[1] == false then
refilledAt = now
tokens = maxTokens
else
refilledAt = tonumber(bucket[1])
tokens = tonumber(bucket[2])
end
if now >= refilledAt + interval then
local numRefills = math.floor((now - refilledAt) / interval)
tokens = math.min(maxTokens, tokens + numRefills * refillRate)
refilledAt = refilledAt + numRefills * interval
end
if tokens == 0 then
return {-1, refilledAt + interval}
end
local remaining = tokens - incrementBy
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining)
redis.call("PEXPIRE", key, expireAt)
return {remaining, refilledAt + interval}
`;

export const cachedFixedWindowScript = `
local key = KEYS[1]
local window = ARGV[1]
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1
local r = redis.call("INCRBY", key, incrementBy)
if r == incrementBy then
-- The first time this key is set, the value will be equal to incrementBy.
-- So we only need the expire command once
redis.call("PEXPIRE", key, window)
end
return r
`;
Loading

0 comments on commit f901874

Please sign in to comment.