Skip to content

Instantly share code, notes, and snippets.

@joshnuss
Last active November 28, 2019 16:39
Show Gist options
  • Save joshnuss/4282f2edccfd1f395091 to your computer and use it in GitHub Desktop.
Save joshnuss/4282f2edccfd1f395091 to your computer and use it in GitHub Desktop.
A module for defining GenServer proxies
# defines a generic proxy
defmodule GenProxy do
defmacro __using__(_) do
quote location: :keep do
use GenServer
def handle_call(msg, from={_process, ref}, state) do
case proxy_call(msg, from, state) do
{:forward, server, new_state} ->
:erlang.send(server, {:"$gen_call", {self, ref}, msg}, [:noconnect])
receive do
{_, reply} ->
{:reply, reply, new_state}
other ->
{:reply, :error, new_state}
end
other -> other
end
end
def handle_cast(msg, state) do
case proxy_cast(msg, state) do
{:forward, server, new_state} ->
:erlang.send(server, {:"$gen_cast", msg}, [:noconnect])
{:noreply, new_state}
other -> other
end
end
def handle_info({:DOWN, ref, :process, _from, reason}, state=%{ref: ref}) do
{:stop, reason, state}
end
def handle_info(msg, state) do
case proxy_info(msg, state) do
{:forward, server, new_state} ->
:erlang.send(server, msg, [:noconnect])
{:noreply, new_state}
other -> other
end
end
def proxy_call(msg, _from, state) do
{:stop, {:bad_call, msg}, state}
end
def proxy_cast(msg, state) do
{:stop, {:bad_cast, msg}, state}
end
def proxy_info(msg, state) do
{:stop, {:bad_info, msg}, state}
end
defoverridable proxy_call: 3, proxy_cast: 2, proxy_info: 2
end
end
end
# MathServer: a simple server that supports addition
defmodule MathServer do
use GenServer
def start_link(opts \\ []) do
name = Keyword.get(opts, :name)
GenServer.start_link(__MODULE__, name, opts)
end
def add(server, a, b) do
GenServer.call(server, {:add, a, b})
end
def handle_call(msg={:add, a, b}, _from, name) do
IO.puts("#{name} got #{inspect msg}")
{:reply, a + b, name}
end
end
# defines a proxy that delegates messages to an underlying server (i.e. MathServer)
# implementing a GenProxy is just like a GenServer except you handle proxy_call, proxy_cast, proxy_info
# you can return anything GenServer supports, i.e. {:reply, response, new_state}
# OR you can return {:forward, pid, new_state}
defmodule MathProxy do
use GenProxy
def start_link(server, opts) do
GenServer.start_link(__MODULE__, %{server: server}, opts)
end
def init(%{server: server}) do
ref = Process.monitor(server)
{:ok, %{server: server, ref: ref}}
end
# handle {:add, 20, 25} in a special way
def proxy_call(msg={:add, 20, 25}, _from, state) do
IO.puts("proxy got #{inspect msg}")
{:reply, :eureka, state}
end
# send all other messages to underlying server
def proxy_call(_msg, _from, state=%{server: server}) do
{:forward, server, state}
end
end
defmodule LoadBalancer.Random do
use GenProxy
def start_link(servers, opts) do
GenServer.start_link(__MODULE__, %{servers: servers}, opts)
end
def proxy_call(_msg, _from, state=%{servers: servers}) do
{:forward, random_server(servers), state}
end
defp random_server(servers) do
servers |> Enum.shuffle |> List.first
end
end
# start up a math server with the names :math1 & :math2
MathServer.start_link(name: :math1)
MathServer.start_link(name: :math2)
# start a "MathProxy" called :math_proxy for the server named :math1
# it will proxy all messages except {:add, 20, 25}
MathProxy.start_link(:math1, name: :math_proxy)
# Both these calls result in 15
IO.puts MathServer.add(:math1, 10, 5) # direct call, returns 15
IO.puts MathServer.add(:math_proxy, 10, 5) # proxied call, returns 15
# The message {:add, 20, 25} is handled by the proxy and returns :eureka
IO.puts MathServer.add(:math_proxy, 20, 25) # returns :eureka
# start a random load balancer for the :math_proxy and :math2
LoadBalancer.Random.start_link [:math_proxy, :math2], name: :load_balancer
IO.puts MathServer.add(:load_balancer, 10, 5) # direct call, returns 15, executed on :math2
IO.puts MathServer.add(:load_balancer, 10, 5) # direct call, returns 15, executed on :math1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment