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.
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
- simplest code possible
- take my time and understand each step
- support SSL
- 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