Skip to content

[Storage] 建立经实板故障验证的 SQLite 持久化底座 #103

Description

@ZhaoXingPeng

结论与下一步

本任务要把“板上能执行 SQLite”升级为“板上事务行为有证据、模块共用一套数据库底座”。当前选择 SQLite 3.53.4 + ESP-IDF FATFS/Wear Levelling,固定 4 KiB 扇区、journal_mode=DELETEsynchronous=EXTRApsow=0;本项目实测的 LittleFS 路线不进入生产实现。

下一步先合入共享 SQLite 内核与实板资格测试工具,再让 Schedule、TimingTask 等模块的 SQLite Adapter 复用同一个连接、迁移和事务边界。PR #101 中的 TimingTask SQLite 代码可作为领域 Adapter 骨架,但不能成为第二套数据库生命周期实现。

为什么现在必须统一

main 上的 Schedule 骨架与 PR #101 已开始分别定义持久化模型。如果每个模块自己打开数据库、设置 PRAGMA、维护迁移和解释错误,跨 Schedule、TimingTask、Outbox、幂等记录的原子提交将无法保证,后续也会出现多套恢复策略。

本任务统一的是数据库底座,不统一业务接口。领域模块继续定义有业务语义的 Store Port;不会引入 Repository<T>、字符串表名或“任意 SQL”一类通用读写协议。

公开方案调查

截至 2026-08-03,没有找到一套可以直接承担 VoiceLife 生产持久化的成熟 ESP32 SQLite 方案:

方案 调查结果 本项目决定
siara-cc/esp32-idf-sqlite3 / Arduino 版本 社区使用最多,但可见实现曾使用内存 journal、空实现 truncate/lock,Arduino VFS 还忽略 fsync 返回值;仓库已有损坏与事务相关报告 不直接依赖
jTecRepos/jSQLite3 重写文件层,但无采用度、无掉电恢复测试,最后提交停在 2023 年 只作代码参考
huming2207/sqlite_espbdl 2026-07 出现的原始块设备 VFS,方向有价值;当前 0 star、0 fork、无测试目录,并依赖 ESP-IDF 6.2 master 作为后续性能对照,不作为当前依赖
SQLite unix-none + FATFS/WL 使用 ESP-IDF 官方文件系统和磨损均衡能力;VoiceLife 已在目标 ESP32-S3 上完成外部 EN 复位恢复验证 当前实现基线

判断依据不是 star 数,而是 SQLite 对 VFS 的同步、截断、锁、扇区和删除语义有明确假设。参考:

实板证据

目标板:ESP32-S3,ESP-IDF 6.0.2,2 MiB voicelife 测试分区,串口固定 115200

被否决的路线

SQLite unix-none 运行于 joltwallet/littlefs 1.22.3 时,以下三组配置都在显式 ROLLBACK 后留下了表记录,而索引记录为 0:

PERSIST + FULL + psow=1  -> FAIL
PERSIST + FULL + psow=0  -> FAIL
DELETE  + FULL + psow=0  -> FAIL

Explicit rollback integrity: table_rows=1 index_rows=0
PROBE_RESULT: FAIL step=explicit rollback leaked row

因此,“LittleFS 自身支持掉电恢复”不能推出“SQLite 的 VFS 假设在该组合上成立”。当前测试栈明确拒绝 LittleFS + SQLite。

当前通过的路线

FATFS/WL 使用 4 KiB 扇区,SQLite 使用 DELETE + EXTRA + psow=0。主机在两个标记点通过串口控制线触发 EN 复位,设备启动原因均为 rst:0x1 (POWERON)

  1. 脏页已刷出、事务尚未提交;重启后 24 条测试记录全部回滚。
  2. COMMIT 返回后立即复位;重启后已提交标记仍存在。

三轮独立测试全部通过。每轮还覆盖显式回滚、40 次四表原子提交、每次提交后的表/索引扫描一致性、PRAGMA quick_check、关闭重开和幂等唯一约束。三轮“平均提交耗时”的中位数为 1,147,655 us,观测到的最慢提交为 1,263,605 us;最终空闲堆为 211,120 bytes。

EN 复位比 esp_restart() 更接近故障恢复,但仍不等于切断电源轨。真实断电、棕断、容量耗尽和长期磨损继续保留为最终验收项,README 不得把当前结果写成完整掉电认证。

子架构

Schedule / TimingTask / 其他领域
        │  各自拥有业务语义 Store Port
        ▼
voicelife_*_sqlite Adapter
        │  只写本领域 SQL 与行映射
        ▼
