网易首页 > 网易号 > 正文 申请入驻

为什么索引永远停在“Preparing… 0/0”?一个没有出口的状态机修复实录

0
分享至

“Preparing… 0/0”——屏幕上的这行提示看起来像是一个正在推进的进度,但当你等了很久,它依然纹丝不动。

这就是 TextStack 的用户在点击“Ask this book”时遇到的真实场景。对于某些上传的书籍,界面就这样永远卡在了准备阶段,既不会报错,也不会完成,重试按钮也毫无作用,唯一的恢复手段竟然是开发者手动去数据库里改动记录。


这个开源技术书籍阅读器的 RAG(检索增强生成)索引流程里,藏着一个状态机设计上的经典反例:一个一旦进入就无法靠自身逻辑离开的状态,一个看起来像“进程”的“黑洞”。修复它的过程,恰好是一堂关于异步任务、原子操作和韧性设计的实操课。

要理解这个 bug 的成因,得先回到旧版索引的请求流程。当用户点击索引按钮,前端发出一个 POST /me/books/{id}/index 请求,事情就开始了。但在旧代码里,这个端点承担了太多职责:

POST /me/books/{id}/index  -> 将 rag_status 设置为 Indexing        (在端点内立即执行)  -> 接着直接执行整个文档分块和视觉解析     (仍在同一个 HTTP 请求内)  -> 所有工作完成后才返回 202

问题就出在“工作直接跑在 HTTP 请求里”和“状态切换先于工作完成”这两个设计上。TextStack 为了支持 PDF 上传,会调用付费的视觉解析服务把每一页转换成可检索的文本,这个过程耗时以分钟计。Cloudflare 等中间代理会在超时后就切断连接,API 容器在每次部署时也会重启,无论哪种情况,正在执行解析的进程都会被悄无声息地杀死。而此时数据库的那一行记录已经变成了:

rag_status = Indexing, rag_chunk_count = 0

接着,更致命的限制来了。因为重试路径只会认领状态为 NotIndexedFailed 的行,所以在代码逻辑里,一条正在“Indexing”的记录永远不可能被重试

// 旧的 claim 逻辑:Indexing 状态的行无法被认领if (book.RagStatus is RagIndexStatus.NotIndexed or RagIndexStatus.Failed) { ... }

结果就是,当某本书因为请求中断、容器重启等意外停在中途时,“chunk 数为 0 的 Indexing”成了一个没有出口的状态:它不是“未索引”,所以不会被端点再次受理;它不是“失败”,所以重试机制跳过它;它也不是“已完成”,所以用户永远看不到答案。一个看起来像“正在准备”的假象,成了前端永远刷不走的死胡同。

修复的思路非常清晰:让端点只负责转换状态并快速返回,将实际工作交给后台工作进程,同时让每一个状态最终都能到达一个“终局”(完成或失败)。具体的改造分三步实现。

第一步,解耦 HTTP 路径和工作负载。端点接到请求后,只做一件事:把对应书籍的 rag_status 置为 Indexing,然后立刻返回 202。任何付费的视觉解析都不再占用请求线程,Cloudflare 的超时杀死、容器重启丢进程等风险被完全隔离在管道之外。

第二步,引入后台 Worker 并用原子更新实现无锁抢单。为了避免引入消息队列等额外基础设施,直接利用 PostgreSQL 的单条 UPDATE 做工作认领。Worker 的 SQL 像这样:

var claimed = await db.Database.ExecuteSqlInterpolatedAsync($"""    UPDATE user_books    SET rag_indexing_started_at = now()    WHERE id = {bookId}      AND rag_status = 1              -- Indexing      AND rag_chunk_count = 0      AND rag_indexing_started_at IS NULL;    """, ct);if (claimed == 0) return;             // 已经被别人领走了,直接退出

