Commit 084d17160292c9a82303ce3ae89e2a888d2b0dab

Authored by yibo
1 parent b69415c6

补文档:api.py 落盘持久化说明 + console 播放/下载三个坑的记录

API.md 加「任务记录会落盘」一节,讲清 jobs_gpu{N}.sqlite3 是什么、
in-flight 状态为什么没法恢复、只影响直连 api.py/gateway.py 的场景。

h3-console/README.md 补上用户反馈"播放/下载好像有问题"后排查出的三个坑
(download/poll 字段抄错 id、5 秒轮询冲掉播放中的 video、下载接口不支持
Range),以及回滚一节更新——这次的持久化改动碰了 api.py 本身,不再是
"kill console 就完事"。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
h3-console/README.md
@@ -48,6 +48,27 @@ supervisor 30 秒后选中一个空闲 ref2va 后端 flip 成 fl2va → reconcil @@ -48,6 +48,27 @@ supervisor 30 秒后选中一个空闲 ref2va 后端 flip 成 fl2va → reconcil
48 被搜索的那串文本,`pkill -f` 把自己所在的 shell 一起杀了,SSH 连接直接断,命令链后半截全没执行。 48 被搜索的那串文本,`pkill -f` 把自己所在的 shell 一起杀了,SSH 连接直接断,命令链后半截全没执行。
49 之后全程改用 `ss -lptn "sport = :PORT" | grep -o "pid=..."` 按端口精确定位再 kill,不再用 `pkill -f`。 49 之后全程改用 `ss -lptn "sport = :PORT" | grep -o "pid=..."` 按端口精确定位再 kill,不再用 `pkill -f`。
50 50
  51 +### 播放/下载查出来的三个坑(用户反馈"播放和下载好像有问题"后排查,均已修)
  52 +
  53 +1. **`download`/`poll` 字段抄了 gateway 内部 id**:`GET /v1/videos/{id}` 原样带回 `api.py`/`gateway.py`
  54 + 响应里的这两个字段,值是 gateway 内部那个 id,不是 console 自己对外用的 id——直接用这两个字段
  55 + 拼链接会 404。页面本身没受影响(前端一直是自己用 `j.id` 拼的 URL,没读这两个字段),但接口本身
  56 + 是错的。`_row_view()` 里统一改成用 `row['id']`(console 自己的 id)重新拼。
  57 +2. **详情页每 5 秒轮询会把播放到一半的 `<video>` 冲掉**:`refreshDetail()` 是整段 `innerHTML` 重建,
  58 + 任务到了 `done`/`failed` 这种终态之后还继续每 5 秒重建一次,会把正在播放的 `<video>` 元素连根
  59 + 拔掉重建,播放进度弹回 0。改成只有任务还在 `queued`/`running` 时才继续拉详情,终态之后不再重建。
  60 +3. **下载接口不支持 HTTP Range,`<video>` 播放前的探测请求拿不到有效响应**:之前经 `gateway.py`
  61 + 转发(`StreamingResponse(urlopen(...))`),Range 请求一律回 200 而不是 206,也没有
  62 + `Content-Length`/`Accept-Ranges`——`<video>` 标签播放前会先探一次 Range 拿时长,探不到就卡在
  63 + `readyState=0` 播不出来(用 `curl`/页面里直接 `fetch()` 都测不出来,因为它们不做这个探测)。
  64 + 改成绕过 gateway 这层转发,直接连去真正跑这条任务的后端(`api.py` 自己是 Starlette
  65 + `FileResponse`,原生支持 Range),把状态码/头原样带回来。
  66 +
  67 + 顺带加了个**磁盘兜底**:如果目标后端进程已经重启、忘了这条任务(`api.py` 当时 `JOBS` 还没
  68 + 落盘,2026-08-20 之后这个根因已经在 `api.py` 修了,见 `minimax-h3/API.md`「任务记录会落盘」),
  69 + console 会用自己 sqlite 里存的 `file` 路径直接读磁盘发送——两台机器是同一台,文件一直都在,
  70 + 只是那个进程的内存不认得这个 id 了。根因修了之后这层兜底退化成第二道保险,不是唯一防线。
  71 +
51 ## 怎么起(本地开发) 72 ## 怎么起(本地开发)
52 73
53 ```bash 74 ```bash
@@ -98,8 +119,13 @@ PID=$(ss -lptn &quot;sport = :8080&quot; | grep -o &quot;pid=[0-9]*&quot; | cut -d= -f2 | sort -u) @@ -98,8 +119,13 @@ PID=$(ss -lptn &quot;sport = :8080&quot; | grep -o &quot;pid=[0-9]*&quot; | cut -d= -f2 | sort -u)
98 119
99 ## 回滚 120 ## 回滚
100 121
101 -console 是独立进程,不碰 `gateway.py`/`api.py` 一行代码,出问题直接 `kill` console 进程即可,  
102 -四个 H3 后端和 gateway 不受影响(不像 `deploy-scripts/` 那套需要镜像级回滚)。 122 +console 本身是独立进程,出问题直接 `kill` console 进程即可,不影响四个 H3 后端和 gateway。
  123 +
  124 +⚠️ **2026-08-20 起这条不再完全成立**:`api.py` 加了任务记录落盘(见 `minimax-h3/API.md`
  125 +「任务记录会落盘」),这是碰了 `api.py` 本身的改动,不属于 console 这一层。要回滚这部分,
  126 +需要把服务器上的 `/data/models/minimax-h3/api.py` 换回旧版本,再按「只重启空闲的那张卡」的
  127 +纪律逐张滚动重启四个后端(参考本文档「部署过程中揪出的两个真实 bug」同样的操作方式)——
  128 +不能像 console 自己的改动那样一个 `kill` 就完事。
103 129
104 ## 已知限制 130 ## 已知限制
105 131
minimax-h3/API.md
@@ -123,11 +123,26 @@ print(&quot;已保存&quot;, j[&quot;size_bytes&quot;], &quot;字节,峰值显存&quot;, j[&quot;peak_vram_gb&quot;], @@ -123,11 +123,26 @@ print(&quot;已保存&quot;, j[&quot;size_bytes&quot;], &quot;字节,峰值显存&quot;, j[&quot;peak_vram_gb&quot;],
123 123
124 这两条不通过时**不会**给你一个"成功但没用"的文件。 124 这两条不通过时**不会**给你一个"成功但没用"的文件。
125 125
  126 +## 任务记录会落盘(2026-08-20 起)
  127 +
  128 +`JOBS` 曾经只在内存里,进程重启(比如 `h3-console` 弹性调度器切 fl2va/ref2va 分区时的
  129 +kill+restart)就把这张表清空——视频文件在 `outputs/` 没事,但 id→文件路径这层映射没了,
  130 +`GET /v1/videos/{id}`、下载都会 404。现在改成每次状态变化都往 sqlite 写一份:
  131 +
  132 +- 落盘文件是 **`jobs_gpu{GPU_ID}.sqlite3`**(跟 `api.py` 同目录),四张卡各一份,不共享——
  133 + 一个进程只会在同一张 GPU 上起来,per-GPU 分文件天然不会有另一个进程来抢,不用处理
  134 + 跨进程写锁竞争。
  135 +- 进程启动时会把这个文件读回内存,日志能看到 `[api] 从磁盘恢复了 N 条历史任务记录`。
  136 +- **in-flight(`queued`/`running`)的记录没法真正恢复**——GPU 上下文和排队队列本来就不落盘,
  137 + 重启后没有任何东西能"接着跑"。这类记录读回来会被**改判为 `failed`**,`error` 字段写明
  138 + "进程重启导致这条任务的执行状态丢失"。只有 `done`/`failed` 这两个终态是完整保留的。
  139 +- 只影响**直接调用 `api.py`/`gateway.py` 的场景**——如果你是通过 `h3-console`(端口 8080)
  140 + 提交的任务,即使碰到上面这种真正没法恢复的 in-flight 丢失,console 自己也存了一份
  141 + 影子记录,历史和已完成任务的文件不受影响。
  142 +
126 ## 已知限制 143 ## 已知限制
127 144
128 - **无鉴权**,和这台机器上其他服务一致。放到公网前必须加。 145 - **无鉴权**,和这台机器上其他服务一致。放到公网前必须加。
129 - **单 worker 串行**。要并发就每张空闲卡起一个实例(不同 PORT),自己在上层做负载。 146 - **单 worker 串行**。要并发就每张空闲卡起一个实例(不同 PORT),自己在上层做负载。
130 -- 任务状态存在**内存**里,进程重启就丢;已生成的 mp4 文件还在 `outputs/`。  
131 - 真要持久化就把 `JOBS` 换成 sqlite——目前按需再说。  
132 - `turbo=false` 时会 `clear_lora()`,任务结束后再挂回来。**混合提交 turbo/非 turbo 时 147 - `turbo=false` 时会 `clear_lora()`,任务结束后再挂回来。**混合提交 turbo/非 turbo 时
133 每次切换都有 LoRA 装卸开销**,批量跑同一种模式更快。 148 每次切换都有 LoRA 装卸开销**,批量跑同一种模式更快。