fix(限流): 用 ORDER BY ts 取窗口内最早一条,不靠 MIN(ts) —— 429 的 retry_after 恒为 60
## 缺陷 `SELECT MIN(ts) FROM rate_limits ...` 的聚合结果被 SQLite 驱动按 **string** 返回,扫进 `*time.Time` 失败 ⇒ 落到兜底 `return false, 60`。 ⇒ 所有 429 的 `retry_after` 恒为 60,与真实剩余窗口 (最长 `sessionRateWindow` = 1h)完全无关。调用方拿到的重试提示是错的: 限流窗口还有 55 分钟,它却说 60 秒后重试。 ## 修法 `SELECT ts FROM rate_limits WHERE bucket = $1 AND ts >= $2 ORDER BY ts ASC LIMIT 1` 排序取值走**结果集本身**,驱动按列类型给 `time.Time`;语义等价。 ★ 同一形状的坑今天已出现两次:上午 2h 冷静期因 UTC vs HKT 差 8 小时而形同虚设, 晚上权限记账因两处 `if` 守卫而静默失效。**根子都是「SQLite 侧的时间/类型处理 与直觉不符」,而症状在别处。** ## 判据 4 格(retry_after 反映真实窗口 / 绝不超过 window / 窗口滚动后放行 / 只数窗口内的记录),其中主判据显式对比「修复前 60,修复后 ≈window」。 本改动此前已随 2026-10-01 的两次部署进入线上二进制(vcs.modified=true), 本次补提交以让 provenance 对得上。
This commit is contained in:
@ -35,9 +35,17 @@ func (l *LoginLimiter) Locked(ctx context.Context, name string) (bool, int) {
|
||||
}
|
||||
|
||||
// 找到最早那条记录 + lockoutDuration = 解锁时间
|
||||
//
|
||||
// 用 ORDER BY ts ASC LIMIT 1 而不是 MIN(ts):SQLite 驱动把聚合结果
|
||||
// MIN(ts) 当 string 返回,扫进 *time.Time 直接报错(unsupported Scan,
|
||||
// storing driver.Value type string into type *time.Time),err != nil
|
||||
// 会让下面直接 return false, 0 —— 也就是「失败次数再多也永远不锁」。
|
||||
// 排序取值走的是结果集本身,驱动按列类型给 time.Time;语义等价。
|
||||
// 同一处方言差异在 db/migrate.go:125 用 CAST(... AS TEXT) 处理过。
|
||||
var earliest time.Time
|
||||
err = db.DB.QueryRowContext(ctx,
|
||||
`SELECT MIN(ts) FROM rate_limits WHERE bucket = $1 AND ts >= $2`,
|
||||
`SELECT ts FROM rate_limits WHERE bucket = $1 AND ts >= $2
|
||||
ORDER BY ts ASC LIMIT 1`,
|
||||
bucket, cutoff).Scan(&earliest)
|
||||
if err != nil || earliest.IsZero() {
|
||||
return false, 0
|
||||
|
||||
Reference in New Issue
Block a user