Posted on 09 Sep 2017
These are the links I gathered while watching talks at ElixirConf 2017.
Posted on 05 Sep 2017
For my side project, ex_venture, I wanted to add a web client that allowed players to connect not just via normal telnet. This meant I needed to add Phoenix. I was excited to try this out because the phoenix devs keep saying how you should just think of it as a layer for your app, not the app itself. Since I had an app already this was the perfect trial.
Brief note: ex_venture is a MUD engine, the protocol up until now was only telnet.
Adding Phoenix
Adding phoenix was a pretty simple affair. It took about 2 hours to get it up and running in a simple form. Here is the commit that adds it entirely. I mostly copied from a new phoenix project and took what I wanted.
The fun part was the TelnetChannel that talked with my session module similar to the normal telnet socket.
Phoenix <-> OTP communication
Another interesting part of this addition was I could add a web admin panel to the game. With this I wanted live updates to the game. If I added a new room to a zone, then I should see it reflected immediately in the game. I achieved this by creating bounded contexts that talk to the data layer then push the update into the OTP layer.
Samples
Updating a zone
This shows off updating a zone and pushing the change live into the game. The controller knows nothing about the OTP. Web.Zone
is a layer between Phoenix and the game.
Web.Admin.ZoneController
alias Web.Zone
def update(conn, %{"id" => id, "zone" => params}) do
case Zone.update(id, params) do
{:ok, zone} -> conn |> redirect(to: zone_path(conn, :show, zone.id))
{:error, changeset} ->
zone = Zone.get(id)
conn |> render("edit.html", zone: zone, changeset: changeset)
end
end
Web.Zone
alias Data.Zone
def update(id, params) do
zone = id |> get()
changeset = zone |> Zone.changeset(params)
case changeset |> Repo.update do
{:ok, zone} ->
Game.Zone.update(zone.id, zone)
{:ok, zone}
anything -> anything
end
end
Game.Zone
# pid expands to the elixir Registry
def update(id, zone) do
GenServer.cast(pid(id), {:update, zone})
end
def handle_cast({:update, zone}, state) do
{:noreply, Map.put(state, :zone, zone)}
end
Adding a new room to a zone
This shows off how the Room admin will spawn a new room in a zone after being created. The controller knows nothing about OTP. Web.Room
is a layer between Phoenix and the game.
Web.Admin.RoomController
alias Web.Room
def create(conn, %{"zone_id" => zone_id, "room" => params}) do
zone = Zone.get(zone_id)
case Room.create(zone, params) do
{:ok, room} -> conn |> redirect(to: room_path(conn, :show, room.id))
{:error, changeset} -> conn |> render("new.html", zone: zone, changeset: changeset)
end
end
Web.Room
alias Data.Room
def create(zone, params) do
changeset = zone |> Ecto.build_assoc(:rooms) |> Room.changeset(params)
case changeset |> Repo.insert() do
{:ok, room} ->
Game.Zone.spawn_room(zone.id, room)
{:ok, room}
anything -> anything
end
end
Game.Zone
When the Room.Supervisor comes online it lets the zone know it’s PID to spawn new rooms in.
def spawn_room(id, room) do
GenServer.cast(pid(id), {:spawn_room, room})
end
def handle_cast({:spawn_room, room}, state = %{room_supervisor_pid: room_supervisor_pid}) do
Room.Supervisor.start_child(room_supervisor_pid, room)
{:noreply, state}
end
Game.Room.Supervisor
The Room.Supervisor
starts the new room in the supervision tree.
def start_child(pid, room) do
child_spec = worker(Room, [room], id: room.id, restart: :permanent)
Supervisor.start_child(pid, child_spec)
end
Conclusion
Adding Phoenix to an regular OTP app was incredibly simple and the Phoenix team did what they set out to. I hope you explore the rest of the app to find more examples of Phoenix <-> OTP communication.
Posted on 11 Aug 2017
For a side project I wanted to figure out how to use :via
with GenServer. I have two different GenServers that should respond to similar GenServer interface, NPCs and User Sessions.
I went this route because I already had two registries going, one for each type of instance. The NPCs registry was a standard unique registry and the session registry was a duplicate registry so I could easily determine which sessions were online and had a connected user.
With that in place it was very easy to get a :via
module set up. I did not have to handle two of the required functions for :via
which deal with registering and unregistering.
:via whereis_name
The first thing I had to get going was finding the correct PID given an existing registry. For NPCs I simply delegated off to the elixir registry since it was already built in. I patterned matched against a tuple to determine the split.
def whereis_name({:npc, id}) do
Registry.whereis_name({Game.NPC.Registry, id})
end
def whereis_name({:user, id}) do
player = Session.Registry.connected_players
|> Enum.find(&(elem(&1, 1).id == id))
case player do
{pid, _} -> pid
_ -> :undefined
end
end
:via send
This was a similar instance, delegate off to the standard registry for NPCs and find the pid for user sessions.
def send({:npc, id}, message) do
Registry.send({Game.NPC.Registry, id}, message)
end
def send({:user, id}, message) do
case whereis_name({:user, id}) do
:undefined ->
{:badarg, { {:user, id}, message} }
pid ->
Kernel.send(pid, message)
pid
end
end
Conclusion
It was fairly easy to get this going and now I can send a message to either set of GenServers by only know their database ids. I can start to hide the knowledge of if the server is a session or an NPC pid going forward for things that won’t care.
Posted on 12 Jul 2017
I started playing around with ranch yesterday and didn’t see any examples on how to use it with Elixir, or an example with a GenServer. I finally managed to get it going, here is how I did it.
Start ranch
I have this module being started as a worker for my application. This will kick off the listener.
defmodule App do
def start_link() do
:ranch.start_listener(make_ref(), :ranch_tcp, [{:port, 5555}], App.GenProtocol, [])
end
end
The first parameter is a reference, I couldn’t find any explanation on how it’s used so I went with make_ref
. The other thing that matters is the App.GenProtocol
part, which is the callback module that ranch will use for starting new connections.
Ranch Protocol
This part is fairly simple to get going as a non-GenServer process, but I really wanted to use GenServer since it handles a lot of extras for me. The tricky part was using :proc_lib
to start the module and then entering the :gen_server.enter_loop
after hooking up to the socket.
To note, the transport
variable is :ranch_tcp
which ends up calling into that erlang module.
defmodule App.GenProtocol do
use GenServer
@behaviour :ranch_protocol
def start_link(ref, socket, transport, _opts) do
pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport])
{:ok, pid}
end
def init(ref, socket, transport) do
IO.puts "Starting protocol"
:ok = :ranch.accept_ack(ref)
:ok = transport.setopts(socket, [{:active, true}])
:gen_server.enter_loop(__MODULE__, [], %{socket: socket, transport: transport})
end
def handle_info({:tcp, socket, data}, state = %{socket: socket, transport: transport}) do
IO.inspect data
transport.send(socket, data)
{:noreply, state}
end
def handle_info({:tcp_closed, socket}, state = %{socket: socket, transport: transport}) do
IO.puts "Closing"
transport.close(socket)
{:stop, :normal, state}
end
end
This will create a simple echo server. You can connect via telnet with telnet localhost 5555
. It is very easy to extend this to perform more complex actions.
Example
Server
$ mix run --no-halt
Compiling 1 file (.ex)
Starting protocol
"Hi\r\n"
Closing
Client
$ telnet localhost 5555
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hi
Hi
^]
telnet> Connection closed.
Posted on 07 Jul 2017
I’ve been reading the Designing for Scalability with Erlang/OTP book and because of that I’ve been getting interested in writing some erlang code. I got curious if I could call my elixir code from erlang.
This is very simple. Since module and function names are just atoms in erlang you can call them like this:
'Elixir.IO':'puts'("Hello, world!").
Elixir puts all of it’s modules “under” the Elixir
atom. So to call an Elixir function named Api.User.changeset()
you would use 'Elixir.Api.User':'changeset'()
.