0

I am building an app which can run in two modes. A sandbox mode and a production one.

In sandbox mode, i want to make many checks in my gen_server against the database : if table doesn't exist then create it ; if column doesn't exist then add it ; if column type doesn't allow the value i want to store then change it, etc.

In production mode, if a tables does not exist or a column does not match the type of the value, it will fail and that is ok.

So, in order to avoid cumbersome code like "case State#state.is_sandbox of true -> ... ", i would like to have two different modules for my gen_server, and i would like to change the current module either in handle_call or handle_info.

Actually, i just want to go from sandbox to production, but i think if it works this way, it could work backwards.

Thanks.

lud
  • 1,941
  • 20
  • 35

4 Answers4

3

You can add module, which is a name of a module, to the state in gen_server. Then you will need 2 modules - sandbox and production that both implement the same functions (you could create a behaviour for it).

The gen_server callbacks will call module:function which will be a function either from sandbox or production module. The module can be set in init function of the gen_server, to change it, simply add a new function(s) to the gen_server:

use_production() ->
    gen_server:cast(production).

....

handle_cast(production, State) ->
    {noreply, State#state{module = production}).

The same for the sandbox module.

An example of a gen_server's callback with the module:

handle_call(Msg, _From, #state{module = Module} = State) ->
    Module:function(Msq),
    {reply, ok, State}.

The function must be implemented in both sandbox and production modules.

juro
  • 631
  • 5
  • 13
  • With this solution, my server needs to check at every call the value of its Module variable and then call the corresponding function. It works, don't you think it may be slow ? – lud Feb 06 '13 at 21:32
  • The only thing you need to do is to get the module name from the state `#state{module = Module}`. You dont have to check whether it is sandbox or production module, you just call a function from this module. I don't think it's slow - calling `Module:function` is like calling a normal function from a different module. – juro Feb 06 '13 at 21:50
  • From a different module, yep. I believe that the compiler optimises calls when it know the module called at compile time. I'll check this. With your solution i would have 3 modules, the caller and two modules with only server side callbacks, which is quite nice. – lud Feb 06 '13 at 22:02
1

You can get module name using os:getenv/1 (of course you have to set different names in different environments before that)

Nik
  • 9,063
  • 7
  • 66
  • 81
  • This would need to check os/getenv at every call of the server, doesn't it ? – lud Feb 06 '13 at 21:26
  • I actually meant that you can create appropriate instance of gen_server on start using value from env or feed supervisor with childspec based on env value (in case you want to spawn many gen_servers in your app). – Nik Feb 06 '13 at 21:31
  • OK. So what i want is to change the module during runtime, and not pick the good one at startup. But you meke me think that i could kill the gen_server and restart the other one. Could be a solution ! – lud Feb 06 '13 at 21:57
1

You could use a gen_event with a single handler instead, which allows you to return a swap_handler tuple (see gen_event/handle_*)

Also, you don't have to use case statements in the gen_server model. If your state contains the sandbox variable, you can define different clauses for your callback functions by binding the sandbox value in the header. For instance:

handle_call(do_stuff, _From, State = #state{sandbox = true}) ->
    do_sandbox_stuff();
handle_call(do_stuff, _From, State) ->
    do_nonsandbox_stuff().

In this setup erlang automatically chooses the correct clause to fire based on the value of the sandbox variable, without you having to define a separate handler or use a case statement. Binding variables in function clauses this way is also good practice for efficiency (since the variables are bound outside of the body of the function, the binding process is done in the scheduler and, as a result, does not count against the function's execution time, whereas all matching is done inside the function body in a case)

Soup d'Campbells
  • 2,333
  • 15
  • 14
  • Do you think that i can call gen_event:swap_handler on my gen_server ? A hack or something ? (I didn't know that pattern matching on clauses was faster, thank you. But my real problem is about code : i would like a heavy module with lots of code, and a tiny module easy to read and maintain) – lud Feb 06 '13 at 21:39
  • No. gen_event actually supports multiple handlers, but regardless the processes have very different internal structures. It's highly improbable that gen_server even accepts the swap_handler message. – Soup d'Campbells Feb 07 '13 at 01:02
  • NO, gen_server does **not** support swap_handler! It only supports `gen_server:call` and `gen_server:cast`, all other messages end up in handle_info! Check the code. The basic idea behind a gen_server, and a gen_fsm, is that you give it one module with the required callbacks which describes what that server is to do. Even when you do code upgrade you use the same module. While `gen_event:swap_handler` will change a module it was not intended for that type of use. – rvirding Feb 08 '13 at 22:22
1

Instead of a gen_server you could use gen_fsm, a finite state machine, which handles this case very easily. You just have multiple states which call functions in different modules depending on the state. It basically does all the handling for you, without the need to carry an explicit state parameter. Which is basically implementing an FSM by hand.

rvirding
  • 20,848
  • 2
  • 37
  • 56
  • The module param suits my case because my two modules share the same interface, so the code is very short, 'limpid'. I'll give a try to gen_fsm for comparison. – lud Feb 08 '13 at 13:37