Packages
x3m_system
0.5.6
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/router.ex
defmodule X3m.System.Router do
@moduledoc """
Registers system wide services.
Each `service/2` macro registers system-wide service and function with
documentation in module that `uses` this module.
Service functions invoke function of the same name of specified module.
If result of that invocation is `{:reply, %X3m.System.Message{}}`,
it sends message to `message.reply_to` pid.
If result of invocation is `:noreply`, nothing is sent to that pid.
In any case function returns `:ok`.
## Examples
### Defining router
defmodule MyRouter do
use X3m.System.Router
@servicedoc false
service :create_user, MessageHandler
@servicedoc \"""
overridden!
\"""
service :get_user, MessageHandler
service :edit_user, MessageHandler
end
### Getting registered services
iex> MyRouter.registered_services()
[create_user: 1, get_user: 1, edit_user: 1]
### Invoking service as a function
iex> :create_user |>
...> X3m.System.Message.new() |>
...> MyRouter.create_user()
:ok
"""
require Logger
alias X3m.System.Message
defmacro service(service_name, message_handler, f) do
quote do
case(@servicedoc) do
nil ->
@doc """
Accepts `#{unquote(service_name)}` service call, routing it's `message` to
`#{unquote(message_handler)}.#{unquote(f)}/1`.
If result of that invocation is `{:reply, %X3m.System.Message{}}`,
it sends message to `message.reply_to` pid.
If result of invocation is `:noreply`, nothing is sent to that pid.
In any case function returns `:ok`.
## Example:
iex> #{inspect(unquote(service_name))} |>
...> X3m.System.Message.new() |>
...> #{__MODULE__}.#{unquote(service_name)}()
:ok
"""
other ->
@doc other
end
@x3m_service [{unquote(service_name), 1}]
@spec unquote(service_name)(Message.t()) :: :ok
def unquote(service_name)(%Message{service_name: unquote(service_name)} = message) do
Logger.metadata(message.logger_metadata)
X3m.System.Instrumenter.execute(:service_request_received, %{}, %{
service: unquote(service_name)
})
message
|> choose_node()
|> _invoke(unquote(message_handler), unquote(f), message)
end
@servicedoc nil
end
end
defmacro service(service_name, message_handler) do
quote do
service(unquote(service_name), unquote(message_handler), unquote(service_name))
end
end
defmacro __using__(_opts) do
quote do
alias X3m.System.Router
require Router
import Router
Module.register_attribute(
__MODULE__,
:x3m_service,
accumulate: true,
persist: true
)
@servicedoc nil
@doc !"""
Returns list of service functions with their arrity.
"""
@spec registered_services :: [{:atom, non_neg_integer}]
def registered_services do
__MODULE__.__info__(:attributes)
|> Keyword.get_values(:x3m_service)
|> List.flatten()
end
@doc !"""
Sends internal event for each service to be registered in runtime.
"""
@spec register_services :: :ok
def register_services do
services =
registered_services()
|> Enum.map(fn {service, _arrity} -> {service, __MODULE__} end)
|> Enum.into(%{})
X3m.System.Instrumenter.execute(:register_local_services, %{}, %{services: services})
:ok
end
@doc false
@spec _invoke(:local | node(), atom, atom, Message.t()) :: :ok
def _invoke(node, message_handler, f, message)
def _invoke(:local, message_handler, f, message) do
Logger.metadata(message.logger_metadata)
mono_start = System.monotonic_time()
X3m.System.Instrumenter.execute(
:invoking_service,
%{start: DateTime.utc_now(), mono_start: mono_start},
%{
node: Node.self(),
service: message.service_name
}
)
case apply(message_handler, f, [message]) do
{:reply, %Message{} = message} ->
send(message.reply_to, message)
X3m.System.Instrumenter.execute(
:service_responded,
%{
time: DateTime.utc_now(),
duration: X3m.System.Instrumenter.duration(mono_start)
},
%{message: message}
)
:ok
:noreply ->
:ok
end
end
def _invoke(node, message_handler, f, message) do
true =
:rpc.cast(node, __MODULE__, :_invoke, [
:local,
message_handler,
f,
message
])
:ok
end
@doc !"""
Choose node on which MFA will be applied.
This is optional callback. By default it will return `:local`,
meaning that `sys_msg` will be handled by local module.
It can be overriden like:
```
def choose_node(%X3m.System.Message{}) do
[:jobs_1@my_comp_name, :local] |> Enum.random()
end
```
"""
@spec choose_node(Message.t()) :: :local | node()
def choose_node(_sys_msg),
do: :local
defoverridable choose_node: 1
end
end
end