Commit 084d17160292c9a82303ce3ae89e2a888d2b0dab
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>
Showing
2 changed files
with
45 additions
and
4 deletions
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 "sport = :8080" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u) | @@ -98,8 +119,13 @@ PID=$(ss -lptn "sport = :8080" | grep -o "pid=[0-9]*" | 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("已保存", j["size_bytes"], "字节,峰值显存", j["peak_vram_gb"], | @@ -123,11 +123,26 @@ print("已保存", j["size_bytes"], "字节,峰值显存", j["peak_vram_gb"], | ||
| 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 装卸开销**,批量跑同一种模式更快。 |