Commits

Fred T-H  committed 90fe9ec

Added basic history log support to each user.

A better implementation might need to care about each user's
discussions with particular users in an independent manner before
truncating the history. Right now it does the basic thing and just keeps
the latest stuff.

  • Participants
  • Parent commits 8e15fe0

Comments (0)

Files changed (4)

File src/client.erl

 %%% module, focused on tasks frequently used by a client process.
 %%%===================================================================
 -module(client).
--export([connect/1, disconnect/2, message/3, listen/2]).
+-export([connect/1, disconnect/2, message/3, listen/2, history/1]).
 
 -define(SESSION_LIFE, 15000).
 -define(LISTEN_TIMEOUT, 40000).
+-define(HISTORY_LIMIT, 10).
 
 %%--------------------------------------------------------------------
 %% Function: connect(Id) -> {ok, HandlerId}.
 %% its session ID for a given user.
 %%--------------------------------------------------------------------
 connect(Id) ->
-    usr:start(Id, ?SESSION_LIFE),
+    usr:start(Id, ?SESSION_LIFE, ?HISTORY_LIMIT),
     usr:subscribe(Id).
 
 %%--------------------------------------------------------------------
     after 0 ->
         []
     end.
+
+%%--------------------------------------------------------------------
+%% Function: history(Id) -> [Message]
+%% Description: Fetches previous messages that were routed through
+%% the user.
+%%--------------------------------------------------------------------
+history(Id) -> usr:history(Id).
 -module(usr).
 
 %% API
--export([start/2, terminate/2, subscribe/1, unsubscribe/2, subscribers/1,
-         message/3, relay/3]).
+-export([start/3, terminate/2, subscribe/1, unsubscribe/2, subscribers/1,
+         message/3, relay/3, history/1]).
 
 %%--------------------------------------------------------------------
-%% Function: Start(UserId, TimeOut) -> ok
+%% Function: Start(UserId, TimeOut, HistoryLimit) -> ok
 %% Description: registers a new user process or does nothing if it
 %% already is registered.
 %%--------------------------------------------------------------------
-start(UserId, TimeOut) ->
+start(UserId, TimeOut, HistoryLimit) ->
     case global:whereis_name(UserId) of
         undefined ->
             %% start it here
             usr_sup:start_link(UserId, TimeOut),
-            HandlerId = usr_dispatch_handler,
-            usr_dispatch_manager:add_handler(UserId, HandlerId, UserId),
+            usr_dispatch_manager:add_handler(UserId, usr_dispatch_handler, UserId),
+            usr_dispatch_manager:add_handler(UserId, usr_history_handler, {UserId,HistoryLimit}),
             io:format("started user process for id ~p~n",[UserId]),
             started;
         _Pid ->
 relay(From, To, Message) ->
     usr_dispatch_manager:notify(To, {received, From, Message}),
     ok.
+
+%%--------------------------------------------------------------------
+%% Function: history(UserId) -> [Messages]
+%% Description: Called by the client. Contacts an event handler
+%% that returns the last messages sent and received by UserId.
+%%--------------------------------------------------------------------
+history(UserId) ->
+    usr_dispatch_manager:notify(UserId, {self(), history}),
+    receive
+        {UserId, {history, History}} ->
+            History
+        after 5000 ->
+            throw(no_history_response)
+    end.

