Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Principles of OTP. How to separate functional and non-functional code in practice?

Consider I have a FSM implemented with gen_fsm. For a some Event in a some StateName I should write data to database and reply to user the result. So the following StateName is represented by a function:

statename(Event, _From, StateData)  when Event=save_data->
    case my_db_module:write(StateData#state.data) of
         ok -> {stop, normal, ok, StateData};
         _  -> {reply, database_error, statename, StateData)
    end.

where my_db_module:write is a part of non-functional code implementing actual database write.

I see two major problems with this code: the first, a pure functional concept of FSM is mixed by part of non-functional code, this also makes unit testing of FSM impossible. Second, a module implementing a FSM have dependency on particular implementation of my_db_module.

In my opinion, two solutions are possible:

  1. Implement my_db_module:write_async as sending an asynchronous message to some process handling database, do not reply in statename, save From in StateData, switch to wait_for_db_answer and wait result from db management process as a message in a handle_info.

    statename(Event, From, StateData)  when Event=save_data->
        my_db_module:write_async(StateData#state.data),
        NewStateData=StateData#state{from=From},
        {next_state,wait_for_db_answer,NewStateData}
    
    handle_info({db, Result}, wait_for_db_answer, StateData) ->
        case Result of
             ok -> gen_fsm:reply(State#state.from, ok),
                   {stop, normal, ok, State};
             _  -> gen_fsm:reply(State#state.from, database_error),
                   {reply, database_error, statename, StateData)
        end.
    

    Advantages of such implementation is possibility to send arbitrary messages from eunit modules without touching actual database. The solution suffers from possible race conditions, if db reply earlier, that FSM changes state or another process send save_data to FSM.

  2. Use a callback function, written during init/1 in StateData:

    init([Callback]) ->
    {ok, statename, #state{callback=Callback}}.
    
    statename(Event, _From, StateData)  when Event=save_data->
        case StateData#state.callback(StateData#state.data) of
             ok -> {stop, normal, ok, StateData};
              _  -> {reply, database_error, statename, StateData)
    end.
    

    This solution doesn't suffer from race conditions, but if FSM uses many callbacks it really overwhelms the code. Although changing to actual function callback makes unit testing possible it doesn't solves the problem of functional code separation.

I am not comfortable with all of this solutions. Is there some recipe to handle this problem in a pure OTP/Erlang way? Of may be it is my problem of understating principles of OTP and eunit.

like image 806
galadog Avatar asked Oct 07 '12 13:10

galadog


People also ask

How does an OTP work?

A one-time password (OTP) is an automatically generated numeric or alphanumeric string of characters that authenticates a user for a single transaction or login session. An OTP is more secure than a static password, especially a user-created password, which can be weak and/or reused across multiple accounts.

What is an OTP code?

OTP means One Time Password: it's a temporary, secure PIN-code sent to you via SMS or e-mail that is valid only for one session.

What is OTP framework?

OTP is a collection of useful middleware, libraries, and tools written in the Erlang programming language. It is an integral part of the open-source distribution of Erlang. The name OTP was originally an acronym for Open Telecom Platform, which was a branding attempt before Ericsson released Erlang/OTP as open source.

Why is OTP safe?

Why is a one-time password safe? The OTP feature prevents some forms of identity theft by making sure that a captured user name/password pair cannot be used a second time. Typically the user's login name stays the same, and the one-time password changes with each login.


1 Answers

One way to solve this is via Dependency Injection of the database module.

You define your state record as

 -record(state, { ..., db_mod }).

And now you can inject db_mod upon init/1 of the gen_server:

 init([]) ->
    {ok, DBMod} = application:get_env(my_app, db_mod),
    ...
    {ok, #state { ..., db_mod = DBMod }}.

So when we have your code:

 statename(save_data, _From,
           #state { db_mod = DBMod, data = Data } = StateData) ->
   case DBMod:write(Data) of
     ok -> {stop, normal, ok, StateData};
     _  -> {reply, database_error, statename, StateData)
   end.

we have the ability to override the database module when testing with another module. Injecting a stub is now pretty easy and you can thus change the database code representation as you see fit.

Another alternative is to use a tool like meck to mock the database module when you are testing, but I usually prefer making it configurable.

In general though, I tend to split the code which is complex into its own module so it can be tested separately. I rarely do much unit testing of other modules and prefer large-scale integration tests to handle errors in such parts. Take a look at Common Test, PropEr, Triq and Erlang QuickCheck (The latter is not open source, nor is the full version free).

like image 63
I GIVE CRAP ANSWERS Avatar answered Jan 12 '23 22:01

I GIVE CRAP ANSWERS