Files
cpa-plugin/docs/modules/persistence.md
T

192 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 本地持久化模块
## 1. 定位
持久化模块负责把 [数据模块](data.md) 的事实安全地保存到本地,并提供事务、查询、迁移、备份、归档和重建能力。它不解释 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](dev.md) 的双 Go runtime soak gate 后才能开启;否则长期任务移到 sidecar。
## 5. 原子事务
一次正常结算至少在一个事务内完成:
1. 幂等插入/确认 Request、Execution、Usage
2. 读取准入时锁定的 cycle 与 pricing snapshot
3. 插入 BillingRecord
4. 插入不可变 LedgerEntry
5. 更新 cycle/account 的缓存余额投影;
6.`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=...`,不能只追加一个“已结算”状态造成双计数。
每类聚合使用独立 checkpointoverview、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` 文件。建议:
- 每日自动备份;
- 默认保留 730 天;
- 管理员可立即创建和下载备份;
- 恢复必须校验 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`