11 KiB
本地持久化模块
1. 定位
持久化模块负责把 数据模块 的事实安全地保存到本地,并提供事务、查询、迁移、备份、归档和重建能力。它不解释 CPA payload、不计算价格、不决定 Key 是否放行,也不包含 UI 逻辑。
第一版采用单文件 SQLite。它符合插件单机、低运维、随 CPA 一起分发的目标,又能提供计费账本需要的事务与唯一约束。不能沿用 cpa-plugin-key-billing 把整个状态写成一个 JSON 文档的方式。
2. 存储分层
2.1 权威事实:不可随意修改
- Request 与 Execution;
- 标准 Usage;
- BillingRecord 与 LedgerEntry;
- 管理员余额调整;
- 请求准入使用的 plan/cycle/price 快照。
事实只允许追加、幂等补全或显式冲正。历史价格变化不能重算并覆盖已经入账金额。
2.2 当前目录与配置事实
- 下游 account、credential、自管 Key public ID/HMAC digest/preview/alias,以及兼容模式的 CPA scope;
- 上游 auth/provider identity;
- Key→plan 与 Key→upstream binding;
- plan、route policy、price catalog/rule;
- plugin settings 和 schema migrations。
同步删除采用 soft delete,历史请求不能因当前 Key 或上游账号被删除而失去解释能力。
2.3 时间点快照
- 上游 quota/subscription snapshot;
- 模型与价格目录版本;
- credential health;
- 同步运行结果。
快照按时间追加,当前值由最新有效快照投影,不覆盖历史。
2.4 可重建派生数据
- 小时/日 overview;
- activity;
- latency 分布;
- identity totals;
- ranking、Top 和 heatmap;
- realtime cache。
这些数据可以删除后从事实表重建,不能作为余额或账本真相。
3. 建议表组
字段细节由数据模块契约决定,持久化初版至少需要以下表组:
| 表组 | 建议表 | 说明 |
|---|---|---|
| 下游身份 | accounts、billing_accounts、credentials、credential_secret_versions |
用户、金额账户、逻辑 Credential 和可轮换 secret version 分开;只存 public ID、HMAC digest/key ID、preview,不存可恢复明文 |
| 上游身份 | upstream_identities |
auth ID/index、provider、账户展示、状态、订阅元数据 |
| 绑定与策略 | billing_account_plans、credential_routes |
金额套餐属于 BillingAccount,上游账户路由属于 Credential/Account |
| 请求事实 | requests、executions、usage_records |
一次 Request 可有多个 Execution/Usage |
| Usage 关联 | usage_correlations、usage_observations |
response ID→Request/Execution 最小映射、canonical vector/hash/revision 与迟到观察 |
| 价格 | price_catalogs、price_rules、pricing_snapshots |
带版本、来源、有效期和审批状态 |
| 计费 | billing_records、ledger_entries、billing_cycles |
定点整数金额,事务提交 |
| 上游配额 | quota_snapshots、subscription_snapshots |
与下游余额严格分离 |
| 同步诊断 | ingest_inbox、sync_runs、diagnostic_events |
失败重试、来源与处理状态 |
| 统一投影流 | projection_events |
全库单调 event_seq,领域事实/修订对应的确定性统计变化 |
| 聚合 | overview_hourly、overview_daily、activity_stats、latency_stats、aggregation_checkpoints |
可以重建 |
| 系统 | schema_migrations、app_settings |
数据库版本和插件设置;只有引入 sidecar/已认证用户路由后才增加 auth_sessions |
billing_records 对 Usage 使用稳定逻辑 ID/revision,ledger_entries.billing_record_id、各 EventID/IdempotencyKey 必须有唯一约束。usage_observations 至少唯一约束 (execution_id, canonical_hash);相同累计 vector 重放不能产生新 revision。
4. SQLite 运行策略
参考 cpa-usage-keeper/internal/repository/db.go:
- 文件库启用 WAL;
busy_timeout=5000;foreign_keys=ON;- 单 writer connection,所有写事务串行;
- 独立只读 pool,可允许少量并发查询;
- 内存数据库测试时复用单连接;
- 数据库路径使用插件专属 data directory,不放入动态库目录或当前工作目录猜测位置。
插件在 CPA 进程内,任何数据库操作都不能长期阻塞请求线程。准入 pending 和 response hook 取得的可靠 canonical Usage 必须在回调返回前耐久化;completion 不是 Usage 唯一落库点。使用有界队列时,队列满不能静默丢弃,应同步落库、进入本地 inbox,或对新请求 fail closed。
Go c-shared 内长期 goroutine/timer 的安全性尚未通过验证,MVP 持久化先采用 host callback/管理请求驱动的同步短事务,不自行启动后台 flusher。连接池、WAL checkpoint、聚合和 backup worker 只有通过 dev.md 的双 Go runtime soak gate 后才能开启;否则长期任务移到 sidecar。
5. 原子事务
一次正常结算至少在一个事务内完成:
- 幂等插入/确认 Request、Execution、Usage;
- 读取准入时锁定的 cycle 与 pricing snapshot;
- 插入 BillingRecord;
- 插入不可变 LedgerEntry;
- 更新 cycle/account 的缓存余额投影;
- 在
projection_events插入可供聚合追赶的单调event_seq,事务后再发送轻量通知;结算事务不提前推进任何聚合 checkpoint。
唯一约束是最终防线。进程内去重缓存只能提高性能,不能替代数据库幂等。
管理员充值、扣减、重置和冲正同样必须写 LedgerEntry,禁止直接修改 spent 或 balance 而不留原因。
6. 金额、时间和未知值
- 金额统一保存为
int64 micros和 currency,不使用 float 作为余额或账本字段; - Token 使用非负
int64; - 时间保存 UTC 或一个固定的规范化格式,展示时再转时区;
NULL表示未知,数值 0 表示确认观察到零;- bool 若来源可能缺失则使用 nullable;
- 枚举按稳定字符串保存,并由 schema version 管理扩展。
7. Hot、Archive 与保留策略
参考 usage-keeper 的 usage_events / usage_events_archive,但不能直接照搬其单表 INSERT SELECT + DELETE:本项目存在 Request→Execution→Usage→Billing→Ledger 永久审计关系。
MVP 决断:不物理移动或删除 Request/Execution/Usage 最小事实行;只清理原始大响应引用、临时诊断和已过期内存缓存。BillingRecord、LedgerEntry 和管理员调整永久保留。待真实容量达到阈值后再设计统一逻辑 ID + hot/cold view,或只归档大字段/诊断明细;任何方案都不能打断账本引用。
- 最小
usage_correlations默认随 hot Request 保留至少 90 天;24 小时只表示内存 cache TTL/awaiting 快速扫描窗口,不立即删除 response-ID 映射; - 超过关联保留期后,只有自带 RequestID/ExecutionID 的迟到事实仍可可靠补记,无 ID 观察不得猜配;
- 小时级高分辨率聚合可以短期保留,日级金额/请求聚合长期保留;
- quota snapshot 可以按策略降采样,保留重置边界和异常快照。
归档不是删除历史。用户要求清除日志时,必须区分“隐藏/清理诊断日志”“删除请求内容引用”和“不可删除的金额账本”。
8. 聚合与 checkpoint
projection_events.event_seq INTEGER PRIMARY KEY AUTOINCREMENT 是全库唯一聚合游标;同一事务还写 event_id UNIQUE、event_kind、fact_id、fact_revision、occurred_at、booked_at、supersedes_event_id 和确定性 delta payload。状态从 unmeasured 变为 measured 时,事件明确携带 unmeasured_count=-1、measured_count=+1、spend_delta=...,不能只追加一个“已结算”状态造成双计数。
每类聚合使用独立 checkpoint:overview、activity、latency、identity、ranking 互不阻塞。checkpoint 保存已处理的最大单调 event_seq,而不是 UUID event_id 或只保存时间;event_id 负责全局身份/幂等,不能拿来比较处理先后。
- 新事件提交后发送轻量通知;
- callback-driven runner、sidecar 或 soak 通过后的后台 runner 批量追赶;
- 启动时检查 lag 并恢复;
- 单个聚合失败只保留自己的旧 checkpoint;
- 聚合 upsert 与 checkpoint 推进处于同一事务;
- 支持从 0 或指定
event_seq重建; - UI 能展示各 checkpoint lag 和最后错误。
9. Inbox、崩溃恢复与备份
如果存在 Redis/HTTP 导入或异步采集,应先把原始消息写入 ingest_inbox,再解码为事实,状态至少包含 pending、processed、process_failed、discarded。失败重试必须有次数和最后错误。
原生回调的 Request 临时状态不能完整依赖内存。至少在准入/执行开始处留下轻量 pending 事实,使进程崩溃后可以将未完成请求标记为 abandoned/unknown,而不是永久占用并发或额度预留。
恢复扫描还必须包括:已保存 canonical Usage 但尚未结算的 Execution、awaiting usage、未发布 projection/outbox。即使 completion 永久丢失,Request 可以转为 abandoned/unknown,可靠 Usage 仍必须结算。
备份使用 SQLite online backup API,不能只复制正在 WAL 模式运行的 .db 文件。建议:
- 每日自动备份;
- 默认保留 7~30 天;
- 管理员可立即创建和下载备份;
- 恢复必须校验 schema version、integrity check 和账本一致性;
- SQLite 备份不包含外部 HMAC secret keyring;运维必须分开加密备份、成对恢复,并用非秘密 fingerprint/key ID 验证匹配;缺失或不匹配时认证 fail closed,禁止自动生成替代 secret;
- shutdown 时停止 runner、checkpoint WAL、关闭 reader/writer,且整个过程幂等有超时。
10. 索引原则
索引围绕真实查询建立:
- request/event ID 唯一索引;
- timestamp + ID 游标;
- downstream credential/account + timestamp;
- upstream identity + timestamp;
- model + timestamp;
- outcome/status + timestamp;
- cycle、ledger account + occurred_at;
- archive 只保留主键和必要的少数索引。
不要为 UI 的每个筛选组合创建索引。使用真实数据容量测试查询计划,再调整复合索引。
11. 迁移策略
- 新库可创建当前完整 schema;
- 已存在数据库必须执行有序、带版本的显式 migration;
- migration 在插件开始接收业务请求前完成;
- 破坏性迁移先备份;
- migration 失败时插件进入不可计费/不放行的安全状态,不能打开空数据库继续免费运行;
- 数据契约 schema version 与 SQLite migration version 分开管理。
12. 参考路径与验收
主要参考:
cpa-usage-keeper/internal/repository/db.go;internal/entities/;internal/repository/migration/;internal/repository/usage_event_archive.go;internal/backup/;internal/repository/usage*.go。
验收必须覆盖事务回滚、重复事件、并发读写、WAL、数据库锁、崩溃恢复、migration、备份恢复、archive 前 checkpoint 检查、账本金额一致性和 go test -race。