Recent Posts

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

Include Phoenix Channels in a Rails App

Posted on 01 Mar 2017

My side project Worfcam has a home page that lists the most recent photo taken from a device. It used to poll every 3 seconds for updates to the photos. I now use Phoenix channels to push new photos to the home page. This is how I did that. I got part of the idea from this blog post at akitonrails.com

Security note: it should be pointed out that this has very lax security as it trusts the rails process and elixir process are on the same machine and firewalled away from the world outside of connecting through nginx.

Rails Configuration

First include phoenix.js in your javascript. I managed to get it included by vendoring a hand compiled version straight from the phoenix github. After this is in place, you write your javascript the same way as you would a normal Phoenix app.

To connect the proper socket I load it from the environment. I add a new method to my AppContainer and use it in the javascript when connecting to the Phoenix socket. For local development this is ws://localhost:5300/socket. On production it is wss://example.com/socket.

class AppContainer
  # ...
  let(:channels_url) do
    ENV["CHANNELS_URL"]
  end
end

Pushing Events

The phoenix app will have the following route:

  scope "/", Channels do
    resources "/events", EventController, only: [:create]
  end

The controller will simply broadcast whatever gets sent in. This is why it trusts the network is locked down. A simple way to add authentication to this could be basic auth or digest auth. Or even OAuth 2.0 server to server workflow.

defmodule Channels.EventController do
  use Channels.Web, :controller

  def create(conn, %{"topic" => topic, "event" => event, "body" => body}) do
    Channels.Endpoint.broadcast(topic, event, body)
    conn |> send_resp(204, "")
  end
end

I have the Ecto Repo pointing to the same database and the models have a schema that matches what rails uses. My channels enforce that only the correct users can connect to channels. Channels will be straight forward normal channels.

To push events over from Rails I have a sidekiq job that sends the data required by the create action. Once in place events flow from Rails to Phoenix to the web browser.

class EventPushWorker
  include Sidekiq::Worker

  def perform(topic, event, body)
    return if Rails.env.test?
    response = Faraday.post(AppContainer.channels_event_url) do |req|
      req.headers["Content-Type"] = "application/json"
      req.body = {
        topic: topic,
        event: event,
        body: body,
      }.to_json
    end
  end
end

nginx Configuration

The only tricky part from here is getting Rails to talk to Phoenix in production. Local development works because you can talk directly to the server. For production I have nginx set up a location before the default location that passes to Rails.

upstram worfcam {
  server localhost:5000;
}

upstream channels {
    server localhost:5300;
}

location /socket {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Origin $http_origin;
    proxy_pass http://channels;
}

location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://worfcam;
}

Once this is in place, your Rails app doesn't even need to know that it's talking to Phoenix from the front end.

Foreman Systemd Export

Posted on 12 Feb 2017

I switched my side project to a single Linode instance this weekend in an effort to simplify its setup. In the process I decided to try foreman as a way of keeping the rails server up and running. I was very pleased with what came out of it.

Export scripts

To start you need to export systemd scripts into the /etc/systemd/system folder. Make sure to replace --user deploy with the user that your app should run as. This capistrano task will handle that.

namespace :app do
  desc "Reload systemd"
  task :systemd do
    on roles(:web) do
      within release_path do
        execute :sudo, :foreman, :export, :systemd, "/etc/systemd/system", "--user deploy"
        execute :sudo, :systemctl, "daemon-reload"
      end
    end
  end
end

The first deploy you won't need to daemon-reload, but whenever a systemd file changes the daemon needs to reload to know about the changes.

The export will create files that are named app-web@.service, where web is the name of the Procfile line. In systemd an @ service allows you to pass in parameters to the script. For this case, it will be the PORT environment variable.

Start/Stop/Restart

Once the scripts are in place. You need capistrano tasks that will. These tasks are all essentially the same and look like this:

namespace :app do
  desc "Start web server"
  task :start do
    on roles(:web) do |host|
      within release_path do
        execute :sudo, :systemctl, :start, "app-web@5000.service"
        execute :sudo, :systemctl, :start, "app-worker@5100.service"
      end
    end
  end
end

This will start up a web and worker instance with the default ports that foreman uses. As you add in new Procfile lines, you will need to add a new line here.

Adding to a deploy

Finally you need to insert systemd into the deploy cycle. This is easy by adding the tasks in after the appropriate capistrano task deploy:publishing.

after 'deploy:publishing', 'app:systemd'
after 'app:systemd', 'app:restart'

Enhancements

Capistrano connects into the deploy user and that user has sudo rights. It would be more secure to only allow these specific commands to be sudoable. Add this to your sudoers file to enable specific commands only.

deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart app-web@5000.service
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart app-worker@5100.service

The full config/deploy.rb file

namespace :app do
  desc "Start web server"
  task :start do
    on roles(:web) do |host|
      within release_path do
        execute :sudo, :systemctl, :start, "app-web@5000.service"
        execute :sudo, :systemctl, :start, "app-worker@5001.service"
        execute :sudo, :systemctl, :start, "app-clock@5002.service"
      end
    end
  end

  desc "Stop web server"
  task :stop do
    on roles(:web) do |host|
      within release_path do
        execute :sudo, :systemctl, :stop, "app-web@5000.service"
        execute :sudo, :systemctl, :stop, "app-worker@5001.service"
        execute :sudo, :systemctl, :stop, "app-clock@5002.service"
      end
    end
  end

  desc "Restart web server"
  task :restart do
    on roles(:web) do |host|
      within release_path do
        execute :sudo, :systemctl, :restart, "app-web@5000.service"
        execute :sudo, :systemctl, :restart, "app-worker@5001.service"
        execute :sudo, :systemctl, :restart, "app-clock@5002.service"
      end
    end
  end

  desc "Reload systemd"
  task :systemd do
    on roles(:web) do
      within release_path do
        execute :sudo, :foreman, :export, :systemd, "/etc/systemd/system", "--user deploy"
        execute :sudo, :systemctl, "daemon-reload"
      end
    end
  end
end

after 'deploy:publishing', 'app:systemd'
after 'app:systemd', 'app:restart'
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.