Last week I wrote about learning Elixir application design by studing the design of Mix. This week I’ll continue looking at OTP design by looking into the design of Ranch which is used by the Cowboy web server. Here is an example of the Ranch process tree:
Ranch Process Tree
Last week I mentioned that I was using a very simple cowboy sample program from Github called IdahoEv/cowboy-elixir-example. I used it to produce the images above, however I made a very small change to the example to spawn only 10 ranch processes instead of 100. With 100 process the picture of the ranch process tree would be too large.
Ranch Design
According to the Github page, “Ranch is a socket acceptor pool for TCP protocols.” In terms of application design all I’m really interested in is that it is a pool. At first glance, it looks like the process tree follows a similar pattern to that described for a pool in Learn You Some Erlang’s chapter on pools. It has a supervisor for the whole ranch called ranch_sup
and an unnamed supervisor labeled <0.128.0> for the worker processes. It does not have a supervisor above, like Learn you Some Erlang’s ppool_supersup as it is presumably unnecessary to support multiple ranch pools. Also, Compared to Learn you Some Erlang, ranch has additional processes labeled <0.129.0> and <0.130>.
I should also be able to learn a lot from the Ranch source. Now of course it is written in Erlang so I won’t be looking for a mix.exs file. I can start with ranch.app.src:
{application, ranch, [
{description, "Socket acceptor pool for TCP protocols."},
{vsn, "1.1.0"},
{id, "git"},
{modules, []},
{registered, [ranch_sup, ranch_server]},
{applications, [
kernel,
stdlib
]},
{mod, {ranch_app, []}},
{env, []}
]}.
which shows that the application starts with two registered child processes, ranch_sup
and ranch_server
.
I spent quite some time looking over the source to understand how the process structure is actually setup. Since this is Erlang things were a little foreign to me but I did discover that Ranch doesn’t use exactly the same pool architecture used in Learn you some Erlang.
There is a function ranch:start_listener
that starts up a ranch_listener_sup
process under the supervision of ranch_sup
. That is, the process <0.128.0> is a ranch_listener_sup
supervisor. There are two additional supervisors under its control which are shown in the tree as processes <0.129.0> and <0.130.0>. Reading ranch_listener_sup.erl, I see that ranch_listener_sup
configures these two supervisors as ranch_cons_sup
(connections) and ranch_acceptors_sup
:
ChildSpecs = [
{ranch_conns_sup, {ranch_conns_sup, start_link,
[Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol]},
permanent, infinity, supervisor, [ranch_conns_sup]},
{ranch_acceptors_sup, {ranch_acceptors_sup, start_link,
[Ref, NbAcceptors, Transport, TransOpts]},
permanent, infinity, supervisor, [ranch_acceptors_sup]}
],
ranch_acceptors_sup:Module:init/1
creates many child processes so it must be process <0.130.0> which, in the tree, supervises 10 processes. That means <0.129.0> must be ranch_conns_sup
.
The ranch_acceptor
processes wait for connections and when the receive a connection they ask the ranch_conns_sup
process to spawn a ranch_conns
worker to handle the connection. Then the acceptor goes back to waiting to accept a new connection.
So then the connections are transient processes that exist just long enough to serve requests. In fact, if I repeatedly reload http://localhost:8080 I can see a connection process show up in the Erlang observer. I am not sure how many connection processes are created. I believe the browser may be caching the result. I tried running
for i in $(seq 1 1000) ; do curl --no-sessionid localhost:8080 & done
to hammer the Cowboy server with lots of requests. When I did this I could see more than one connection process created over time and the process number would jump by a lot. So I believe the connection process is only use a single time.
It is interesting to note that ranch_conns_sup
is not an OTP supervisor and in fact seems to serve two purposes
- It supervises the connection processes
- It acts as a server that spawns connection processes.
In standard OTP this might have been implemented as two separate processes (a supervisor and a gen_server). I wonder ranch_conns_sup
’s design is motivated by performance in the acceptance speed. The acceptor worker needs to wait for the connection process to spawn and take over the connection before returning to the pool.
Edit June, 27, 2015: Loïc Hoguin, was kind enough to reply on Twitter and let me know:
@renatomoya @Joseph_Kain "I wonder ranch_conns_sup’s design is motivated by performance" exactly why; used to be separate
— Loïc Hoguin (@lhoguin) June 27, 2015
Summary
So, in summary Ranch uses a pool of acceptors and maintains a set of active connections. The acceptor pool is implemented by the following processes:
ranch_sup
ranch_listener_sup
ranch_server
ranch_acceptors_sup
ranch_acceptor
workers
The active connection set is managed ranch_conns_sup
which in turn is supervised by ranch_listener_sup
.
Next Week
Next week I will look at Cowboy itself. I’ve looked ahead and it has a rather small process tree so I will read look at the way it builds up from multiple applications (itself, Ranch and others). This is the next level up in OTP design.