Search code examples
erlangdispatchererlang-otperlang-supervisorgen-server

Erlang: How to properly dispatch a gen_server with start_child in a supervisor and call the API


I have a gen_server in my cavv application that I need to start first to execute a call to. I want to use a command dispatcher for this. For a short example, this it the gen_server's API:

a gen_server: cavv_user

-module(cavv_user).

-behavior(gen_server).

-define(SERVER(UserId), {via, gproc, {n, l, {?MODULE, UserId}}}).

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

change_email_address(UserId, EmailAddress) ->
    gen_server:call(?SERVER(AggregateId), {execute_command, #change_user_email_address{user_id=UserId, email_address=EmailAddress}}). 

Before I can call cavv_user:change_email_address(). I need to start the cavv_user. I do this is as a simple_one_for_one child in a supervisor, like so:

a supervisor: cavv_user_sup

-module(cavv_user_sup).

-behaviour(supervisor).

-define(CHILD(ChildName, Type, Args), {ChildName, {ChildName, start_link, Args}, temporary, 5000, Type, [ChildName]}).

start_link() ->
    supervisor:start_link({local, ?SERVER}, ?MODULE, []).

start_child(UserId) ->
    supervisor:start_child(?SERVER, [UserId]).

init([]) ->
    RestartStrategy = {simple_one_for_one, 1, 5},

    Children = [?CHILD(cavv_user, worker, [])],

    {ok, { RestartStrategy, Children} }.

The problem I am now facing is how to dispatch commands to a cavv_user. I want to make sure the proper user is started first using start_child, and then call the cavv_user:change_email_address().

I have found this anwser, to use a dispatcher: Erlang: what supervision tree should I end with writing a task scheduler?

So I created a command dispatcher and end up with a cavv_user_dispatcher and a cavv_user_dispatcher_sup that in turn contains the cavv_user_dispatcher and the earlier cavv_user_sup:

         cavv_user_dispatch_sup
            |               |
 cavv_user_dispatcher       |
       (gen_server)         |
                            |
                            |
                      cavv_user_sup
                        |   |   |
                 cavv_user_1...cavv_user_N

The cavv_user_dispatcher

This works beautifully.

The problem I am facing now is, how do I properly write the code in cavv_user_dispatcher? I am facing a problem with code duplication. How to properly call start_child and call the appropriate API of cavv_user?

Should I use some kind of Fun like so?

-module(cavv_user_dispatcher).

dispatch_command(UserId, Fun) ->
    gen_server:call(?SERVER, {dispatch_command, {UserId, Fun}}).

handle_call({dispatch_command, {UserId, Fun}}, _From, State) ->
    cavv_user_sup:start_child(UserId),
    Fun(), %% How to pass: cavv_user:change_email_address(..,..)?
    {reply, ok, State};

Or duplicate the cavv_user's API like so?

-module(cavv_user_dispatcher).

change_user_email_address(UserId, EmailAddress) ->
    gen_server:call(?SERVER, {change_user_email_address, {UserId, EmailAddress}}).

handle_call({change_user_email_address, {UserId, EmailAddress}}, _From, State) ->
    cavv_user_sup:start_child(UserId),
    cavv_user:change_email_address(UserId, EmailAddress),
    {reply, ok, State};

Or should I re-use the command records from cavv_user into some kind of util to properly build them and pass them around? Maybe some better way to pass the function I want to call at cavv_user?

I would like to solve the problem in the best Erlang way as possible, without code duplication.


Solution

  • Is your dispatcher supposed to handle other commands?

    • If yes then then how will the next command will come, I mean will the requester know the process pid of the user or not?

      • if yes then you need 2 functions, one to create a user, it will return the pid to the requester for next call, and one to handle next requests by sending the command to the given pid

      • if no, then you need also 2 functions, one to create the a user and store the user_id along with the user process pid and one to handle next request by retrieving the process pid and then forward it the command (I suppose this is what you want to do).

    • if no then you don't need to handle any command and should pass directly the email address when creating the user process. Note that this is true for all cases since you need a different interface to create a user.

    I would modify your code this way (not tested, it is too late :o) !)

    -module(cavv_user_dispatcher).
    
    create_user(UserId,UserMail) ->
        gen_server:call(?SERVER,{new_user,UserId,UserMail}).
    
    % Args is a list of argument, empty if
    % F needs only one argument (the user Pid)
    dispatch_command(UserId, Fun, Args) ->  
        gen_server:call(?SERVER, {dispatch_command, {UserId, Fun,Args}}).
    
    handle_call({dispatch_command, {UserId, Fun,Args}}, _From, State) ->
        Pid = get_pid(UserId,State),
        Answer = case Pid of
            unknown_user_id -> unknown_user_id;
            _ -> apply(Fun,[Pid|Args]),
                 ok
        end,
        {reply, Answer, State};
    handle_call({new_user,UserId,UserMail},_From,State) ->
        % verify that the user id does not already exists
        CheckId = check_id(UserId,State),
        {Answer,NewState} = case CheckId of 
            false -> {already_exist,State};
            true  -> {ok,Pid} = cavv_user_sup:start_child(UserId,UserMail)
                     {ok,[{UserId,Pid}|State]}
                     % State must be initialized as an empty list in the init function.
        {reply, Answer, NewState};
    ...
    get_pid(UserId,State) ->
        proplists:get_value(UserId, State, unknown_user_id).
    check_id(UserId,State) -> 
        not proplists:is_defined(UserId, State).
    

    and the user supervisor mus be modified this way:

    start_child(UserId,UserMail) -> % change arity in the export
    supervisor:start_child(?SERVER, [UserId,UserMail]).
    

    and then the user server:

    start_link(UserId,UserMail) ->
    gen_server:start_link(?SERVER(UserId), ?MODULE, [UserId,UserMail],[]).
    
    init([UserId,UserMail]) ->
        {ok,[{user_id,UserId},{user_mail,UserMail}]}.