192 lines
11 KiB
Markdown
192 lines
11 KiB
Markdown
# 本地持久化模块
|
||
|
||
## 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=...`,不能只追加一个“已结算”状态造成双计数。
|
||
|
||
每类聚合使用独立 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`。
|