Recent Posts

Links from ElixirConf 2017

Posted on 09 Sep 2017

These are the links I gathered while watching talks at ElixirConf 2017.

Adding Phoenix to an OTP Elixir App

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.

Using GenServer :via

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.

Using Ranch with Elixir

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.

Call Elixir from Erlang

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'().

Eric Oestrich
I am:
All posts
Creative Commons License
This site's content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License unless otherwise specified. Code on this site is licensed under the MIT License unless otherwise specified.