Search code examples
erlangtimeoutstate-machine

How do I correctly update state_timeout in gen_statem?


I need to write a module that implements gen_statem behaviour. I need to simulate ATM work and one of the features is to give the card back if the person doesn't push any button for 10 secs. I have to states:

waiting_for_card - the only event that is possible is inserting a card; when the card is inserted I change the state to waiting_for_pin with state_timeout = 10000ms after which the state should be automatically changed to waiting_for_card.

waiting_for_pin - if the number-button is pressed I should put the number to an input list and restart the timer, while the enter and cancel buttons have different state transitions that should just cancel the timer

Here is the code snippet:

waiting_for_card({call, From}, {insert, Card}, #state{accounts = Accounts}) ->
  case check_if_user_is_in_list(Card, Accounts) of
    true ->
      NewState = #state{
        accounts = Accounts,
        insertedCard = Card
      },
      {next_state, waiting_for_pin, NewState, [{reply, From, ok}, {state_timeout, 10000, timeout}]};
    false ->
      {keep_state_and_data, [{reply, From, {error, "ATM doesn't servise this card"}}]}
  end;

waiting_for_pin({call, From}, {button, cancel}, #state{accounts = Accounts}) ->
  NewState = #state{accounts = Accounts},
  {next_state, waiting_for_card, NewState, [{reply, From, {ok, "Card returned"}}, {state_timeout, cancel}]};

waiting_for_pin({call, From}, {button, enter}, #state{accounts = Accounts, input = Input, insertedCard = Card}) ->
  case check_pin(Card, lists:reverse(Input), Accounts) of
    true ->
      NewState = #state{
        accounts = Accounts,
        insertedCard = Card
      },
      {next_state, waiting_for_sum, NewState, [{reply, From, {ok, "Valid pin, enter the sum to withdraw"}}, {state_timeout, cancel}]};
    false ->
      NewState = #state{
        accounts = Accounts
      },
      {next_state, waiting_for_card, NewState, [{reply, From, {error, "Wrong pin, get your card"}}, {state_timeout, cancel}]}
  end;

waiting_for_pin(cast, {button, Button}, #state{accounts = Accounts, input = Input, insertedCard = Card}) ->
  NewState = #state{accounts = Accounts, input = [Button + 48 | Input], insertedCard = Card},
  {keep_state, NewState, [{state_timeout, cancel}, {state_timeout, 10000, timeout}]};

As you can see, now I just cancelling and start a new timeout in Action argument of returned value of state function.

Pattern {state_timeout, update, EventContent} doesn't restart the timer is I have tested.

Am I right or I missed something?


Solution

  • I need to simulate ATM work and one of the features is to give the card back if the person doesn't push any button for 10 secs.
    ...
    As you can see, now I just cancelling and start a new timeout in Action argument of returned value of state function.

    According to the gen_statem docs for a state_timeout:

    Setting this timer while it is running will restart it with the new time-out value.

    And,

    enter_action() =
    hibernate |
    {hibernate, Hibernate :: hibernate()} |
    timeout_action() |
    reply_action()

    These transition actions can be invoked by returning them from the state callback ...

    ...

    Actions that set transition options override any previous of the same type... For example, the last event_timeout() overrides any previous event_timeout() in the list.

    As for this:

    Pattern {state_timeout, update, EventContent} doesn't restart the timer is I have tested.

    According to the timeout_update_action() docs:

    Updates a time-out with a new EventContent. See timeout_action() for how to start a time-out.

    So, update only updates the EventContent for the timeout--not the time for the timeout.

    Here's an example of what you can do:

    -module(card).
    -behaviour(gen_statem).
    -compile(export_all).
    
    %client functions:
    start_transaction() ->
        gen_statem:start_link({local, ?MODULE}, ?MODULE, no_args, []).
    
    end_transaction() ->
        gen_statem:stop(?MODULE).
    
    insert_card() ->
        gen_statem:call(?MODULE, card_inserted).
    
    button(Number) ->
        gen_statem:call(?MODULE, {button, Number}).
    
    % required callback functions
    callback_mode() ->
        state_functions.
    
    init(no_args) ->
        {ok, waiting_for_card, no_data}.
    
    terminate(_Reason, _PreviousState, _Data) ->
        io:format("terminating..."),
        ok.
    
    
    % waiting_for_card state:
    waiting_for_card({call, From}, card_inserted, _Data) ->
        io:format("state waiting_for_card: call.~n"),
        {
            next_state, waiting_for_pin, _Pin=[], 
            [{reply, From, card_detected}, {state_timeout, 10000, return_card}] 
        }.
    
    % waiting_for_pin state: 
    waiting_for_pin(state_timeout, return_card, _Pin) ->
        io:format("state waiting_for_pin: state_timeout.~n"),
        io:format("Sorry, you took too long to enter your pin.~n"),
        io:format("Here is your card back.~n"), 
        {next_state, waiting_for_card, reset_pin};
    
    waiting_for_pin({call, From}, {button, Button}, Pin) ->
        io:format("state waiting_for_pin: call.~n"),
        io:format("You pressed button: ~w.~n", [Button]),
        NewPin = [Button|Pin], %whatever
        {
            keep_state,
            NewPin,
            [{state_timeout, 10000, return_card}, {reply, From, got_number}]
        }.
    

    Here's a test run in the shell:

    ~/erlang_programs/gen_statem$ erl
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    Eshell V9.3  (abort with ^G)
    
    1> c(card).                 
    card.erl:3: Warning: export_all flag enabled - all functions will be exported
    {ok,card}
    
    2> card:start_transaction().
    {ok,<0.71.0>}
    
    3> card:insert_card().      
    state waiting_for_card: call.
    card_detected
    state waiting_for_pin: state_timeout.
    Sorry, you took too long to enter your pin.
    Here is your card back.
    
    4> card:insert_card().
    state waiting_for_card: call.
    card_detected
    
    5> card:button(4).          
    state waiting_for_pin: call.
    You pressed button: 4.
    got_number
    state waiting_for_pin: state_timeout.
    Sorry, you took too long to enter your pin.
    Here is your card back.
    
    6> card:insert_card().
    state waiting_for_card: call.
    card_detected
    
    7> card:button(4).    
    state waiting_for_pin: call.
    You pressed button: 4.
    got_number
    
    8> card:button(5).    
    state waiting_for_pin: call.
    You pressed button: 5.
    got_number
    state waiting_for_pin: state_timeout.
    Sorry, you took too long to enter your pin.
    Here is your card back.
    
    9> ...waits here for 1 minute plus...