# -*- coding: utf-8 -*- """下载/播客任务登记。 与原实现的唯一差异:不依赖请求上下文(flask.g),请求级账本键由 Billing 显式携带。 其余(归属、容量闸、task_id 白名单与碰撞换新、账本承接与撞名重试)逐条对齐。 """ import logging import re import uuid from collections.abc import Callable from typing import Any from core.config import MAX_ACTIVE_TASKS_PER_USER, MAX_TERMINAL_TASKS, MAX_TOTAL_TASKS from core.state import DOWNLOAD_TASKS, DOWNLOAD_TASKS_LOCK from web.billing import Billing logger = logging.getLogger('web.tasks') _TASK_ID_RE = re.compile(r'^[A-Za-z0-9_-]{1,32}$') async def register_download_task(billing: Billing, task: dict[str, Any], task_id_hint: str = '') -> str | None: """任务登记:失败返回错误描述,成功返回 None。task['task_id'] 为最终 id。""" user_id = billing.user.id task['user_id'] = user_id task['billing_reserved'] = billing.need with DOWNLOAD_TASKS_LOCK: tid = (task_id_hint or '').strip() or uuid.uuid4().hex[:12] if not _TASK_ID_RE.match(tid): tid = uuid.uuid4().hex[:12] while tid in DOWNLOAD_TASKS: tid = uuid.uuid4().hex[:12] active = sum(1 for t in DOWNLOAD_TASKS.values() if t.get('user_id') == user_id and t.get('status') in ('queued', 'running', 'paused')) if active >= MAX_ACTIVE_TASKS_PER_USER: return f'您有 {active} 个任务正在进行,请等待完成或取消后再提交' active_total = sum(1 for t in DOWNLOAD_TASKS.values() if t.get('status') in ('queued', 'running', 'paused')) if active_total >= MAX_TOTAL_TASKS: return '当前任务队列已满,请稍后再试' if len(DOWNLOAD_TASKS) - active_total >= MAX_TERMINAL_TASKS: return '当前任务队列已满,请稍后再试' task['task_id'] = tid DOWNLOAD_TASKS[tid] = task # 账本承接(撞名重试) try: renamed = await billing.rename_to(tid) retries = 0 while not renamed and retries < 5: retries += 1 with DOWNLOAD_TASKS_LOCK: DOWNLOAD_TASKS.pop(tid, None) tid = uuid.uuid4().hex[:12] while tid in DOWNLOAD_TASKS: tid = uuid.uuid4().hex[:12] task['task_id'] = tid DOWNLOAD_TASKS[tid] = task renamed = await billing.rename_to(tid) if not renamed: raise RuntimeError('任务账本键撞名,请重新提交') billing.ledger_key = tid except Exception: with DOWNLOAD_TASKS_LOCK: if DOWNLOAD_TASKS.get(tid) is task: DOWNLOAD_TASKS.pop(tid, None) raise return None async def start_task(task_id: str, worker: Callable, args: tuple = ()) -> str | None: """启动 worker 线程(复用 services 的 _start_task_thread,失败收敛终态并退费)。""" import asyncio import services.runtime as services return await asyncio.to_thread(services._start_task_thread, task_id, worker, args)