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 req_id = hash(unquote(cmd), params) execute(state, req_id, from, fn -> Logger.info(fn -> "Routing #{inspect(unquote(cmd))}" end) Logger.debug(fn -> inspect(params) end) unquote(controller).unquote(action)(params) end) {:noreply, state} end def handle_call({unquote(cmd), params, metadata}, from, state) do req_id = hash(unquote(cmd), params) execute(state, req_id, from, fn -> Logger.metadata(metadata) Logger.info(fn -> "Routing #{inspect(unquote(cmd))}" end) Logger.debug(fn -> inspect(params) end) 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 req_id = hash(unquote(cmd), params) execute(state, req_id, from, fn -> Logger.info(fn -> "Routing #{inspect(unquote(cmd))}" end) Logger.debug(fn -> inspect(params) end) unquote(controller).unquote(cmd)(params) end) {:noreply, state} end def handle_call({unquote(cmd), params, metadata}, from, state) do req_id = hash(unquote(cmd), params) execute(state, req_id, from, fn -> Logger.metadata(metadata) Logger.info(fn -> "Routing #{inspect(unquote(cmd))}" end) Logger.debug(fn -> inspect(params) end) 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 req_id = hash(:alarm, unquote(msg)) handler = unquote(fun) execute(state, req_id, from, fn -> Logger.info(fn -> "Received #{inspect(on_time?)} alarm message #{inspect(unquote(msg))}" end) 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 start = System.monotonic_time() Task.Supervisor.start_child(state.request_sup, fn -> response = fun.() GenServer.reply(from, response) _log(response, start) end) end defp _log(response, start) do stop = System.monotonic_time() diff = System.convert_time_unit(stop - start, :native, :microsecond) log_payload = sanitize_response(response) Logger.info(fn -> "Responded with `#{log_payload}` in #{_formatted_diff(diff)}" end) Logger.debug(fn -> "Sent #{inspect(response)}" end) response end defp _formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] defp _formatted_diff(diff), do: [Integer.to_string(diff), "µs"] 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 @before_compile X3m.System.Facade end end defmacro __before_compile__(_env) do quote do def handle_call(cmd, from, state) do Logger.warn(fn -> "Not handled message: #{inspect(cmd)}" end) {:reply, {:error, :message_not_implemented}, state} end defp sanitize_response(response), do: inspect(response) end end end