Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for system messages #754

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ PROJECT = cowboy

ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \
+warn_shadow_vars +warn_obsolete_guard +warn_missing_spec
COMPILE_FIRST = cowboy_middleware cowboy_sub_protocol
COMPILE_FIRST = cowboy_middleware cowboy_sub_protocol cowboy_sys
CT_OPTS += -pa test -ct_hooks cowboy_ct_hook []
PLT_APPS = crypto public_key ssl

Expand Down
57 changes: 53 additions & 4 deletions doc/src/guide/middlewares.ezdoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ Middlewares can return one of four different values:

* `{ok, Req, Env}` to continue the request processing
* `{suspend, Module, Function, Args}` to hibernate
* `{system, From, Msg, Module, Req, State}` to handle system messages
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe name the tuple sys too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A system message is of the form {system, From, Msg} so I am worried turning it in to {sys, From, Msg, ..} will confuse people.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.

* `{halt, Req}` to stop processing and move on to the next request

Of note is that when hibernating, processing will resume on the given
MFA, discarding all previous stacktrace. Make sure you keep the `Req`
and `Env` in the arguments of this MFA for later use.

When handling system messages the given module must implement the
`cowboy_sys` callbacks. For more information about handling system
messages see `System messages` below.

If an error happens during middleware processing, Cowboy will not try
to send an error back to the socket, the process will just crash. It
is up to the middleware to make sure that a reply is sent if something
Expand All @@ -41,14 +46,15 @@ In the previous chapters we saw it briefly when we needed to pass
the routing information. It is a list of tuples with the first
element being an atom and the second any Erlang term.

Two values in the environment are reserved:
Three values in the environment are reserved:

* `listener` contains the name of the listener
* `result` contains the result of the processing
* `parent` contains the pid of the parent process

The `listener` value is always defined. The `result` value can be
set by any middleware. If set to anything other than `ok`, Cowboy
will not process any subsequent requests on this connection.
The `listener` and `parent` values are always defined. The `result`
value can be set by any middleware. If set to anything other than `ok`,
Cowboy will not process any subsequent requests on this connection.

The middlewares that come with Cowboy may define or require other
environment values to perform.
Expand All @@ -66,3 +72,46 @@ and `handler_opts` values of the environment, respectively.

The handler middleware requires the `handler` and `handler_opts`
values. It puts the result of the request handling into `result`.

:: System messages

A middleware may choose to handle system messages. Handling system
messages will allow OTP tools to debug the middleware. To handle system
messages a middleware implements the two `cowboy_sys` callbacks. If a
middleware does not receive any messages it should not try to handle
system messages.

A system message is of the form: `{system, From, Msg}`, and to handle
it the middleware should return:
`{system, From, Msg, Module, Req, State}`.

`Module` is the module with the `cowboy_sys` callbacks. This module
will be used to handle system requests and is typically the current
module: `?MODULE`. `State` should be the state of the module. It should
contain all data required to return the middleware back to its previous
state. Therefore `State` will have to include `Env`, should a
middleware wish to return `{ok, Req, Env}` at a later stage.

Once the middleware has returned, the `sys` and `cowboy_sys` modules
will control the process. Note the process might be hibernated while
these modules control it.

The first callback is `sys_continue/2`. This is called when `sys`
returns control of the process to Cowboy and the middleware. Therefore
it should continue to act as normal and return the same values as
`execute/2`:

``` erlang
sys_continue(Req, #state{env=Env}) ->
{ok, Req, Env}
```

The second callback is `sys_terminate/3`. This is called when the
process should terminate because its parent did. The middleware should
exit with the same reason as the first argument (after handling any
termination or clean up):

``` erlang
sys_terminate(Reason, _Req, _ModuleState) ->
exit(Reason).
```
3 changes: 2 additions & 1 deletion doc/src/guide/sub_protocols.ezdoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ upgrade(Req, Env, Handler, HandlerOpts, Timeout, Hibernate) ->
```

This callback is expected to behave like a middleware and to
return an updated environment and Req object.
return an updated environment and Req object. Note that sub protocols
may halt, hibernate and handle system messages like a middleware.

Sub protocols are expected to call the `cowboy_handler:terminate/4`
function when they terminate. This function will make sure that
Expand Down
4 changes: 4 additions & 0 deletions doc/src/manual/cowboy_loop.ezdoc
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ received first.

A socket error ocurred.

: {shutdown, Reason}

Parent process exited with reason `Reason`.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion needed here. Is it only ever the parent process doing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. system_terminate/4 is only called when receiving an exit signal message from the parent process.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that shutdown is already used for unrelated things. Maybe parent is better?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the shutdown tag as it is caused by the shutdown protocol[1] and gen_event uses the atom shutdown in this case and is the only OTP behaviour to distinguish the terminate reasons in the same way as cowboy handlers. I agree it might be confused with shutdown the atom. I am happy to change the tag to anything you want.

Note I view the current cowboy API as confusing because shutdown conflicts with its use in OTP. If API changes are still possible for 2.0 it would be nice if the handlers could copy OTP behaviours and use {stop, Reason, Req, State} or {stop, Req, State} instead of {shutdown, Req, State}. This would turn the current terminate reason shutdown to {stop, Reason}(/stop), which is the same as gen_event and a similar return value to other OTP behaviours. I am not suggesting it because of this change, but only to be more inline with OTP.

[1] http://www.erlang.org/documentation/doc-4.9.1/doc/design_principles/sup_princ.html#shutdown

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I will change shutdown into stop later today/tomorrow. I may also change REST's halt into stop for consistency. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess all halts should become stops?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only halt there is is in cowboy_rest when branching out early. Everything else is already shutdown and will become stop. I am considering doing the same for the halt in cowboy_rest too.

:: Callbacks

: info(Info, Req, State)
Expand Down
8 changes: 8 additions & 0 deletions doc/src/manual/cowboy_middleware.ezdoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ optionally with its contents modified.
: execute(Req, Env)
-> {ok, Req, Env}
| {suspend, Module, Function, Args}
| {system, From, Msg, Module, Req, State}
| {halt, Req}

Types:
Expand All @@ -30,6 +31,9 @@ Types:
* Module = module()
* Function = atom()
* Args = [any()]
* From = {pid(), any()}
* Msg = any()
* State = any()

Execute the middleware.

Expand All @@ -41,6 +45,10 @@ The `suspend` return value will hibernate the process until
an Erlang message is received. Note that when resuming, any
previous stacktrace information will be gone.

The `system` return value indicates that `cowboy_sys` should be
used to handle the system message. The given module will be used
for `cowboy_sys` callbacks when handling system messages.

The `halt` return value stops Cowboy from doing any further
processing of the request, even if there are middlewares
that haven't been executed yet. The connection may be left
Expand Down
4 changes: 4 additions & 0 deletions doc/src/manual/cowboy_sub_protocol.ezdoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ by modules that implement a protocol on top of HTTP.
: upgrade(Req, Env, Handler, Opts)
-> {ok, Req, Env}
| {suspend, Module, Function, Args}
| {system, From, Msg, Module, Req, State}
| {halt, Req}
| {error, StatusCode, Req}

Expand All @@ -20,6 +21,9 @@ Types:
* Module = module()
* Function = atom()
* Args = [any()]
* From = {pid(), any()}
* Msg = any()
* State = any()
* StatusCode = cowboy:http_status()

Upgrade the protocol.
Expand Down
42 changes: 42 additions & 0 deletions doc/src/manual/cowboy_sys.ezdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
::: cowboy_sys

The `cowboy_sys` behaviour defines the interface used to handle system
messages in Cowboy middleware and sub protocol modules.

:: Callbacks

: sys_continue(Req, State)
-> {ok, Req, Env}
| {suspend, Module, Function, Args}
| {system, From, Msg, Module, Req, State}
| {halt, Req}

Types:

* Req = cowboy_req:req()
* Env = env()
* Module = module()
* Function = atom()
* Args = [any()]
* From = {pid(), any()}
* Msg = any()
* State = any()

Continue processsing after handling system messages.

Please refer to the `cowboy_middleware` manual for possible return values.

: sys_terminate(Reason, Req, State)
-> no_return().

Types:

* Reason = any()
* Req = cowboy_req:req()
* State = any()

Terminate due to exit signal from parent while handling system messages.

The process should exit with `Reason` after doing any cleanup or miscellaneous
operations. The process should not continue as normal. Usually the reason will
be `shutdown` when the parent process is terminating itself.
4 changes: 4 additions & 0 deletions doc/src/manual/cowboy_websocket.ezdoc
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ The remote endpoint closed the connection with the given
The handler requested to close the connection, either by returning
a `shutdown` tuple or by sending a `close` frame.

: {shutdown, Reason}

The parent process exited with reason `Reason`.

: timeout

The connection has been closed due to inactivity. The timeout
Expand Down
6 changes: 4 additions & 2 deletions src/cowboy_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ execute(Req, Env) ->
Stacktrace = erlang:get_stacktrace(),
cowboy_req:maybe_reply(Stacktrace, Req),
terminate({crash, Class, Reason}, Req, HandlerOpts, Handler),
erlang:Class([
exit([
{class, Class},
{reason, Reason},
{mfa, {Handler, init, 2}},
{stacktrace, Stacktrace},
Expand All @@ -68,7 +69,8 @@ terminate(Reason, Req, State, Handler) ->
try
Handler:terminate(Reason, cowboy_req:lock(Req), State)
catch Class:Reason2 ->
erlang:Class([
exit([
{class, Class},
{reason, Reason2},
{mfa, {Handler, terminate, 3}},
{stacktrace, erlang:get_stacktrace()},
Expand Down
43 changes: 37 additions & 6 deletions src/cowboy_loop.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
%% <em>{error, overflow}</em> reason if this threshold is reached.
-module(cowboy_loop).
-behaviour(cowboy_sub_protocol).
-behaviour(cowboy_sys).

-export([upgrade/6]).
-export([loop/4]).
-export([sys_continue/2]).
-export([sys_terminate/3]).

-callback init(Req, any())
-> {ok | module(), Req, any()}
Expand All @@ -42,6 +45,7 @@

-record(state, {
env :: cowboy_middleware:env(),
parent :: pid(),
hibernate = false :: boolean(),
buffer_size = 0 :: non_neg_integer(),
max_buffer = 5000 :: non_neg_integer() | infinity,
Expand All @@ -51,14 +55,20 @@
}).

-spec upgrade(Req, Env, module(), any(), timeout(), run | hibernate)
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]}
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
| {system, {pid(), any()}, any(), ?MODULE, Req, {#state{}, module(), any()}}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, Timeout, run) ->
State = #state{env=Env, max_buffer=get_max_buffer(Env), timeout=Timeout},
{_, Parent} = lists:keyfind(parent, 1, Env),
State = #state{env=Env, parent=Parent, max_buffer=get_max_buffer(Env),
timeout=Timeout},
State2 = timeout(State),
after_call(Req, State2, Handler, HandlerState);
upgrade(Req, Env, Handler, HandlerState, Timeout, hibernate) ->
State = #state{env=Env, max_buffer=get_max_buffer(Env), hibernate=true, timeout=Timeout},
Parent = proplists:get_value(parent, Env, self()),
State = #state{env=Env, parent=Parent, max_buffer=get_max_buffer(Env),
hibernate=true, timeout=Timeout},
State2 = timeout(State),
after_call(Req, State2, Handler, HandlerState).

Expand Down Expand Up @@ -102,9 +112,11 @@ timeout(State=#state{timeout=Timeout,
State#state{timeout_ref=TRef}.

-spec loop(Req, #state{}, module(), any())
-> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]}
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
| {system, {pid(), any()}, any(), ?MODULE, Req, {#state{}, module(), any()}}
when Req::cowboy_req:req().
loop(Req, State=#state{buffer_size=NbBytes,
loop(Req, State=#state{parent=Parent, buffer_size=NbBytes,
max_buffer=Threshold, timeout_ref=TRef,
resp_sent=RespSent}, Handler, HandlerState) ->
[Socket, Transport] = cowboy_req:get([socket, transport], Req),
Expand All @@ -131,6 +143,10 @@ loop(Req, State=#state{buffer_size=NbBytes,
after_loop(Req, State, Handler, HandlerState, timeout);
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
loop(Req, State, Handler, HandlerState);
{system, From, Msg} ->
{system, From, Msg, ?MODULE, Req, {State, Handler, HandlerState}};
{'EXIT', Parent, Reason} ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this only for trap_exit? Do we trap_exit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is but the handler might trap exits. It is important that OTP processes always exit if their parent does.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but maybe it should be left to the handler to decide that? What is gen_server doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the behaviour used by gen_server (and all other OTP behaviours). I believe that offering the choice to handler is wrong because if a handler ignores this message (does not terminate) it is going against the guarantees of OTP supervision trees by breaking the shutdown protocol[1][2]. Note that ignoring the message (due to a catch all clause) or a function_clause error would be the default behaviour - which is very undesirable.

[1] http://www.erlang.org/documentation/doc-4.9.1/doc/design_principles/spec_proc.html#7.2
[2] http://www.erlang.org/documentation/doc-4.9.1/doc/design_principles/sup_princ.html#shutdown

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.

sys_terminate(Reason, Req, {State, Handler, HandlerState});
Message ->
%% We set the socket back to {active, false} mode in case
%% the handler is going to call recv. We also flush any
Expand Down Expand Up @@ -161,7 +177,8 @@ call(Req, State=#state{resp_sent=RespSent},
cowboy_req:maybe_reply(Stacktrace, Req)
end,
cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler),
erlang:Class([
exit([
{class, Class},
{reason, Reason},
{mfa, {Handler, info, 3}},
{stacktrace, Stacktrace},
Expand Down Expand Up @@ -203,3 +220,17 @@ flush_timeouts() ->
after 0 ->
ok
end.

-spec sys_continue(Req, {#state{}, module(), any()})
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
| {system, {pid(), any()}, any(), ?MODULE, Req, {#state{}, module(), any()}}
when Req::cowboy_req:req().
sys_continue(Req, {State, Handler, HandlerState}) ->
loop(Req, State, Handler, HandlerState).

-spec sys_terminate(any(), Req, {#state{}, module(), any()}) -> no_return()
when Req::cowboy_req:req().
sys_terminate(Reason, Req, {_, Handler, HandlerState}) ->
_ = cowboy_handler:terminate(Reason, Req, HandlerState, Handler),
exit(Reason).
1 change: 1 addition & 0 deletions src/cowboy_middleware.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
-callback execute(Req, Env)
-> {ok, Req, Env}
| {suspend, module(), atom(), [any()]}
| {system, {pid(), any()}, any(), module(), Req, any()}
| {halt, Req}
when Req::cowboy_req:req(), Env::env().
57 changes: 57 additions & 0 deletions src/cowboy_proc.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
%% Copyright (c) 2014, James Fish <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_proc).

%% API.
-export([spawn_link/3]).
-export([hibernate/3]).
-export([continue/3]).

%% Internal.
-export([init/3]).

%% API.

-spec spawn_link(module(), atom(), [any()]) -> pid().
spawn_link(Module, Fun, Args) ->
proc_lib:spawn_link(?MODULE, init, [Module, Fun, Args]).

-spec hibernate(module(), atom(), [any()]) -> no_return().
hibernate(Module, Fun, Args) ->
proc_lib:hibernate(?MODULE, continue, [Module, Fun, Args]).

-spec continue(module(), atom(), [any()]) -> any().
continue(Module, Fun, Args) ->
try
apply(Module, Fun, Args)
catch
error:Reason ->
exit({Reason, erlang:get_stacktrace()});
throw:Value ->
exit({{nocatch, Value}, erlang:get_stacktrace()})
end.

%% Internal.

-spec init(module(), atom(), [any()]) -> any().
init(Module, Fun, Args) ->
_ = put('$initial_call', {Module, Fun, length(Args)}),
try
apply(Module, Fun, Args)
catch
error:Reason ->
exit({Reason, erlang:get_stacktrace()});
throw:Value ->
exit({{nocatch, Value}, erlang:get_stacktrace()})
end.
Loading