自动重登修两处:并发去重读错缓存、只读订单查询掉登录被静默吞掉
- try_relogin 的并发去重原本读 status().logged_in,但重登成功后的 reload() 会把它重置成 None,排队在 site 锁上的调用方一律判「还没人登上」,N 个并发 调用会串行触发 N 次真实登录(各自最长 relogin_timeout)。改用 _relogin_epochs 计数:等锁期间 epoch 变过就真探测一次,已登录即跳过;仍未登录说明这轮站点侧 就是登不上(验证码/密码错/风控),直接失败,不在同一波并发里重复触发。 原并发测试的桩自相矛盾(login_one 返回成功、探针页始终回未登录),断言只能 松到 count >= 1;桩改为登录成功时翻转探针页,断言收紧到 count == 1。 - check_order_status / list_recent_orders 执行中掉登录此前会被静默吞掉:订单页 被踢到 SSO 后既不报错也没订单号,_parse_order_status 返回 found=False 被 _monitor_order 当成「订单还没反映出来」继续轮询(默认 3 小时一轮), _parse_order_list 则退化成空列表让 verify_on_site 转 unknown 卡住等人工。 新增 SiteInteractor._read_with_relogin_retry 外壳:只读操作中途判定掉登录时 重登一次并整个重跑,第二次仍失败抛 NotLoggedInError。只给读操作用——写操作 中途掉登录不能重跑(上次动作可能已在站点侧生效),这条边界在两边文档里写明。 - 判据是新增的 auth_site.looks_logged_out:与探针页上权威的 is_logged_in 分开, 它是业务页上的单边启发式(返回 False 不代表登录着),只用于「判错最多多花一次 重登」的重试决策。刻意排除 session/upgrade——那是已登录时的站点风控复核密码, 不是 cookie 过期,误判会把风控当掉登录去重登。 判据里「掉登录会跳到 SSO 域」这一步没有真实探测证据(要复现得先让一份真实登录态 过期),是按站点通行行为的推断,已在常量注释标注;新增测试用替身页面,不是真实 站点 HTML。399 测试全绿(仓库未配 ruff/flake8/mypy,只跑了 pytest)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,11 @@ class AuthSession:
|
||||
self._sites: dict[str, _SiteAuth] = {}
|
||||
# 自动重登的 site 级互斥锁:同账号同时只能一个登录流程(user_data_dir 被锁)
|
||||
self._relogin_locks: dict[str, asyncio.Lock] = {}
|
||||
# 每个 site 已完成的重登尝试计数。用途只有一个:让在锁上排队的调用方能分辨
|
||||
# 「我等的这段时间里已经有人替我登过了」。不能用 status().logged_in 代替——
|
||||
# 重登成功后的 reload() 会把 logged_in 重置成 None(未探测),排队者读到
|
||||
# None 会误判成「还没人登过」,于是 N 个并发调用串行触发 N 次真实登录。
|
||||
self._relogin_epochs: dict[str, int] = {}
|
||||
|
||||
# ---- 生命周期 ----
|
||||
|
||||
@@ -211,8 +216,11 @@ class AuthSession:
|
||||
2. 重登成功 → 重新 check 一次,登录态转好即放行
|
||||
3. 重登失败 / 未启用 / account.yaml 缺失 → 抛 NotLoggedInError 让 worker 转 needs_human
|
||||
|
||||
重登只在这一层(任务前置检查)触发;任务执行过程中失效不重试,
|
||||
避免脏状态(cart 已提交但响应后 cookie 失效等场景)。
|
||||
**写**操作(加购 / 提交订单 / 付款)只在这一层(动作前置检查)触发重登;
|
||||
执行过程中失效不重试,避免脏状态(cart 已提交但响应后 cookie 失效等场景)。
|
||||
**只读**操作(订单列表 / 订单详情查询)另有一层执行中重试,见
|
||||
`site_interact.SiteInteractor._read_with_relogin_retry`——重跑一次查询没有
|
||||
副作用,这条边界是刻意区分的,不要把它推广到写操作上。
|
||||
"""
|
||||
status = await self.check(site)
|
||||
if status.logged_in:
|
||||
@@ -246,11 +254,34 @@ class AuthSession:
|
||||
|
||||
# site 级锁:同账号同 user_data_dir,并发重登会撞锁
|
||||
lock = self._relogin_locks.setdefault(site, asyncio.Lock())
|
||||
epoch_before_wait = self._relogin_epochs.get(site, 0)
|
||||
async with lock:
|
||||
# 拿锁后再 check 一次——可能别的协程刚重登过
|
||||
status = self.status(site)
|
||||
if status.logged_in:
|
||||
return True
|
||||
# 在锁上等过、且期间有人跑完了一轮重登 → 大概率不需要再登一次。
|
||||
# 这里必须**真探测**而不是读 self.status():重登成功后的 reload() 把
|
||||
# logged_in 重置成 None,读缓存会一律判「还没登上」,白跑一次 login_one
|
||||
# (最坏 N 个并发调用串行触发 N 次真实登录,各自最长 relogin_timeout)。
|
||||
if self._relogin_epochs.get(site, 0) != epoch_before_wait:
|
||||
# 探测失败(网络问题)时不在这里抛错——try_relogin 的契约是「降级
|
||||
# 返回 False / 继续走登录」,不是抛异常;这种情况直接落到下面正常
|
||||
# 跑一次 login_one(它自己也会先探测登录态,已登录就跳过填表)。
|
||||
try:
|
||||
status = await self.check(site)
|
||||
except UpstreamRequestError:
|
||||
logger.warning(
|
||||
"自动重登:site=%s 等锁后复核登录态失败,继续尝试登录", site, exc_info=True
|
||||
)
|
||||
else:
|
||||
if status.logged_in:
|
||||
logger.info("自动重登跳过:site=%s 等锁期间已由其他调用方登录完成", site)
|
||||
return True
|
||||
# 刚有人登过还是没登上,说明这轮站点侧就是登不上(验证码/密码错/
|
||||
# 风控)。同一波并发里再串一遍只是把每个调用方各卡一个
|
||||
# relogin_timeout,站点侧结果不会变——直接失败,交人工。
|
||||
logger.warning(
|
||||
"自动重登跳过:site=%s 等锁期间刚失败过一次,不在同一波并发里重复触发",
|
||||
site,
|
||||
)
|
||||
return False
|
||||
|
||||
# 读 account.yaml(失败时降级,不抛错)
|
||||
try:
|
||||
@@ -282,6 +313,11 @@ class AuthSession:
|
||||
except Exception:
|
||||
logger.exception("自动重登异常:site=%s account_id=%s", site, account.id)
|
||||
return False
|
||||
finally:
|
||||
# 成败都记一次「本站跑过一轮登录流程」。放 finally 是因为排队者要
|
||||
# 分辨的是「有没有人替我尝试过」,失败的尝试同样算——否则失败后
|
||||
# 排队的调用方会一个接一个重跑同一个注定失败的登录。
|
||||
self._relogin_epochs[site] = self._relogin_epochs.get(site, 0) + 1
|
||||
|
||||
if not ok:
|
||||
logger.warning("自动重登失败:site=%s account_id=%s", site, account.id)
|
||||
|
||||
Reference in New Issue
Block a user