Search code examples
websocketerlanggen-servercowboy

Updating state in Cowboy Websocket handler crashes or ignores silently


I have a ws_handler which receives a websocket connection.

This process waits for input starting with <<"h to login.

It then saves the default Websocket state, a Player process ID (which is spawned with a reference to the current PID), and an outbox message, `<<"turn1">>

websocket_handle({text, <<"h", Name/binary>>}, State) ->
  {ok, PID} = player:go(self(), <<"profiledata", Name/binary>>),
  erlang:start_timer(1000, self(), <<"Hello!">>),
  {reply, {text, <<"You joined, ", Name/binary>>}, {State, PID, <<"turn1">>}};

I want to control data flow from this seperate player process, then have my websocket handler retrieve messages and pass them on to its client via the Outbox element.

So I add this in to manually trigger a message:

websocket_handle({text, <<"myscreen">>}, S = {_, P, _}) ->
  gen_server:call(P, myscreen),
  {ok, S};

and in player.erl,

handle_call(myscreen, _, {WS, Profile, Cab}) ->
  gen_server:cast(WS, myscreenupdate),
  {reply, ok, {WS, Profile, Cab}};

Back in ws_handler I expect this to get called:

websocket_info(myscreenupdate, State = {St,P, _}) ->
  {reply, {text, <<"My screen update">>}, {St, P, <<"turn2">>}};

But the websocket output in my browser continuously prints turn1, instead of turn2.

I tried gen_server:call in player.erl and I get a timeout crash. I think this is because the {reply tuple of websocket_handle in ws_handler is supposed to be replying to the websocket.. but if that were true, then I'd expect the data to be updated:

websocket_info(myscreenupdate, State = {St,P, _}) ->
  {reply, {text, <<"My screen update">>}, {St, P, <<"turn2">>}};

So I'm uncertain what's happening here.

How do I update state from a Player process then have my websocket handler retrieve that State and send it out to its connection?

ws_handler.erl:

-module(ws_handler).

-export([init/2]).
-export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).

init(Req, Opts) ->
    {cowboy_websocket, Req, Opts}.

websocket_init(State) ->
    {ok, State}.

websocket_handle({text, <<"h", Name/binary>>}, State) ->
  {ok, PID} = player:go(self(), <<"profiledata", Name/binary>>),
  erlang:start_timer(1000, self(), <<"Hello!">>),
  {reply, {text, <<"You joined, ", Name/binary>>}, {State, PID, <<"turn1">>}};

websocket_handle({text, <<"myscreen">>}, S = {_, P, _}) ->
  gen_server:call(P, myscreen),
  {ok, S};

websocket_handle({text, <<"auth", Auth/binary>>}, S = {_St, P, _}) ->
  case s:s(P, Auth) of
    {ok, Valid} -> {reply, {text, << "Authorized">>}, S};
    _ -> {reply, {text, <<"Error">>}, S}
  end;
websocket_handle({text, Msg}, S = {_St, P, Outbox}) ->
    {reply, {text, Outbox}, S};
websocket_handle(_Data, State) ->
    {ok, State}.

websocket_info(myscreenupdate, State = {St,P, _}) ->
  {reply, {text, <<"My screen update">>}, {St, P, <<"turn2">>}};

websocket_info({timeout, _Ref, _Ignored}, State = {_, P, Outbox}) ->
    erlang:start_timer(1000, self(), <<"This is ignored">>),
  Msg = Outbox,
    {reply, {text, Msg}, State};
websocket_info(_Info, State) ->
    {ok, State}.

player.erl:

-module(player).
-compile(export_all).

handle_call(myscreen, _, {WS, Profile, Cab}) ->
  gen_server:cast(WS, myscreenupdate),
  {reply, ok, {WS, Profile, Cab}};
handle_call(get_profile, _, State = {_WSPID, Profile, _}) ->
  {reply, Profile, State}.

init([WSPID, Profile]) ->
  {ok, {WSPID, Profile, null}};
init([WSPID, Profile, CabinetPID]) ->
  {ok, {WSPID, Profile, CabinetPID}}.

go(WSPID, Profile, CabinetPID) ->
  gen_server:start_link(?MODULE, [WSPID, Profile, CabinetPID], []).
go(WSPID, Profile) ->
  gen_server:start_link(?MODULE, [WSPID, Profile], []).

Solution

  • The problem is that cowboy's websocket_info/2 handler will only receive messages sent to the websocket process by using the erlang built-in message operator ! (or, equivalently, the erlang:send/{2,3} functions).

    So you should do:

    WS ! myscreenupdate

    instead of

    gen_server:cast(WS, myscreenupdate)

    When you use gen_server:cast the message is probably discarded by the cowboy message loop since it's not a recognized message. And when you use gen_server:call you get a deadlock.