Files
order_site/apps/backend/src/middleware/rate-limit.test.ts
T
yml2213 1ca15e9025 perf(db): 统计金额落到列上、会话刷新节流、补齐保留与内存治理
背景:生产 32 天内 >500ms 慢查询从 2.4k/天涨到 1.3 万/天(09-26 尖峰 14.3 万),
pg_stat_statements 显示后台工单列表族占 57%、打手端 visible_orders 占 27%;
同时 backend 容器十天从 194MB 涨到 754MB(768MB 限额的 98%),贴顶后整体变慢。

1. 新增迁移 083:work_orders 增加派生列 source_order_amount_fen,并从
   material_json.source.orderAmountFen 回填(只认 15 位以内整数,与 JS/SQL 口径一致)。
   统计查询原来每行要解析 1.2KB JSON(全表约 132MB)且同一表达式求值两次,
   生产实测 3.5~4.4 秒;改走派生列后 COALESCE 短路,未命中列的行才回退 JSON/orders.total_amount,
   口径与改造前完全一致。写入点:createWorkOrder / updateWorkOrder 各一处,并加静态守卫。
2. worker_sessions.last_seen_at 从"每个请求都写"改为"5 分钟才写一次":
   32 天被写了 3300 万次(占 DB 总时间 2.8%,并堆出 14% 死元组)。仍用一条语句返回鉴权有效性
   (EXISTS target AS valid),不能因为不写就让上层 401。
3. 保留策略补上 work_order_events(71.7 万行 / 269MB,此前完全没纳入清理,只增不减),
   新增配置 retention.workOrderEventDays(默认 180 天)与 WORK_ORDER_EVENT_RETENTION_DAYS 出口。
4. 内存治理:compose 后端限额 768m→1g 并显式设置 V8 老生代上限(--max-old-space-size=768),
   避免贴 cgroup 上限后 GC 抖动;新增每 10 分钟的进程内存快照日志(rss/heapUsed/heapLimit),
   用于区分"正常 GC 曲线"与"真泄漏"。
5. 限流桶加 5 万条硬上限:key 里含请求体字段(collect 的 orderNo)时可被构造成高基数,
   只按过期清理挡不住内存增长。

验证:后端 530 项单测全绿(新增 11 项,含派生列/会话/保留的静态守卫与解析函数单测)。
2026-10-01 12:55:09 +08:00

147 lines
3.7 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import type { NextFunction, Request, Response } from 'express'
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
test('createRateLimitMiddleware allows requests within the window', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:allow',
windowMs: 60_000,
max: 2,
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1
}
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 2)
assert.equal(res.statusCode, 200)
})
test('createRateLimitMiddleware blocks requests over the limit', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:block',
windowMs: 60_000,
max: 1,
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1
}
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 1)
assert.equal(res.statusCode, 429)
assert.equal(res.headers['Retry-After'], '60')
assert.equal(res.body.errorCode, 'rate_limited')
})
test('createRateLimitMiddleware supports custom limit responses', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:custom',
windowMs: 60_000,
max: 1,
onLimit: (_req, res) => {
res.status(200).json({ code: 429, message: 'limited' })
},
})
const req = createMockRequest()
const res = createMockResponse()
limiter(req, res, () => {})
limiter(req, res, () => {})
assert.equal(res.statusCode, 200)
assert.deepEqual(res.body, { code: 429, message: 'limited' })
})
test('createRateLimitMiddleware can cap an IP across different accounts', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:ip-wide',
windowMs: 60_000,
max: 2,
})
const first = createMockRequest()
const second = createMockRequest()
const third = createMockRequest()
const response = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1
}
limiter(first, response, next)
limiter(second, response, next)
limiter(third, response, next)
assert.equal(nextCount, 2)
assert.equal(response.statusCode, 429)
})
function createMockRequest(): Request {
return {
ip: '127.0.0.1',
originalUrl: '/test',
headers: {},
socket: {
remoteAddress: '127.0.0.1',
},
} as Request
}
function createMockResponse(): Response & {
statusCode: number
headers: Record<string, string>
body: any
} {
const res = {
statusCode: 200,
headers: {} as Record<string, string>,
body: null as any,
setHeader(name: string, value: string) {
this.headers[name] = value
return this
},
status(statusCode: number) {
this.statusCode = statusCode
return this
},
json(body: any) {
this.body = body
return this
},
}
return res as Response & {
statusCode: number
headers: Record<string, string>
body: any
}
}
test('限流桶有数量硬上限:key 含用户可控字段时不能无限膨胀', async () => {
const { readFile } = await import('node:fs/promises')
const src = await readFile(new URL('./rate-limit.ts', import.meta.url), 'utf8')
// collect 路由的限流 key 里带请求体 orderNo(可被构造),只按过期清理挡不住高基数灌键
assert.ok(src.includes('MAX_RATE_LIMIT_BUCKETS'), '缺少限流桶数量上限')
assert.ok(src.includes('buckets.size > MAX_RATE_LIMIT_BUCKETS'), '必须在清理时按上限淘汰最老的桶')
})