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

11 KiB
Raw Blame History

本地持久化模块

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. 建议表组

字段细节由数据模块契约决定,持久化初版至少需要以下表组:

表组 建议表 说明
下游身份 accountsbilling_accountscredentialscredential_secret_versions 用户、金额账户、逻辑 Credential 和可轮换 secret version 分开;只存 public ID、HMAC digest/key ID、preview,不存可恢复明文
上游身份 upstream_identities auth ID/index、provider、账户展示、状态、订阅元数据
绑定与策略 billing_account_planscredential_routes 金额套餐属于 BillingAccount,上游账户路由属于 Credential/Account
请求事实 requestsexecutionsusage_records 一次 Request 可有多个 Execution/Usage
Usage 关联 usage_correlationsusage_observations response ID→Request/Execution 最小映射、canonical vector/hash/revision 与迟到观察
价格 price_catalogsprice_rulespricing_snapshots 带版本、来源、有效期和审批状态
计费 billing_recordsledger_entriesbilling_cycles 定点整数金额,事务提交
上游配额 quota_snapshotssubscription_snapshots 与下游余额严格分离
同步诊断 ingest_inboxsync_runsdiagnostic_events 失败重试、来源与处理状态
统一投影流 projection_events 全库单调 event_seq,领域事实/修订对应的确定性统计变化
聚合 overview_hourlyoverview_dailyactivity_statslatency_statsaggregation_checkpoints 可以重建
系统 schema_migrationsapp_settings 数据库版本和插件设置;只有引入 sidecar/已认证用户路由后才增加 auth_sessions

billing_records 对 Usage 使用稳定逻辑 ID/revisionledger_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. 原子事务

一次正常结算至少在一个事务内完成:

  1. 幂等插入/确认 Request、Execution、Usage
  2. 读取准入时锁定的 cycle 与 pricing snapshot
  3. 插入 BillingRecord
  4. 插入不可变 LedgerEntry
  5. 更新 cycle/account 的缓存余额投影;
  6. projection_events 插入可供聚合追赶的单调 event_seq,事务后再发送轻量通知;结算事务不提前推进任何聚合 checkpoint。

唯一约束是最终防线。进程内去重缓存只能提高性能,不能替代数据库幂等。

管理员充值、扣减、重置和冲正同样必须写 LedgerEntry,禁止直接修改 spentbalance 而不留原因。

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 UNIQUEevent_kindfact_idfact_revisionoccurred_atbooked_atsupersedes_event_id 和确定性 delta payload。状态从 unmeasured 变为 measured 时,事件明确携带 unmeasured_count=-1measured_count=+1spend_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