Spaces:
Running
Running
sync processor from GitLab d3aa79b: get_episode 未命中强制重扫(多进程缓存可见性修复)
Browse files- processor/app/localstore.py +37 -7
processor/app/localstore.py
CHANGED
|
@@ -34,6 +34,9 @@ _sessions_cache_at = 0.0
|
|
| 34 |
# fast fallback; the ingest queue refreshes it after the batch is committed.
|
| 35 |
_task_sessions_cache: list[dict] | None = None
|
| 36 |
_SESSIONS_CACHE_TTL = 300.0
|
|
|
|
|
|
|
|
|
|
| 37 |
# 全量遍历远程挂载很慢:只允许单一线程执行(_scan_lock),遍历本身不持有
|
| 38 |
# _lock —— 缓存过期触发的重扫不再把状态读写请求拖在锁后面排队。
|
| 39 |
_scan_lock = threading.Lock()
|
|
@@ -202,13 +205,18 @@ def _run_session_refresh() -> None:
|
|
| 202 |
_sessions_refresh_running = False
|
| 203 |
|
| 204 |
|
| 205 |
-
def _scan_sessions_blocking() -> list[dict]:
|
| 206 |
-
"""Full remote tree walk; callers block for the duration.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
global _sessions_cache, _sessions_cache_at, _task_sessions_cache
|
| 208 |
while True:
|
| 209 |
with _lock:
|
| 210 |
now = time.monotonic()
|
| 211 |
-
if (_sessions_cache is not None
|
| 212 |
and now - _sessions_cache_at < _SESSIONS_CACHE_TTL):
|
| 213 |
return copy.deepcopy(_sessions_cache)
|
| 214 |
generation = _scan_generation
|
|
@@ -216,7 +224,9 @@ def _scan_sessions_blocking() -> list[dict]:
|
|
| 216 |
with _scan_lock:
|
| 217 |
with _lock:
|
| 218 |
now = time.monotonic()
|
| 219 |
-
|
|
|
|
|
|
|
| 220 |
and now - _sessions_cache_at < _SESSIONS_CACHE_TTL):
|
| 221 |
# 等待 _scan_lock 期间已有线程完成扫描
|
| 222 |
return copy.deepcopy(_sessions_cache)
|
|
@@ -619,10 +629,30 @@ def _probe_video_cached(path: Path) -> tuple[int, float]:
|
|
| 619 |
|
| 620 |
|
| 621 |
def get_episode(episode_id: str) -> dict | None:
|
| 622 |
-
"""按批次名查 episode(扫描目录)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
name = str(episode_id)
|
| 624 |
-
|
| 625 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
|
| 627 |
|
| 628 |
# ── 审核状态 ─────────────────────────────────────────
|
|
|
|
| 34 |
# fast fallback; the ingest queue refreshes it after the batch is committed.
|
| 35 |
_task_sessions_cache: list[dict] | None = None
|
| 36 |
_SESSIONS_CACHE_TTL = 300.0
|
| 37 |
+
# get_episode 未命中后允许强制重扫的最小间隔:既让刚上传的批次立刻可见,
|
| 38 |
+
# 又不至于被反复查询的无效 id 触发远程全量遍历风暴。
|
| 39 |
+
_MISS_RESCAN_MIN_INTERVAL = 5.0
|
| 40 |
# 全量遍历远程挂载很慢:只允许单一线程执行(_scan_lock),遍历本身不持有
|
| 41 |
# _lock —— 缓存过期触发的重扫不再把状态读写请求拖在锁后面排队。
|
| 42 |
_scan_lock = threading.Lock()
|
|
|
|
| 205 |
_sessions_refresh_running = False
|
| 206 |
|
| 207 |
|
| 208 |
+
def _scan_sessions_blocking(force: bool = False) -> list[dict]:
|
| 209 |
+
"""Full remote tree walk; callers block for the duration.
|
| 210 |
+
|
| 211 |
+
``force=True`` 跳过 TTL 判断,即使快照还新鲜也重新遍历 —— 供
|
| 212 |
+
:func:`get_episode` 在未命中时使用:快照里没有不代表磁盘上没有,
|
| 213 |
+
可能只是本进程还没看到刚上传的批次。
|
| 214 |
+
"""
|
| 215 |
global _sessions_cache, _sessions_cache_at, _task_sessions_cache
|
| 216 |
while True:
|
| 217 |
with _lock:
|
| 218 |
now = time.monotonic()
|
| 219 |
+
if (not force and _sessions_cache is not None
|
| 220 |
and now - _sessions_cache_at < _SESSIONS_CACHE_TTL):
|
| 221 |
return copy.deepcopy(_sessions_cache)
|
| 222 |
generation = _scan_generation
|
|
|
|
| 224 |
with _scan_lock:
|
| 225 |
with _lock:
|
| 226 |
now = time.monotonic()
|
| 227 |
+
# force 时不吃这个捷径:那份快照可能正是我们刚刚未命中的
|
| 228 |
+
# 那一份。既然调用方明确要求重扫,就实实在在走一遍。
|
| 229 |
+
if (not force and _sessions_cache is not None
|
| 230 |
and now - _sessions_cache_at < _SESSIONS_CACHE_TTL):
|
| 231 |
# 等待 _scan_lock 期间已有线程完成扫描
|
| 232 |
return copy.deepcopy(_sessions_cache)
|
|
|
|
| 629 |
|
| 630 |
|
| 631 |
def get_episode(episode_id: str) -> dict | None:
|
| 632 |
+
"""按批次名查 episode(扫描目录)。
|
| 633 |
+
|
| 634 |
+
未命中时会强制同步重扫一次再判断 —— **快照里没有不等于磁盘上没有**:
|
| 635 |
+
上传只清发起上传那个进程的缓存,别的进程(worker / 另一个 API 实例)
|
| 636 |
+
要等自己的 TTL 过期才看得见新批次。而未命中恰恰是不能相信缓存的时刻:
|
| 637 |
+
导出链路里 ``get_episode`` 返回 None 会让整条 run 直接失败(实测 ep74-79
|
| 638 |
+
在 17:25-17:31 全部因此 failed,派生产物没能合并回 canonical)。
|
| 639 |
+
|
| 640 |
+
重扫有最小间隔,避免反复查询不存在的 id 时把远程全量遍历打成风暴;
|
| 641 |
+
真正的"不存在"在重扫后依然返回 None,只是多付一次遍历。
|
| 642 |
+
"""
|
| 643 |
name = str(episode_id)
|
| 644 |
+
episode = next((item for item in scan_sessions()
|
| 645 |
+
if str(item.get("id")) == name), None)
|
| 646 |
+
if episode is not None:
|
| 647 |
+
return episode
|
| 648 |
+
with _lock:
|
| 649 |
+
just_scanned = (time.monotonic() - _sessions_cache_at
|
| 650 |
+
< _MISS_RESCAN_MIN_INTERVAL)
|
| 651 |
+
if just_scanned:
|
| 652 |
+
# 刚刚才遍历过,这次未命中是可信的
|
| 653 |
+
return None
|
| 654 |
+
return next((item for item in _scan_sessions_blocking(force=True)
|
| 655 |
+
if str(item.get("id")) == name), None)
|
| 656 |
|
| 657 |
|
| 658 |
# ── 审核状态 ─────────────────────────────────────────
|