voicelife_storage_sqlite
        ├── 单数据库连接与单写者队列
        ├── TransactionGuard / PreparedStatement
        ├── schema_migrations
        ├── PRAGMA 配置与启动时自检
        ├── SQLite -> ErrorCode 映射
        └── 容量、延迟和完整性指标
        ▼
ESP-IDF FATFS + Wear Levelling + 2 MiB 分区

边界规则:

  • Runtime 只创建一个数据库实例;模块 Adapter 不自行 mount、format 或重复初始化 SQLite。
  • 跨领域原子操作由用例层定义粗粒度 Port,SQLite Adapter 在一次事务中写入业务事实、幂等记录和 Outbox;不由多个 Repository 依次提交。
  • 设备侧采用单写者队列。语音实时任务不得直接等待约 1.1 秒的持久化提交。
  • 生产 mount 使用 format_if_mount_failed=false。挂载或完整性检查失败时保留现场并进入受限模式,不自动格式化用户数据。
  • PRAGMA 必须在打开后读取回验;编译参数、分区几何和运行参数不匹配时启动失败。
  • 默认限制单事务行数和 payload,预留数据库空间;SQLITE_FULL 映射为可恢复的容量错误,不触发自动删库。
  • Raw BDL VFS 只有在同一套实板契约测试和寿命评估优于 FATFS/WL 后,才能通过新 ADR 替换当前底座。

TDD 顺序

RED

  • 同一故障点在不合格文件系统上能稳定暴露回滚泄漏或恢复失败;
  • 两个模块各自打开连接时,跨模块写入无法通过同一原子契约测试;
  • 旧 schema、损坏 schema、满容量、重复请求和 busy/IO 错误首先产生可观察失败;
  • 音频/语音任务直接提交数据库时,延迟预算测试失败。

GREEN

  • 建立单连接、单写者 SQLite 内核和显式事务保护;
  • 建立版本化迁移、Prepared Statement、错误映射和容量保护;
  • Schedule/TimingTask Adapter 通过共享 Store 契约;
  • 实板探针可备份、校验分区、写入非活动槽、注入 EN 复位并恢复原数据。

REFACTOR

  • PR ✨ feat(timing): 补全定时任务调度骨架 #101 的 TimingTask SQLite Adapter 复用共享内核,不保留第二套 driver/runtime 生命周期;
  • SQL 只存在于 *_sqlite Adapter,Domain 和公共契约不出现 sqlite3*、表名、PRAGMA 或 ESP-IDF 句柄;
  • 把真机探针、串口规则和恢复流程收敛为可重复运行的工具和说明。

验收标准

  • voicelife_storage_sqlite 的接口、依赖方向和 Runtime 组装经架构检查;
  • Host TDD 覆盖迁移、事务回滚、跨表原子性、幂等、错误映射、容量满和损坏数据库;
  • FATFS/WL、SQLite 编译参数和所有 PRAGMA 有读回校验;
  • 同一实板探针至少连续 10 轮通过,日志记录固件摘要、分区表、复位原因、延迟和资源水位;
  • 增加真实电源切断与棕断测试,已提交事务保留、未提交事务回滚、表与索引一致;
  • 2 MiB 分区达到容量边界时返回稳定错误,恢复空间后数据库仍通过完整性检查;
  • 工具在任何擦除/恢复前核对分区名、偏移、长度和备份摘要,写后再次读回或芯片端摘要校验;
  • 测试结束后恢复原 voicelife 分区和 OTA 元数据,并核对原有 7 条日程、8 条提醒;
  • README 清楚写明当前通过项、LittleFS 否决证据、外部复位与真实断电的差别,以及该能力为什么是项目亮点;
  • ./scripts/run_checks.sh、ESP-IDF 6.0.2 构建和 Qiniu Runner CI 全部通过。

明确不做

  • 不把 SQLite 类型放进 Domain/Application 公共接口;
  • 不提供无业务语义的通用 CRUD 协议;
  • 不在 mount 失败时静默格式化生产分区;
  • 不用 esp_restart()、单次成功或 quick_check=ok 代替故障注入;
  • 不因新 Raw BDL 原型性能更高就跳过采用度、许可、恢复和寿命验证;
  • 不在本任务中关闭父任务 [Epic] VoiceLife 设备端 MVP 模块化开发与交付 #91

回退

  • Runtime Profile 可切回内存 Adapter,数据库分区保留不动;
  • 迁移失败不升级 schema_version,保留旧库供恢复;
  • 真机工具只操作经分区表解析确认的测试区域,任何摘要不匹配立即停止;
  • 阶段性 PR 使用 Refs #本IssueRefs #91,只有全部验收完成的最终 PR 才关闭本 Issue,任何 PR 都不关闭 [Epic] VoiceLife 设备端 MVP 模块化开发与交付 #91

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions