-
-
Notifications
You must be signed in to change notification settings - Fork 184
fix: bound SessionTracker active_sessions zsets by env TTL #718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,12 @@ import { getRedisClient } from "./redis"; | |
| * - user:${userId}:active_sessions (ZSET): 同上 | ||
| */ | ||
| export class SessionTracker { | ||
| private static readonly SESSION_TTL = 300000; // 5 分钟(毫秒) | ||
| private static readonly SESSION_TTL_SECONDS = (() => { | ||
| const parsed = Number.parseInt(process.env.SESSION_TTL ?? "", 10); | ||
| return Number.isFinite(parsed) && parsed > 0 ? parsed : 300; | ||
| })(); | ||
| private static readonly SESSION_TTL_MS = SessionTracker.SESSION_TTL_SECONDS * 1000; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [CRITICAL] [TYPE-MISSING-VALIDATION] Why this is a problem: In
If Suggested fix: private static readonly SESSION_TTL_SECONDS = (() => {
const ttl = Number(process.env.SESSION_TTL);
if (!Number.isFinite(ttl) || ttl <= 0) return 300;
return Math.floor(ttl);
})();
private static readonly SESSION_TTL_MS = SessionTracker.SESSION_TTL_SECONDS * 1000; |
||
| private static readonly CLEANUP_PROBABILITY = 0.01; | ||
|
|
||
| /** | ||
| * 初始化 SessionTracker,自动清理旧格式数据 | ||
|
|
@@ -174,26 +179,26 @@ export class SessionTracker { | |
| try { | ||
| const now = Date.now(); | ||
| const pipeline = redis.pipeline(); | ||
| const ttlSeconds = SessionTracker.SESSION_TTL_SECONDS; | ||
| const providerZSetKey = `provider:${providerId}:active_sessions`; | ||
|
|
||
| // 更新所有相关 ZSET 的时间戳(滑动窗口) | ||
| pipeline.zadd("global:active_sessions", now, sessionId); | ||
| pipeline.zadd(`key:${keyId}:active_sessions`, now, sessionId); | ||
| pipeline.zadd(`provider:${providerId}:active_sessions`, now, sessionId); | ||
| pipeline.zadd(providerZSetKey, now, sessionId); | ||
| // Use dynamic TTL based on session TTL (at least 1h to cover active sessions) | ||
| pipeline.expire(providerZSetKey, Math.max(3600, ttlSeconds)); | ||
| if (userId !== undefined) { | ||
| pipeline.zadd(`user:${userId}:active_sessions`, now, sessionId); | ||
| } | ||
|
|
||
| // 修复 Bug:同步刷新 session 绑定信息的 TTL | ||
| // | ||
| // 问题:ZSET 条目(上面 zadd)会在每次请求时更新时间戳,但绑定信息 key 的 TTL 不会自动刷新 | ||
| // 导致:session 创建 5 分钟后,ZSET 仍有记录(仍被计为活跃),但绑定信息已过期,造成: | ||
| // 1. 并发检查被绕过(无法从绑定信息查询 session 所属 provider/key,检查失效) | ||
| // 2. Session 复用失败(无法确定 session 绑定关系,被迫创建新 session) | ||
| // | ||
| // 解决:每次 refreshSession 时同步刷新绑定信息 TTL(与 ZSET 保持 5 分钟生命周期一致) | ||
| pipeline.expire(`session:${sessionId}:provider`, 300); // 5 分钟(秒) | ||
| pipeline.expire(`session:${sessionId}:key`, 300); | ||
| pipeline.setex(`session:${sessionId}:last_seen`, 300, now.toString()); | ||
| pipeline.expire(`session:${sessionId}:provider`, ttlSeconds); | ||
| pipeline.expire(`session:${sessionId}:key`, ttlSeconds); | ||
| pipeline.setex(`session:${sessionId}:last_seen`, ttlSeconds, now.toString()); | ||
|
|
||
| if (Math.random() < SessionTracker.CLEANUP_PROBABILITY) { | ||
| const cutoffMs = now - SessionTracker.SESSION_TTL_MS; | ||
| pipeline.zremrangebyscore(providerZSetKey, "-inf", cutoffMs); | ||
| } | ||
|
Comment on lines
+198
to
+201
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probabilistic cleanup of the
This redundancy adds complexity and a small performance overhead on the write path. To simplify the logic, you could remove this probabilistic cleanup and rely on the other two mechanisms. |
||
|
|
||
| const results = await pipeline.exec(); | ||
|
|
||
|
|
@@ -374,14 +379,14 @@ export class SessionTracker { | |
|
|
||
| try { | ||
| const now = Date.now(); | ||
| const fiveMinutesAgo = now - SessionTracker.SESSION_TTL; | ||
| const cutoffMs = now - SessionTracker.SESSION_TTL_MS; | ||
|
|
||
| // 第一阶段:批量清理过期 session 并获取 session IDs | ||
| const cleanupPipeline = redis.pipeline(); | ||
| for (const providerId of providerIds) { | ||
| const key = `provider:${providerId}:active_sessions`; | ||
| // 清理过期 session | ||
| cleanupPipeline.zremrangebyscore(key, "-inf", fiveMinutesAgo); | ||
| cleanupPipeline.zremrangebyscore(key, "-inf", cutoffMs); | ||
| // 获取剩余 session IDs | ||
| cleanupPipeline.zrange(key, 0, -1); | ||
| } | ||
|
|
@@ -480,10 +485,10 @@ export class SessionTracker { | |
| } | ||
|
|
||
| const now = Date.now(); | ||
| const fiveMinutesAgo = now - SessionTracker.SESSION_TTL; | ||
| const cutoffMs = now - SessionTracker.SESSION_TTL_MS; | ||
|
|
||
| // 清理过期 session | ||
| await redis.zremrangebyscore(key, "-inf", fiveMinutesAgo); | ||
| await redis.zremrangebyscore(key, "-inf", cutoffMs); | ||
|
|
||
| // 获取剩余的 session ID | ||
| return await redis.zrange(key, 0, -1); | ||
|
|
@@ -514,10 +519,10 @@ export class SessionTracker { | |
|
|
||
| try { | ||
| const now = Date.now(); | ||
| const fiveMinutesAgo = now - SessionTracker.SESSION_TTL; | ||
| const cutoffMs = now - SessionTracker.SESSION_TTL_MS; | ||
|
|
||
| // 1. 清理过期 session(5 分钟前) | ||
| await redis.zremrangebyscore(key, "-inf", fiveMinutesAgo); | ||
| await redis.zremrangebyscore(key, "-inf", cutoffMs); | ||
|
|
||
| // 2. 获取剩余的 session ID | ||
| const sessionIds = await redis.zrange(key, 0, -1); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[CRITICAL] [LOGIC-BUG] Lua
ttlMsneeds clamping to avoid clearing the ZSET (limit bypass)Why this is a problem: In
src/lib/redis/lua-scripts.ts:32-36the script trustsARGV[4]:local ttl = tonumber(ARGV[4]) or 300000When
ttlis0/negative,cutoff = now - ttlbecomes>= now, andZREMRANGEBYSCORE ... -inf cutoffcan remove all existing sessions, making the concurrency check under-count and effectively bypass limits.Suggested fix: