的根因与解法)
桌面应用跨平台前端【免费下载链接】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 仓库中apps/readest-app/.claude/memory/koplugin-stats-push-chunking-5666.md的故障复盘为骨架结合 readest_syncstats.lua、readest_syncclient.lua、应用端 statsSync.ts 与 syncstats_spec.lua 的源码与测试完整还原 Issue #5666 的根因、修复方案PR #5670与可复用的同步工程经验。读完本文你将掌握为什么无界单请求同步 仅在成功时推进游标 硬性 socket 超时三者组合必然导致永久卡死KOReader 插件端如何用 500 条/块的分块推送与逐块游标推进打破这一死锁以及永不拆分同一 start_time、UIManager:nextTick链式调度等关键实现细节背后的原理。事故现象每次点击 Push stats now 都提示失败2026-08-13 报告的 Issue #5666标题为 Failed to push reading statistics推送阅读统计失败。现象是在 Kindle 与 Kobo 设备上每次点击 Push stats now立即推送统计都返回失败该问题在用户升级 Readest 存储容量后开始出现失败表现为表面上的静默界面只弹出一条通用的失败 toast日志级别为logger.dbg的诊断行不会出现在用户的 crash.log 中排障者很难定位到真正原因。该问题由合并的 PR #5670commitb25a2a88c2026-08-13修复修复后工作区与分支已清理设备端验证等待下一个 koplugin 版本发布。根因一个请求塞下全部积压撞上 10 秒总超时复盘记录把根因指向三个条件的叠加1. 无界单请求一次 POST 发送全部积压修复前的SyncStats:push会把自stats_push_cursor以来的全部阅读事件放进一个/api/syncPOST 请求里不做任何分批。问题的时间线是用户升级存储后拉取了完整的多设备历史当时 koplugin 的统计拉取是故意不分页的对应应用端 statsSync.ts 中的拉取实现于是本设备的推送游标显得非常落后collectSince(cursor)一次收集了数万条page 事件服务端处理stat_pages时按500 行 select upsert的顺序批处理整批处理时间远超客户端等待上限。2. 硬性超时socketutil:set_timeout(5, 10)KOReader 插件的同步客户端在 readest_syncclient.lua 中定义local SYNC_TIMEOUTS { 5, 10 }并在每次 RPC 分发前调用socketutil:set_timeout(SYNC_TIMEOUTS[1], SYNC_TIMEOUTS[2])readest_syncclient.lua其中10是总时长上限。数万条事件在服务端顺序批量处理下请求永远不可能在 10 秒内完成。3. 仅在成功时推进游标失败即重发相同负载游标stats_push_cursor只在请求成功时推进。这意味着第一次请求超时失败 → 游标不动下一次重试时collectSince收集到完全相同的负载再次超时 → 游标仍然不动如此循环每次 Push stats now 都重发同一批数据、都失败永久卡死。复盘记录将这一组合总结为一个通用判定式任何无界单请求同步 仅在成功时推进游标 硬超时的组合一旦积压量超过超时阈值就会永久卡住。关键对照应用端早已有修复模式值得注意的是Readest 应用端早在这次事故之前就采用了正确的模式。在 statsSync.ts 中/** Events per push request — bounds request size for a large offline backlog. */ const PUSH_CHUNK 500; /** Page events per pull request — bounds the receiving devices memory. */ const PULL_PAGE 1000;应用端pushStats按PUSH_CHUNK 500分块推送每个块成功后才推进游标。而 koplugin 插件端一直没有同步这一模式——这是本次事故的直接缺口。修复方案分块推送 每块成功后推进并保存游标修复后的SyncStats:push完整实现位于 readest_syncstats.lua核心逻辑如下1. 块大小与镜像关系-- Page events per push request. A large backlog (e.g. a fresh push cursor -- after pulling the full multi-device history, #5666) cannot go out as one -- request: the sync clients 10s total socket timeout kills it, the cursor -- never advances, and every retry re-sends the same payload — wedging Push -- stats now forever. Mirrors PUSH_CHUNK in the apps statsSync.ts and the -- servers stat_pages batch size. local PUSH_CHUNK 500PUSH_CHUNK 500是刻意与两处对齐的值应用端 statsSync.ts 的PUSH_CHUNK以及服务端stat_pages处理时的 500 行 selectupsert 批大小。三者一致保证单个请求的负载既不会撑爆客户端超时也不会超出服务端单批处理的能力。2. 游标作为普通字段持久化koplugin 插件端没有独立的游标存储游标是readest_sync设置表中的一个普通字段通过保存整个表来持久化local cursor settings.stats_push_cursor or 0 local books, pages self:collectSince(cursor) ... settings.stats_push_cursor pages[last].start_time G_reader_settings:saveSetting(readest_sync, settings)collectSincereadest_syncstats.lua的 SQL 以start_time cursor为过滤条件并按start_time ASC排序SELECT b.md5, b.title, b.authors, p.page, p.start_time, p.duration, p.total_pages FROM page_stat_data p JOIN book b ON b.id p.id_book WHERE p.start_time ? ORDER BY p.start_time ASC3. 永不拆分同一个 start_time推送事件按start_time排序而游标推进到的是块内最后一条事件的start_time。如果分块边界恰好落在共享同一start_time的事件中间例如并行视图 split-view 产生的同一秒事件恢复时游标会越过这些事件导致同一秒内剩余的事件被永久丢弃。因此推送逻辑在切块边界做了保护local last math.min(i PUSH_CHUNK - 1, #pages) -- Never split a start_time across chunks (pages are ordered by -- start_time): advancing the cursor past it would drop the remaining -- same-second events on resume. while last #pages and pages[last 1].start_time pages[last].start_time do last last 1 end应用端 statsSync.ts 有完全等价的逻辑events[end].startTime lastStart这是两端必须保持的行为一致性。4. 用UIManager:nextTick链式调度而不是在回调里直接递归分块之间通过UIManager:nextTick链式衔接UIManager:nextTick(function() pushFrom(last 1) end)复盘记录特别强调了这个细节的必要性如果直接在 HTTP 成功回调内部链式调用下一块那么在同步非 TurboHTTP 路径上每个块都会在回调栈里嵌套一次coroutine.resume积压巨大例如数万条、约 200 个块时会打爆 C 栈。nextTick让下一块从事件循环重新开始栈深度始终是 O(1)。这也是 readest_syncclient.lua 中AsyncHTTP中间件在缺少 Turbo looper 时走同步回退路径时的关键约束。5. 失败时保持游标不动交互时给出明确提示else logger.dbg(ReadestStats push: failed, cursor kept at .. tostring(settings.stats_push_cursor) .. ; body .. tostring(body)) if interactive then UIManager:show(InfoMessage:new{ text _(Failed to push reading statistics), timeout 2 }) end end失败只记录日志并保持游标下一次重试从上次成功的块之后继续——部分成功可恢复是这套机制的核心收益。拉取侧的双胞胎问题PULL_PAGE 1000分页复盘记录指出拉取侧存在同样隐患的双胞胎版本koplugin 的统计拉取曾经不分页一次拉回整个多设备历史会形成多 MB 的响应同样撞上 10 秒总超时。修复后的pullreadest_syncstats.lua使用PULL_PAGE 1000-- Page events per pull request. The server pages stat_pages by updated_at and -- completes the trailing millisecond of each page, so a strict -- since cursor next request never skips rows that share an updated_at -- (same contract as PULL_PAGE in the apps statsSync.ts). A full multi-device -- history would otherwise come back as one multi-MB response under the sync -- clients 10s total socket timeout: the pull-side twin of #5666, failing -- forever without ever advancing the cursor. Paging also keeps every -- applyRemote transaction bounded on the device. local PULL_PAGE 1000拉取的关键机制游标语义不同拉取游标stats_pull_cursor跟踪的是updated_at_ms而非start_time。服务端按updated_at对stat_pages分页并补全每页的尾毫秒因此下一次严格since cursor的请求不会跳过共享同一updated_at的行——这与应用端 statsSync.ts 的pullStats是同一契约每页先落库再取下一页applyRemote在回调内、网络往返完成后执行live_book_id在回调内解析以反映应用时的会话状态保证内存平稳且新设备回填可断点续传满页才继续npages PULL_PAGE 且游标有推进时通过nextTick拉下一页否则视为已追平结束拉取401/403 触发登出回调logout_fn在鉴权失败时被调用。每页应用后都会调用G_reader_settings:saveSetting(readest_sync, settings)保存游标因此已应用的页保持已应用、游标停留在最后成功页重试从断点恢复。本地落库的配套能力applyRemote与重复书籍行折叠拉取/推送的数据落库由applyRemotereadest_syncstats.lua完成两个细节值得注意冲突解决取更长时长同一(id_book, page, start_time)冲突时duration取两者最大值并刷新total_pages避免短时长的脏数据覆盖真实阅读时长INSERT INTO page_stat_data (id_book, page, start_time, duration, total_pages) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id_book, page, start_time) DO UPDATE SET duration max(duration, excluded.duration), total_pages excluded.total_pages;重复书籍行折叠关联 #4861KOReader 原生统计插件以(title, authors, md5)唯一索引建行当它提取的元数据与 Readest 漂移时首次原生打开会新建一条归零行导致同步的阅读时长滞留在同步创建的行上。mergeDuplicateBooksreadest_syncstats.lua只折叠原生插件从未认领pages IS NULL的行且绝不舍弃已被认领的行打开的阅读会话可能缓存了它的 book id并跳过当前会话缓存的live_book_id。落库后还会镜像应用端的recomputeBookTotals逻辑重新计算total_read_time、total_read_pages并保证last_open永不回退让 KOReader 设备在拉取后立即看到新鲜的总量数据。测试覆盖分块、断点续传与边界条件修复的每个关键行为都有对应的测试用例位于 syncstats_spec.lua游标过滤语义collectSince只收集start_time cursor的事件并按 md5 关联书籍游标为 0 时返回全部无新事件时返回空表冲突解决applyRemote对同(id_book, page, start_time)的远程事件保留更长duration逐块推进游标advances stats_push_cursor as a plain field after a successful push大积压分块pushes a large backlog in bounded chunks, advancing the cursor per chunk (#5666)—— 测试直接以 #5666 命名部分失败续传keeps partial progress when a chunk fails, and a retry resumes from the cursor不拆分同一 start_timenever splits page events sharing a start_time across chunks拉取分页pulls a large history in bounded pages, advancing the cursor per page、keeps partial progress when a page fails, and a retry resumes from the cursor。测试通过drainScheduled()配合stubs.UIManager:drain()把nextTick链上的分块全部跑完syncstats_spec.lua从而在纯同步环境下完整验证异步链式推送。排障陷阱两个容易误判的假线索复盘记录记录了修复过程中踩到的两个坑对后续维护者极具参考价值1. crash.log 里的红鲱鱼WebDavApi 404statistics open income DB failed ... notadb来自cloudstorage.koplugin是KOReader 自身的统计云同步在报错——用户同时也配置了它——与 Readest 插件完全无关。排查 koplugin 问题时务必先区分日志来自哪个插件。2. spec 测试的 preload 竞态syncannotations_spec.lua注册了一个私有package.preload[ui/uimanager]show为 no-op在完整套件的 busted 运行中会赢得package.loaded。因此在 syncstats 的测试里绝不能通过koreader_stubs.UIManager._shown断言 toast——正确做法是对require()出来的实例打补丁show/InfoMessage.new并在测试后恢复即现有 toast 测试的既有模式。当前syncstats_spec已改用共享的koreader_stubspreload其私有 preload 均为死代码。可复用的工程经验这次事故沉淀出几条适用于所有离线积压 云端同步场景的通用原则任何同步请求都必须是有界的bound**要么按数量分块push 500 条/请求要么按时间分页pull 1000 条/页禁止无界单请求游标必须逐块推进并立即持久化而不是整体成功才推进——这是断点续传与防重发的前提块边界必须与游标粒度对齐游标按start_time推进时同一start_time的事件绝不能被拆到两个块异步链式调用要从事件循环调度不要在回调栈内直接递归否则深度积压下栈会溢出多端实现必须共享同一套契约常量与语义应用端PUSH_CHUNK 500、PULL_PAGE 1000statsSync.ts与插件端、服务端stat_pages批大小保持一致才能保证任意一端都不会成为超时瓶颈排障先排除其他插件KOReader 生态插件众多日志中的错误可能来自无关组件应先用模块归属确认再下结论。对于 Readest KOReader 插件的维护者如需深入验证或复现可重点阅读 readest_syncstats.lua推送/拉取核心、readest_syncclient.lua超时与中间件栈、应用端对照实现 statsSync.ts以及测试 syncstats_spec.lua 与桩模块 koreader_stubs.lua。赞分享桌面应用跨平台前端【免费下载链接】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点击查看免费下载相关推荐深度解析CowboyErlang高性能HTTP服务器架构设计与百万并发实践深度解析CowboyErlang高性能HTTP服务器架构设计与百万并发实践 在当今微服务架构和云原生应用盛行的时代选择适合的Web服务器框架对于构建高性能、桌面应用跨平台前端Readest 阅读进度同步中的空起始 CFI 缺陷根因分析、畸形 CFI 检测与“丢弃优先”修复策略Readest 阅读进度同步中的空起始 CFI 缺陷根因分析、畸形 CFI 检测与“丢弃优先”修复策略 导读 本文以 Readest 开源仓库中的问题记忆文档桌面应用跨平台前端Readest 修复 Android 端 Edge TTS 播放随机卡死Tauri WebSocket 传输永不 settle的根因剖析与工程实践Readest 修复 Android 端 Edge TTS 播放随机卡死Tauri WebSocket 传输永不 settle的根因剖析与工程实践 本文以桌面应用跨平台前端上一篇终极指南gh_mirrors/py/pygdf性能回归测试策略——防止性能退化的完整方案下一篇如何选择适合的模型Phi-3.5-vision-instruct的比较创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考