David Beck

Thoughts

Follow on GitHub

Using Ranch From Elixir

30 Sep 2015 by David Beck on [LinkedIn] / [Feed]
submit to reddit

This post goes reverse order. Results and conclusion first. Background and motivation last. I keep my random ramblings to the end so you can save yourself earlier.

TestMe2 - the result

This is the result of my experiment. A TCP echo server in Elixir that uses the Erlang ranch library.

I used mix new testme2 --sup --module TestMe2 to generate the project.

Then I added ranch into the dependencies and to the OTP applications. The resulting mix.exs looks like this:

defmodule TestMe2.Mixfile do
  use Mix.Project

  def project do
    [app: :testme2,
     version: "0.0.1",
     elixir: "~> 1.0",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  def application do
    [applications: [:logger, :ranch],
     mod: {TestMe2, []}]
  end

  defp deps do
    [
     {:ranch, "~> 1.1"}
    ]
  end
end

Then I integrated my application into OTP in lib/testme2.ex:

defmodule TestMe2 do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false
    
    children = [
      worker(TestMe2.Worker, [])
    ]

    opts = [strategy: :one_for_one, name: TestMe2.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Added a Worker like this in lib/testme2_worker.ex:

defmodule TestMe2.Worker do
  def start_link do
    opts = [port: 8000]
    {:ok, _} = :ranch.start_listener(:Testme2, 100, :ranch_tcp, opts, TestMe2.Handler, [])
  end
end

Finally I needed a handler to actually talk TCP in lib/testme2_handler.ex:

defmodule TestMe2.Handler do

  def start_link(ref, socket, transport, opts) do
    pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
    {:ok, pid}
  end
         
  def init(ref, socket, transport, _Opts = []) do
    :ok = :ranch.accept_ack(ref)
    loop(socket, transport)
  end

  def loop(socket, transport) do
    case transport.recv(socket, 0, 5000) do
      {:ok, data} ->
        transport.send(socket, data)
        loop(socket, transport)
      _ ->
        :ok = transport.close(socket)
    end
  end
end

That’s about it. If you do the same you should have a fast TCP echo server.

The conclusion

The conclusion is simple: if you are new to both Elixir and Erlang, it is not a good idea to start by integrating an Erlang lib into Elixir. I made a very silly mistake and the result compiled without a warning. It was running without any warnings. It was listening to the port I specified. Only that my telnet connection was closed immediately after it had beed connected. No debug log, nothing.

Even though I like Elixir a lot and respect Erlang VM a whole other lot, it was very annoying. Making no mistakes, let me emphasize again: I made the mistake. It was just bad that it didn’t bark even a bit at me.

Testme1 - the bad code

defmodule Testme1.Handler do

  def start_link(Ref, Socket, Transport, Opts) do
    Pid = spawn_link(__MODULE__, :init, [Ref, Socket, Transport, Opts])
    {:ok, Pid}
  end
         
  def init(Ref, Socket, Transport, _Opts = []) do
    :ok = :ranch.accept_ack(Ref)
    loop(Socket, Transport)
  end

  def loop(Socket, Transport) do
    case :Transport.recv(Socket, 0, 5000) do
      {:ok, Data} ->
        :Transport.send(Socket, Data)
        loop(Socket, Transport)
      _ ->
        :ok = :Transport.close(Socket)
    end
  end
end

I guess any seasoned Elixir folk would immediately recognize the problem. If you don’t then compare this to the handler above in lib/testme2_handler.ex. The difference is subtle. The function parameters are capitalized. No one would do such a mistake, except me when I copied and adapted the ranch Erlang TCP echo example from here.

I fixed all the Erlang and Elixir syntax differences but somehow overlooked these capitalized names.

Motivation

The reason I am looking at Elixir is to do a distributed service with TCP. I did a lot of BSD socket programming in C++ so I know the options there. I started my research to see what people say about fast TCP servers in Elixir and Erlang. Cowboy and Ranch came out quite fast and I also found mentions about the default OTP TCP might not be fast enough to accept large number of connections.

Then I spent quite some time looking for an Elixir alternative to Ranch. What I found was either gen_tcp based or advertised that it is not for production.

Background

I have no Erlang or Elixir experience. I have read Dave Thomas’ Programming in Elixir. I have looked at Erlang for long and always wanted to use the BEAM Virtual Machine for a real project.

One great plus on the Elixir side is the possibility to use Erlang libraries. I read that this should be easy. For further preparation I also read the Little Elixir and & OTP Guidebook from Benjamin Tan Wei Hao. I always liked the OTP concepts and expected that most Erlang and Elixir libs will use it.