xiaolongmr commited on
Commit
fbd51cc
·
1 Parent(s): 7820347

yongjiu shibai de fenxiang jilu zhijie tuo li duilie, bu zai rang shangyou chongshi

Browse files
Files changed (1) hide show
  1. services/share_evidence_service.py +82 -17
services/share_evidence_service.py CHANGED
@@ -45,7 +45,11 @@ SHARE_EVIDENCE_STORE = DATA_DIR / "share_evidence.json"
45
  STATUS_PENDING = "pending" # 已登记,等用户在保留期内按需取证
46
  STATUS_SHARED = "shared" # 已生成分享链接,链接有效中
47
  STATUS_RELEASED = "released" # 已回收(会话已隐藏、分享已撤)
48
- STATUS_ORPHAN = "orphan" # 账号已不可用,回收失败待重试
 
 
 
 
49
 
50
  _ACTIVE_STATUSES = (STATUS_PENDING, STATUS_SHARED, STATUS_ORPHAN)
51
  # 回收的兜底周期:正常路径由 `_next_due_ts()` 精确唤醒、到期即回收,这个周期只负责
@@ -77,6 +81,22 @@ def _is_permanent_failure(exc: BaseException) -> bool:
77
  return any(marker in text for marker in _PERMANENT_FAILURE_MARKERS)
78
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def _now_iso() -> str:
81
  return datetime.now(timezone.utc).isoformat()
82
 
@@ -511,6 +531,7 @@ class ShareEvidenceService:
511
  and float(record.get("next_attempt_ts") or 0) <= now
512
  and effective_expires_ts(record) > now
513
  and _clean(record.get("conversation_id")) not in self._inflight
 
514
  ]
515
  due.sort(key=lambda item: float(item.get("created_ts") or 0))
516
 
@@ -537,13 +558,15 @@ class ShareEvidenceService:
537
  attempts = int(stored.get("attempts") or 0) + 1
538
  stored["attempts"] = attempts
539
  permanent = _is_permanent_failure(exc) or attempts >= _MAX_RELEASE_ATTEMPTS
540
- # 30s → 60s → 120s → 240s → 480s → 600s 封顶;永久失败直接远期搁置
541
- backoff = (
542
- _LONG_SLEEP_SECONDS
543
- if permanent
544
- else min(30 * (2 ** min(attempts - 1, 5)), 600)
545
- )
546
- stored["next_attempt_ts"] = time.time() + backoff
 
 
547
  stored["last_error"] = str(exc)[:300]
548
  self._save_locked()
