Current section
Files
Jump to
Current section
Files
lib/cmdc/checkpoint/backend/ets.ex
defmodule CMDC.Checkpoint.Backend.ETS do
@moduledoc """
默认 ETS 后端 — 进程内、零依赖、最快。
适合:开发 / 短会话 / 单测。
不适合:BEAM 重启需保留状态、跨节点访问。
## 表设计
统一一张表 `:cmdc_checkpoints`(`:bag` 类型):
- key:`{session_id, checkpoint_id}`
- value:`%CMDC.Checkpoint.Snapshot{}`
并发:`:public` + `:read_concurrency` + `:write_concurrency`,
写入靠 ETS 原子操作保证一致。
## 启动方式
- **自动**:`CMDC.Application` 启动时 `ensure_table/0`
- **手动**:测试中 `CMDC.Checkpoint.Backend.ETS.ensure_table/0`
"""
@behaviour CMDC.Checkpoint.Backend
alias CMDC.Checkpoint.Snapshot
@table :cmdc_checkpoints
# ==========================================================================
# 表管理
# ==========================================================================
@doc """
确保表存在(幂等)。`CMDC.Application` 启动时调用。
"""
@spec ensure_table() :: :ok
def ensure_table do
case :ets.info(@table) do
:undefined ->
:ets.new(@table, [
:bag,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
:ok
_ ->
:ok
end
end
@doc """
清空整张表(仅测试用)。
"""
@spec reset() :: :ok
def reset do
ensure_table()
:ets.delete_all_objects(@table)
:ok
end
# ==========================================================================
# CMDC.Checkpoint.Backend callbacks
# ==========================================================================
@impl true
def save(session_id, %Snapshot{checkpoint_id: ckpt_id} = snapshot, _opts) do
ensure_table()
# bag 表里同 key 多 value,先按 (session_id, checkpoint_id) 去重
:ets.match_delete(@table, {{session_id, ckpt_id}, :_})
:ets.insert(@table, {{session_id, ckpt_id}, snapshot})
:ok
end
@impl true
def load(session_id, opts) do
ensure_table()
case Keyword.fetch(opts, :checkpoint_id) do
{:ok, ckpt_id} ->
case :ets.lookup(@table, {session_id, ckpt_id}) do
[{_, snapshot}] -> {:ok, snapshot}
[] -> {:error, :not_found}
end
:error ->
case list_snapshots(session_id) do
[] -> {:error, :not_found}
[latest | _] -> {:ok, latest}
end
end
end
@impl true
def list(session_id, _opts) do
ensure_table()
{:ok, list_snapshots(session_id)}
end
@impl true
def delete(session_id, opts) do
ensure_table()
case Keyword.fetch(opts, :checkpoint_id) do
{:ok, ckpt_id} ->
:ets.delete(@table, {session_id, ckpt_id})
:ok
:error ->
:ets.match_delete(@table, {{session_id, :_}, :_})
:ok
end
end
# ==========================================================================
# 私有
# ==========================================================================
defp list_snapshots(session_id) do
@table
|> :ets.match_object({{session_id, :_}, :_})
|> Enum.map(fn {_, snap} -> snap end)
|> Enum.sort_by(& &1.saved_at, {:desc, DateTime})
end
end