拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Readest 同步数据丢失修复实录:整行 LWW 抹掉书籍分组与描述的根本原因链,以及 groupUpdatedAt 字段时钟修复

Readest 同步数据丢失修复实录整行 LWW 抹掉书籍分组与描述的根本原因链以及 groupUpdatedAt 字段时钟修复【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest导读本文完整还原 Readest 中两个曾被报告为独立缺陷的同步数据丢失问题——**#5911「全新安装 WebDAV 全量同步抹掉书籍分组」**与#5912「第三方同步丢失书籍描述」——如何被确认为同一根因并通过一次「字段级时钟field-clock」修复PR #5921彻底根治。你将掌握整行 LWWLast-Writer-Wins合并为什么会在updatedAt被上传等无关操作误打戳时产生不可逆的数据擦除Readest 如何用groupUpdatedAt复刻此前readingStatusUpdatedAt/coverUpdatedAt/metadataUpdatedAt的修复范式以及修复过程中「未打戳 TIE 不回退行胜者」「缺失 metadata 永不获胜」「增量同步 O(changed)」三条承重设计原则为何缺一不可。一、问题全景#5911 与 #5912 是同一个缺陷#5911全新安装后执行 WebDAV 全量同步会把已有的书籍分组book groups整组抹掉#5912第三方同步文件型同步后端会丢失书籍描述metadata.description#5910阅读器菜单对第三方同步用户显示「Never synced」属于独立问题不在本次修复范围内。文档明确记录#5911 与 #5912 是同一处缺陷的两个表象而 PR #5905 对三者一个都没有修——它只动了engine.ts、runLibrarySync.ts和useBooksSync.ts的三行merge.ts、wire.ts、transform.ts、api/sync.ts以及阅读器菜单均未触及。三个报告者运行的 0.12.1 版本也早于该 PR。二、根因分组与描述挂在整行updatedAt时钟上且是「裸覆盖赋值」核心机制是 Readest 的同步合并策略整行 LWW。groupId/groupName和metadata其中承载description都依靠整行的updatedAt判断谁新谁旧合并时用对象展开spread直接覆盖。问题的关键在updatedAt本身被与分组、描述无关的操作反复打戳。最典型的是上传src/services/cloudService.ts 中的uploadBook在每一次上传时都会执行book.updatedAt Date.now()见 L236同时打戳uploadedAt、downloadedAt、coverDownloadedAt该调用由transferManager.executeBookTransfer以队列驱动产生相隔数秒的连续时间戳——这正是报告者观察到的「19 条记录在约 8 秒内updatedAt连续递增」指纹。于是出现这样的链条一台设备上传书籍文件 → 本行updatedAt被推高 → 它持有的「从未分组的旧行」在整行比较中胜出 → 把真正做过分组编辑的另一台设备的分组覆盖掉 → 空行再被同步回每一台设备形成全舰队级擦除。transformBookFromDB永远物化groupId/groupName/metadata键列值为空时输出undefined/null所以updateLibrary里的 spread 一定会覆盖且比较用的是仅仅是时间戳持平TIE也会抹掉分组。更隐蔽的是库内其他具有同类风险的字段早就拥有了独立时钟——readingStatusUpdatedAt#4634 / 迁移 015、coverUpdatedAt#4544 / 迁移 017、metadataUpdatedAt#5438 / 迁移 018。只有分组从未获得自己的时钟而 metadata 的时钟在「旧数据未打戳」的 TIE 场景下会被绕过——pickFresherMetadata在0 0时返回null于是行级 spread 照常生效描述照样被清空。三、为什么「全新安装 WebDAV」恰好触发对只启用 WebDAV 的用户Readest Cloud 分支为何会介入答案在 src/services/sync/cloudSyncProvider.tsexport const isReadestCloudEnabled (settings) settings?.readestCloud?.enabled ?? !hasAnyThirdPartyEnabled(settings);纯 WebDAV 用户readestCloud.enabled未显式设置时推导为false其分组编辑永远不会到达 Readest Cloud云上行停留在「最后一次开启云同步时」的旧状态全新安装尚未配置任何后端推导为true登录后立即把那些陈旧的未分组行恢复下来随后 WebDAV 全量同步的索引重推index re-push把这些空行写进好行全舰队拉取到被清空的行。这条链路恰好验证了报告者自己的第 2、3 条猜测。四、探针验证在真实引擎上复现的八个场景文档记录了针对真实同步引擎运行后即删除的探针结果下表完整列出探针结果A文件同步本地未分组行 较新updatedAt→推送的 library.json 中分组被抹掉B同上但时间戳持平→ 分组仍被抹掉拉取侧shouldApplyRemoteBookMetadata是严格推送侧无条件把本地写在远端之上C书籍只存在于远端 hash 目录 → 被搁置为author: Unknown无 metadata、无分组且updatedAt: Date.now()使其胜过所有真实行engine.ts约 L965DmergeBookMetadata遇到无 metadata 的远端 → metadata 通过?? local保留正常EuseBooksSync.updateLibrary中{...localRow, ...cloudRow}在 TIE 下 →groupId: undefined、metadata: nullF推送的索引确实携带 metadata 与分组正常G本地无 metadata 的行 →抹掉 library.json 中的metadata.description#5912 传播链I完整 #5911 链路端到端BOOX 发布未分组行 → PC 上已分组的行在拉取时被覆盖五、修复落地完整字段时钟镜像 015/017/018PR #5921squash 80f196a9b按既有范式为分组补上了自己的时钟全链路共六块1. 数据库迁移 022docker/volumes/db/migrations/022_add_group_updated_at.sql 新增可空列ALTER TABLE public.books ADD COLUMN IF NOT EXISTS group_updated_at timestamp with time zone NULL;迁移文件明确声明故意不回填DELIBERATELY NOT BACKFILLED两条理由中第二条是承重级的updated_at记录的不是「分组何时被设置」它被翻页、上传随意打戳这正是缺陷本身把它抄进group_updated_at等于伪造一次从未发生过的分组编辑会把 #5911 永久复现服务端回填够不到真正坏掉的数据——#5911 报告自 WebDAV其行数据存在于各设备的 library.json 与第三方存储中任何迁移都不触及。历史数据必须在合并逻辑下安全而非依赖一次性 UPDATE否则只有 Readest Cloud 用户受益。2. 类型与双向 transformBook类型新增groupUpdatedAt见 src/utils/book.ts 的BookGroupFields与types/book.tsDBBook新增group_updated_atsrc/types/records.tssrc/utils/transform.ts 两个方向都做了转换transformBookToDB输出 ISO 字符串L102、transformBookFromDB还原为毫秒时间戳L154。3. 客户端共享解析器pickFresherGroup/bookGroupDifferssrc/utils/book.ts 的pickFresherGroup是分组合并的唯一真源同时被 Readest Cloud 原生合并useBooksSync与第三方文件同步合并services/sync/file/merge.ts复用保证两个后端解析结果一致。解析顺序如下源码注释即契约打戳不同 → 新戳胜出真实的分组编辑——包括移除分组——无论谁赢得整行都能传播#4942 契约打戳相同、一边有组一边无组 → 有组的一边胜出TIE 下「从未分组」与「被过老客户端移除了分组」无法区分而存量设备全部未打戳0 0。抹掉真实分组不可恢复丢失一次「未分组」可以恢复因此保留分组是唯一安全解读打戳相同且两边都有组 → 行胜者两个真正竞争的组回退到历史整行行为。bookGroupDiffersL303-L305做值级差异比较用于判断解析结果是否真的改变了当前分组避免纯时间戳差异引发无谓写回。4. 服务端镜像resolveGroupMerge/bookGroupChangedsrc/pages/api/sync.ts 的resolveGroupMerge是服务端同名实现语义与客户端完全一致clientRowWins对应remoteRowWinsbookGroupChangedL209-L214是传播阶段的 no-op 守卫仅时间戳不同而分组相同不得重写服务端行。在合并主流程中L768分组与阅读状态、封面、metadata 一样在自己的时钟上解析然后接入toUpdate/ 传播分支L782-L784、L820。5. 客户端合并接入点Readest Cloudsrc/app/library/hooks/useBooksSync.ts 在行级 spread 之后调用pickFresherGroup(oldBook, matchingBook, remoteRowWins)并把结果写回mergedBook随后用mergedBook.metadata mergedBook.metadata ?? oldBook.metadata ?? matchingBook.metadata兜底 #5912第三方文件同步src/services/sync/file/merge.ts 在mergeBookMetadata末尾以pickFresherGroup(local, remote, remoteMetaNewer)解析分组再以 L202 的??链保护 metadata blob。6. 全部分组变更点打戳groupUpdatedAt只在真实的分组编辑发生时被设置仓库中的四处均已覆盖GroupingModal.tsx 四处移除分组L138、重命名L160、L165、确认分组L218且明确「移除也必须打戳——未打戳的未分组行会被解读为『从未知道过该组』而在设计上输给有组的对端」ingestService.ts导入时指定分组包括空串「降级到根目录」也必须打戳useClipUrlIngress.ts剪贴板 URL 导入入库时打戳。六、承重设计选择未打戳的 TIE 不回退到行胜者这是与 metadata / status / cover 三套时钟最大的不同——它们打戳相同时回退到行胜者而分组绝不在 TIE 上有组的一边获胜。否则修复只能防止新损伤每一行存量数据都未打戳整个存量舰队会一直坏下去。代价是改变了 #4942 契约merge.test.ts中「propagates group removal」用例被重新打戳后通过而旧客户端发来的未打戳移除不再传播——这是刻意为之的 fail-safe真正的移除总是带更新的group_updated_at在第一步就能赢会输掉的只有「未打戳的移除」它本就无法与「从未分组」区分。七、#5912 的另一半缺失的 metadata blob 永不获胜文档与代码共同确认一条原则应用内没有任何代码会清空book.metadata因此缺失永远意味着「从未有过」绝不意味着「用户清除了它」。所以mergeBookMetadatamerge.ts L202、resolveMetadataMerge服务端与useBooksSync.updateLibraryL302三处都改为??兜底——缺失 blob 在任何时钟上都不允许获胜否则一个 metadata-less 的对端云书架行、发现行、旧客户端会随手抹掉全舰队的书籍描述。八、性能陷阱第一次写错了以及 O(changed) 铁律初版修复commit af4c3355f被 chrox 发现存在性能问题在 22a8841f3 中修正。问题链条无时钟的修复性合并子句让shouldApplyRemoteBookMetadata在修复后首次运行时对整个图书馆同时触发最初以为把封面/config 的 GET 用新的isRemoteBookClockNewer门控就够了——但昂贵的部分其实是写store.updateBookMetadata→useLibraryStore.updateBook→saveLibraryBooks会重写整个 library 文件外加其.bak备份每本书一次181 本书 后台同步期间 181 次全库写字节量呈二次方增长。由此确立chrox 的规则与 [[file-sync-converge-5900]] 相同增量同步默认必须 O(changed)修复属于全量同步Full Sync的职责。评估 O(changed) 时数的是本地库写入次数而非远端请求——saveLibraryBooks是整文件写任何逐书调用都是 O(library)。修复 把两个关注点拆开isRemoteBookMissingLocally修复本机书架→只在fullSync时执行resolvePublishedBook止损→无条件且免费FREE。传播完全发生在推送侧索引重推用本地行重建 library.json一个仅仅在行时钟上打平的行会把自己的空副本发布到对端之上。而「逐行对照远端索引条目做解析」是在推送本来就要遍历的 map 上纯内存完成的——无请求、无写入。它只主张一句话「发布绝不允许删除远端已拥有的东西」本地行在自己时钟上仍然获胜。最终效果增量同步永不修复、永不写入但也不再能清空对端的书架全量同步把设备自己的分组/描述放回去。resolvePublishedBook的完整实现见 src/services/sync/file/merge.ts。九、CodeRabbit 补刀发布路径的 metadata 必须按组解析CodeRabbitr3880791184fix 8211233aa指出resolvePublishedBook解析 metadata 组时必须在metadataUpdatedAt时钟上、且作为「组」整体移动title / author / tags / blob / 打戳一起而不能偏好任何非空本地 blob——否则发布行会把一台设备的书名配另一台设备的简介。该路径在两种同步策略下可达性不同silent策略下不可达远端metadataUpdatedAt更新本身就令isRemoteBookClockNewer为真reconcile 会在推送之前把解析结果应用到本地行send策略下可达且承重该策略不应用远端任何东西整个 reconcile 块被跳过发布解析器是唯一的防线。探针验证send把「Stale blurb」发布到「Newer blurb」之上silent则不会。由此沉淀一条普适规则在 PUSH/发布路径上加任何守卫都要单独在send策略下验证——canPull为 false 会跳过每个 reconcile 块那时发布路径独自承重。十、验证与测试覆盖门禁pnpm test10349 通过 / 16 跳过pnpm lint与pnpm format:check均干净三个新守卫做了 mutation 检查各自被删除时分别有 7 / 3 / 6 个用例翻红客户端解析器测试 src/tests/utils/group-clock.test.ts 覆盖新戳胜出、打戳分组输了行时钟仍保留、带戳移除传播、未打戳未分组行永不抹组#5911 数据丢失场景、无组侧采纳对端分组、同戳双组回退行胜者、双未分组保持未分组服务端镜像测试 src/tests/pages/api/sync-group-merge.test.ts 覆盖同名语义以及bookGroupChanged的 undefined/null 等价与无传播抖动引擎级端到端测试 src/tests/services/sync/file/engine-group-metadata-5911.test.ts 直接复现 BOOX 场景未打戳未分组本地行不抹索引分组、全量同步恢复丢失分组、TIE 不抹组、带戳移除仍传播、输了行时钟的分组编辑仍到达索引以及增量保持 O(changed)、发布永不抹掉对端 metadata 编辑等分组。十一、遗留问题#5910 未修复阅读器菜单的同步状态行仍只读取 Readest Cloud 的四个BookConfig打戳字段且其点击派发的是sync-book-progress事件——useFileSync/ KOSync并不监听该事件因此对第三方同步用户这个手动操作是无效的。属于独立工单修复时需要注意与本次工作区分。十二、工程经验共享 worktree 并行会话的 git 陷阱本次修复还踩到一个协作坑同一共享 worktree 上并行的另一个会话提交并推送到了当前分支导致 PR #5921 静默地把对方的 AZW3 提交377a26164、foliate 重固定等带进了自己的 diff。给出的实践规范是git checkout -B branch origin/main git cherry-pick mine... git push --force-with-lease并且在宣告 PR 干净之前永远执行git diff --name-only origin/main..HEAD核对文件清单。对方提交在其自身分支74aea1bf4上是安全的。十三、相关记忆与后续阅读[[file-sync-converge-5900]]增量同步 O(changed) 规则的发源地[[sync-fixes]]、[[sync-clock-skew-lastsynced-5661]]如需从源码继续深入分组合并客户端真源在 src/utils/book.ts服务端镜像在 src/pages/api/sync.ts推送止损逻辑在 src/services/sync/file/merge.ts迁移脚本在 docker/volumes/db/migrations/022_add_group_updated_at.sql。【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门