ops(会话): 加一次性脚本清掉全部会话,闭掉存量会话吊销不掉的空窗

反向索引 user-sessions:<uid> 是 498fc1c 才加的,在那之前签发的会话不在索引里,
revokeUserSessions 靠 SMEMBERS 找不到它们 —— 改密码、重置密码、禁用账号对这批会话
统统无效,只能等最长一个 SESSION_TTL_SECONDS(默认 7 天)自然过期。学生密码是明文
存着给老师查的,改密码正是密码泄露后唯一的补救手段,这个空窗不留。代价是所有人重新
登录一次。跑过一次就不用再跑,此后签发的会话都带索引。

**两个站点都要跑。** 机房和服务器共用一个数据库,但各有各的 Redis,会话存在各自的
Redis 里,只清一边等于只解决一半。脚本结尾会提醒这件事。

只删 session:* 和 user-sessions:*,不用 FLUSHALL:同一个 Redis 里还装着 BullMQ 的
判题队列,清掉等于把还在队列里的提交全丢了,那些 submission 会永远停在 PENDING。
删完会核对 bull:* 的键数没变,变了就报错让人工检查。

扫描用 SCAN 不用 KEYS(KEYS 会阻塞住整个 Redis,而这上面还挂着所有人的会话读写),
xargs 分批 DEL 避免顶到命令行长度上限。

拿一次性容器验过:1200 条 session + 50 条索引全删(跨过分批边界),30 个 bull 键和
throttling 桶原样不动;空库上重跑是幂等的;容器名写错时报错退出不做任何事。

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:45:47 -06:00
parent 57bc652629
commit 01d7924faa

78
docker/clear-sessions.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
#
# 一次性运维动作:清掉所有会话,强制全员重新登录。
#
# ## 为什么要跑
#
# 会话的反向索引 `user-sessions:<uid>` 是 498fc1c 才加的。在那之前签发的会话不在索引
# 里revokeUserSessions 靠 SMEMBERS 找不到它们 —— 也就是说**改密码、重置密码、禁用
# 账号对这批会话统统无效**,只能等最长一个 SESSION_TTL_SECONDS默认 7 天)自然过期。
#
# 学生密码是明文存着给老师查的,改密码正是密码泄露之后唯一的补救手段,这个空窗不能留。
# 代价只是所有人重新登录一次。
#
# 跑过一次就不用再跑了:此后签发的会话都带索引。
#
# ## 两个站点都要跑
#
# 机房和服务器**共用一个数据库,但各有各的 Redis**(见 compose.school.yml 头部)。
# 会话存在各自的 Redis 里,只清一边等于只解决一半。
#
# ## 为什么不是 FLUSHALL
#
# 同一个 Redis 里还装着 BullMQ 的判题队列(`bull:*`。FLUSHALL 会把还在队列里的提交
# 一起丢掉,那些 submission 会永远停在 PENDING只能超管逐条重判 —— 而重判还会把
# user_profile 的反范式计数带偏(见 apps/api/src/scripts/recount.ts
# 这里只删 `session:*` 和 `user-sessions:*`。
#
# ## 用法
#
# docker/clear-sessions.sh # 容器名默认 oj-redis
# CONTAINER=oj2-redis docker/clear-sessions.sh # 本机 dev
# YES=1 docker/clear-sessions.sh # 跳过确认
#
set -euo pipefail
CONTAINER="${CONTAINER:-oj-redis}"
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$*" >&2; exit 1; }
docker exec "$CONTAINER" redis-cli ping >/dev/null 2>&1 \
|| die "连不上容器 $CONTAINER 里的 redis用 CONTAINER=... 指定容器名)"
# 用 SCAN 而不是 KEYSKEYS 会阻塞住整个 Redis而这上面还挂着判题队列和所有人的
# 会话读写。xargs 分批是因为单条 DEL 传太多 key 会顶到命令行长度上限;每批 DEL 返回
# 删掉的个数,累加起来就是总数。
purge() {
docker exec "$CONTAINER" redis-cli --scan --pattern "$1" 2>/dev/null \
| xargs -r -n 400 docker exec "$CONTAINER" redis-cli del \
| awk '{ sum += $1 } END { print sum + 0 }'
}
count() { docker exec "$CONTAINER" redis-cli --scan --pattern "$1" 2>/dev/null | wc -l; }
say "容器 $CONTAINER"
before_sessions=$(count 'session:*')
before_index=$(count 'user-sessions:*')
before_bull=$(count 'bull:*')
printf ' session:* %s\n' "$before_sessions"
printf ' user-sessions:* %s\n' "$before_index"
printf ' bull:* %s不动\n' "$before_bull"
if [ "${YES:-}" != "1" ]; then
read -rp $'\n 删掉上面的会话、让所有人重新登录?[y/N] ' answer
[ "$answer" = "y" ] || [ "$answer" = "Y" ] || die "已取消,什么都没做"
fi
say "清理"
ok "session:* 删掉 $(purge 'session:*')"
ok "user-sessions:* 删掉 $(purge 'user-sessions:*')"
after_bull=$(count 'bull:*')
[ "$after_bull" = "$before_bull" ] \
|| die "判题队列的键数变了($before_bull$after_bull),这不该发生,请人工检查"
ok "bull:* 仍是 $after_bull 个,判题队列没被动过"
say "完成 —— 另一个站点的 Redis 也要跑一遍"