As a beginner, this is the “big” personal project am currently working on. So, the basic premise is: I am building a TCP server (-which adheres certain “spec”) which accepts a “command” and sends a response to it. My only criteria for this server is how fast does it respond back
After much debating, I chose :ranch
as my preferred TCP server. There are loads of blogs, articles praising its performance.
Given this I tried two different versions of the respond handler. There are some interesting articles I studied along with the way, based on which I have the below handlers. Both work fine and does their job. But, here is the problem:
the logic:
CmdHandler.handle_command(raw_command)
takes ~30 μs (99%)
but, the req-response latency is around ~10 mill seconds(99%) and I barely get 9k RPS for a single connection. THIS is not what I expected for all the tall claims from ranch. I am not sure where I am doing wrong, I want to fix this. any help is good.
Environment:
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.6.4 (compiled with OTP 20)
macOs 10.13.2 Kernel Version 17.3.0
Some good articles that gave a lot of insights on ranch:
TCP Handlers
defmodule Server.GenServerHandler do
require Logger
alias Command.Handler, as: CmdHandler
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
log(socket, "New Connection")
:ok = :ranch.accept_ack(ref)
:ok = transport.setopts(socket, [{:active, true}, {:nodelay, true}, {:reuseaddr, true}])
:gen_server.enter_loop(__MODULE__, [], %{socket: socket, transport: transport})
end
def handle_info({:tcp, socket, input}, %{socket: socket, transport: transport} = state) do
resp =input |> CmdHandler.handle_command()
transport.send(socket, resp)
{:noreply, state}
end
def handle_info({:tcp_closed, socket}, %{socket: socket, transport: transport} = state) do
log(socket, "Terminating connection")
transport.close(socket)
{:stop, :normal, state}
end
defp log(ref, input, resp \\ "") do
Logger.debug(
fn ->
"[#{inspect(ref)}]: #{input} || #{resp}"
end
)
end
end
defmodule Server.SimpleHandler do
alias Command.Handler, as: CmdHandler
def start_link(ref, socket, transport, opts) do
pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
{:ok, pid}
end
def init(ref, socket, transport, _Opts = []) do
:ok = :ranch.accept_ack(ref)
:ok = transport.setopts(socket, [{:nodelay, true}, {:reuseaddr, true}])
loop(socket, transport)
end
def loop(socket, transport) do
case transport.recv(socket, 0, 5000) do
{:ok, input} ->
resp = input |> CmdHandler.handle_command()
transport.send(socket, resp)
loop(socket, transport)
{:error, :closed} ->
:ok = transport.close(socket)
{:error, :timeout} ->
:ok = transport.close(socket)
{:error, _} -> # err_message
:ok = transport.close(socket)
end
end
end