xiaolongmr commited on
Commit ·
216368f
1
Parent(s): ab1d2ab
xiufu jiandeng yu shangdian huxiang dengdai de sizi suo; tanzhen buzai zhudong tuichu
Browse files- api/app.py +4 -3
- 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 |
-
|
| 217 |
-
|
|
|
|
|
|
|
| 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
|
| 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 =
|
| 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 |
-
|
| 596 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
next_items, result = mutator(current)
|
| 598 |
-
|
| 599 |
-
|
|
|
|
| 600 |
return normalized_next, result
|
| 601 |
|
| 602 |
return update_auth_keys_locked(wrapped)
|
| 603 |
|
| 604 |
-
self.
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 2646 |
matched_item = find_match(self._items)
|
| 2647 |
-
|
| 2648 |
-
|
| 2649 |
-
|
| 2650 |
-
|
| 2651 |
-
|
| 2652 |
-
|
| 2653 |
-
|
| 2654 |
-
|
| 2655 |
-
|
| 2656 |
-
|
| 2657 |
-
|
| 2658 |
-
|
| 2659 |
-
|
| 2660 |
-
|
| 2661 |
-
|
| 2662 |
-
|
| 2663 |
-
|
| 2664 |
-
|
| 2665 |
-
|
| 2666 |
-
|
| 2667 |
-
|
| 2668 |
-
|
| 2669 |
-
|
| 2670 |
-
|
| 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())
|