This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/services/_taskcore.py
T
snowy-yang 7a70eef636 refactor: 删除 services.runtime 兼容门面,7 处调用点直连真实子模块
为什么:上一次分包重构把 1900 行巨石按职责拆成 9 个子模块后,留了一个
runtime.py 作为「原样再导出」的兼容门面,好让调用点零改动。但迁移期结束后
没有一个调用点迁走——门面成了永久中间层:每次读代码都要先跳一层才知道
符号真正住在哪,pyright 也无法从门面追溯定义位置。

改法:保持「函数内延迟导入」的形式不变,只把导入目标换成真实子模块。
**刻意不提到模块顶部**——services/__init__.py 里有依赖自检
(缺 auth_system/podcast_api/music_api/music_downloader/qr_login 就 sys.exit(1)),
提到顶层会让「import web.*」这一步就触发该自检,改变启动语义,超出本次范围。

7 处调用点(AST 精确扫描确认全集):
- web/routers/download.py ×4:download / download_batch_zip / podcast_download /
  podcast_batch_zip,分别直连 services.downloads / batch / podcast / tasks
  外加 _shared._build_download_base 与 api_service.api_service
- web/routers/tasks.py ×2:_semaphores 取 _shared.BATCH_ZIP_SEM + api_service,
  topen 取 _shared.ENDPOINT_NAMES
- web/tasks_registry.py ×1:start_task 直连 _taskcore._start_task_thread

子模块短名统一加 `services_` 前缀别名(services_tasks 等),避免与函数内
既有局部名相撞。

顺带清理:9 个子模块 docstring 里「(自 services/runtime.py 拆出……)」的
历史说明在门面删除后已成悬空引用,一并去掉;包的模块划分说明改为
「不再有兼容门面层」。

验证:
- 符号可达:11 个被引符号在真实子模块上全部 hasattr 通过;
  services.runtime 已 ModuleNotFoundError;api_service 仍是同一实例对象
- 三个调用方模块均可正常 import
- 门禁:ruff 0 · pyright 0 errors 0 warnings · pytest 175 passed
- 真实 uvicorn 冒烟 8/8(含 POST /auth/login 400),临时数据目录独立落库
  9 张表,生产 data/ 指纹三项一致
2026-09-26 14:48:05 +08:00

111 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""任务生命周期原语(启动 / 终态 / 文件收尾 / 结算)。
"""
import threading
import time
import traceback
from typing import Any
import auth_system
from core.state import (
DOWNLOAD_TASKS,
DOWNLOAD_TASKS_LOCK,
)
from .api_service import (
api_service,
)
def _start_task_thread(task_id: str, target, args=()) -> str | None:
"""启动任务 worker(启动失败时收敛任务终态并按账本退费,不留僵尸 queued 记录)。
退款只走账本(本处 settle 后装饰器看到账已关,不会重复退)。
线程句柄存入任务字典——监控循环对 4h 超时任务若发现 worker 线程已死
(无法自行走到终态结算),可代为按账本结算退款(settle 幂等,无双退)。"""
try:
th = threading.Thread(target=target, args=args, daemon=True)
with DOWNLOAD_TASKS_LOCK:
t = DOWNLOAD_TASKS.get(task_id)
if t is not None:
t['worker_thread'] = th
th.start()
return None
except Exception as e:
with DOWNLOAD_TASKS_LOCK:
t = DOWNLOAD_TASKS.pop(task_id, None)
if t:
try:
auth_system.ledger_settle(task_id, 0, 'submit_failed')
except Exception:
pass
api_service.logger.error(f"任务线程启动失败: {task_id} {e}")
return f'任务启动失败:{e}'
def _mark_task_terminal(task_id: str, status: str, msg: str = '') -> None:
"""任务进入终态的统一入口(记录 completed_at,保留期从完成时刻起算,
而不是从提交时刻起算)。
/F-E:置 error/canceled 终态时若 worker 线程已死(无人再走正常结算),
代为按账本结算退款——ledger_settle 幂等,worker 已自行结算过则此处为 0。
终态先到先得——已终态(done/error/canceled)的任务
不再被覆盖(堵住 canceled→error 的非法迁移边:取消后解析阶段的自身失败
不得把"已取消"改写成"失败",completed_at 也不再刷新)。"""
need_settle = False
with DOWNLOAD_TASKS_LOCK:
t = DOWNLOAD_TASKS.get(task_id)
if t and t.get('status') in ('done', 'error', 'canceled'):
return
if t:
t['status'] = status
t['msg'] = msg or t.get('msg', '')
t['completed_at'] = time.time()
if status in ('canceled', 'error'):
th = t.get('worker_thread')
if th is not None and not th.is_alive():
need_settle = True
# settle 移到任务锁外——ledger_settle 内部要抢 _DB_LOCK 写库
# (busy 最长 15s),持任务锁调用会让全站进度/提交/取消/领取随库停摆;
# settle 幂等,锁外调用无双退
if need_settle:
try:
auth_system.ledger_settle(task_id, 0, status)
except Exception:
pass
def _task_files(task: dict[str, Any]) -> list[str]:
"""任务当前关联的可清理文件路径(交付 ZIP/交付副本;缓存文件不在此列)"""
result = task.get('result') or {}
files = []
for f in (result.get('zip_path'), result.get('file_path')):
if f:
files.append(f)
return files
def _settle_batch_billing(task_id: str, ok_count: int, final_status: str = 'settled') -> int:
"""批量任务结算(走持久账本,按实际预扣-实际交付幂等退款;
重复调用/重启后调用都只会退一次)"""
try:
refund = auth_system.ledger_settle(task_id, ok_count, final_status)
except Exception:
# 本函数多在异常处理器内被调用——结算再抛错(库忙超时)
# 会杀死 worker 收尾路径,跳过终态标记;失败留周期对账兜底(幂等)
api_service.logger.error(
f"批量结算异常(留对账兜底): {task_id}\n{traceback.format_exc()}")
return 0
if refund:
try:
with DOWNLOAD_TASKS_LOCK:
t = DOWNLOAD_TASKS.get(task_id) or {}
uid = int(t.get('billing_user_id') or 0)
u = auth_system.get_user_by_id(uid) if uid else None
uname = u['username'] if u else 'system'
remaining = u['duration_seconds'] if u else '?'
auth_system.add_log('success', uname, 'system', '批量退费',
f'失败部分 {refund} 次已退回,剩余 {remaining} 次')
except Exception:
pass
return refund