Skip to content

Commit 5acc368

Browse files
authored
perf: use redis for api key rate limit (anomalyco#29242)
1 parent e2dc89c commit 5acc368

2 files changed

Lines changed: 12 additions & 54 deletions

File tree

packages/console/app/src/routes/zen/util/ipRateLimiter.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
2-
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
31
import { FreeUsageLimitError } from "./error"
42
import { logger } from "./logger"
53
import { buildRateLimitKey, getRedis } from "./redis"
@@ -22,7 +20,6 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
2220

2321
const ip = !rawIp.length ? "unknown" : rawIp
2422
const now = Date.now()
25-
const lifetimeInterval = ""
2623
const dailyInterval = rateLimit ? `${buildYYYYMMDD(now)}${modelId.substring(0, 2)}` : buildYYYYMMDD(now)
2724
const retryAfter = getRetryAfterDay(now)
2825
const redis = getRedis()
@@ -32,33 +29,12 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
3229

3330
return {
3431
check: async () => {
35-
const [counts, rows] = await Promise.all([
36-
redis.mget<(string | number | null)[]>(isDefaultModel ? [lifetimeKey, dailyKey] : [dailyKey]).catch(() => []),
37-
Database.use((tx) =>
38-
tx
39-
.select({ interval: IpRateLimitTable.interval, count: IpRateLimitTable.count })
40-
.from(IpRateLimitTable)
41-
.where(
42-
and(
43-
eq(IpRateLimitTable.ip, ip),
44-
isDefaultModel
45-
? inArray(IpRateLimitTable.interval, [lifetimeInterval, dailyInterval])
46-
: inArray(IpRateLimitTable.interval, [dailyInterval]),
47-
),
48-
),
49-
),
50-
])
51-
const redisLifetimeCount = isDefaultModel ? Number(counts[0] ?? 0) : 0
52-
const redisDailyCount = Number(counts[isDefaultModel ? 1 : 0] ?? 0)
53-
const databaseLifetimeCount = rows.find((r) => r.interval === lifetimeInterval)?.count ?? 0
54-
const databaseDailyCount = rows.find((r) => r.interval === dailyInterval)?.count ?? 0
55-
const lifetimeCount = Math.max(redisLifetimeCount, databaseLifetimeCount)
56-
const dailyCount = Math.max(redisDailyCount, databaseDailyCount)
32+
const counts = await redis.mget<(string | number | null)[]>(isDefaultModel ? [lifetimeKey, dailyKey] : [dailyKey])
33+
const lifetimeCount = isDefaultModel ? Number(counts[0] ?? 0) : 0
34+
const dailyCount = Number(counts[isDefaultModel ? 1 : 0] ?? 0)
5735
logger.debug(`rate limit lifetime: ${lifetimeCount}, daily: ${dailyCount}`)
5836

5937
isNew = isDefaultModel && lifetimeCount < dailyLimit * 7
60-
if (isDefaultModel && databaseLifetimeCount > redisLifetimeCount)
61-
await redis.set(lifetimeKey, databaseLifetimeCount).catch(() => {})
6238

6339
if ((isNew && dailyCount >= dailyLimit * 2) || (!isNew && dailyCount >= dailyLimit))
6440
throw new FreeUsageLimitError(dict["zen.api.error.rateLimitExceeded"], retryAfter)
@@ -68,18 +44,7 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
6844
pipeline.incr(dailyKey)
6945
pipeline.expire(dailyKey, retryAfter)
7046
if (isNew) pipeline.incr(lifetimeKey)
71-
await Promise.all([
72-
pipeline.exec().catch(() => {}),
73-
Database.use((tx) =>
74-
tx
75-
.insert(IpRateLimitTable)
76-
.values([
77-
{ ip, interval: dailyInterval, count: 1 },
78-
...(isNew ? [{ ip, interval: lifetimeInterval, count: 1 }] : []),
79-
])
80-
.onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
81-
),
82-
])
47+
await pipeline.exec()
8348
},
8449
}
8550
}
Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { Database, eq, and, sql } from "@opencode-ai/console-core/drizzle/index.js"
2-
import { KeyRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
31
import { RateLimitError } from "./error"
2+
import { buildRateLimitKey, getRedis } from "./redis"
43
import { i18n } from "~/i18n"
54
import { localeFromRequest } from "~/lib/language"
65

@@ -19,26 +18,20 @@ export function createRateLimiter(
1918
.replace(/[^0-9]/g, "")
2019
.substring(0, 12)
2120
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
21+
const redis = getRedis()
22+
const key = buildRateLimitKey("key", zenApiKey, interval)
2223

2324
return {
2425
check: async () => {
25-
const rows = await Database.use((tx) =>
26-
tx
27-
.select({ interval: KeyRateLimitTable.interval, count: KeyRateLimitTable.count })
28-
.from(KeyRateLimitTable)
29-
.where(and(eq(KeyRateLimitTable.key, zenApiKey), eq(KeyRateLimitTable.interval, interval))),
30-
).then((rows) => rows[0])
31-
const count = rows?.count ?? 0
26+
const count = Number((await redis.mget<(string | number | null)[]>([key]))[0] ?? 0)
3227

3328
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
3429
},
3530
track: async () => {
36-
await Database.use((tx) =>
37-
tx
38-
.insert(KeyRateLimitTable)
39-
.values({ key: zenApiKey, interval, count: 1 })
40-
.onDuplicateKeyUpdate({ set: { count: sql`${KeyRateLimitTable.count} + 1` } }),
41-
)
31+
const pipeline = redis.pipeline()
32+
pipeline.incr(key)
33+
pipeline.expire(key, 60)
34+
await pipeline.exec()
4235
},
4336
}
4437
}

0 commit comments

Comments
 (0)