A Simple Erlang Application

November 9, 2016

An HTTP server structured as an Erlang application. Built with Elli, it uses ERL_LIBS and make instead of rebar.

Luminous box with the word service on it in block capitals.
© 2016 Mike Wilson for Unsplash

The Code

Directory Structure


    .
    ├── Makefile
    ├── ebin
    └── src
        ├── erlsrv.app.src
        ├── erlsrv.erl
        ├── es_callback.erl
        └── es_sup.erl
    

The Erlang documentation mentions four directories (src, ebin, priv and include) but the application started fine with just two.

Makefile


    all: ebin/es_sup.beam ebin/es_callback.beam ebin/erlsrv.app ebin/erlsrv.beam
    
    ELLI=${HOME}/src/elli
    ${ELLI}/ebin:
    	(cd ${ELLI} ; make)
    
    ebin/%.beam: src/%.erl ${ELLI}/ebin
    	ERL_LIBS=${ELLI} erlc -o ebin/ $<
    
    ebin/erlsrv.app: src/erlsrv.app.src
    	cp $? $@
    
    .PHONY: clean
    clean:
    	rm -f ebin/*
    

ERL_LIBS makes this work. I cloned the Elli repository under $HOME/src and set ERL_LIBS to that before calling erlc. A simple way to manage dependencies that is built in to Erlang.

Environment variable ERL_LIBS (defined in the operating system) can be used to define more library directories to be handled in the same way as the standard OTP library directory described above, except that directories without an ebin directory are ignored.

All application directories found in the additional directories appears before the standard OTP applications, except for the Kernel and STDLIB applications, which are placed before any additional applications. In other words, modules found in any of the additional library directories override modules with the same name in OTP, except for modules in Kernel and STDLIB.

Environment variable ERL_LIBS (if defined) is to contain a colon-separated (for Unix-like systems) or semicolon-separated (for Windows) list of additional libraries.

src/erlsrv.app.src


    {application, erlsrv, [{mod, {erlsrv,[]}}]}.
    

The smallest possible application configuration file.

src/erlsrv.erl


    -module(erlsrv).
    -behavior(application).
    
    -export([start/2, stop/1]).
    
    start(_Type, _Args) -> es_sup:start_link().
    
    stop(_State) -> ok.
    

The job of the application behavior is to start the main supervisor.

src/es_callback.erl


    -module(es_callback).
    -export([handle/2, handle_event/3]).
    
    -include_lib("elli/include/elli.hrl").
    -behaviour(elli_handler).
    
    % Dispatch to handler functions
    handle(Req, _Args) ->
        handle(Req#req.method, elli_request:path(Req), Req).
    
    handle(’GET’,[<<"hello">>, <<"world">>], _Req) ->
        {ok, [], <<"Hello World!">>};
    
    handle(_, _, _Req) ->
        {404, [], <<"Not Found">>}.
    
    %% @doc: Handle request events, like request completed, exception
    %% thrown, client timeout, etc. Must return ’ok’.
    handle_event(_Event, _Data, _Args) ->
        ok.
    

Dispatch HTTP calls to handlers. Elli spawns a process for each request so they are isolated from each other.

This is the file that will grow as I build a server that does something.

src/es_sup.erl


    -module(es_sup).
    -behaviour(supervisor).
    -export([start_link/0]).
    -export([init/1]).
    
    start_link() ->
        supervisor:start_link({local, ?MODULE}, ?MODULE, []).
    
    init([]) ->
        ElliOpts = [{callback, es_callback}, {port, 3000}],
        ElliSpec = {
            es_http,
            {elli, start_link, [ElliOpts]},
            permanent,
            5000,
            worker,
            [elli]},
    
        {ok, { {one_for_one, 5, 10}, [ElliSpec]} }.
    

The supervisor for Elli. Copied right from Elli’s github readme.

Running the application


    $ ERL_LIBS=$HOME/src/elli erl -pa ebin
    Erlang/OTP 19 [erts-8.0.1] [source-ca40008] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
    
    Eshell V8.0.1  (abort with ^G)
    1> application:start(erlsrv).
    ok
    2>
    

Note the use of ERL_LIBS when starting interpreter.


    $ curl -v http://127.0.0.1:3000/hello/world
    *   Trying 127.0.0.1…
    * Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
    > GET /hello/world HTTP/1.1
    > Host: 127.0.0.1:3000
    > User-Agent: curl/7.49.1
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < Connection: Keep-Alive
    < Content-Length: 12
    < 
    * Connection #0 to host 127.0.0.1 left intact
    Hello World!$
    

Notes

My Goals

  1. simplest code possible
  2. take my time and understand each step
  3. support SSL
  4. support basic auth

Why no rebar?


    $ cd $HOME/src/rebar3/
    $ du -sh src
    692K	src
    $ 
    

Violates goal #1.

Surprising behavior when no mod in application configuration file.

Don’t leave out the mod parameter in the app configuration file.

For example, if I change the config file to:


    {application, erlsrv, []}.
    

I can start the application without error


    $ ERL_LIBS=$HOME/src/elll erl -pa ebin
    Erlang/OTP 19 [erts-8.0.1] [source-ca40008] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
    
    Eshell V8.0.1  (abort with ^G)
    1> application:start(erlsrv).
    ok
    2> application:info().
    

and the application is loaded


    2> application:which_applications().
    [{erlsrv,[],[]},
     {stdlib,"ERTS  CXC 138 10","3.0"},
     {kernel,"ERTS  CXC 138 10","5.0"}]
    3> 
    

and shows as running


    3> application:info().
    [{loaded,[{kernel,"ERTS  CXC 138 10","5.0"},
              {stdlib,"ERTS  CXC 138 10","3.0"},
              {erlsrv,[],[]}]},
     {loading,[]},
     {started,[{erlsrv,temporary},
               {stdlib,permanent},
               {kernel,permanent}]},
     {start_p_false,[]},
     {running,[{erlsrv,undefined},
               {stdlib,undefined},
               {kernel,<0.33.0>}]},
     {starting,[]}]
    4> 
    
    

(There is supposed to be a process ID instead of the atom undefined on the line {running,[{erlsrv,undefined}.)

Hitting the server with curl produces Connection refused:


    $ curl -v http://127.0.0.1:3000/hello/world
    *   Trying 127.0.0.1…
    * connect to 127.0.0.1 port 3000 failed: Connection refused
    * Failed to connect to 127.0.0.1 port 3000: Connection refused
    * Closing connection 0
    curl: (7) Failed to connect to 127.0.0.1 port 3000: Connection refused
    $ 
    

What’s happening is that the the mod argument tells the application behavior what module holds the start/2 method to call. If we don’t provide that module name, the behavior does not call any start method and the server is not started.

Why Elli?

Tried mochiweb, but I was too new to Erlang.

Tried yaws and while creating an app was easy, but I got confused by yapp and embedded options.

Tried cowboy (or maybe it was Elixer?), but the tutorial didn’t work for me (and I gave up quickly).

By the time I found Elli, I was more familiar with Erlang. Also, the github readme had exactly the code I needed.

Also, it’s lightweight:


    $ cd $HOME/src/elli
    $ du -sh src
     92K	src
    $ 
    

Next steps …

This server is lacking pretty basic functionality. Like logging. Or including a Date HTTP header in the response.

Some future steps, in no particular order:

  • hot code reload
  • build a release
  • wrap into command-line with escript or sh
  • add logging and basic auth

Tags: erlang

Site generated by mkws and styled by Tufte CSS. Source at github.

© 2016 - 2022 Mark Bucciarelli