Current section

Files

Jump to
eventstore lib eventstore storage appender.ex
Raw

lib/eventstore/storage/appender.ex

defmodule EventStore.Storage.Appender do
require Logger
alias EventStore.EventData
alias EventStore.Sql.Statements
alias EventStore.Storage.Appender
def append(conn, stream_id, expected_version, events) do
case Appender.Query.latest_version(conn, stream_id) do
{:ok, ^expected_version} -> execute(conn, stream_id, expected_version, events)
{:ok, latest_version} -> wrong_expected_version(stream_id, expected_version, latest_version)
{:error, reason} -> failed_to_append(stream_id, reason)
end
end
defp wrong_expected_version(stream_id, expected_version, latest_version) do
Logger.warn "failed to append events to stream id #{stream_id}, expected version #{expected_version} but latest version is #{latest_version}"
{:error, :wrong_expected_version}
end
defp failed_to_append(stream_id, reason) do
Logger.warn "failed to append events to stream id #{stream_id} due to #{reason}"
{:error, reason}
end
defp execute(conn, stream_id, expected_version, events) do
conn
|> Postgrex.transaction(&execute_within_transaction(&1, stream_id, expected_version, events))
|> handle_response(stream_id)
end
defp execute_within_transaction(transaction, stream_id, expected_version, events) do
{:ok, query} = prepare_query(transaction)
initial_stream_version = expected_version + 1
persisted_events =
events
|> Enum.map(&assign_stream_id(&1, stream_id))
|> Enum.with_index(initial_stream_version)
|> Enum.map(&assign_stream_version/1)
|> Enum.map(&assign_event_type/1)
|> Enum.map(&encode_headers/1)
|> Enum.map(&encode_payload/1)
|> Enum.reduce_while({:ok, []}, fn(event, {:ok, events}) ->
case append_event(transaction, query, event) do
{:ok, _} -> {:cont, {:ok, [event | events]}}
{:error, %Postgrex.Error{postgres: %{message: reason}}} -> {:halt, {:error, reason}}
response -> {:halt, response}
end
end)
case persisted_events do
{:ok, events} -> length(events)
{:error, reason} -> Postgrex.rollback(transaction, reason)
end
end
defp assign_stream_id(%EventData{} = event, stream_id) do
%EventData{event | stream_id: stream_id}
end
defp assign_stream_version({%EventData{} = event, stream_version}) do
%EventData{event | stream_version: stream_version}
end
defp assign_event_type(%EventData{payload: payload} = event) do
event_type = payload.__struct__ |> Atom.to_string
%EventData{event | event_type: event_type}
end
defp encode_headers(%EventData{headers: headers} = event) do
%EventData{event | headers: Poison.encode!(headers)}
end
defp encode_payload(%EventData{payload: payload} = event) do
%EventData{event | payload: Poison.encode!(payload)}
end
defp prepare_query(transaction) do
Postgrex.prepare(transaction, "create_event", Statements.create_event)
end
defp append_event(transaction, query, %EventData{} = event) do
Postgrex.execute(transaction, query, [
event.stream_id,
event.stream_version,
event.correlation_id,
event.event_type,
event.headers,
event.payload
])
end
defp handle_response({:ok, result}, stream_id) do
Logger.info "appended #{result} events to stream id #{stream_id}"
{:ok, result}
end
defp handle_response({:error, reason}, stream_id) do
Logger.warn "failed to append events to stream id #{stream_id} due to #{reason}"
{:error, reason}
end
defmodule Query do
def latest_version(conn, stream_id) do
conn
|> Postgrex.query(Statements.query_latest_version, [stream_id])
|> handle_response
end
defp handle_response({:ok, %Postgrex.Result{num_rows: 0}}) do
# no stored events, so latest version is 0
{:ok, 0}
end
defp handle_response({:ok, %Postgrex.Result{rows: rows}}) do
latest_version = rows |> List.first |> List.first
{:ok, latest_version}
end
end
end