fix(library): 跨源同步按目标范围改写并校验残留 #13
Reference in New Issue
Block a user
Delete Branch "fix/library-cross-scope-sync-residue"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
真实事故:源 KB(10000/0c84a098) 同步到目标 KB(10001/e64e82b5) 时,归档行被原样插入目标库(落在来源租户/知识库),资源阶段因按目标租户查不到刚写入的资源而失败,回滚只按目标范围回放 → 页面报"已恢复到预备份"但目标残留 67 行。
本 PR:写入与校验共用 retargetRow(tenant/kb/资源定位符/后端/provider/embedding 模型),行身份不符即拒写;Verify 增加来源残留判定;Restore 增加整库指纹比对与孤儿资源清理,回滚后强制 Verify(不通过即 rollback_failed,不再放开维护);新增 cmd/libraryresidue 做可备份可复核的残留清理。
验证:make verify 15 项全绿;真实 PG+ES 用例复现"源目标 KB 不同+目标非空+资源阶段失败",断言回滚无残留、其它租户未动、恢复后=预备份。
全新 WeKnora 会用空映射({})先建好索引,范围字段等第一次写入才动态生成。 首次同步在读目标当前状态时就报 "missing release scope fields" 并失败,等于 "往空的生产实例做第一次同步"这条路根本走不通。 现在:"没有范围字段 + 索引确实 0 篇文档" = 空基线;有文档却看不出归属仍然报错 (绝不把无法归属的文档当空库覆盖)。Count 与整库导出共用同一判断。 试过:make verify 15 项全绿;新增空/非空两种索引的用例;真实 sync 复现原错误。上游 task_dead_letters 没有解决标记,purge/DELETE 会销毁"失败过"的唯一证据。 治理侧新增解除记录(迁移 073):绑定实时行逐字节指纹、重放引用、源侧观测摘要与 操作人;行被改过、重放后又出现新死信、源侧未结清都不命中,封存照旧阻断。 指纹先压紧空白再摘要,避免 PG to_jsonb::text 与 json.Marshal 的空白差异。 试过:go test ./internal/{librarydeadletter,libraryagent,httpapi,releaseadapter/v071, platform/postgres},make verify 15 项全绿。storage://<id> 是来源实例 storage_backends 的主键;只换桶不改 id 时,目标原版 应用以 storage backend not found 落回默认服务并拒绝整条路径(真实下载 500 code1007)。写入前按租户+目标桶解析目标后端,写入与校验共用同一份行变换; 解析不到就拒绝写入,不再产出目标读不出来的定位符。 试过:目标实例真实同步后 /api/v1/knowledge/{id}/download 读回 500 → 修后复测; go test ./internal/releaseadapter/v071 与 make verify 15 项全绿。原版应用先按 resources.storage_backend_id 找文件服务;只改定位符不改这一列, 目标仍报 storage backend not found 并拒绝整条路径(真实下载 500 code1007)。 行变换同时改写该列,写入与校验共用。 试过:目标同步后 /api/v1/knowledge/{id}/download 500 → 修后复测读回 507B 且 sha256 与源对象一致;go test ./internal/releaseadapter/v071 与 make verify 全绿。同步目标与封存源常常不是同一个实例(kgseal vs kgmaint);用目标名去绑来源死信, 真实观测的 project 永远对不上,解除会被一律拒绝。新增 LIBRARY_SOURCE_INSTANCE, 留空回落到目标实例名,API 写记录与 worker 取指纹共用同一个来源实例名。 试过:真实 API GET/POST、go test ./internal/{httpapi,appconfig} 与 make verify 全绿。页面上的"可发起封存"原来只判 LIBRARY_AGENT_SOCKET 非空:正式栈的 API 根本没挂 那个 socket,而 agent 曾顶着正式单元名冻结候选实例——配置非空、页面可点、真发起 却封错库。 现在:agent 新增 health 动作(不占封存操作锁、不跑子进程,只回答身份/降级/忙), worker 每 30 秒探测并写入迁移 074 的 library_executor_status;overview、快照列表与 POST /library/captures 共用同一份判定(过期、不可达、实例不符、降级、正忙都拒绝并 给原因)。封存结果由 agent 附上自报身份,worker 在停写入者之前再探测一次、执行后 用结果复核,不一致就不登记测试封存——心跳只能证明"刚才"。 期望身份用 LIBRARY_SOURCE_*(封存源),不是 LIBRARY_TARGET_*(同步目标),否则 正确的执行器会被判成不匹配。另加 GET /library/restore-availability:恢复能否执行由 双闸、该快照的恢复授权与实例维护状态决定,页面不再写死 disabled。 真跑(候选隔离栈,两处 socket 隔离不变):overview 返回 executor {kgmaint-weknora/kgmaint-app};停 agent 后转不可达并 503 带原因;重启后恢复。