Permalink
Browse files

WIP socket handover to handler.

  • Loading branch information...
1 parent 7534e4b commit 658f1e2fa6d6da6cb1028564cc5c7c3b1f37c38e @knutin committed Apr 17, 2013
Showing with 194 additions and 71 deletions.
  1. +2 −1 include/elli.hrl
  2. +31 −0 src/elli_example_callback_handover.erl
  3. +96 −68 src/elli_http.erl
  4. +61 −0 test/elli_handover_tests.erl
  5. +4 −2 test/elli_tests.erl
View
@@ -40,7 +40,8 @@
headers :: headers(),
body :: body(),
pid :: pid(),
- socket :: inet:socket()
+ socket :: inet:socket(),
+ callback :: callback()
}).
-define(EXAMPLE_CONF, [{callback, elli_example_callback},
@@ -0,0 +1,31 @@
+-module(elli_example_callback_handover).
+-export([init/2, handle/2, handle_event/3]).
+
+init(Req, Args) ->
+ case elli_request:path(Req) of
+ [<<"hello">>, <<"world">>] ->
+ handover;
+ _ ->
+ pure
+ end.
+
+handle(Req, Args) ->
+ handle(elli_request:method(Req), elli_request:path(Req), Req).
+
+
+handle('GET', [<<"hello">>, <<"world">>], Req) ->
+ Body = <<"Hello World!">>,
+ Size = list_to_binary(integer_to_list(size(Body))),
+ elli_http:send_response(Req, 200, [{"Connection", "close"},
+ {"Content-Length", Size}], Body),
+
+ {close, <<>>};
+
+handle('GET', [<<"hello">>], Req) ->
+ %% Fetch a GET argument from the URL.
+ Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>),
+ {ok, [], <<"Hello ", Name/binary>>}.
+
+
+handle_event(_, _, _) ->
+ ok.
View
@@ -11,6 +11,8 @@
%% API
-export([start_link/4]).
+-export([send_response/4]).
+
-export([mk_req/7]). %% useful when testing.
%% Exported for looping with a fully-qualified module name
@@ -25,8 +27,8 @@ start_link(Server, ListenSocket, Options, Callback) ->
-spec accept(pid(), port(), proplists:proplist(), callback()) -> ok.
%% @doc: Accept on the socket until a client connects. Handles the
%% request, then loops if we're using keep alive or chunked
-%% transfer. If accept doesn't give us a socket within 10 seconds, we
-%% loop to allow code upgrades.
+%% transfer. If accept doesn't give us a socket within a configurable
+%% timeout, we loop to allow code upgrades of this module.
accept(Server, ListenSocket, Options, Callback) ->
case catch elli_tcp:accept(ListenSocket, accept_timeout(Options)) of
{ok, Socket} ->
@@ -65,74 +67,86 @@ keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) ->
%% containing (parts of) the next request.
handle_request(S, PrevB, Opts, {Mod, Args} = Callback) ->
{Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), t(request_start),
- {RequestHeaders, B1} = get_headers(S, V, B0, Opts, Callback), t(headers_end),
- {RequestBody, B2} = get_body(S, RequestHeaders, B1, Opts, Callback), t(body_end),
+ {RequestHeaders, B1} = get_headers(S, V, B0, Opts, Callback), t(headers_end),
+
+ Req = mk_req(Method, RawPath, RequestHeaders, <<>>, V, S, Callback),
- Req = mk_req(Method, RawPath, RequestHeaders, RequestBody, V, S, Callback),
+ case behaviour(Req) of
+ pure ->
+ {RequestBody, B2} = get_body(S, RequestHeaders, B1, Opts, Callback), t(body_end),
+ Req1 = Req#req{body = RequestBody},
- t(user_start),
- case execute_callback(Req, Callback) of
- {response, ResponseCode, UserHeaders, UserBody} ->
+ t(user_start),
+ Response = execute_callback(Req1),
t(user_end),
- ResponseHeaders = [connection(Req, UserHeaders),
- content_length(UserHeaders, UserBody)
- | UserHeaders],
- send_response(S, Method, ResponseCode,
- ResponseHeaders, UserBody, Callback),
+ handle_response(Req1, B2, Response);
+ handover ->
+ Req1 = Req#req{body = B1},
+ Mod:handle(Req1, Args)
+ end.
- t(request_end),
- handle_event(Mod, request_complete,
- [Req, ResponseCode, ResponseHeaders, UserBody, get_timings()],
- Args),
+handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) ->
+ #req{callback = {Mod, Args}} = Req,
- {close_or_keepalive(Req, UserHeaders), B2};
+ Headers = [connection(Req, UserHeaders),
+ content_length(UserHeaders, Body)
+ | UserHeaders],
+ send_response(Req, Code, Headers, Body),
- {chunk, UserHeaders, Initial} ->
- t(user_end),
+ t(request_end),
+ handle_event(Mod, request_complete, [Req, Code, Headers, Body, get_timings()], Args),
- ResponseHeaders = [{<<"Transfer-Encoding">>, <<"chunked">>},
- connection(Req, UserHeaders)
- | UserHeaders],
- send_response(S, Method, 200, ResponseHeaders, <<"">>, Callback),
- Initial =:= <<"">> orelse send_chunk(S, Initial),
+ {close_or_keepalive(Req, UserHeaders), Buffer};
- ClosingEnd = case start_chunk_loop(S) of
- {error, client_closed} -> client;
- ok -> server
- end,
- t(request_end),
- handle_event(Mod, chunk_complete,
- [Req, 200, ResponseHeaders, ClosingEnd, get_timings()],
- Args),
- {close, <<>>};
+handle_response(Req, _Buffer, {chunk, UserHeaders, Initial}) ->
+ #req{callback = {Mod, Args}} = Req,
- {file, ResponseCode, UserHeaders, Filename, Range} ->
- t(user_end),
+ ResponseHeaders = [{<<"Transfer-Encoding">>, <<"chunked">>},
+ connection(Req, UserHeaders)
+ | UserHeaders],
+ send_response(Req, 200, ResponseHeaders, <<"">>),
+ Initial =:= <<"">> orelse send_chunk(Req#req.socket, Initial),
- ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders],
- send_file(S, ResponseCode, ResponseHeaders, Filename, Range, Callback),
+ ClosingEnd = case start_chunk_loop(Req#req.socket) of
+ {error, client_closed} -> client;
+ ok -> server
+ end,
- t(request_end),
+ t(request_end),
+ handle_event(Mod, chunk_complete,
+ [Req, 200, ResponseHeaders, ClosingEnd, get_timings()],
+ Args),
+ {close, <<>>};
+
+handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, Filename, Range}) ->
+ #req{callback = {Mod, Args}} = Req,
+
+ ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders],
+ send_file(Req, ResponseCode, ResponseHeaders, Filename, Range),
+
+ t(request_end),
+ handle_event(Mod, request_complete,
+ [Req, ResponseCode, ResponseHeaders, <<>>, get_timings()],
+ Args),
+
+ {close_or_keepalive(Req, UserHeaders), Buffer}.
- handle_event(Mod, request_complete,
- [Req, ResponseCode, ResponseHeaders, <<>>, get_timings()],
- Args),
- {close_or_keepalive(Req, UserHeaders), B2}
- end.
--spec mk_req(Method::http_method(), {PathType::atom(), RawPath::binary()}, RequestHeaders::headers(),
- RequestBody::body(), V::version(), Socket::inet:socket() | undefined,
- Callback::callback()) -> record(req).
+-spec mk_req(Method::http_method(), {PathType::atom(), RawPath::binary()},
+ RequestHeaders::headers(), RequestBody::body(), V::version(),
+ Socket::inet:socket() | undefined, Callback::callback()) ->
+ record(req).
mk_req(Method, RawPath, RequestHeaders, RequestBody, V, Socket, Callback) ->
{Mod, Args} = Callback,
case parse_path(RawPath) of
{ok, {Path, URL, URLArgs}} ->
#req{method = Method, path = URL, args = URLArgs, version = V,
raw_path = Path, headers = RequestHeaders,
- body = RequestBody, pid = self(), socket = Socket};
+ body = RequestBody, pid = self(), socket = Socket,
+ callback = Callback};
{error, Reason} ->
handle_event(Mod, request_parse_error,
[{Reason, {Method, RawPath}}], Args),
@@ -143,8 +157,8 @@ mk_req(Method, RawPath, RequestHeaders, RequestBody, V, Socket, Callback) ->
%% @doc: Generates a HTTP response and sends it to the client
-send_response(Socket, Method, Code, Headers, UserBody, {Mod, Args}) ->
- Body = case {Method, Code} of
+send_response(Req, Code, Headers, UserBody) ->
+ Body = case {Req#req.method, Code} of
{'HEAD', _} -> <<>>;
{_, 304} -> <<>>;
{_, 204} -> <<>>;
@@ -155,29 +169,31 @@ send_response(Socket, Method, Code, Headers, UserBody, {Mod, Args}) ->
encode_headers(Headers), <<"\r\n">>,
Body],
- case elli_tcp:send(Socket, Response) of
+ case elli_tcp:send(Req#req.socket, Response) of
ok -> ok;
{error, closed} ->
+ #req{callback = {Mod, Args}} = Req,
handle_event(Mod, client_closed, [before_response], Args),
ok
end.
--spec send_file(Socket::inet:socket(), Code::response_code(), Headers::headers(),
- Filename::file:filename(), Range::range(),
- Callback::callback()) -> ok.
+-spec send_file(Request::#req{}, Code::response_code(), Headers::headers(),
+ Filename::file:filename(), Range::range()) -> ok.
+
%% @doc: Sends a HTTP response to the client where the body is the
%% contents of the given file. Assumes correctly set response code
%% and headers.
-send_file(Socket, Code, Headers, Filename, {Offset, Length}, {Mod, Args}) ->
+send_file(Req, Code, Headers, Filename, {Offset, Length}) ->
+ #req{callback = {Mod, Args}} = Req,
ResponseHeaders = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>,
encode_headers(Headers), <<"\r\n">>],
case file:open(Filename, [read, raw, binary]) of
{ok, Fd} ->
- try elli_tcp:send(Socket, ResponseHeaders) of
+ try elli_tcp:send(Req#req.socket, ResponseHeaders) of
ok ->
- case elli_tcp:sendfile(Fd, Socket, Offset, Length, []) of
+ case elli_tcp:sendfile(Fd, Req#req.socket, Offset, Length, []) of
{ok, _BytesSent} ->
ok;
{error, closed} ->
@@ -205,7 +221,7 @@ send_bad_request(Socket) ->
%% @doc: Executes the user callback, translating failure into a proper
%% response.
-execute_callback(Req, {Mod, Args}) ->
+execute_callback(#req{callback = {Mod, Args}} = Req) ->
try Mod:handle(Req, Args) of
{ok, Headers, {file, Filename}} -> {file, 200, Headers, Filename, {0, 0}};
{ok, Headers, {file, Filename, Range}}-> {file, 200, Headers, Filename, Range};
@@ -233,16 +249,6 @@ execute_callback(Req, {Mod, Args}) ->
{response, 500, [], <<"Internal server error">>}
end.
-handle_event(Mod, Name, EventArgs, ElliArgs) ->
- try
- Mod:handle_event(Name, EventArgs, ElliArgs)
- catch
- EvClass:EvError ->
- error_logger:error_msg("~p:handle_event/3 crashed ~p:~p~n~p",
- [Mod, EvClass, EvError,
- erlang:get_stacktrace()])
- end.
-
%%
%% CHUNKED-TRANSFER
%%
@@ -546,6 +552,28 @@ split_args(Qs) ->
end || Token <- Tokens].
+%%
+%% CALLBACK HELPERS
+%%
+
+behaviour(#req{callback = {Mod, Args}} = Req) ->
+ case erlang:function_exported(Mod, init, 2) of
+ true ->
+ Mod:init(Req, Args);
+ false ->
+ pure
+ end.
+
+handle_event(Mod, Name, EventArgs, ElliArgs) ->
+ try
+ Mod:handle_event(Name, EventArgs, ElliArgs)
+ catch
+ EvClass:EvError ->
+ error_logger:error_msg("~p:handle_event/3 crashed ~p:~p~n~p",
+ [Mod, EvClass, EvError,
+ erlang:get_stacktrace()])
+ end.
+
%%
%% TIMING HELPERS
%%
@@ -0,0 +1,61 @@
+-module(elli_handover_tests).
+-include_lib("eunit/include/eunit.hrl").
+-include("elli.hrl").
+
+
+-define(i2b(I), list_to_binary(integer_to_list(I))).
+
+elli_test_() ->
+ {setup,
+ fun setup/0, fun teardown/1,
+ [
+ ?_test(hello_world()),
+ ?_test(echo())
+ ]}.
+
+
+
+setup() ->
+ application:start(crypto),
+ application:start(public_key),
+ application:start(ssl),
+ inets:start(),
+ {ok, P} = elli:start_link([{callback, elli_example_callback_handover}, {port, 3003}]),
+ unlink(P),
+ [P].
+
+teardown(Pids) ->
+ [elli:stop(P) || P <- Pids].
+
+
+%%
+%% INTEGRATION TESTS
+%% Uses inets httpc to actually call Elli over the network
+%%
+
+hello_world() ->
+ {ok, Response} = httpc:request("http://localhost:3003/hello/world"),
+ ?assertEqual(200, status(Response)),
+ ?assertEqual([{"connection", "close"},
+ {"content-length", "12"}], headers(Response)),
+ ?assertEqual("Hello World!", body(Response)).
+
+echo() ->
+ {ok, Response} = httpc:request("http://localhost:3003/hello?name=knut"),
+ ?assertEqual(200, status(Response)),
+ ?assertEqual("Hello knut", body(Response)).
+
+
+
+%%
+%% HELPERS
+%%
+
+status({{_, Status, _}, _, _}) ->
+ Status.
+
+body({_, _, Body}) ->
+ Body.
+
+headers({_, Headers, _}) ->
+ lists:sort(Headers).
View
@@ -371,7 +371,8 @@ to_proplist_test() ->
headers = [{<<"Host">>,<<"localhost:3001">>}],
body = <<>>,
pid = self(),
- socket = socket},
+ socket = socket,
+ callback = {mod, []}},
Prop = [{method,'GET'},
{path,[<<"crash">>]},
@@ -381,7 +382,8 @@ to_proplist_test() ->
{headers,[{<<"Host">>,<<"localhost:3001">>}]},
{body,<<>>},
{pid,self()},
- {socket,socket}],
+ {socket,socket},
+ {callback, {mod, []}}],
?assertEqual(Prop, elli_request:to_proplist(Req)).
is_request_test() ->

0 comments on commit 658f1e2

Please sign in to comment.