Packages
commanded
0.15.1
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.3
1.4.2
1.4.1
1.4.0
1.4.0-rc.0
1.3.1
1.3.0
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.1
1.0.0-rc.0
0.19.1
0.19.0
0.18.1
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.16.0-rc.1
0.16.0-rc.0
0.15.1
0.15.0
0.14.0
0.14.0-rc.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.5
0.8.4
0.8.3
0.8.1
0.8.0
0.7.1
0.6.2
0.6.1
0.6.0
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.0
Use Commanded to build your own Elixir applications following the CQRS/ES pattern.
Current section
Files
Jump to
Current section
Files
lib/commanded/process_managers/process_router.ex
defmodule Commanded.ProcessManagers.ProcessRouter do
@moduledoc false
use GenServer
use Commanded.Registration
require Logger
alias Commanded.ProcessManagers.{
ProcessManagerInstance,
ProcessRouter,
Supervisor,
}
alias Commanded.EventStore
alias Commanded.EventStore.RecordedEvent
alias Commanded.Subscriptions
defmodule State do
@moduledoc false
defstruct [
command_dispatcher: nil,
consistency: nil,
process_manager_name: nil,
process_manager_module: nil,
subscribe_from: nil,
process_managers: %{},
supervisor: nil,
last_seen_event: nil,
pending_events: [],
subscription: nil,
]
end
def start_link(process_manager_name, process_manager_module, command_dispatcher, opts \\ []) do
name = {ProcessRouter, process_manager_name}
state = %State{
process_manager_name: process_manager_name,
process_manager_module: process_manager_module,
command_dispatcher: command_dispatcher,
consistency: opts[:consistency] || :eventual,
subscribe_from: opts[:start_from] || :origin,
}
Registration.start_link(name, __MODULE__, state)
end
def init(%State{command_dispatcher: command_dispatcher} = state) do
{:ok, supervisor} = Supervisor.start_link(command_dispatcher)
state = %State{state | supervisor: supervisor}
GenServer.cast(self(), :subscribe_to_events)
{:ok, state}
end
@doc """
Acknowledge successful handling of the given event by a process manager instance
"""
def ack_event(process_router, %RecordedEvent{} = event) do
GenServer.cast(process_router, {:ack_event, event})
end
@doc """
Fetch the pid of an individual process manager instance identified by the given `process_uuid`
"""
def process_instance(process_router, process_uuid) do
GenServer.call(process_router, {:process_instance, process_uuid})
end
@doc """
Fetch the `process_uuid` and pid of all process manager instances
"""
def process_instances(process_router) do
GenServer.call(process_router, {:process_instances})
end
def handle_call({:process_instances}, _from, %State{process_managers: process_managers} = state) do
reply = Enum.map(process_managers, fn {process_uuid, pid} -> {process_uuid, pid} end)
{:reply, reply, state}
end
def handle_call({:process_instance, process_uuid}, _from, %State{process_managers: process_managers} = state) do
reply = case Map.get(process_managers, process_uuid) do
nil -> {:error, :process_manager_not_found}
process_manager -> process_manager
end
{:reply, reply, state}
end
def handle_cast({:ack_event, event}, %State{} = state) do
state = confirm_receipt(event, state)
# continue processing any pending events
GenServer.cast(self(), :process_pending_events)
{:noreply, state}
end
@doc """
Subscribe the process router to all events
"""
def handle_cast(:subscribe_to_events, %State{} = state) do
{:noreply, subscribe_to_all_streams(state)}
end
def handle_cast(:process_pending_events, %State{pending_events: []} = state), do: {:noreply, state}
def handle_cast(:process_pending_events, %State{pending_events: [event | pending_events]} = state) do
case length(pending_events) do
0 -> :ok
1 -> Logger.debug(fn -> describe(state) <> " has 1 pending event to process" end)
count -> Logger.debug(fn -> describe(state) <> " has #{count} pending events to process" end)
end
state = handle_event(event, state)
{:noreply, %State{state | pending_events: pending_events}}
end
def handle_info({:events, events}, %State{pending_events: pending_events} = state) do
Logger.debug(fn -> describe(state) <> " received #{length(events)} event(s)" end)
unseen_events = Enum.reject(events, &event_already_seen?(&1, state))
state = case {pending_events, unseen_events} do
{[], []} ->
# no pending or unseen events, so state is unmodified
state
{[], _} ->
# no pending events, but some unseen events so start processing them
GenServer.cast(self(), :process_pending_events)
%State{state | pending_events: unseen_events}
{_, _} ->
# already processing pending events, append the unseen events so they are processed afterwards
%State{state | pending_events: pending_events ++ unseen_events}
end
{:noreply, state}
end
# remove a process manager instance that has stopped with a normal exit reason
def handle_info({:DOWN, _ref, :process, pid, :normal}, %State{process_managers: process_managers} = state) do
{:noreply, %State{state | process_managers: remove_process_manager(process_managers, pid)}}
end
# stop process router when a process manager instance terminates abnormally
def handle_info({:DOWN, _ref, :process, _pid, reason}, %State{} = state) do
Logger.warn(fn -> describe(state) <> " is stopping due to: #{inspect reason}" end)
{:stop, reason, state}
end
defp subscribe_to_all_streams(%State{consistency: consistency, process_manager_name: process_manager_name, subscribe_from: subscribe_from} = state) do
{:ok, subscription} = EventStore.subscribe_to_all_streams(process_manager_name, self(), subscribe_from)
# register this event handler as a subscription with the given consistency
:ok = Subscriptions.register(process_manager_name, consistency)
%State{state | subscription: subscription}
end
# ignore already seen event
defp event_already_seen?(%RecordedEvent{event_number: event_number}, %State{last_seen_event: last_seen_event}) do
not is_nil(last_seen_event) and event_number <= last_seen_event
end
defp handle_event(%RecordedEvent{event_number: event_number, data: data, stream_id: stream_id, stream_version: stream_version} = event, %State{process_manager_module: process_manager_module, process_managers: process_managers} = state) do
{process_uuid, process_manager} = case process_manager_module.interested?(data) do
{:start, process_uuid} -> {process_uuid, start_process_manager(process_uuid, state)}
{:continue, process_uuid} -> {process_uuid, continue_process_manager(process_uuid, state)}
{:stop, process_uuid} -> {:stopped, stop_process_manager(process_uuid, state)}
false -> {nil, nil}
end
case process_uuid do
nil ->
Logger.debug(fn -> describe(state) <> " is not interested in event: #{inspect event_number} (#{inspect stream_id}@#{inspect stream_version})" end)
ack_and_continue(event, state)
:stopped ->
Logger.debug(fn -> describe(state) <> " has been stopped by event: #{inspect event_number} (#{inspect stream_id}@#{inspect stream_version})" end)
ack_and_continue(event, state)
_ ->
Logger.debug(fn -> describe(state) <> " is interested in event: #{inspect event_number} (#{inspect stream_id}@#{inspect stream_version})" end)
# delegate event to process instance who will ack event processing on success
:ok = delegate_event(process_manager, event)
%State{state | process_managers: Map.put(process_managers, process_uuid, process_manager)}
end
end
# continue processing any pending events and confirm receipt of the given event id
defp ack_and_continue(%RecordedEvent{} = event, %State{} = state) do
GenServer.cast(self(), :process_pending_events)
confirm_receipt(event, state)
end
# confirm receipt of given event
defp confirm_receipt(%RecordedEvent{event_number: event_number} = event, %State{} = state) do
Logger.debug(fn -> describe(state) <> " confirming receipt of event: #{inspect event_number}" end)
do_ack_event(event, state)
%State{state | last_seen_event: event_number}
end
defp start_process_manager(process_uuid, %State{process_manager_name: process_manager_name, process_manager_module: process_manager_module, supervisor: supervisor}) do
{:ok, process_manager} = Supervisor.start_process_manager(supervisor, process_manager_name, process_manager_module, process_uuid)
Process.monitor(process_manager)
process_manager
end
defp continue_process_manager(process_uuid, %State{process_managers: process_managers} = state) do
case Map.get(process_managers, process_uuid) do
nil -> start_process_manager(process_uuid, state)
process_manager -> process_manager
end
end
defp stop_process_manager(process_uuid, %State{process_managers: process_managers}) do
case Map.get(process_managers, process_uuid) do
nil -> nil
process_manager ->
:ok = ProcessManagerInstance.stop(process_manager)
nil
end
end
defp remove_process_manager(process_managers, pid) do
Enum.reduce(process_managers, process_managers, fn
({process_uuid, process_manager_pid}, acc) when process_manager_pid == pid -> Map.delete(acc, process_uuid)
(_, acc) -> acc
end)
end
defp do_ack_event(event, %State{consistency: consistency, process_manager_name: name, subscription: subscription}) do
:ok = EventStore.ack_event(subscription, event)
:ok = Subscriptions.ack_event(name, consistency, event)
end
defp delegate_event(nil, _event), do: :ok
defp delegate_event(process_manager, %RecordedEvent{} = event) do
ProcessManagerInstance.process_event(process_manager, event, self())
end
defp describe(%State{process_manager_module: process_manager_module}),
do: inspect(process_manager_module)
end