Bin Wang
Bin Wang

Reputation: 2747

handle illegality events while not clearing timeout in gen_fsm behaviour

There is an locked door example about gen_fsm in the Elrang Otp System Documentation. I have a question about timeout. I will copy the code here first:

-module(code_lock).
-behaviour(gen_fsm).
-export([start_link/1]).
-export([button/1]).
-export([init/1, locked/2, open/2]).

start_link(Code) ->
    gen_fsm:start_link({local, code_lock}, code_lock, lists:reverse(Code), []).

button(Digit) ->
    gen_fsm:send_event(code_lock, {button, Digit}).

init(Code) ->
    {ok, locked, {[], Code}}.

locked({button, Digit}, {SoFar, Code}) ->
    case [Digit|SoFar] of
    Code ->
        do_unlock(),
        {next_state, open, {[], Code}, 30000};
    Incomplete when length(Incomplete)<length(Code) ->
        {next_state, locked, {Incomplete, Code}};
    _Wrong ->
        {next_state, locked, {[], Code}}
    end.

open(timeout, State) ->
    do_lock(),
    {next_state, locked, State}.

Here is the question: when the door is opened, if I press the button, the gen_fsm will have an {button, Digit} event at the state open. An error will occurs. But if I add these code after open function:

open(_Event, State) ->
   {next_state, open, State}.

Then if I press the button in 30s, the timeout will not be occurs. The door will be opened forever. What should I do?

Thanks.

Update:

I know I could use send_event_after or something like that. But I don't think it is a good idea. Because the state you excepted to handle the message may be changed in a complex application.

For example, if I have a function to lock the door manually after the door opened in 30s. Then locked will handle the timeout message, which is not the excepted behaviour.

Upvotes: 1

Views: 215

Answers (3)

Roger Lipscombe
Roger Lipscombe

Reputation: 91865

You could maintain the remaining timeout in StateData. To do this, add a third item to the tuple:

init(Code) ->
    {ok, locked, {[], Code, infinity}}.

You'll need to change locked to set the initial value:

locked({button, Digit}, {SoFar, Code, _Until}) ->
    case [Digit|SoFar] of
        Code ->
            do_unlock(),
            Timeout = 30000,
            Now = to_milliseconds(os:timestamp()),
            Until = Now + Timeout,
            {next_state, open, {[], Code, Until}, Timeout};
        Incomplete when length(Incomplete)<length(Code) ->
            {next_state, locked, {Incomplete, Code, infinity}};
        _Wrong ->
            {next_state, locked, {[], Code, infinity}}
    end.

And, if a button is pressed while open, calculate the new timeout and go around again:

open({button, _Digit}, {_SoFar, _Code, Until} = State) ->
    Now = to_milliseconds(os:timestamp()),
    Timeout = Until - Now,
    {next_state, open, State, Timeout};

You'll also need the following helper function:

to_milliseconds({Me, S, Mu}) ->
    (Me * 1000 * 1000 * 1000) + (S * 1000) + (Mu div 1000).

Upvotes: 1

Pascal
Pascal

Reputation: 14042

Using the fsm timeout, it is not possible - as far as I know - to avoid the re-initialization of it:

  • If you don't specify a new timeout when you skip the event while the door is open, it will remain open forever, as you notice.
  • If you specify one, it will restart from the beginning.

If none of these solutions satisfy you, you can use an external process to create the timeout:

-module(code_lock).
-behaviour(gen_fsm).


-export([start_link/1]).
-export([button/1,stop/0]).
-export([init/1, locked/2, open/2,handle_event/3,terminate/3]).

start_link(Code) ->
    gen_fsm:start_link({local, code_lock}, code_lock, lists:reverse(Code), []).

button(Digit) ->
    gen_fsm:send_event(code_lock, {button, Digit}).
stop() ->
    gen_fsm:send_all_state_event(code_lock, stop).


init(Code) ->
    {ok, locked, {[], Code}}.

locked({button, Digit}, {SoFar, Code}) ->
    case [Digit|SoFar] of
    Code ->
        do_unlock(),
        timeout(10000,code_lock),
        {next_state, open, {[], Code}};
    Incomplete when length(Incomplete)<length(Code) ->
        {next_state, locked, {Incomplete, Code}};
    _Wrong ->
        {next_state, locked, {[], Code}}
    end.

open(timeout, State) ->
    do_lock(),
    {next_state, locked, State};
open(_, State) ->
    {next_state, open, State}.

handle_event(stop, _StateName, StateData) ->
    {stop, normal, StateData}.


terminate(normal, _StateName, _StateData) ->
    ok.

do_lock() -> io:format("locking the door~n").
do_unlock() -> io:format("unlocking the door~n").

timeout(X,M) ->
        spawn(fun () -> receive
                        after X -> gen_fsm:send_event(M,timeout)
                        end
                end).

There are a bunch of functions in the module timer to do that, preferable to my custom example.

maybe a better usage of the Fsm timeout should be in the lock state:

  • wait for the first digit without timeout
  • a digit is entered and code is complete -> test it and continue without timeout (lock or open depending on code entered)
  • a digit is entered and code is not complete-> store it and continue with timeout
  • if an unexpected event occurs -> restart from begining without timeout
  • if timeout barks, restart from begining without timeout

EDIT: to Bin Wang: what you say in your update is correct, but you cannot avoid to manage this situation. I don't know any built in function that cover your use case. To satisfy it you will need to manage the unexpected timeout message in the lock state, but to avoid multiple timeout running, you will need also to stop the current one before to go to lock state. Note that this does not prevent you to manage the timeout message in lock state, because there is a race between the message to stop the timer and the timeout itself. I wrote for one of my application a general purpose apply_after function that can be canceled, stopped and resumed:

applyAfter_link(T, F, A) ->
    V3 = time_ms(),
    spawn_link(fun () -> applyAfterp(T, F, A, V3) end).

applyAfterp(T, F, A, Time) ->
    receive
        cancel -> ok;
        suspend when T =/= infinity ->
           applyAfterp(infinity, F, A, T + Time - time_ms());
        suspend ->
           applyAfterp(T, F, A, Time);
        resume when T == infinity ->
           applyAfterp(Time, F, A, time_ms());
        resume ->
           Tms = time_ms(), applyAfterp(T + Time - Tms, F, A, Tms)
        after T ->
           %% io:format("apply after time ~p, function ~p, arg ~p , stored time ~p~n",[T,F,A,Time]),
           catch F(A)
    end.

time_us() ->
    {M, S, U} = erlang:now(),
    1000000 * (1000000 * M + S) + U.

time_ms() -> time_us() div 1000.

You will need to sore the Pid of the timeout process in the FSM state.

Upvotes: 0

Marutha
Marutha

Reputation: 1814

You should be specifying a timeout at the open function "open(_Event, State)"

Since the next state is proceeded without timeout.. the door will remain open forever and no where a timeout occurs..

The newly defined function should be

open(_Event, State) -> {next_state, open, State, 30000}. %% State should be re-initialized

Upvotes: 0

Related Questions