fix: 防止分页漏读和上游异常快照误标 orphaned

This commit is contained in:
SmartUp Developer
2026-07-02 14:21:09 +08:00
parent 00af34c487
commit e2129d3796
4 changed files with 349 additions and 9 deletions
+22 -1
View File
@@ -248,6 +248,27 @@ def _sync_upstream_keys(upstream_id: int, snapshot: dict[str, Any], captured_at:
db = SessionLocal()
try:
active_group_ids = set(snapshot.get("groups", {}).keys())
# 获取倒数第二个快照作为 previous snapshot
prev_active_group_ids: set[str] | None = None
from app.models.snapshot import UpstreamRateSnapshot
snapshots = (
db.query(UpstreamRateSnapshot)
.filter(UpstreamRateSnapshot.upstream_id == upstream_id)
.order_by(UpstreamRateSnapshot.captured_at.desc())
.limit(2)
.all()
)
if len(snapshots) >= 2:
prev_snapshot = snapshots[1]
try:
prev_data = json.loads(prev_snapshot.snapshot_json or "{}")
prev_groups = prev_data.get("groups") or {}
if isinstance(prev_groups, dict):
prev_active_group_ids = set(prev_groups.keys())
except Exception:
pass
# 用 UpstreamClient 查询远端活跃 Key ID 集合
remote_key_ids: set[str] | None = None
try:
@@ -269,7 +290,7 @@ def _sync_upstream_keys(upstream_id: int, snapshot: dict[str, Any], captured_at:
except Exception as exc:
logger.warning("sync upstream keys list failed for %s: %s", upstream_id, exc)
website_sync.reconcile_upstream_keys(db, upstream_id, active_group_ids, remote_key_ids, captured_at)
website_sync.reconcile_upstream_keys(db, upstream_id, active_group_ids, remote_key_ids, captured_at, prev_active_group_ids=prev_active_group_ids)
db.commit()
except Exception:
logger.exception("key sync failed for upstream %s", upstream_id)
+56 -8
View File
@@ -848,6 +848,7 @@ def reconcile_upstream_keys(
active_group_ids: set[str] | None,
remote_key_ids: set[str] | None,
captured_at: datetime,
prev_active_group_ids: set[str] | None = None,
) -> None:
"""对账上游 Key 的本地缓存与远端状态。
@@ -857,6 +858,7 @@ def reconcile_upstream_keys(
"""
if active_group_ids is None and remote_key_ids is None:
return
key_rows = (
db.query(UpstreamGeneratedKey)
.filter(
@@ -865,16 +867,40 @@ def reconcile_upstream_keys(
.all()
)
for row in key_rows:
# 1. 增加自恢复逻辑:已导入的 orphaned key 如果后续快照中来源分组恢复,且远端 key id 仍存在,则自动恢复为 imported 并清空错误。
if (
row.status == "orphaned"
and row.error == "来源分组已不存在"
and row.imported_website_id is not None
and row.imported_account_id is not None
and active_group_ids is not None
and row.group_id in active_group_ids
and remote_key_ids is not None
and row.key_id in remote_key_ids
):
row.status = "imported"
row.error = None
row.updated_at = captured_at
logger.info("recovered orphaned key %s to imported (group %s recovered and key_id %s exists remotely)", row.id, row.group_id, row.key_id)
# 2. 修改上游 key 对账逻辑:来源分组缺失时,不因单次快照缺失立即把已导入 key 标为 orphaned。
# 分组级 orphan 判断改为“两次连续快照都缺失该分组”才生效,避免上游接口临时返回不完整分组导致误标记。
if active_group_ids is not None and row.group_id not in active_group_ids:
if row.imported_website_id and row.imported_account_id:
row.status = "orphaned"
row.error = "来源分组已不存在"
row.updated_at = captured_at
logger.info("marked key %s orphaned (group %s no longer in snapshot)", row.id, row.group_id)
if prev_active_group_ids is not None and row.group_id not in prev_active_group_ids:
if row.imported_website_id and row.imported_account_id:
row.status = "orphaned"
row.error = "来源分组已不存在"
row.updated_at = captured_at
logger.info("marked key %s orphaned (group %s no longer in snapshot consecutively)", row.id, row.group_id)
else:
db.delete(row)
logger.info("removed key %s (group %s no longer in snapshot consecutively)", row.id, row.group_id)
continue
else:
db.delete(row)
logger.info("removed key %s (group %s no longer in snapshot)", row.id, row.group_id)
continue
# 只有单次快照缺失,跳过标记/清理,避免误标
logger.info("skipped marking key %s orphaned (group %s missing in single snapshot, waiting for next consecutive check)", row.id, row.group_id)
# 3. 远端 key id 级清理的现有保守策略:只有远端 key 列表成功拉取后,才允许判断 key 是否不存在。
if row.key_id and remote_key_ids is not None and row.key_id not in remote_key_ids:
if row.imported_website_id and row.imported_account_id:
row.status = "orphaned"
@@ -885,6 +911,7 @@ def reconcile_upstream_keys(
db.delete(row)
logger.info("removed key %s (key_id %s gone from remote)", row.id, row.key_id)
continue
if remote_key_ids is not None and row.key_id in remote_key_ids:
row.updated_at = captured_at
@@ -914,11 +941,31 @@ def reconcile_upstream_keys_full(db: Session, upstream_id: int) -> bool:
now = datetime.now(timezone.utc)
# 从最新快照获取活跃分组 ID(与调度器 _sync_upstream_keys 一致)
prev_active_group_ids: set[str] | None = None
groups = latest_rate_map(db, upstream_id)
if groups:
active_group_ids = set(groups.keys())
groups_fetched = True
# 获取倒数第二个快照作为 previous snapshot
from app.models.snapshot import UpstreamRateSnapshot
snapshots = (
db.query(UpstreamRateSnapshot)
.filter(UpstreamRateSnapshot.upstream_id == upstream_id)
.order_by(UpstreamRateSnapshot.captured_at.desc())
.limit(2)
.all()
)
if len(snapshots) >= 2:
prev_snapshot = snapshots[1]
try:
prev_data = json.loads(prev_snapshot.snapshot_json or "{}")
prev_groups = prev_data.get("groups") or {}
if isinstance(prev_groups, dict):
prev_active_group_ids = set(prev_groups.keys())
except Exception:
pass
try:
with UpstreamClient(
base_url=upstream.base_url,
@@ -944,6 +991,7 @@ def reconcile_upstream_keys_full(db: Session, upstream_id: int) -> bool:
active_group_ids if groups_fetched else None,
remote_key_ids if keys_fetched else None,
now,
prev_active_group_ids=prev_active_group_ids,
)
db.commit()
return keys_fetched