From 16dffe0a5210969251715dabdb0abb163ea26b17 Mon Sep 17 00:00:00 2001 From: yibo Date: Wed, 9 Sep 2026 12:10:11 +0800 Subject: [PATCH] raylight: 重启不再让排队/运行中的任务永久失败 --- raylight/notes.md | 26 ++++++++++++++++++++++++++ raylight/scripts/server.py | 32 +++++++++++++++++++++++++++----- raylight/server.py | 32 +++++++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/raylight/notes.md b/raylight/notes.md index 9c7d296..72cab58 100644 --- a/raylight/notes.md +++ b/raylight/notes.md @@ -484,3 +484,29 @@ capacity of 21.48 GiB of which 262.25 MiB is free. ... this process has 20.26 Gi 所有真实OOM都发生在GPU0(跟之前的结论一致:GPU0同时扛文本编码器+DiT分片+这块的attention buffer,是四张卡里最先顶到天花板的一张),而且都在提交后30秒内就报错——说明失败发生在采样 刚开始分配显存的时候,不是跑到一半才炸,比较干脆,不存在"跑了很久才失败"的浪费。 + +## 2026-09-09 — `/health`排队计数修正 + 重启不再让排在后面的任务永久失败 + +**背景**:`/health`的`queued`字段一直用`Q.qsize()`——取消一条排队中的任务只是把`JOBS`里 +那条的`status`标成`cancelled`,不会把它从`Q`摘除,`worker()`要等真正`Q.get()`到它才跳过 +丢弃。堆量取消场景下`qsize()`会把这些还没被捞到的"僵尸"entry也算进去,实测显示"排队253", +但直接读`jobs.sqlite3`统计真实`status=='queued'`只有137条,差了115条。改成跟`running`同 +口径,直接数`JOBS`里`status=='queued'`的条目。 + +**推翻的假设**:`_load_jobs()`里`status=='running'`的任务之前直接判`failed`,注释写死 +"无法恢复"。**这次实际踩了一次坑才发现这个假设站不住**:为了不打断当前正在跑的任务,等它 +跑完再重启——但下一条排队任务在当前任务跑完的瞬间就被`worker()`零延迟捞起转成`running`, +这个时间窗口比manual操作的反应速度短得多,重启命令和"抢下一条任务"几乎同时发生,结果新 +running的这条被这段逻辑直接判死。**"无法恢复"这个结论本身就是错的**——请求的原始参数 +(`req`)完好无损,跟用户手动点"重跑"没有本质区别,没理由不能自动退回`queued`重跑一遍。 + +**修复**:`status=='running'`时改成自动退回`queued`(清空`started_at`/`step`/`total_steps`/ +`comfy_prompt_id`等本次执行的残留状态,保留原`queue_seq`不打乱排队顺序),新增`Job. +restart_recoveries`计数,封顶`MAX_RESTART_RECOVERIES=2`——超过上限才老实判`failed`,避免 +某条请求本身有问题(比如参数组合注定OOM)每次一跑就把进程带崩,陷入"重启→判running→ +退回queued→再跑崩"的无限循环拖垮整条队列。 + +**没有做的事**:曾经考虑过更复杂的方案——重启前加一个"暂停"开关阻止worker摘取下一条排队 +任务,等到真正`running==0`再重启,这样能省掉被打断那条任务已经跑掉的GPU时间(不用整个 +重跑)。这次先没做,因为`restart_recoveries`已经能保证"不会永久失败"这个底线;真要再优化 +"重启不浪费已跑的算力",可以在这个基础上加暂停开关,属于锦上添花,不是这次的必需项。 diff --git a/raylight/scripts/server.py b/raylight/scripts/server.py index a8e57b4..7021bd1 100644 --- a/raylight/scripts/server.py +++ b/raylight/scripts/server.py @@ -39,6 +39,7 @@ COMFY_WS = f"ws://{COMFY_HOST}:{COMFY_PORT}/ws" # 没有这个字段就会一直认为任务"还没分配",播放/下载会被 409 拒绝——踩过这个坑,2026-08-22。 SERVER_PORT = os.environ.get("PORT", "8100") BACKEND_URL = f"http://127.0.0.1:{SERVER_PORT}" +MAX_RESTART_RECOVERIES = 2 # 见 _load_jobs 里 status=='running' 分支的注释 ROOT = Path("/data/models/raylight/ComfyUI") INPUT_DIR = ROOT / "input" @@ -166,6 +167,7 @@ class Job(BaseModel): comfy_prompt_id: Optional[str] = None backend: str = BACKEND_URL queue_seq: int = 0 # 2026-09-03: 排队顺序+插队用,见 _queue_position/prioritize_video + restart_recoveries: int = 0 # 2026-09-09: 见 _load_jobs 里 status=='running' 分支的注释 JOBS: dict[str, Job] = {} @@ -230,11 +232,31 @@ def _load_jobs(): _persist(j) Q.put((j.queue_seq, j.id)) elif j.status == "running": - j.status = "failed" - j.error = "进程重启导致这条任务的执行状态丢失,无法恢复" - j.finished_at = time.time() - _persist(j) # 2026-08-23: 之前只改内存不落盘,sqlite里的status永远停在running/queued, - # 下次重启会被这段逻辑重新"发现"一遍、把人工写的详细错因覆盖回这句通用文案——必须写回去 + # 2026-09-09: 之前直接判failed,"无法恢复"——但这个结论只是"当前进度丢了",不代表 + # 请求本身有问题,跟用户手动点"重跑"没有本质区别。改成自动退回queued重新跑一遍, + # 不用等人工发现一条卡死的failed记录再手动重试(真实事故:等一条任务跑完再重启, + # 下一条排队任务在重启瞬间被worker捞起转成running,紧接着被这段逻辑判死)。 + # restart_recoveries封顶重试次数——防止某条请求本身有问题(比如注定OOM),每次 + # 一跑就把服务带崩,会陷入"重启->判running->退回queued->再跑崩"的无限循环, + # 拖慢/堵死整条队列;超过上限就老实判failed,等人工看一眼。 + if j.restart_recoveries < MAX_RESTART_RECOVERIES: + j.restart_recoveries += 1 + j.status = "queued" + j.started_at = None + j.step = 0 + j.total_steps = 0 + j.comfy_prompt_id = None + if j.queue_seq == 0: + j.queue_seq = int(j.created_at * 1e9) + _persist(j) + Q.put((j.queue_seq, j.id)) + else: + j.status = "failed" + j.error = (f"进程重启导致这条任务的执行状态丢失,已自动重试 " + f"{MAX_RESTART_RECOVERIES} 次仍未跑完,不再自动重试") + j.finished_at = time.time() + _persist(j) # 2026-08-23: 之前只改内存不落盘,sqlite里的status永远停在running/queued, + # 下次重启会被这段逻辑重新"发现"一遍、把人工写的详细错因覆盖回这句通用文案——必须写回去 JOBS[j.id] = j conn.close() diff --git a/raylight/server.py b/raylight/server.py index a8e57b4..7021bd1 100644 --- a/raylight/server.py +++ b/raylight/server.py @@ -39,6 +39,7 @@ COMFY_WS = f"ws://{COMFY_HOST}:{COMFY_PORT}/ws" # 没有这个字段就会一直认为任务"还没分配",播放/下载会被 409 拒绝——踩过这个坑,2026-08-22。 SERVER_PORT = os.environ.get("PORT", "8100") BACKEND_URL = f"http://127.0.0.1:{SERVER_PORT}" +MAX_RESTART_RECOVERIES = 2 # 见 _load_jobs 里 status=='running' 分支的注释 ROOT = Path("/data/models/raylight/ComfyUI") INPUT_DIR = ROOT / "input" @@ -166,6 +167,7 @@ class Job(BaseModel): comfy_prompt_id: Optional[str] = None backend: str = BACKEND_URL queue_seq: int = 0 # 2026-09-03: 排队顺序+插队用,见 _queue_position/prioritize_video + restart_recoveries: int = 0 # 2026-09-09: 见 _load_jobs 里 status=='running' 分支的注释 JOBS: dict[str, Job] = {} @@ -230,11 +232,31 @@ def _load_jobs(): _persist(j) Q.put((j.queue_seq, j.id)) elif j.status == "running": - j.status = "failed" - j.error = "进程重启导致这条任务的执行状态丢失,无法恢复" - j.finished_at = time.time() - _persist(j) # 2026-08-23: 之前只改内存不落盘,sqlite里的status永远停在running/queued, - # 下次重启会被这段逻辑重新"发现"一遍、把人工写的详细错因覆盖回这句通用文案——必须写回去 + # 2026-09-09: 之前直接判failed,"无法恢复"——但这个结论只是"当前进度丢了",不代表 + # 请求本身有问题,跟用户手动点"重跑"没有本质区别。改成自动退回queued重新跑一遍, + # 不用等人工发现一条卡死的failed记录再手动重试(真实事故:等一条任务跑完再重启, + # 下一条排队任务在重启瞬间被worker捞起转成running,紧接着被这段逻辑判死)。 + # restart_recoveries封顶重试次数——防止某条请求本身有问题(比如注定OOM),每次 + # 一跑就把服务带崩,会陷入"重启->判running->退回queued->再跑崩"的无限循环, + # 拖慢/堵死整条队列;超过上限就老实判failed,等人工看一眼。 + if j.restart_recoveries < MAX_RESTART_RECOVERIES: + j.restart_recoveries += 1 + j.status = "queued" + j.started_at = None + j.step = 0 + j.total_steps = 0 + j.comfy_prompt_id = None + if j.queue_seq == 0: + j.queue_seq = int(j.created_at * 1e9) + _persist(j) + Q.put((j.queue_seq, j.id)) + else: + j.status = "failed" + j.error = (f"进程重启导致这条任务的执行状态丢失,已自动重试 " + f"{MAX_RESTART_RECOVERIES} 次仍未跑完,不再自动重试") + j.finished_at = time.time() + _persist(j) # 2026-08-23: 之前只改内存不落盘,sqlite里的status永远停在running/queued, + # 下次重启会被这段逻辑重新"发现"一遍、把人工写的详细错因覆盖回这句通用文案——必须写回去 JOBS[j.id] = j conn.close() -- libgit2 0.21.4