549
  logger.warning({
@@ -557,6 +580,35 @@ class ShareEvidenceService:
557
 
558
  # ------------------------------------------------------------------ 回收
559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  def reap_expired(self, *, limit: int = 20) -> dict[str, Any]:
561
  """回收所有已过保留期的会话:隐藏会话(顺带废掉链接)+ 删除分享。
562
 
@@ -564,6 +616,7 @@ class ShareEvidenceService:
564
  快照值——站长把保留时长调短后,旧记录必须立刻跟着到期,而不是继续按旧值熬。
565
  """
566
  now = time.time()
 
567
  with self._lock:
568
  due = [
569
  dict(record)
@@ -574,6 +627,7 @@ class ShareEvidenceService:
574
  # 「到期即唤醒」精确调度就会变成每 0.5 秒狂刷上游(旧实现靠 5 分钟大
575
  # 周期兜着才没暴露)。
576
  and float(record.get("next_attempt_ts") or 0) <= now
 
577
  ]
578
  due.sort(key=effective_expires_ts)
579
 
@@ -591,17 +645,18 @@ class ShareEvidenceService:
591
  stored = self._record_locked(conversation_id)
592
  if stored is not None and stored.get("status") in _ACTIVE_STATUSES:
593
  attempts = int(stored.get("attempts") or 0) + 1
594
- stored["status"] = STATUS_ORPHAN
595
  stored["attempts"] = attempts
596
- permanent = _is_permanent_failure(exc) or attempts >= _MAX_RELEASE_ATTEMPTS
597
- # 30s → 60s → 120s → 240s → 480s → 600s 封顶;永久失败直接远期搁置
598
- backoff = (
599
- _LONG_SLEEP_SECONDS
600
- if permanent
601
- else min(30 * (2 ** min(attempts - 1, 5)), 600)
602
- )
603
- stored["next_attempt_ts"] = time.time() + backoff
604
  stored["last_error"] = str(exc)[:300]
 
 
 
 
 
 
 
 
 
 
605
  self._save_locked()
606
  logger.warning({
607
  "event": "share_evidence_release_failed",
@@ -733,6 +788,10 @@ class ShareEvidenceService:
733
  for record in self._records.values():
734
  if record.get("status") not in _ACTIVE_STATUSES:
735
  continue
 
 
 
 
736
  expires_ts = effective_expires_ts(record)
737
  if expires_ts <= 0:
738
  continue
@@ -785,6 +844,12 @@ class ShareEvidenceService:
785
  self.reconcile_retention()
786
  except Exception as exc:
787
  logger.warning({"event": "share_evidence_reconcile_error", "error": str(exc)})
 
 
 
 
 
 
788
 
789
  def run() -> None:
790
  last_reap = time.time()
 
45
  STATUS_PENDING = "pending" # 已登记,等用户在保留期内按需取证
46
  STATUS_SHARED = "shared" # 已生成分享链接,链接有效中
47
  STATUS_RELEASED = "released" # 已回收(会话已隐藏、分享已撤)
48
+ STATUS_ORPHAN = "orphan" # 账号已不可用,回收失败待重试(仅限可恢复的失败)
49
+ # 上游永久拒绝(token 吊销 / CF 拦截 / 会话已删):重试不可能成功,直接移出队列。
50
+ # ⚠️ 不能用「ORPHAN + 30 天退避」凑合 —— 30 天后又会全量重试一次,而记录只增不减,
51
+ # 每次都把 cpu-basic 打满(2026-10-19 事故复发的直接原因)。
52
+ STATUS_ABANDONED = "abandoned"
53
 
54
  _ACTIVE_STATUSES = (STATUS_PENDING, STATUS_SHARED, STATUS_ORPHAN)
55
  # 回收的兜底周期:正常路径由 `_next_due_ts()` 精确唤醒、到期即回收,这个周期只负责
 
81
  return any(marker in text for marker in _PERMANENT_FAILURE_MARKERS)
82
 
83
 
84
+ def _hopeless(record: dict[str, Any]) -> bool:
85
+ """该记录是否已经不可能成功、不该再碰上游。
86
+
87
+ 两种:① 已被判为永久失败(历史遗留的 ``permanent`` / ``abandoned`` 标记);
88
+ ② 尝试次数超过上限(老版本会无限重试,台账里积压了大量 attempts 200+ 的记录)。
89
+ """
90
+ if record.get("status") not in _ACTIVE_STATUSES:
91
+ return False
92
+ if record.get("permanent"):
93
+ return True
94
+ try:
95
+ return int(record.get("attempts") or 0) >= _MAX_RELEASE_ATTEMPTS
96
+ except (TypeError, ValueError):
97
+ return False
98
+
99
+
100
  def _now_iso() -> str:
101
  return datetime.now(timezone.utc).isoformat()
102
 
 
531
  and float(record.get("next_attempt_ts") or 0) <= now
532
  and effective_expires_ts(record) > now
533
  and _clean(record.get("conversation_id")) not in self._inflight
534
+ and not _hopeless(record)
535
  ]
536
  due.sort(key=lambda item: float(item.get("created_ts") or 0))
537
 
 
558
  attempts = int(stored.get("attempts") or 0) + 1
559
  stored["attempts"] = attempts
560
  permanent = _is_permanent_failure(exc) or attempts >= _MAX_RELEASE_ATTEMPTS
561
+ if permanent:
562
+ # 上游已判死刑:直接移出队列,别再让它在下次重启后重来。
563
+ stored["status"] = STATUS_ABANDONED
564
+ stored["permanent"] = True
565
+ else:
566
+ # 30s → 60s → 120s → 240s → 480s → 600s 封顶
567
+ stored["next_attempt_ts"] = time.time() + min(
568
+ 30 * (2 ** min(attempts - 1, 5)), 600
569
+ )
570
  stored["last_error"] = str(exc)[:300]
571
  self._save_locked()
572
  logger.warning({
 
580
 
581
  # ------------------------------------------------------------------ 回收
582
 
583
+ def _abandon_hopeless(self) -> int:
584
+ """把所有「重试不可能成功」的记录一次性移出队列,且**不碰上游**。
585
+
586
+ 必须在做任何 due 计算之前跑:老版本在台账里留下了大量 attempts 200+ 的
587
+ 401/403 记录,每次容器重启都会整批重新尝试,每一条都要等 Cloudflare JS
588
+ 挑战超时(CPU 密集、持 GIL),2 核直接被吃满,整站连 /health 都不响应。
589
+ 只有「启动即清理、且永不重试」才能彻底断掉这个循环。
590
+ """
591
+ with self._lock:
592
+ victims = [cid for cid, rec in self._records.items() if _hopeless(rec)]
593
+ if not victims:
594
+ return 0
595
+ now = time.time()
596
+ for conversation_id in victims:
597
+ record = self._records.get(conversation_id)
598
+ if record is None:
599
+ continue
600
+ record["status"] = STATUS_ABANDONED
601
+ record["permanent"] = True
602
+ record["next_attempt_ts"] = now + _LONG_SLEEP_SECONDS
603
+ record.setdefault("last_error", "上游永久拒绝(401/403/404)或尝试次数超限,已停止重试")
604
+ self._save_locked()
605
+ logger.info({
606
+ "event": "share_evidence_abandoned_hopeless",
607
+ "count": len(victims),
608
+ "reason": "permanent_upstream_rejection_or_attempt_cap",
609
+ })
610
+ return len(victims)
611
+
612
  def reap_expired(self, *, limit: int = 20) -> dict[str, Any]:
613
  """回收所有已过保留期的会话:隐藏会话(顺带废掉链接)+ 删除分享。
614
 
 
616
  快照值——站长把保留时长调短后,旧记录必须立刻跟着到期,而不是继续按旧值熬。
617
  """
618
  now = time.time()
619
+ self._abandon_hopeless()
620
  with self._lock:
621
  due = [
622
  dict(record)
 
627
  # 「到期即唤醒」精确调度就会变成每 0.5 秒狂刷上游(旧实现靠 5 分钟大
628
  # 周期兜着才没暴露)。
629
  and float(record.get("next_attempt_ts") or 0) <= now
630
+ and not _hopeless(record)
631
  ]
632
  due.sort(key=effective_expires_ts)
633
 
 
645
  stored = self._record_locked(conversation_id)
646
  if stored is not None and stored.get("status") in _ACTIVE_STATUSES:
647
  attempts = int(stored.get("attempts") or 0) + 1
 
648
  stored["attempts"] = attempts
 
 
 
 
 
 
 
 
649
  stored["last_error"] = str(exc)[:300]
650
+ if _is_permanent_failure(exc) or attempts >= _MAX_RELEASE_ATTEMPTS:
651
+ # 永久失败:标记后永久移出队列。之前只做「30 天远期搁置」,
652
+ # 重启后整批重新到期 → 又全体撞一次上游 403 → V8 求解打满 CPU。
653
+ stored["status"] = STATUS_ABANDONED
654
+ stored["permanent"] = True
655
+ else:
656
+ stored["status"] = STATUS_ORPHAN
657
+ stored["next_attempt_ts"] = time.time() + min(
658
+ 30 * (2 ** min(attempts - 1, 5)), 600
659
+ )
660
  self._save_locked()
661
  logger.warning({
662
  "event": "share_evidence_release_failed",
 
788
  for record in self._records.values():
789
  if record.get("status") not in _ACTIVE_STATUSES:
790
  continue
791
+ if _hopeless(record):
792
+ # 已判死刑但还没被 _abandon_hopeless 收走的记录,不能参与唤醒,
793
+ # 否则 worker 会被它反复叫醒、空转抢 CPU。
794
+ continue
795
  expires_ts = effective_expires_ts(record)
796
  if expires_ts <= 0:
797
  continue
 
844
  self.reconcile_retention()
845
  except Exception as exc:
846
  logger.warning({"event": "share_evidence_reconcile_error", "error": str(exc)})
847
+ # 启动先清掉不可能成功的积压记录,**在**任何一轮 due 计算之前。否则重启后
848
+ # 整批 401/403 会重新撞上游(403 → V8 求解打满 2 核 → 整站无响应)。
849
+ try:
850
+ self._abandon_hopeless()
851
+ except Exception as exc:
852
+ logger.warning({"event": "share_evidence_abandon_error", "error": str(exc)})
853
 
854
  def run() -> None:
855
  last_reap = time.time()