Current section
Files
Jump to
Current section
Files
src/elli.erl
%% @doc: Elli acceptor manager
%%
%% This gen_server owns the listen socket and manages the processes
%% accepting on that socket. When a process waiting for accept gets a
%% request, it notifies this gen_server so we can start up another
%% acceptor.
%%
-module(elli).
-behaviour(gen_server).
-include("elli.hrl").
-include("elli_util.hrl").
%% API
-export([start_link/0,
start_link/1,
stop/1,
get_acceptors/1,
get_open_reqs/1,
get_open_reqs/2,
set_callback/3
]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export_type([req/0, http_method/0, body/0, headers/0, response_code/0]).
%% @type req(). A record representing an HTTP request.
-type req() :: #req{}.
%% @type http_method(). An uppercase atom representing a known HTTP verb or a
%% binary for other verbs.
-type http_method() :: 'OPTIONS' | 'GET' | 'HEAD' | 'POST'
| 'PUT' | 'DELETE' | 'TRACE' | binary().
%% @type body(). A binary or iolist.
-type body() :: binary() | iolist().
-type header() :: {Key::binary(), Value::binary() | string()}.
-type headers() :: [header()].
-type response_code() :: 100..999.
-record(state, {socket :: elli_tcp:socket(),
acceptors :: ets:tid(),
open_reqs = 0 :: non_neg_integer(),
options = [] :: [{_, _}], % TODO: refine
callback :: elli_handler:callback()
}).
%% @type state(). Internal state.
-opaque state() :: #state{}.
-export_type([state/0]).
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link() -> Result when
Result :: {ok, Pid} | ignore | {error, Error},
Pid :: pid(),
Error :: {already_started, Pid} | term().
%% @equiv start_link({@EXAMPLE_CONF})
%% @doc Create an Elli server process as part of a supervision tree, using the
%% default configuration.
start_link() -> start_link(?EXAMPLE_CONF).
-spec start_link(Opts) -> Result when
Opts :: [{_, _}], % TODO: refine
Result :: {ok, Pid} | ignore | {error, Error},
Pid :: pid(),
Error :: {already_started, Pid} | term().
start_link(Opts) ->
valid_callback(required_opt(callback, Opts))
orelse throw(invalid_callback),
case proplists:get_value(name, Opts) of
undefined ->
gen_server:start_link(?MODULE, [Opts], []);
Name ->
gen_server:start_link(Name, ?MODULE, [Opts], [])
end.
-spec get_acceptors(atom()) -> {reply, {ok, [ets:tid()]}, state()}.
get_acceptors(S) ->
gen_server:call(S, get_acceptors).
-spec get_open_reqs(S :: atom()) -> {reply, {ok, non_neg_integer()}, state()}.
%% @equiv get_open_reqs(S, 5000)
get_open_reqs(S) ->
get_open_reqs(S, 5000).
-spec get_open_reqs(S :: atom(), Timeout :: non_neg_integer()) -> Reply when
Reply :: {reply, {ok, non_neg_integer()}, state()}.
get_open_reqs(S, Timeout) ->
gen_server:call(S, get_open_reqs, Timeout).
-spec set_callback(S, Callback, CallbackArgs) -> Reply when
S :: atom(),
Callback :: elli_handler:callback_mod(),
CallbackArgs :: elli_handler:callback_args(),
Reply :: {reply, ok, state()}.
set_callback(S, Callback, CallbackArgs) ->
valid_callback(Callback) orelse throw(invalid_callback),
gen_server:call(S, {set_callback, Callback, CallbackArgs}).
%% @doc Stop `Server'.
-spec stop(Server :: atom()) -> {stop, normal, ok, state()}.
stop(S) ->
gen_server:call(S, stop).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @hidden
-spec init([Opts :: [{_, _}]]) -> {ok, state()}.
init([Opts]) ->
%% Use the exit signal from the acceptor processes to know when
%% they exit
process_flag(trap_exit, true),
Callback = required_opt(callback, Opts),
CallbackArgs = proplists:get_value(callback_args, Opts),
IPAddress = proplists:get_value(ip, Opts, {0, 0, 0, 0}),
Port = proplists:get_value(port, Opts, 8080),
MinAcceptors = proplists:get_value(min_acceptors, Opts, 20),
UseSSL = proplists:get_value(ssl, Opts, false),
KeyFile = proplists:get_value(keyfile, Opts),
CertFile = proplists:get_value(certfile, Opts),
SockType = ?IF(UseSSL, ssl, plain),
SSLSockOpts = ?IF(UseSSL,
[{keyfile, KeyFile}, {certfile, CertFile}],
[]),
AcceptTimeout = proplists:get_value(accept_timeout, Opts, 10000),
RequestTimeout = proplists:get_value(request_timeout, Opts, 60000),
HeaderTimeout = proplists:get_value(header_timeout, Opts, 10000),
BodyTimeout = proplists:get_value(body_timeout, Opts, 30000),
MaxBodySize = proplists:get_value(max_body_size, Opts, 1024000),
Options = [{accept_timeout, AcceptTimeout},
{request_timeout, RequestTimeout},
{header_timeout, HeaderTimeout},
{body_timeout, BodyTimeout},
{max_body_size, MaxBodySize}],
%% Notify the handler that we are about to start accepting
%% requests, so it can create necessary supporting processes, ETS
%% tables, etc.
ok = Callback:handle_event(elli_startup, [], CallbackArgs),
{ok, Socket} = elli_tcp:listen(SockType, Port, [binary,
{ip, IPAddress},
{reuseaddr, true},
{backlog, 32768},
{packet, raw},
{active, false}
| SSLSockOpts]),
Acceptors = ets:new(acceptors, [private, set]),
[begin
Pid = elli_http:start_link(self(), Socket, Options,
{Callback, CallbackArgs}),
ets:insert(Acceptors, {Pid})
end
|| _ <- lists:seq(1, MinAcceptors)],
{ok, #state{socket = Socket,
acceptors = Acceptors,
open_reqs = 0,
options = Options,
callback = {Callback, CallbackArgs}}}.
%% @hidden
-spec handle_call(get_acceptors, {pid(), _Tag}, state()) ->
{reply, {ok, [ets:tid()]}, state()};
(get_open_reqs, {pid(), _Tag}, state()) ->
{reply, {ok, OpenReqs :: non_neg_integer()}, state()};
(stop, {pid(), _Tag}, state()) -> {stop, normal, ok, state()};
({set_callback, Mod, Args}, {pid(), _Tag}, state()) ->
{reply, ok, state()} when
Mod :: elli_handler:callback_mod(),
Args :: elli_handler:callback_args().
handle_call(get_acceptors, _From, State) ->
Acceptors = [Pid || {Pid} <- ets:tab2list(State#state.acceptors)],
{reply, {ok, Acceptors}, State};
handle_call(get_open_reqs, _From, State) ->
{reply, {ok, State#state.open_reqs}, State};
handle_call({set_callback, Callback, CallbackArgs}, _From, State) ->
ok = Callback:handle_event(elli_reconfigure, [], CallbackArgs),
{reply, ok, State#state{callback = {Callback, CallbackArgs}}};
handle_call(stop, _From, State) ->
{stop, normal, ok, State}.
%% @hidden
-spec handle_cast(accepted | _Msg, State0) -> {noreply, State1} when
State0 :: state(),
State1 :: state().
handle_cast(accepted, State) ->
{noreply, start_add_acceptor(State)};
handle_cast(_Msg, State) ->
{noreply, State}.
%% @hidden
-spec handle_info({'EXIT', _Pid, Reason}, State0) -> Result when
State0 :: state(),
Reason :: {error, emfile},
Result :: {stop, emfile, State0}
| {noreply, State1 :: state()}.
handle_info({'EXIT', _Pid, {error, emfile}}, State) ->
?ERROR("No more file descriptors, shutting down~n"),
{stop, emfile, State};
handle_info({'EXIT', Pid, normal}, State) ->
{noreply, remove_acceptor(State, Pid)};
handle_info({'EXIT', Pid, Reason}, State) ->
?ERROR("Elli request (pid ~p) unexpectedly crashed:~n~p~n", [Pid, Reason]),
{noreply, remove_acceptor(State, Pid)}.
%% @hidden
-spec terminate(_Reason, _State) -> ok.
terminate(_Reason, _State) ->
ok.
%% @hidden
-spec code_change(_OldVsn, State, _Extra) -> {ok, State} when State :: state().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec remove_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state().
remove_acceptor(State, Pid) ->
ets:delete(State#state.acceptors, Pid),
dec_open_reqs(State).
-spec start_add_acceptor(State0 :: state()) -> State1 :: state().
start_add_acceptor(State) ->
Pid = elli_http:start_link(self(), State#state.socket,
State#state.options, State#state.callback),
add_acceptor(State, Pid).
-spec add_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state().
add_acceptor(#state{acceptors = As} = State, Pid) ->
ets:insert(As, {Pid}),
inc_open_reqs(State).
-spec required_opt(Name, Opts) -> Value when
Name :: any(),
Opts :: proplists:proplist(),
Value :: term().
required_opt(Name, Opts) ->
case proplists:get_value(Name, Opts) of
undefined ->
throw(badarg);
Value ->
Value
end.
-spec valid_callback(Mod :: module()) -> Exported :: boolean().
valid_callback(Mod) ->
lists:member({handle, 2}, Mod:module_info(exports)) andalso
lists:member({handle_event, 3}, Mod:module_info(exports)).
-spec dec_open_reqs(State0 :: state()) -> State1 :: state().
dec_open_reqs(#state{open_reqs = OpenReqs} = State) ->
State#state{open_reqs = OpenReqs - 1}.
-spec inc_open_reqs(State0 :: state()) -> State1 :: state().
inc_open_reqs(#state{open_reqs = OpenReqs} = State) ->
State#state{open_reqs = OpenReqs + 1}.