So I’m trying to get a better grasp of Elixir macros. In order to do so I’ve written a module. It’s called GenRouter, and what it does it allow the user to write a GenServer like module, but when it’s started, it will n instances of that server, and route calls to one of those instances, either by :caller
(ensuring that every pid will always reach the same server), or by :rand
(each pid could hit any one of the server instances). This is not something I need, but just a dummy module to learn macros. I guess it’s sort of like a pool. Below is the code as well as it being used
defmodule GenRouter do
@moduledoc """
GenRouter is a module that allows you to define a single GenServer like module.
When that module is started it will start multiple processes running the same code.
Calls to the module will be routed across each of the instances, like a pool.
When you define a module that use's GenRouter, it will define a module called __MODULE__.Sup
that is the module you want to start when your application starts.
Options:
num_children:
This is the number of copies of your module that will be started and have requests routed to them
Default: 2
route_by:
This is the method by which a call will be routed to a particuler instance of the module
If `:caller` is chosen, then every process will always be routed to the same instance of the module, this
is good if you have lots of different processes calling the module, however if you have only a few proccess sending
lots of messages and it doesn't matter that they are always handled by the same instance, then the option
`:rand` will ensure that all calls are evenly spread across the instances, but the different callers may be handled
by a different instance on each call
options: [:caller, :rand]
Default: :caller
"""
defmacro __using__(opts) do
quote do
use GenServer
@num_children Keyword.get(unquote(opts), :num_children, 2)
@route_by Keyword.get(unquote(opts), :route_by, :caller)
def start_link(args \\ nil) do
GenServer.start_link(__MODULE__, args, [])
end
def init(_), do: {:ok, nil}
defoverridable init: 1
@before_compile GenRouter
import GenRouter
end
end
defmacro defaction(fundef, do: body) do
quote do
# @type unquote(fundef) :: any()
def unquote(fundef) do
pid = __MODULE__.Sup.call()
case unquote(body) do
{:call, args} -> GenServer.call(pid, args)
{:cast, args} -> GenServer.cast(pid, args)
end
end
end
end
defmacro __before_compile__(_) do
quote do
mod = __MODULE__
supervisor_contents =
quote do
use DynamicSupervisor
def start_link(_) do
{:ok, pid} = DynamicSupervisor.start_link(__MODULE__, nil, name: __MODULE__)
start_children()
{:ok, pid}
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(strategy: :one_for_one)
end
def call() do
case unquote(@route_by) do
:caller ->
num = DynamicSupervisor.count_children(__MODULE__)[:workers]
children = DynamicSupervisor.which_children(__MODULE__)
{_, pid, _, _} = Enum.at(children, :erlang.phash2(self(), num))
pid
:rand ->
num = DynamicSupervisor.count_children(__MODULE__)[:workers]
children = DynamicSupervisor.which_children(__MODULE__)
rand = :rand.uniform(num)
{_, pid, _, _} = Enum.at(children, rand - 1)
pid
end
end
def start_children() do
for i <- 1..unquote(@num_children) do
DynamicSupervisor.start_child(__MODULE__, %{
id: i,
start: {unquote(mod), :start_link, []}
})
end
end
end
Module.create(__MODULE__.Sup, supervisor_contents, Macro.Env.location(__ENV__))
end
end
end
defmodule GenRouterImp do
use GenRouter, num_children: 10, route_by: :caller
defaction put_and_get(key, val) do
{:call, {:put_and_get, {key, val}}}
end
def init(_), do: {:ok, %{}}
def handle_call({:put_and_get, {k, v}}, _from, state) do
new_state = Map.put(state, k, v)
{:reply, new_state, new_state}
end
end
This would create a GenRouterImp.Sup
module that would be started. This would then start 10 instance of GenRouterImp
.
What I’d like to know is if there’s a cleaner way to do this with macros. Especiailly if there’s a way to do it so the implementing module doesn’t need to use the defaction
. Is there way to make it more like a regular GenServer implementation (perhaps with a module doc above each function stating whether it’s a call or a cast). I know that ExUnit makes use of module docs above each test in order to alter the way they behave at run time.