Question

I have seen many examples of chat room systems over websocket implemented with erlang and cowboy.

Most of the examples I have seen use gproc. In practice each websocket handler registers itself with gproc and then broadcasts/receives messages from it.

Since a user could close by accident the webpage I am thinking about connecting to the websocket handler a gen_fsm which actually broadcasts/receives all the messages from gproc. In this way the gen_fsm could switch from a "connected" state to a "disconnected" state whenever the user exits and still buffer all the messages. After a while if the user is not back online the gen_fsm will terminate.

Is this a good solution? How can I make the new websocket handler to recover the gen_fsm process? Should I register the gen_fsm using the user name or is there any better solution?

Was it helpful?

Solution

What i do is the folowing :

When an user connects to the site, i swpawn a gen_server reprensenting the user. Then, the gen server registers itself in gproc as {n,l, {user, UserName}}. (It can register properties like {p,l, {chat, ChannelID}} to listen to chat channels. (see gproc pub/sub))

Ok so now the user websocket connection starts the cowboy handler (i use Bullet). The handlers asks gproc the pid() of the user's gen_server and registrers itself as a receiver of messages. So now, when the user gen_server receives messages, it redirects them to the websocket handler.

When the websocket connexion ends, the handler uregister from the user gen_server, so the user gen_server will keep messages until the next connection, or the next timeout. At the timeout, you can simply terminate the server (messages will be lost but it is ok).

See : (not tested)

-module(user_chat).

-record(state, {mailbox,receiver=undefined}).

-export([start_link/1,set_receiver/1,unset_receiver/1]).
%% API

start_link(UserID) ->
    gen_server:start_link(?MODULE,[UserID],[]).

set_receiver(UserID) ->
    set_receiver(UserID,self()).

unset_receiver(UserID) ->
    %% Just set the receiver to undefined
    set_receiver(UserID,undefined).

set_receiver(UserID, ReceiverPid) ->
    UserPid = gproc:where({n,l,UserID}),
    gen_server:call(UserPid,{set_receiver,ReceiverPid}).


%% Gen server internals

init([UserID]) ->
    gproc:reg({n,l,{user,UserID}}),
    {ok,#state{mailbox=[]}}.

handle_call({set_receiver,ReceiverPid},_From,#state{mailbox=MB}=State) ->
    NewMB = check_send(MB,State),
    {reply,ok,State#state{receiver=ReceiverPid,mailbox=NewMB}}.

handle_info({chat_msg,Message},#state{mailbox=MB}=State) ->
    NewMB = check_send([Message|MB],State),
    {noreply, State#state{mailbox=NewMB}}.

%% Mailbox empty
check_send([],_) -> [];
%% Receiver undefined, keep messages
check_send(Mailbox,#state{receiver=undefined}) -> Mailbox
%% Receiver is a pid
check_send(Mailbox,#state{receiver=Receiver}) when is_pid(Receiver) ->
    %% Send all messages
    Receiver ! {chat_messages,Mailbox},
    %% Then return empty mailbox
    [].

OTHER TIPS

With the solution you propose you may have many processes pending and you will have to write a "process cleaner" for all user that never come back. Anyway it will not support a shutdown of the chat server VM, all messages stored in living FSM will vanish if the node is down.

I think that a better way should be to store all messages in a database like mnesia, with sender, receiver, expiration date... and check for any stored message at connection, and have a message cleaner process to destroy all expired messages from time to time.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top