Current section

Files

Jump to
agent_session_manager lib asm extensions persistence writer.ex
Raw

lib/asm/extensions/persistence/writer.ex

defmodule ASM.Extensions.Persistence.Writer do
@moduledoc """
Async writer process that persists events outside the run process path.
"""
use GenServer
alias ASM.{Error, Event, Telemetry}
alias ASM.Extensions.Persistence
@type t :: %__MODULE__{
store: pid(),
notify: pid() | nil
}
@enforce_keys [:store]
defstruct [:store, :notify]
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
name = Keyword.get(opts, :name)
GenServer.start_link(__MODULE__, opts, name: name)
end
@spec enqueue(pid(), Event.t()) :: :ok
def enqueue(writer, %Event{} = event) when is_pid(writer) do
GenServer.cast(writer, {:append_event, event})
end
@spec flush(pid(), timeout()) :: :ok | {:error, Error.t()}
def flush(writer, timeout \\ 5_000) when is_pid(writer) do
GenServer.call(writer, :flush, timeout)
catch
:exit, reason ->
{:error, Error.new(:unknown, :runtime, "persistence writer flush failed", cause: reason)}
end
@impl true
def init(opts) do
case Keyword.fetch(opts, :store) do
{:ok, store} when is_pid(store) ->
{:ok, %__MODULE__{store: store, notify: Keyword.get(opts, :notify)}}
_ ->
{:stop,
Error.new(:config_invalid, :config, "persistence writer requires :store pid option")}
end
end
@impl true
def handle_cast({:append_event, %Event{} = event}, %__MODULE__{} = state) do
case Persistence.append_event(state.store, event) do
:ok ->
:ok
{:error, %Error{} = error} ->
emit_error_telemetry(event, error)
maybe_notify(state.notify, event, error)
end
{:noreply, state}
end
def handle_cast(_other, state), do: {:noreply, state}
@impl true
def handle_call(:flush, _from, state) do
# `flush/2` is a synchronization barrier. Any prior cast writes have
# already been handled when this call is processed.
{:reply, :ok, state}
end
def handle_call(_other, _from, state) do
{:reply,
{:error, Error.new(:config_invalid, :config, "unsupported persistence writer operation")},
state}
end
defp emit_error_telemetry(%Event{} = event, %Error{} = error) do
Telemetry.execute([:asm, :ext, :persistence, :append, :error], %{}, %{
session_id: event.session_id,
run_id: event.run_id,
event_id: event.id,
event_kind: event.kind,
error_kind: error.kind,
error_domain: error.domain
})
end
defp maybe_notify(pid, event, error) when is_pid(pid) do
send(pid, {:asm_persistence_error, error, event})
:ok
end
defp maybe_notify(_other, _event, _error), do: :ok
end