修复转图浏览器预热跨事件循环失效问题

变更项:\n1. 新增 async_job 启动钩子能力 add_startup_job,在调度器事件循环中执行一次性初始化任务。\n2. 将 main.py 的 Markdown 转图预热从独立线程改为调度器 loop 内执行,确保预热实例可被后续任务复用。\n3. 增强 markdown_to_image 常驻浏览器管理:记录 owner loop、检测跨 loop 复用并自动重建。\n4. 补充预热与常驻浏览器日志,输出 loop 标识和浏览器 PID,便于线上排查进程状态。\n5. 保持现有转图超时与重试逻辑不变,仅修复预热生效链路与可观测性。
This commit is contained in:
liuwei
2026-04-17 09:55:03 +08:00
parent c39b3ba566
commit 3b9bd02b5f
3 changed files with 84 additions and 11 deletions

View File

@@ -19,6 +19,9 @@ class AsyncJob:
def __init__(self):
self._jobs: Dict[str, Dict[str, Any]] = {}
self._running_tasks: Dict[str, asyncio.Task] = {}
# 启动钩子任务:在调度器事件循环就绪后仅执行一次。
# 典型场景:浏览器预热、外部连接预热等需要“与调度器同一事件循环”执行的初始化逻辑。
self._startup_jobs: List[Dict[str, Any]] = []
self._running = False
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._stop_event: Optional[asyncio.Event] = None
@@ -428,6 +431,33 @@ class AsyncJob:
job_key=job_key,
)
def add_startup_job(self, func: Callable, name: Optional[str] = None):
"""注册调度器启动钩子。
关键语义:
1. 只在 `run_all` 对应的事件循环中执行;
2. 每次调度器启动最多执行一次;
3. 支持同步函数和异步函数。
"""
if not callable(func):
raise ValueError("startup job 必须是可调用对象")
display_name = str(name or getattr(func, "__name__", "") or "startup_job").strip()
with self._lock:
self._startup_jobs.append(
{
"name": display_name,
"func": func,
"done": False,
}
)
async def _run_startup_job(self, startup_job: Dict[str, Any]):
"""执行单个启动钩子,并吞掉异常,避免影响主调度循环。"""
func = startup_job.get("func")
result = func()
if inspect.isawaitable(result):
await result
def set_job_enabled(self, job_id: str, enabled: bool) -> Tuple[bool, str]:
with self._lock:
job = self._jobs.get(job_id)
@@ -575,10 +605,25 @@ class AsyncJob:
self._loop = asyncio.get_running_loop()
self._stop_event = asyncio.Event()
job_ids = list(self._jobs.keys())
startup_jobs = list(self._startup_jobs)
for job_id in job_ids:
self._start_job_in_loop(job_id)
# 启动钩子采用“并发后台执行”策略,避免阻塞调度循环。
# 失败不会中断 run_all由各钩子自身负责记录日志。
for startup_job in startup_jobs:
if startup_job.get("done"):
continue
async def _runner(job_entry=startup_job):
try:
await self._run_startup_job(job_entry)
finally:
job_entry["done"] = True
asyncio.create_task(_runner(), name=f"async_job:startup:{startup_job.get('name', 'job')}")
await self._stop_event.wait()
def stop_all(self):