File src/usr_SUITE.erl

 %%--------------------------------------------------------------------
 all() -> 
     [usr_timeout, usr_subscribe, usr_message, usr_relay, usr_conv,
-     client_conv, two_clients_one_usr, client_aware_crashed_user].
+     usr_hist,
+     client_conv, two_clients_one_usr, client_aware_crashed_user,
+     client_hist].
 
 %%--------------------------------------------------------------------
 %% Function: usr_timeout(_) ->
 usr_timeout(_Config) ->
     [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
     IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
-    usr:start(Id, Sleep),
+    usr:start(Id, Sleep, 0),
     true = lists:all(IsProc, Expected),
     timer:sleep(Sleep*3),
     io:format("procs: ~p~n", [global:registered_names()]),
     process_flag(trap_exit, true), % only needed for the test to succeed?
     [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
     IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
-    usr:start(Id, Sleep),
+    usr:start(Id, Sleep, 0),
     {ok, Handler} = usr:subscribe(Id),
     timer:sleep(Sleep*3),
     true = lists:all(IsProc, Expected),
 %% to.
 %%--------------------------------------------------------------------
 usr_message(_Config) ->
-    usr:start(send_id, 500),
+    usr:start(send_id, 500, 0),
     {ok, Handler} = usr:subscribe(send_id),
     usr:message(send_id, invalid_id, hello),
     X = receive
 %% receives a message also receives the same message.
 %%--------------------------------------------------------------------
 usr_relay(_Config) ->
-    usr:start(sender, 500),
+    usr:start(sender, 500, 0),
     {ok, HandlerS} = usr:subscribe(sender),
-    usr:start(rec, 500),
+    usr:start(rec, 500, 0),
     {ok, HandlerR} = usr:subscribe(rec),
     usr:message(sender, rec, hello),
     X = receive
 %%--------------------------------------------------------------------
 usr_conv(_Config) ->
     {A,B} = {make_ref(), make_ref()},
-    usr:start(A, 500),
+    usr:start(A, 500, 0),
     {ok, HandlerA} = usr:subscribe(A),
-    usr:start(B, 500),
+    usr:start(B, 500, 0),
     {ok, HandlerB} = usr:subscribe(B),
     usr:message(A,B,"Hello, B"),
     usr:message(B,A,"Oh, Hello, A!"),
     usr:unsubscribe(A, HandlerA),
     usr:unsubscribe(B, HandlerB).
 
+
+%%--------------------------------------------------------------------
+%% Function: usr_hist(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Simulates a short conversation and checks that a
+%% history log can be obtained.
+%%--------------------------------------------------------------------
+usr_hist(_Config) ->
+    {A,B} = {make_ref(), make_ref()},
+    usr:start(A, 500, 2),
+    {ok, HandlerA} = usr:subscribe(A),
+    usr:start(B, 500, 0),
+    {ok, HandlerB} = usr:subscribe(B),
+    [] = usr:history(A),
+    usr:message(A,B,"Hello, B"),
+    usr:message(B,A,"Oh, Hello, A!"),
+    timer:sleep(500),
+    [{sent,B,"Hello, B"},
+     {received,B,"Oh, Hello, A!"}] = usr:history(A),
+    usr:message(A,B,"Hello2, B"),
+    timer:sleep(500),
+    [{received,B,"Oh, Hello, A!"},
+     {sent,B,"Hello2, B"}] = usr:history(A),
+    usr:unsubscribe(A,HandlerA),
+    usr:unsubscribe(B,HandlerB).
+
 %%--------------------------------------------------------------------
 %% Function: client_conv(_) ->
 %%               ok | exit() | {skip,Reason} | {comment,Comment} |
     catch
         exit:Reason -> Reason
     end.
+
+%%--------------------------------------------------------------------
+%% Function: client_hist(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Checks that a client can receive the history log from
+%% the user processes. Should just be a wrapper around usr:history/1
+%%--------------------------------------------------------------------
+client_hist(_Config) ->
+    {A,B} = {make_ref(),make_ref()},
+    {ok, HA} = client:connect(A),
+    {ok, HB} = client:connect(B),
+    [] = client:history(B),
+    client:message(A,B,"hah!"),
+    client:message(A,B,"hoh!"),
+    timer:sleep(200),
+    [{sent,B,"hah!"},{sent,B,"hoh!"}] = client:history(A),
+    client:disconnect(A,HA),
+    client:disconnect(B,HB).

File src/usr_history_handler.erl

+%%%===================================================================
+%%% This event handler is to be attached once per user.
+%%% Its role is to log messages routed through a user and
+%%% message them back to a client on demand.
+%%%===================================================================
+-module(usr_history_handler).
+-compile(export_all).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init({UserId, Limit}) -> {ok, {UserId, Limit, []}}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler. The state
+%% is constituted of the User's Id (to couple it to messages) and
+%% a limit (how many messages need to be kept in history).
+%%--------------------------------------------------------------------
+init({UserId, Limit}) ->
+    {ok, {UserId, Limit, []}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%%                               {swap_handler, Args1, State1, Mod2, Args2} |
+%%                               remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%
+%% Handles two kind of messages: received and sent messages. It wraps
+%% them with a tuple containing the handler's id and the user's id
+%% so many clients can be within a single process.
+%%--------------------------------------------------------------------
+handle_event(Event={received, _, _}, {UserId, Limit, History}) ->
+    {ok, {UserId, Limit, add_to_history(Event, History, Limit)}};
+handle_event({send,To,Msg}, {UserId, Limit, History}) ->
+    Event = {sent, To, Msg},
+    {ok, {UserId, Limit, add_to_history(Event, History, Limit)}};
+handle_event({From, history}, S={UserId, _, History}) ->
+    From ! {UserId, {history, lists:reverse(History)}},
+    {ok, S};
+handle_event(_Event, State) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%%                                {swap_handler, Reply, Args1, State1,
+%%                                  Mod2, Args2} |
+%%                                {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Request, State) ->
+    Reply = ok,
+    {ok, Reply, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%%                             {swap_handler, Args1, State1, Mod2, Args2} |
+%%                              remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+  {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description:Whenever an event handler is deleted from an event manager,
+%% this function is called. It should be the opposite of Module:init/1 and
+%% do any necessary cleaning up.
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+  ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.
+
+%%===================================================================
+%% Internal functions
+%%===================================================================
+%%-------------------------------------------------------------------
+%% Function: add_to_history(Event, History, Limit) -> NewHistory
+%% Description: prepends a new event to the history (a list of
+%% events). If the list gets larger than the given limit, it is
+%% truncated.
+%%-------------------------------------------------------------------
+add_to_history(_, _, 0) -> [];
+add_to_history(Event, History, Limit) ->
+    case length(History) of
+        X when X >= Limit-1 ->
+            [Event|lists:sublist(History,Limit-1)];
+        _ ->
+            [Event|History]
+    end.