CMDCSkillEngine.Store (cmdc_skill_engine v0.3.0)

Copy Markdown View Source

Skill 档案存储 —— 可插拔后端 + 串行化写入的 GenServer。

自 v0.2 起 Store 不再直接操作 ETS,而是把存储细节委托给 CMDCSkillEngine.Store.Backend。默认后端仍是 ETS(零依赖、适合开发/单元测试), 需要跨重启持久化时可切到 SQLite:

# ETS(默认)
children = [CMDCSkillEngine.Store]

# SQLite
children = [
  {CMDCSkillEngine.Store,
    backend: CMDCSkillEngine.Store.Backend.SQLite,
    backend_opts: [path: "./priv/skills.db"]}
]

v0.3 多租户切片(:scope keyword)

所有按 skill_id 查询 / 写入计数的 API 都接收可选 :scope keyword(默认 "global")。

  • 单租户部署:完全不传 :scope,行为等价 v0.2.x
  • SaaS 多租户:每个租户独立 scope(建议传 tenant_id),不同 scope 互不可见
CMDCSkillEngine.Store.list_active()                     # scope = "global"
CMDCSkillEngine.Store.list_active(scope: "tenant_a")    # 仅 tenant_a 的 Skills
CMDCSkillEngine.Store.get_record(id, scope: "tenant_a") # 跨 scope 查不到
CMDCSkillEngine.Store.update_counters(id, [selections: 1], scope: "tenant_a")

save_record/1 的 scope 由 record.scope 字段承载,不通过 keyword 显式传入。 如果调用方手工构造 SkillRecord 没设置 scope,默认入 "global"

责任

  • SkillRecord 的 CRUD
  • 版本 DAG 查询(按 lineage.parent_skill_ids 追溯,限定同 scope 内)
  • 原子计数器(selections / applied / completions / fallbacks)
  • 版本血统 DAG 的本地图遍历(跨后端共用逻辑)

并发语义

  • 所有写入通过 GenServer 串行化,保证原子性
  • 读取(get_record / list_active / get_version_chain)与写入同走 GenServer, 避免 backend 内部的并发一致性问题;后端实现可以假设任意时刻只有一个 handle_call 在执行

Summary

Functions

返回当前挂载的 backend 模块与状态(调试/运维用)。

Returns a specification to start this module under a supervisor.

按 skill_id 取回档案。

批量取回档案,返回只含存在 id 的 map。

返回某个 Skill 的完整版本链:根节点在最前,按 generation 升序。

返回该 scope 下所有 is_active = true 的 SkillRecord,按 name 升序。

清空所有档案,跨 scope 全部清空(测试专用)。

插入/覆盖一条 SkillRecord。

启动 Store GenServer。

原子递增计数器。支持 :selections / :applied / :completions / :fallbacks

Functions

backend_info()

@spec backend_info() :: {module(), term()}

返回当前挂载的 backend 模块与状态(调试/运维用)。

注意:backend_state 可能包含底层 handle(例如 SQLite conn), 调用方不应直接操作。

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

get_record(skill_id, opts \\ [])

@spec get_record(
  String.t(),
  keyword()
) :: {:ok, CMDCSkillEngine.Types.SkillRecord.t()} | {:error, :not_found}

按 skill_id 取回档案。

选项

  • :scope — 数据切片维度(默认 "global");跨 scope 查询返回 :not_found

get_records(skill_ids, opts \\ [])

@spec get_records(
  [String.t()],
  keyword()
) :: {:ok, %{required(String.t()) => CMDCSkillEngine.Types.SkillRecord.t()}}

批量取回档案,返回只含存在 id 的 map。

选项

  • :scope — 数据切片维度(默认 "global");所有 id 在同一 scope 下查询

get_version_chain(skill_id, opts \\ [])

@spec get_version_chain(
  String.t(),
  keyword()
) :: {:ok, [CMDCSkillEngine.Types.SkillRecord.t()]}

返回某个 Skill 的完整版本链:根节点在最前,按 generation 升序。

在传入 skill_id 同 scope 内回溯。如果中间缺链,返回能重建的 最长前缀(遇到第一个缺失祖先即停止向上追溯)。

选项

  • :scope — 数据切片维度(默认 "global"

list_active(opts \\ [])

@spec list_active(keyword()) :: {:ok, [CMDCSkillEngine.Types.SkillRecord.t()]}

返回该 scope 下所有 is_active = true 的 SkillRecord,按 name 升序。

选项

  • :scope — 数据切片维度(默认 "global"

reset()

@spec reset() :: :ok

清空所有档案,跨 scope 全部清空(测试专用)。

save_record(record)

插入/覆盖一条 SkillRecord。

scope 由 record.scope 字段携带(默认 "global"),不通过 keyword。

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

启动 Store GenServer。

选项

update_counters(skill_id, increments, opts \\ [])

@spec update_counters(String.t(), keyword(), keyword()) :: :ok | {:error, :not_found}

原子递增计数器。支持 :selections / :applied / :completions / :fallbacks

CMDCSkillEngine.Store.update_counters("sk_123", selections: 1, applied: 1)
CMDCSkillEngine.Store.update_counters("sk_123", [selections: 1], scope: "tenant_a")

选项(最后一个参数)

  • :scope — 数据切片维度(默认 "global"