perf(限流): 桶参数加 60 秒进程内缓存;Redis 连接补 error 监听

getBucketConfig 每次都查一次 `throttling` 配置项,而限流点在提交判题、AI 分析、
流程图评分上(5 处调用)—— 判题高峰期等于每条提交多一趟数据库,只为读一个几乎
从不变的值。上一代在 options/options.py 的 my_property 里也是带 TTL 缓存的,
重写时漏掉了。

缓存放进程内而不是 Redis:每站只有一个 api 进程服务读请求(oj-api 单容器、
Bun.serve 没有 reusePort、worker 只消费队列),进程内 Map 就等于全站缓存,
放 Redis 只是多一趟网络加一次序列化。异常分支特意不写缓存 —— 数据库抖一下不该
让接下来一整分钟全站都按默认参数限流。`throttling` 没有后台界面、只能直接改库,
改完最多一分钟后生效。

顺带给三条 Redis 连接都挂上 error 监听。ioredis 对没有监听者的 error 走
silentEmit:不崩进程,但把连接错误直接 console.error 到 stderr,绕开这里的日志,
而且不说是哪条连接 —— 这个进程同时开着会话读写、两条队列、一条订阅,「哪条」正是
要先知道的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
This commit is contained in:
2026-09-07 07:41:09 -06:00
parent 22a7700b89
commit ec1509c46d
2 changed files with 55 additions and 11 deletions

View File

@@ -2,18 +2,40 @@ import Redis from "ioredis"
import { config } from "./config"
export const redis = new Redis(config.redisUrl, {
maxRetriesPerRequest: 1,
})
/**
* 每条连接都要挂 error 监听。
*
* ioredis 对没有监听者的 error 走 silentEmit —— 不会像普通 EventEmitter 那样崩进程,
* 但会把连接错误直接 `console.error("[ioredis] Unhandled error event:", ...)` 打到
* stderr绕开这里的日志而且不说是哪条连接出的事。这个进程同时开着会话读写、
* 两条队列、一条订阅,出问题时「哪条」正是要先知道的。
*/
function withErrorLogging(client: Redis, name: string) {
client.on("error", (error) => {
console.error(`Redis connection error (${name})`, error)
})
return client
}
/**
* 会话、限流、发布事件都走这条。`maxRetriesPerRequest: 1` 是故意的:每个带鉴权的
* 请求都要读一次会话Redis 不可用时快速失败成 500比让请求挂在重试里更好。
*/
export const redis = withErrorLogging(
new Redis(config.redisUrl, { maxRetriesPerRequest: 1 }),
"main",
)
export function createBlockingRedis() {
return new Redis(config.redisUrl, {
maxRetriesPerRequest: null,
})
return withErrorLogging(
new Redis(config.redisUrl, { maxRetriesPerRequest: null }),
"blocking",
)
}
export function createSubscriberRedis() {
return new Redis(config.redisUrl, {
maxRetriesPerRequest: null,
})
return withErrorLogging(
new Redis(config.redisUrl, { maxRetriesPerRequest: null }),
"subscriber",
)
}