这里的并发控制完全交给了数据库的 WHERE 条件。只有那些状态确为 Indexing、chunk 数为 0 且尚未被任何 Worker 打上时间戳的行,才会被更新。如果多个 Worker 同时尝试认领同一行,PostgreSQL 会立刻将 UPDATE 序列化,最终只有一条语句能够实际影响一行,其余的 claimed 返回 0 并自动退出。没有锁表、没有复杂的分布式协调,数据库本身就是公正的裁判。

第三步,增加一个周期性清理任务来回收“死行”。即使有了 Worker 机制,仍然可能发生 Worker 进程意外被杀的情况,从而产生与之前类似的“卡死”行。清理逻辑会定期扫描那些 Indexing 状态且 rag_indexing_started_at 超时仍未完成的行,将其状态重置为 NotIndexed,让下一次端点请求或 Worker 周期调度能重新捡起。

三个动作组合在一起,就形成了一个首尾相接的闭环:端点只负责标记“你要被处理了”,Worker 通过原子更新抢走任务并务必终结为完成或失败,清扫进程则包揽所有中途夭折的“僵尸”记录,把它们推回起点。这时候再回顾当初那个“没有出口”的设计,会发现根源其实是状态机设计中常见的陷阱:有了中间态,却没有覆盖所有可能的路径将其转换为终态。看上去多了一个状态,实际上多了一条可以一直流连进去的死胡同。

对开发者而言,这个案例也提供了一个低成本的实践参照:异步处理不一定需要引入 Redis、Kafka 等独立组件,一次数据库的原子更新配合后台服务,就能解决很多因长任务超时和中断带来的状态不一致问题。当然,这意味着每一列的含义必须足够精确,WHERE 条件必须能确切地挑出尚未被认领的任务,并且终态必须有兜底机制。TextStack 的这次修复就展示了:即使是最简单的“准备中”状态,也需要一条明明白白的出口路径——要么完成,要么失败,绝不能永久地停留在 0/0 的幻影里。

全文对应的具体代码改动可以在 TextStack 仓库 中的 RagIndexingService.csRagIndexingWorker.cs 两个文件找到。如果你也在构建类似的异步任务流水线,不妨看看那个 SQL WHERE 条件——一个简单的判断,也许就能帮你省掉一整块中间件的复杂度。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
10万入场费的“换妻”盛宴:记者冒死暗访,组织者3年狂赚2亿

10万入场费的“换妻”盛宴:记者冒死暗访,组织者3年狂赚2亿

说点事
2026-07-16 11:29:11
詹姆斯效应炸裂!2大冠军后卫欲投奔76人,湖人悔断肠

詹姆斯效应炸裂!2大冠军后卫欲投奔76人,湖人悔断肠

涵有话说
2026-07-31 09:42:46
Here we go!罗马诺:马马杜-桑加雷将以4100万镑加盟小蜜蜂

Here we go!罗马诺:马马杜-桑加雷将以4100万镑加盟小蜜蜂

懂球帝
2026-07-31 13:29:03
第九个“野莓”物流中心被炸!30%炼油能力被瘫痪!里海管道联盟又被炸

第九个“野莓”物流中心被炸!30%炼油能力被瘫痪!里海管道联盟又被炸

鹰眼Defence
2026-07-31 16:39:26
99.6%仓位,一天浮盈近28万!网红峰哥高调晒截图:7月完美收工,投资最重要的是耐心

99.6%仓位,一天浮盈近28万!网红峰哥高调晒截图:7月完美收工,投资最重要的是耐心

金融界
2026-07-31 16:22:12
局势失控,外资纷纷撤离,正副总统同台断交,印尼政坛迎来大清洗

局势失控,外资纷纷撤离,正副总统同台断交,印尼政坛迎来大清洗

凡知
2026-07-30 23:12:05
全面绞杀开始?日本突然宣布,中方亮明底线:不认错,就没得谈

全面绞杀开始?日本突然宣布,中方亮明底线:不认错,就没得谈

疯狂小菠萝
2026-07-30 21:31:20
彻底摊牌了?被中方制裁后,菲防长狂言:台海若有事菲绝不中立!

彻底摊牌了?被中方制裁后,菲防长狂言:台海若有事菲绝不中立!

