xiaolongmr commited on
Commit
216368f
·
1 Parent(s): ab1d2ab

xiufu jiandeng yu shangdian huxiang dengdai de sizi suo; tanzhen buzai zhudong tuichu

Browse files
Files changed (2) hide show
  1. api/app.py +4 -3
  2. services/auth_service.py +54 -43
api/app.py CHANGED
@@ -213,14 +213,15 @@ def create_app() -> FastAPI:
213
  "strikes": strikes,
214
  "threads": active_count(),
215
  })
216
- if strikes >= 3:
217
- _logger.error({"event": "event_loop_wedged", "action": "dump_stacks_and_exit"})
 
 
218
  try:
219
  faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
220
  except Exception:
221
  pass
222
  sys.stderr.flush()
223
- os._exit(70)
224
 
225
  Thread(target=run, daemon=True, name="loop-wedge-guard").start()
226
 
 
213
  "strikes": strikes,
214
  "threads": active_count(),
215
  })
216
+ # ⚠️ 只抓堆栈、**不主动退出**:os._exit 会让 HF 把 Space 置为
217
+ # RUNTIME_ERROR(不会自动重建),反而要人工点重启;只留证据更好。
218
+ if strikes == 3 or strikes % 12 == 0:
219
+ _logger.error({"event": "event_loop_wedged", "action": "dump_stacks"})
220
  try:
221
  faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
222
  except Exception:
223
  pass
224
  sys.stderr.flush()
 
225
 
226
  Thread(target=run, daemon=True, name="loop-wedge-guard").start()
227
 
services/auth_service.py CHANGED
@@ -7,7 +7,7 @@ import time
7
  import uuid
8
  from datetime import datetime, timedelta, timezone
9
  from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
10
- from threading import Lock
11
  from typing import Callable, Literal, TypeVar
12
 
13
  try:
@@ -462,7 +462,7 @@ class AuthService:
462
 
463
  def __init__(self, storage: StorageBackend):
464
  self.storage = storage
465
- self._lock = Lock()
466
  self._items = self._load()
467
  # key_id -> 上次写 last_used_at 的时间戳;只在本进程内存里,重启即清空(无害)。
468
  self._last_touch_ts: dict[str, float] = {}
@@ -592,19 +592,26 @@ class AuthService:
592
  update_auth_keys_locked = getattr(self.storage, "update_auth_keys_locked", None)
593
  if callable(update_auth_keys_locked):
594
  def wrapped(raw_items: list[dict[str, object]]) -> tuple[list[dict[str, object]], ResultT]:
595
- current = self._normalize_items(raw_items, fallback=self._items)
596
- self._items = current
 
 
 
 
 
597
  next_items, result = mutator(current)
598
- normalized_next = self._normalize_items(next_items, fallback=current)
599
- self._items = normalized_next
 
600
  return normalized_next, result
601
 
602
  return update_auth_keys_locked(wrapped)
603
 
604
- self._reload_locked()
605
- next_items, result = mutator(self._items)
606
- self._items = self._normalize_items(next_items, fallback=self._items)
607
- self._save()
 
608
  return result
609
 
610
  @staticmethod
@@ -782,8 +789,12 @@ class AuthService:
782
  }
783
 
784
  def list_keys(self, role: AuthRole | None = None) -> list[dict[str, object]]:
 
 
 
 
 
785
  with self._lock:
786
- self._reload_locked()
787
  items = [item for item in self._items if role is None or item.get("role") == role]
788
  return [self._public_item(item) for item in items]
789
 
@@ -2641,40 +2652,40 @@ class AuthService:
2641
  return next_items, self._public_item(next_item)
2642
  return items, None
2643
 
 
 
 
 
2644
  with self._lock:
2645
- for attempt in range(2):
 
 
 
2646
  matched_item = find_match(self._items)
2647
- if matched_item is not None:
2648
- if not touch:
2649
- return self._public_item(matched_item)
2650
- # ⚠️⚠️ last_used_at 必须节流,不能每个请求都写。
2651
- # `_update_items_locked` 是「读出整张 auth_keys 表 → 改一条 → 写回整表」,
2652
- # 而且是在**事件循环线程**上同步执行的;前端轮询(0.8s/次 × 几十人)
2653
- # 会把整表重写几百次/分钟,SQLite 事务锁排队直接卡死整个 HTTP 服务
2654
- # (2026-10-19:发了 30 个卡密、��间集中兑换后网站彻底无响应)。
2655
- # last_used_at 只用于展示「最近使用」,60 秒粒度完全够用。
2656
- throttle_key = self._clean(matched_item.get("id")) or candidate_hash
2657
- now_ts = time.time()
2658
- if now_ts - self._last_touch_ts.get(throttle_key, 0.0) < self._TOUCH_INTERVAL_SECONDS:
2659
- return self._public_item(matched_item)
2660
- try:
2661
- updated = self._update_items_locked(mutate)
2662
- except Exception as exc:
2663
- print(f"[auth-service] update last_used_at failed: {exc}")
2664
- fallback_item = dict(matched_item)
2665
- fallback_item["last_used_at"] = datetime.now(timezone.utc).isoformat()
2666
- return self._public_item(fallback_item)
2667
- self._last_touch_ts[throttle_key] = now_ts
2668
- if len(self._last_touch_ts) > 4096:
2669
- # 防止长期运行后字典无限增长
2670
- cutoff = now_ts - self._TOUCH_INTERVAL_SECONDS
2671
- self._last_touch_ts = {
2672
- key: ts for key, ts in self._last_touch_ts.items() if ts >= cutoff
2673
- }
2674
- return updated
2675
- if attempt == 0:
2676
- self._reload_locked()
2677
- return None
2678
 
