Current section

Files

Jump to
cmdc lib cmdc agent emitter.ex
Raw

lib/cmdc/agent/emitter.ex

defmodule CMDC.Agent.Emitter do
@moduledoc """
Agent 事件广播器。
将 Agent 内部事件统一通过 `CMDC.EventBus` 广播给外部订阅者。
可选在 debug 模式下记录至 ETS ring buffer,供调试查询。
## 使用方式
CMDC.Agent.Emitter.broadcast(state, {:stream_chunk, session_id, "hello"})
CMDC.Agent.Emitter.recent("session-id", 20)
CMDC.Agent.Emitter.clear("session-id")
"""
alias CMDC.Agent.State
alias CMDC.EventBus
@table :cmdc_agent_event_log
@max_entries 400
@max_limit 500
# ==========================================================================
# Public API
# ==========================================================================
@doc "广播事件至所有订阅者,并在 debug 模式下记录至 ETS。"
@spec broadcast(State.t(), term()) :: :ok
def broadcast(%State{session_id: session_id, config: config}, event) do
if debug_enabled?(config), do: append(session_id, event)
EventBus.broadcast(session_id, event)
end
@doc """
返回指定会话最近的事件列表(最新在前)。
`limit` 最大为 #{@max_limit},最小为 1。
"""
@spec recent(String.t(), pos_integer()) :: [%{timestamp_ms: integer(), event: term()}]
def recent(session_id, limit \\ 50)
when is_binary(session_id) and is_integer(limit) do
ensure_table()
limit = limit |> max(1) |> min(@max_limit)
@table
|> :ets.lookup(session_id)
|> Enum.sort_by(fn {_, ts, _} -> ts end, :desc)
|> Enum.take(limit)
|> Enum.map(fn {_, ts, event} -> %{timestamp_ms: ts, event: event} end)
end
@doc "清空指定会话的所有 debug 日志。"
@spec clear(String.t()) :: :ok
def clear(session_id) when is_binary(session_id) do
ensure_table()
:ets.delete(@table, session_id)
:ok
end
# ==========================================================================
# 私有辅助
# ==========================================================================
defp append(session_id, event) do
ensure_table()
ts = System.system_time(:millisecond)
:ets.insert(@table, {session_id, ts, event})
trim(session_id)
:ok
end
defp trim(session_id) do
entries = :ets.lookup(@table, session_id)
overflow = length(entries) - @max_entries
if overflow > 0 do
entries
|> Enum.sort_by(fn {_, ts, _} -> ts end)
|> Enum.take(overflow)
|> Enum.each(&:ets.delete_object(@table, &1))
end
end
defp debug_enabled?(%{debug: true}), do: true
defp debug_enabled?(_), do: false
defp ensure_table do
if :ets.whereis(@table) == :undefined do
try do
:ets.new(@table, [
:named_table,
:public,
:duplicate_bag,
{:read_concurrency, true},
{:write_concurrency, true}
])
rescue
ArgumentError -> :ok
end
end
:ok
end
end