战争史
2026-07-31 11:37:42
重庆市政府同意:评定龚宝冬同志为烈士

重庆市政府同意:评定龚宝冬同志为烈士

新浪财经
2026-07-31 17:57:43
谢贤遗产风波反转仅5天,张柏芝小儿子身世曝光,是我们误会了

谢贤遗产风波反转仅5天,张柏芝小儿子身世曝光,是我们误会了

生命之泉的奥秘
2026-07-31 07:38:29
56岁因凡蒂诺可能被直接弹劾!欧足联可推出新的世界杯 FIFA玩砸了

56岁因凡蒂诺可能被直接弹劾!欧足联可推出新的世界杯 FIFA玩砸了

风过乡
2026-07-31 07:18:23
特斯拉在新加坡创下交付量新高!Model Y最畅销

特斯拉在新加坡创下交付量新高!Model Y最畅销

CNMO科技
2026-07-31 18:45:12
CBA最新消息!威瑟斯庞或加盟北京首钢,上海男篮签约争议外援

CBA最新消息!威瑟斯庞或加盟北京首钢,上海男篮签约争议外援

体坛瞎白话
2026-07-31 15:19:47
上海专家发现:吃甜食的老人,健康指数是吃蔬菜的人的8倍不止?

上海专家发现:吃甜食的老人,健康指数是吃蔬菜的人的8倍不止?

霹雳炮
2025-10-17 21:31:29
定了!宇树科技8月10日申购!王兴兴将成超级巨富

定了!宇树科技8月10日申购!王兴兴将成超级巨富

EETOP半导体社区
2026-07-31 07:44:42
姆巴佩27岁牵手西班牙国宝级女星

姆巴佩27岁牵手西班牙国宝级女星

手工制作阿歼
2026-07-31 01:36:09
一群小伙赤裸上身吃烧烤,周围站着一堆民警,网友:防范太到位

一群小伙赤裸上身吃烧烤,周围站着一堆民警,网友:防范太到位

映射生活的身影
2026-07-30 20:13:00
一步慢步步慢!巴萨一年没找到莱万接班人,皇马五分钟谈妥新中锋

一步慢步步慢!巴萨一年没找到莱万接班人,皇马五分钟谈妥新中锋

体育世界
2026-07-31 18:23:16
湖人有戏了!勇士给7500万嫌少,现在1500万就签,被经纪人坑苦了

湖人有戏了!勇士给7500万嫌少,现在1500万就签,被经纪人坑苦了

你的篮球频道
2026-07-31 10:39:19
“极限施压”变“定点拆解”:特朗普加速铺平毁灭伊朗之路

“极限施压”变“定点拆解”:特朗普加速铺平毁灭伊朗之路

高博新视野
2026-07-30 18:54:02
2026-07-31 19:19:00
摸鱼算法
摸鱼算法
致力于用最前沿的AI技术,换取更多发呆时间的三十岁青年。
460文章数 62关注度
往期回顾 全部

科技要闻

DeepSeek-V4-Flash正式版API上线公测

头条要闻

价值200万设备离奇消失 民警凭一串数字"捞"出嫌疑人

头条要闻

价值200万设备离奇消失 民警凭一串数字"捞"出嫌疑人

体育要闻

欧足联掀桌!因凡蒂诺这次真玩大了?

娱乐要闻

百花奖影帝影后即将决出

财经要闻

打新日确认!宇树科技IPO冲刺“冰与火”

汽车要闻

中国车企站到了合资桌子的另一边

态度原创

亲子
家居
手机
艺术
公开课

亲子要闻

爸爸在家带娃玩手机,吃东西三不误,宝宝还没学会走路脚后跟就长茧了

家居要闻

2026建博会(广州) 公装联探展交流活动

手机要闻

存储成本超处理器!今年上半年全球手机SoC出货同比下滑15%:联发科、高通均跌幅超1/4

艺术要闻

美国银行体育场翻新,75年老球场“旧壳穿新衣”!

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版