2679
 
2680
  auth_service = AuthService(config.get_user_storage_backend())
 
7
  import uuid
8
  from datetime import datetime, timedelta, timezone
9
  from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
10
+ from threading import RLock
11
  from typing import Callable, Literal, TypeVar
12
 
13
  try:
 
462
 
463
  def __init__(self, storage: StorageBackend):
464
  self.storage = storage
465
+ self._lock = RLock()
466
  self._items = self._load()
467
  # key_id -> 上次写 last_used_at 的时间戳;只在本进程内存里,重启即清空(无害)。
468
  self._last_touch_ts: dict[str, float] = {}
 
592
  update_auth_keys_locked = getattr(self.storage, "update_auth_keys_locked", None)
593
  if callable(update_auth_keys_locked):
594
  def wrapped(raw_items: list[dict[str, object]]) -> tuple[list[dict[str, object]], ResultT]:
595
+ # ⚠️ 这里已经持有**存储**锁(update_auth_keys_locked 内部),再去拿鉴权锁时
596
+ # 必须保证没有「持鉴权锁等存储锁」的线程 —— 这就是 ABBA 死锁的另一半。
597
+ # 现在 authenticate 已改为「先放鉴权锁、再写存储」,加上 list_keys 不再回读
598
+ # 存储,环被打断;锁顺序统一为 存储 → 鉴权。
599
+ with self._lock:
600
+ current = self._normalize_items(raw_items, fallback=self._items)
601
+ self._items = current
602
  next_items, result = mutator(current)
603
+ with self._lock:
604
+ normalized_next = self._normalize_items(next_items, fallback=current)
605
+ self._items = normalized_next
606
  return normalized_next, result
607
 
608
  return update_auth_keys_locked(wrapped)
609
 
610
+ with self._lock:
611
+ self._reload_locked()
612
+ next_items, result = mutator(self._items)
613
+ self._items = self._normalize_items(next_items, fallback=self._items)
614
+ self._save()
615
  return result
616
 
617
  @staticmethod
 
789
  }
790
 
791
  def list_keys(self, role: AuthRole | None = None) -> list[dict[str, object]]:
792
+ # ⚠️⚠️ 这里**绝对不能**再调 _reload_locked()(那会去拿存储锁)。
793
+ # 调用方常常是「已经持有存储事务锁」的商店逻辑(兑换卡密 / 每日礼包在算可用模型),
794
+ # 一旦这里回头去要存储锁,就和 authenticate(持鉴权锁去写存储)形成 ABBA 死锁,
795
+ # 而 authenticate 是跑在**事件循环线程**上的 ⇒ 整站彻底无响应、连接被关闭。
796
+ # 2026-10-19 事故根因。单进程内所有写入都会同步更新 self._items,缓存即权威。
797
  with self._lock:
 
798
  items = [item for item in self._items if role is None or item.get("role") == role]
799
  return [self._public_item(item) for item in items]
800
 
 
2652
  return next_items, self._public_item(next_item)
2653
  return items, None
2654
 
2655
+ # ⚠️⚠️ 锁顺序铁律:**存储 → 鉴权**。鉴权锁内绝不等待存储锁。
2656
+ # 之前 authenticate 在 `with self._lock:` 里调 _update_items_locked(=去拿存储锁),
2657
+ # 而商店侧(兑换卡密)持存储锁反过来调 list_keys(=去拿鉴权锁),形成 ABBA 死锁。
2658
+ # authenticate 跑在事件循环线程上 ⇒ 整站 HTTP 全部无响应(2026-10-19 事故)。
2659
  with self._lock:
2660
+ matched_item = find_match(self._items)
2661
+ if matched_item is None:
2662
+ # 缓存未命中才回读一次;这是冷路径(未知/刚创建的密钥),不在热路径上。
2663
+ self._reload_locked()
2664
  matched_item = find_match(self._items)
2665
+ if matched_item is None:
2666
+ return None
2667
+ if not touch:
2668
+ return self._public_item(matched_item)
2669
+ # last_used_at 节流:同一密钥 60 秒内只回写一次(见下)。
2670
+ throttle_key = self._clean(matched_item.get("id")) or candidate_hash
2671
+ now_ts = time.time()
2672
+ if now_ts - self._last_touch_ts.get(throttle_key, 0.0) < self._TOUCH_INTERVAL_SECONDS:
2673
+ return self._public_item(matched_item)
2674
+ self._last_touch_ts[throttle_key] = now_ts
2675
+ if len(self._last_touch_ts) > 4096:
2676
+ cutoff = now_ts - self._TOUCH_INTERVAL_SECONDS
2677
+ self._last_touch_ts = {
2678
+ key: ts for key, ts in self._last_touch_ts.items() if ts >= cutoff
2679
+ }
2680
+
2681
+ # ---- 已放掉鉴权锁,再去写存储 ----
2682
+ try:
2683
+ return self._update_items_locked(mutate)
2684
+ except Exception as exc:
2685
+ print(f"[auth-service] update last_used_at failed: {exc}")
2686
+ fallback_item = dict(matched_item)
2687
+ fallback_item["last_used_at"] = datetime.now(timezone.utc).isoformat()
2688
+ return self._public_item(fallback_item)
 
 
 
 
 
 
 
2689
 
2690
 
2691
  auth_service = AuthService(config.get_user_storage_backend())