feat(sites): cross-process site rate limit via PG token bucket

Migration 0041_rate_limit adds rate_limit_bucket. PgTokenBucket (@umt/db)
refills and takes tokens under SELECT ... FOR UPDATE on the DB clock and
polls with jitter within a wait budget; penalize() carries Retry-After.
Site adapters get createSharedSiteRateLimiter (key site:<id>), shared by
api / worker / worker:agent, with an in-process serial queue in front and
a local fallback when the bucket is unavailable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 21:51:32 +08:00
1 parent 57346d438c
commit 61f585a707
12 files changed
+466 -5

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
import type { Kysely } from "kysely";
import { dropTables, execDdl } from "../src/migration-helpers.ts";
// C2-A 跨进程站点限流:PG 令牌桶(api / worker / worker:agent 共用)。
// - key:`site:<站点 id>` 等;tokens 为上次结算时的令牌数(可为负,表示被 Retry-After 顺延);
// - capacity / refill_per_sec 由调用方每次扣令牌时带上(站点限流设置变化即时生效);
// - blocked_until:站点要求暂停(429 Retry-After)时在此之前不发放令牌。
// 扣令牌走 `select … for update`,补充按 DB 时钟(clock_timestamp)结算,多进程时钟不一致也不影响。
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
create table rate_limit_bucket (
key text primary key,
tokens double precision not null,
capacity double precision not null check (capacity > 0),
refill_per_sec double precision not null check (refill_per_sec > 0),
blocked_until timestamptz,
updated_at timestamptz not null default now()
);
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, dropTables(["rate_limit_bucket"]));
}