0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add shutdown_reason Websocket command

This allows changing the normal exit reason of Websocket
processes, providing a way to signal other processes of
why the exit occurred.
This commit is contained in:
Loïc Hoguin 2019-10-10 11:33:35 +02:00
parent cc54c207e3
commit d52e84bdd9
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
5 changed files with 87 additions and 6 deletions

View file

@ -81,6 +81,12 @@ Cowboy 2.7 requires Erlang/OTP 20.0 or greater.
is now considered stable and has been documented. is now considered stable and has been documented.
The old interface is now deprecated. The old interface is now deprecated.
* A new Websocket handler command `shutdown_reason`
can be used to change the normal exit reason of
Websocket processes. By default `normal` is used;
with this command the exit reason can be changed
to `{shutdown, ShutdownReason}`.
* The experimental stream handlers `cowboy_metrics_h` * The experimental stream handlers `cowboy_metrics_h`
and `cowboy_tracer_h` are now considered stable and and `cowboy_tracer_h` are now considered stable and
have been documented. have been documented.

View file

@ -141,6 +141,7 @@ commands() :: [Command]
Command :: {active, boolean()} Command :: {active, boolean()}
| {deflate, boolean()} | {deflate, boolean()}
| {set_options, #{idle_timeout => timeout()}} | {set_options, #{idle_timeout => timeout()}}
| {shutdown_reason, any()}
| Frame :: cow_ws:frame() | Frame :: cow_ws:frame()
---- ----
@ -163,6 +164,15 @@ set_options::
Set Websocket options. Currently only the option `idle_timeout` Set Websocket options. Currently only the option `idle_timeout`
may be updated from a Websocket handler. may be updated from a Websocket handler.
shutdown_reason::
Change the shutdown reason. The Websocket process will exit
with reason `normal` by default. This command can be used to
exit with reason `{shutdown, ShutdownReason}` under normal
conditions. This command has no effect when the Websocket
process exits abnormally, for example following a crash in a
handler callback.
Frame:: Frame::
Send the corresponding Websocket frame. Send the corresponding Websocket frame.
@ -266,8 +276,9 @@ normal circumstances if necessary.
== Changelog == Changelog
* *2.7*: The commands based interface has been added. The old * *2.7*: The commands based interface has been documented.
interface is now deprecated. The old interface is now deprecated.
* *2.7*: The command `shutdown_reason` was introduced.
* *2.7*: The option `validate_utf8` has been added. * *2.7*: The option `validate_utf8` has been added.
* *2.6*: Deflate options can now be configured via `deflate_opts`. * *2.6*: Deflate options can now be configured via `deflate_opts`.
* *2.0*: The Req object is no longer passed to Websocket callbacks. * *2.0*: The Req object is no longer passed to Websocket callbacks.

View file

@ -35,6 +35,7 @@
| {active, boolean()} | {active, boolean()}
| {deflate, boolean()} | {deflate, boolean()}
| {set_options, map()} | {set_options, map()}
| {shutdown_reason, any()}
]. ].
-export_type([commands/0]). -export_type([commands/0]).
@ -95,7 +96,8 @@
utf8_state :: cow_ws:utf8_state(), utf8_state :: cow_ws:utf8_state(),
deflate = true :: boolean(), deflate = true :: boolean(),
extensions = #{} :: map(), extensions = #{} :: map(),
req = #{} :: map() req = #{} :: map(),
shutdown_reason = normal :: any()
}). }).
%% Because the HTTP/1.1 and HTTP/2 handshakes are so different, %% Because the HTTP/1.1 and HTTP/2 handshakes are so different,
@ -546,6 +548,8 @@ commands([{set_options, SetOpts}|Tail], State0=#state{opts=Opts}, Data) ->
State0 State0
end, end,
commands(Tail, State, Data); commands(Tail, State, Data);
commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) ->
commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data);
commands([Frame|Tail], State, Data0) -> commands([Frame|Tail], State, Data0) ->
Data = [frame(Frame, State)|Data0], Data = [frame(Frame, State)|Data0],
case is_close_frame(Frame) of case is_close_frame(Frame) of
@ -623,9 +627,12 @@ frame(Frame, #state{extensions=Extensions}) ->
cow_ws:frame(Frame, Extensions). cow_ws:frame(Frame, Extensions).
-spec terminate(#state{}, any(), terminate_reason()) -> no_return(). -spec terminate(#state{}, any(), terminate_reason()) -> no_return().
terminate(State, HandlerState, Reason) -> terminate(State=#state{shutdown_reason=Shutdown}, HandlerState, Reason) ->
handler_terminate(State, HandlerState, Reason), handler_terminate(State, HandlerState, Reason),
exit(normal). case Shutdown of
normal -> exit(normal);
_ -> exit({shutdown, Shutdown})
end.
handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) -> handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) ->
cowboy_handler:terminate(Reason, Req, HandlerState, Handler). cowboy_handler:terminate(Reason, Req, HandlerState, Handler).

View file

@ -0,0 +1,38 @@
%% This module sends the process pid to the test pid
%% found in the x-test-pid header, then changes the
%% shutdown reason and closes the connection normally.
-module(ws_shutdown_reason_commands_h).
-behavior(cowboy_websocket).
-export([init/2]).
-export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).
init(Req, RunOrHibernate) ->
TestPid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))),
{cowboy_websocket, Req, {TestPid, RunOrHibernate}}.
websocket_init(State={TestPid, RunOrHibernate}) ->
TestPid ! {ws_pid, self()},
ShutdownReason = receive
{TestPid, SR} ->
SR
after 1000 ->
error(timeout)
end,
Commands = [
{shutdown_reason, ShutdownReason},
close
],
case RunOrHibernate of
run -> {Commands, State};
hibernate -> {Commands, State, hibernate}
end.
websocket_handle(_, State) ->
{[], State}.
websocket_info(_, State) ->
{[], State}.

View file

@ -52,7 +52,8 @@ init_dispatch(Name) ->
{"/info", ws_info_commands_h, RunOrHibernate}, {"/info", ws_info_commands_h, RunOrHibernate},
{"/active", ws_active_commands_h, RunOrHibernate}, {"/active", ws_active_commands_h, RunOrHibernate},
{"/deflate", ws_deflate_commands_h, RunOrHibernate}, {"/deflate", ws_deflate_commands_h, RunOrHibernate},
{"/set_options", ws_set_options_commands_h, RunOrHibernate} {"/set_options", ws_set_options_commands_h, RunOrHibernate},
{"/shutdown_reason", ws_shutdown_reason_commands_h, RunOrHibernate}
]}]). ]}]).
%% Support functions for testing using Gun. %% Support functions for testing using Gun.
@ -286,3 +287,21 @@ websocket_set_options_idle_timeout(Config) ->
after 2000 -> after 2000 ->
error(timeout) error(timeout)
end. end.
websocket_shutdown_reason(Config) ->
doc("The command {shutdown_reason, any()} can be used to "
"change the shutdown reason of a Websocket connection."),
ConnPid = gun_open(Config),
StreamRef = gun:ws_upgrade(ConnPid, "/shutdown_reason", [
{<<"x-test-pid">>, pid_to_list(self())}
]),
{upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef),
WsPid = receive {ws_pid, P} -> P after 1000 -> error(timeout) end,
MRef = monitor(process, WsPid),
WsPid ! {self(), {?MODULE, ?FUNCTION_NAME}},
receive
{'DOWN', MRef, process, WsPid, {shutdown, {?MODULE, ?FUNCTION_NAME}}} ->
ok
after 1000 ->
error(timeout)
end.