mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
fix(usage): session touch 独立提交避免行锁阻塞 & 管理员页面顺序加载降低并发压力
后端: 将 session touch 的 commit 从请求事务中分离,防止管理员 usage 页面的长查询持有 user_sessions 行锁阻塞后续请求。touch_session 改为 返回 bool 以支持按需提交。 前端: 管理员 Usage 页面将并行 API 调用改为顺序加载,优先显示记录表格, 统计面板在后台异步刷新,避免瞬时并发打满后端 worker。loadRecords 支持 传入 dateRange 参数确保时间范围一致性。
This commit is contained in:
@@ -57,6 +57,30 @@ class ApiRequestPipeline:
|
||||
self.usage_service = usage_service
|
||||
self.audit_service = audit_service
|
||||
|
||||
def _commit_session_touch(self, db: Session, *, scope: str) -> None:
|
||||
"""Persist session last_seen updates immediately to avoid holding row locks.
|
||||
|
||||
Admin usage views can execute heavy read queries after authentication.
|
||||
If the request later stalls, leaving the session touch inside the request
|
||||
transaction can block all subsequent requests that update the same
|
||||
`user_sessions` row. Commit the touch in its own short transaction so
|
||||
later long-running reads cannot keep the session row locked.
|
||||
"""
|
||||
original_expire_on_commit = getattr(db, "expire_on_commit", None)
|
||||
try:
|
||||
if original_expire_on_commit is not None:
|
||||
db.expire_on_commit = False
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception as rollback_exc:
|
||||
logger.debug("[Pipeline] {} session touch rollback failed: {}", scope, rollback_exc)
|
||||
logger.warning("[Pipeline] failed to persist {} session touch: {}", scope, exc)
|
||||
finally:
|
||||
if original_expire_on_commit is not None:
|
||||
db.expire_on_commit = original_expire_on_commit
|
||||
|
||||
async def run(
|
||||
self,
|
||||
adapter: ApiAdapter,
|
||||
@@ -492,11 +516,13 @@ class ApiRequestPipeline:
|
||||
if not session:
|
||||
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
|
||||
SessionService.assert_session_device_matches(session, client_device_id)
|
||||
SessionService.touch_session(
|
||||
session_touched = SessionService.touch_session(
|
||||
session,
|
||||
client_ip=get_client_ip(request),
|
||||
user_agent=request.headers.get("user-agent", "unknown"),
|
||||
)
|
||||
if session_touched:
|
||||
self._commit_session_touch(db, scope="admin")
|
||||
request.state.user_session_id = session.id
|
||||
|
||||
request.state.user_id = db_user.id
|
||||
@@ -549,11 +575,13 @@ class ApiRequestPipeline:
|
||||
if not session:
|
||||
raise HTTPException(status_code=401, detail="登录会话已失效,请重新登录")
|
||||
SessionService.assert_session_device_matches(session, client_device_id)
|
||||
SessionService.touch_session(
|
||||
session_touched = SessionService.touch_session(
|
||||
session,
|
||||
client_ip=get_client_ip(request),
|
||||
user_agent=request.headers.get("user-agent", "unknown"),
|
||||
)
|
||||
if session_touched:
|
||||
self._commit_session_touch(db, scope="user")
|
||||
request.state.user_session_id = session.id
|
||||
request.state.user_id = db_user.id
|
||||
return db_user, None
|
||||
|
||||
@@ -328,19 +328,20 @@ class SessionService:
|
||||
*,
|
||||
client_ip: str | None,
|
||||
user_agent: str,
|
||||
) -> None:
|
||||
) -> bool:
|
||||
now = datetime.now(timezone.utc)
|
||||
last_seen_at = session.last_seen_at
|
||||
if last_seen_at.tzinfo is None:
|
||||
last_seen_at = last_seen_at.replace(tzinfo=timezone.utc)
|
||||
if (now - last_seen_at).total_seconds() < SESSION_TOUCH_INTERVAL_SECONDS:
|
||||
return
|
||||
return False
|
||||
|
||||
session.last_seen_at = now
|
||||
if client_ip:
|
||||
session.ip_address = client_ip
|
||||
if user_agent:
|
||||
session.user_agent = user_agent[:1000]
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def revoke_session(
|
||||
|
||||
Reference in New Issue
Block a user