Packages
x3m_system
0.1.0
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
retired
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
retired
0.7.7
0.7.6
retired
0.7.5
0.7.4
retired
0.7.3
retired
0.7.2
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
retired
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.1.1
0.1.0
Building blocks for distributed and/or CQRS/ES systems
Current section
Files
Jump to
Current section
Files
lib/facade.ex
defmodule X3m.System.Facade do
@moduledoc """
defmodule MyFacade do
use X3m.System.Facade
route :do_something, MyController # calls MyController.do_something(params)
route :do_something_else, {MyController, :else} # calls MyController.else(params)
on_alarm {:expire!, id}, fn ->
with {:ok, _} <- Offer.expire!(id),
do: :ok
end
def handle_call({:get_timezone, :point, lat, long, metadata}=payload, from, state) do
hash = hash(:get_timezone, payload)
execute(state, hash, from, fn ->
Logger.metadata metadata
Timezone.get_timezone(:point, lat, long)
end)
{:noreply, state}
end
end
Facade is GenServer. It is used as:
pid = :global.whereis_name MyFacade
cmd1 = {:do_something, %{"lat" => "21", "long" => "21"}}
cmd2 = {:do_something_else, %{"lat" => "21", "long" => "21"}, [req_id: "1231231"]}
GenServer.call pid, cmd1
GenServer.call pid, cmd2
`cmd` is tuple of 2 or 3 elements. First one is route, second one are params that will be proxied to controller,
and optional 3rd is Logger metadata that will be set for that task.
Result of MyController.do_something/1 will be sent in reply to GenServer.call. If format of message
is different, manual handle_call/3 handler can be set.
"""
@doc """
Returns specification for `X3m.System.Facade.Supervisor`.
"""
def child_spec([_facade_module, _facade_name | _opts] = args) do
%{
id: X3m.System.Facade.Supervisor,
start: {X3m.System.Facade.Supervisor, :start_link, args}
}
end
defmacro route(cmd, {controller, action}) do
quote do
def handle_call({unquote(cmd), params}, from, state) do
Logger.info(fn ->
"Routing #{inspect(unquote(cmd))} to #{inspect(unquote(controller))}#{
inspect(unquote(action))
}"
end)
req_id = hash(unquote(cmd), params)
execute(state, req_id, from, fn ->
unquote(controller).unquote(action)(params)
end)
{:noreply, state}
end
def handle_call({unquote(cmd), params, metadata}, from, state) do
Logger.metadata(metadata)
Logger.info(fn ->
"Routing #{inspect(unquote(cmd))} to #{inspect(unquote(controller))}#{
inspect(unquote(action))
}"
end)
req_id = hash(unquote(cmd), params)
execute(state, req_id, from, fn ->
Logger.metadata(metadata)
unquote(controller).unquote(action)(params)
end)
{:noreply, state}
end
end
end
defmacro route(cmd, controller) do
quote do
def handle_call({unquote(cmd), params}, from, state) do
Logger.info(fn ->
"Routing #{inspect(unquote(cmd))} to #{inspect(unquote(controller))}"
end)
req_id = hash(unquote(cmd), params)
execute(state, req_id, from, fn ->
unquote(controller).unquote(cmd)(params)
end)
{:noreply, state}
end
def handle_call({unquote(cmd), params, metadata}, from, state) do
Logger.metadata(metadata)
Logger.info(fn ->
"Routing #{inspect(unquote(cmd))} to #{inspect(unquote(controller))}"
end)
req_id = hash(unquote(cmd), params)
execute(state, req_id, from, fn ->
Logger.metadata(metadata)
unquote(controller).unquote(cmd)(params)
end)
{:noreply, state}
end
end
end
defmacro on_alarm(msg, fun) do
quote do
def handle_call({:alarm, on_time?, unquote(msg)}, from, state) do
Logger.info(fn ->
"Received #{inspect(on_time?)} alarm message #{inspect(unquote(msg))}"
end)
req_id = hash(:alarm, unquote(msg))
handler = unquote(fun)
execute(state, req_id, from, fn -> handler.(on_time?) end)
{:noreply, state}
end
end
end
defmacro __using__(opts \\ []) do
quote do
use GenServer
require X3m.System.Facade
import X3m.System.Facade
require Logger
@default_cache unquote(opts[:default_cache] || :no_cache)
@cache_overrides unquote(opts[:cache_overrides] || [])
defp cache_ttl(cmd),
do: _cache_ttl(cmd, @cache_overrides)
defp _cache_ttl(cmd, nil),
do: @default_cache
defp _cache_ttl(cmd, overrides),
do: overrides[cmd] || @default_cache
@doc false
def start_link(request_sup, cache, opts \\ []),
do: GenServer.start_link(__MODULE__, {request_sup, cache}, opts)
@doc false
def init({request_sup, cache}) do
on_init()
{:ok, %{request_sup: request_sup, cache: cache}}
end
def handle_cast({:response, hash, response, ttl}, state) do
Cachex.transaction!(state.cache, [hash], fn cache_state ->
case Cachex.get(cache_state, hash) do
# no request in cache
{:missing, nil} ->
Logger.warn("We don't have cached callers for this request anymore")
{:ok, %{callers: callers, response: :pending}} ->
respond_to(callers, response)
# Logger.debug "Setting expiration time to #{inspect ttl}"
Cachex.set!(cache_state, hash, %{callers: [], response: response})
Cachex.expire(cache_state, hash, ttl)
other ->
Logger.warn("WTF is #{inspect(other)} ?!")
end
end)
{:noreply, state}
end
defp respond_to([], response),
do: :ok
defp respond_to([caller | others], response) do
# Logger.debug "Responding to caller #{inspect caller} with #{inspect response}"
:ok = GenServer.reply(caller, response)
respond_to(others, response)
end
defp on_init, do: :ok
defp execute(state, {:hash, hash, ttl}, from, fun) do
facade = self()
Cachex.transaction!(state.cache, [hash], fn cache_state ->
case Cachex.get(cache_state, hash) do
{:ok, nil} ->
Logger.debug(fn -> "We don't have cached result. Create queue of callers" end)
Cachex.put!(cache_state, hash, %{callers: [from], response: :pending}, ttl: ttl)
in_task(state, from, fn ->
response = fun.()
Logger.debug(fn -> "Sending response: #{inspect(response)}" end)
GenServer.cast(facade, {:response, hash, response, ttl})
response
end)
{:noreply, state}
{:ok, %{callers: callers, response: :pending}} ->
Logger.debug(fn -> "Command is processing ... appending caller to queue" end)
Cachex.put!(cache_state, hash, %{callers: [from | callers], response: :pending},
ttl: ttl
)
{:noreply, state}
{:ok, %{response: response}} ->
Logger.debug(fn -> "We have response #{inspect(response)}" end)
GenServer.reply(from, response)
end
end)
end
defp execute(state, _, from, fun),
do: in_task(state, from, fun)
defp in_task(state, from, fun) do
Task.Supervisor.start_child(state.request_sup, fn ->
response = fun.()
GenServer.reply(from, response)
end)
end
defp hash(cmd, params), do: _hash(cmd, params, cache_ttl(cmd))
defp _hash(cmd, params, :no_cache), do: :no_cache
defp _hash(cmd, params, ttl),
do: {:hash, :crypto.hash(:sha256, inspect({cmd, params})), ttl}
defoverridable on_init: 0
end
end
end