Introduction to gen_fsm: Erlybank ATM

Tomcat

Professional
Messages
2,686
Reputation
10
Reaction score
690
Points
113
Scenario: We delivered an ErlyBank server to customers, and they were very satisfied. But this is the 21st century and they also want a secure and easy to use ATM, so they asked us to expand our server and create ATM software. User accounts must be protected with a 4-digit PIN. You can log in to an ATM using a previously created account, make a deposit or withdraw money from your account. There is no need to make a beautiful interface; other people do this.

Goal: First we will expand the server by adding support for PIN code for accounts and authorization via PIN code. Then we will use gen_fsm to create the ATM backend. Data verification will be carried out on the server side.

What is gen_fsm?​


gen_fsm is another Erlang/OTP interface module. It is used to implement a finite state machine.

I apologize in advance, since in this article the concept of “state” will be used to mean two things:
  • state gen_fsm — State of the finite state machine, the current “mode” of its operation. It has nothing to do with the state(data) from gen_server.
  • state(data) - Server state data is what you learned about from the previous article about gen_server.

A little awkward, of course, but I will try to refer to them only in the context of the above conditions.

gen_fsm starts in some state. Any call/cast calls to gen_fsm are processed in special callback methods, which must be named after the current state of gen_fsm (state machine). Based on the action performed, the module can change state ([current state, incoming symbol] -> new state, note). A textbook example of a state machine is a closed door. At the beginning, the door is in the “closed” state. You must enter a 4-digit code to open it. After entering 1 digit, the door saves it, but one digit is not enough, so it continues to wait in the “closed” state. After entering 4 numbers, if they are correct, the door changes state to “open” for a while. If the numbers are not correct, it remains in the "closed" state and clears the memory. Perhaps now you already have some guesses about how we will implement the finite machine using gen_fsm :)

Just as in the case of gen_server, I present a list of callback methods that should be implemented in gen_fsm. You will find a lot in common with gen_server:
  • init/1 - Initializes the state machine server. Almost identical to gen_server.
  • StateName/2 - StateName will be replaced with the name of the state. This method is called when the state machine is in this state and receives a message. As a result, a certain action is performed. This is an asynchronous callback method.
  • handle_event/3 - Same as StateName/2, except that this method fires when the client calls gen_fsm:send_all_state_event, regardless of the current state of the machine. Again, asynchronous.
  • StateName/3 - Synchronous version of StateName/2. The client waits for the server's response.
  • handle_sync_event/4 — Synchronous version of handle_event/3.
  • handle_info/3 - Equivalent to gen_server handle_info. This method receives all messages that were sent by non-standard gen_fsm means. These can be timeout messages, process exit messages, or any other messages sent to the server process using "!".
  • terminate/3 - Called when the server shuts down, where you can release occupied resources.
  • code_change/4 - Called when the server is updated in real time. We don't use it now, but it will be used in future articles.

gen_fsm skeleton​


Just like with gen_server, I start creating the state machine with some general skeleton. The skeleton for gen_fsm can be found here .

There's nothing extraordinary there. start_link is similar to the one we created for gen_server. :) Save the skeleton as eb_atm.erl . And now we are ready to begin!

eb_server extension to create an account authorization mechanism.​


This is another task that I leave to you. Changes we need:
  1. Now, when creating an account, you must require a PIN code, which will be stored with the account, without encryption.
  2. Add an authorize/2 method with Name and PIN arguments. Return values must be okor {error, Reason}.

Also, it would be great to require a PIN code for every deposit/withdrawal transaction, but to save time and also because our bank is a fake one (my heart is broken:( ha!), we will not do this.

To be honest, it's not that easy to do, but if you teach Erlang yourself, you should be pretty smart ;) So I think you can do it! Test your changes before proceeding, or at least compare them with the answer below.

After making the changes, your eb_server.erl should look something like this. Please note that the messages you send to the server may be different, and that's okay. Everyone's thinking is different. It is very important that the API outputs the same data, correctly. (The important thing is that the API outputs the same data, correctly, English)

ATM Design Strategy​


I want to take a short “no code” break to tell you the operating plan of the ATM state machine. We are going to execute it according to the diagram below:

Sequence Diagram for ATM


The three blue blocks represent different states of the server. The arrows indicate what actions are necessary to move from one state to another.

Initializing gen_fsm​


To Start ATM, we use the same start_link method as in gen_server. But the initialization is a little different.
Code:
init([]) -> 
  {ok, unauthorized, nobody}.

The init/1 method of the gen_fsm module must return {ok, StateName, StateData}. StateName is the initial state of the server, and StateData is the initial state(s) of the server. In our case, we start in the unauthorized state and the data is exposed to nobody. The state(data) will be the name of the account we are working with, so at first there is nothing there. Erlang does not have a null/nil/nothing data type, instead of which a talking atom is usually used, like we have nobody, for example.

Account authorization​


Now we need to implement the authorization API for ATM. First, the API definition:

Code:
authorize(Name, PIN) -> 
  gen_fsm:sync_send_event(?SERVER, {authorize, Name, PIN}).

The sync_send_event method is equivalent to the call method of the gen_server module. It sends a message (second argument) to the current state of the server (first argument). Therefore, now we need to write a handler for this message:
Code:
unauthorized({authorize, Name, Pin}, _From, State) -> 
  case eb_server:authorize(Name, Pin) of 
    ok -> 
      {reply, ok, authorized, Name}; 
    {error, Reason} -> 
      {reply, {error, Reason}, unauthorized, State} 
  end; 
unauthorized(_Event, _From, State) -> 
  Reply = {error, invalid_message}, 
  {reply, Reply, unauthorized, State}.

The function is called unauthorized because it must receive a message when the server is in the unauthorized state. We do a pattern match to process the tapl {authorize, Name, Pin}and use the API methods exported by the eb_server server to authorize the user.

If the username and PIN are correct, we send okto the client. Response format: {reply, Response, NewStateName, NewStateData}. According to the format, we change the state to authorized and store the account name in the state(data).

If the account information was incorrect, we send an error response and the reason for the error, status and state (data) do not change.

At the end we will implement another “catch-all” function. You should always do this, but it's especially important here because states can receive messages addressed to other states. For example: what happens if for some reason someone tries to make a deposit in an unauthorized state? We need a "catch-all" method to send back an error message.

Deposit​


Once we have entered the authorized state, the user is about to make a deposit or withdraw money from their bank account. We implement the deposit using an asynchronous call to the server. Again, this is not very secure: we don’t check at all whether the deposit was successful, but since our bank is a fake, I’ll forget about that. ;)

So, first, the API!
Code:
%%------------------------------------------------ -------------------- 
%% Function: deposit(Amount) -> ok 
%% Description: Deposits a certain amount in the currently authorized 
%% account. 
%%------------------------------------------------ -------------------- 
deposit(Amount) -> 
  gen_fsm:send_event(?SERVER, {deposit, Amount}).

It's simple, this time we use the method send_event/2instead of sync_send_event. It makes an asynchronous call to the server. And now, handler...
Code:
authorized({deposit, Amount}, State) -> 
  eb_server:deposit(State, Amount), 
  {next_state, thank_you, State, 5000}; 
authorized(_Event, State) -> 
  {next_state, authorized, State}.

Again, it's very simple. This method simply forwards the information to the deposit method of the eb_server module, which also does all the checking. But there is something unusual about the return value of the deposit method! Not only does the state change to thank_you, but also that number “5000” is there at the end. It's just a timeout . If no message is received within 5000 milliseconds (5 seconds), then the current state will be sent таймаут-сообщение.
Which leads us to the next topic...

Short “Thank You!” state​


Many (or everyone) who have used ATMs know that there is such a small “Thank You!” screen that is displayed for a short time. Actually, we could easily do without this screen in our implementation - I just wanted to show you a feature with a timeout in gen_fsm. After 5000 milliseconds, or if no message is received, I change the state back to "unauthorized" and so the ATM can start over with the next user. Here's the code:
Code:
thank_you(timeout, _State) -> 
  {next_state, unauthorized, nobody}; 
thank_you(_Event, _State) -> 
  {next_state, unauthorized, nobody}.

Note: The trained eye will notice that both methods are equivalent and there is no need for the first sample. It's true, I just included the first sample to make sure I caught the timeout.

And here is the currently completed version of eb_atm.erl.

Withdrawing money from an account​


Again I will leave the development of methods for withdrawing money as an exercise to the reader. You can implement this puzzle however you want! Just make sure that yours actually withdraws money ;)

Here is my version of eb_atm.erl after implementing the mechanisms for withdrawing money from the account. Please note that a successful operation will transfer the machine to the thank_you state with a timeout.

“Cancel-No-Matter-What” Button​


One of the biggest problems with computers is that there is no “Cancel” button to interrupt whatever you are doing. And although I know that the power off button on the computer copes with this task with a bang, users of Erlybank ATMs are deprived of this opportunity. So let's implement a cancel method that would cancel all transactions, no matter what state you are in.

How would you implement this? In general, I would suggest that you, based on the information in this article, would make the cancel method send the message cancel. Then, in each state, you would process it and exit back to unauthorizedthe state.

Witty, but not correct, but it's not your fault! What I didn't mention (or too briefly, you may have missed it) is that there is a method gen_fsm:send_all_state_event/2that sends a message to the server regardless of what state the server is in. We use it to keep our code clean.

Our API:
Code:
%%------------------------------------------------ -------------------- 
%% Function: cancel/0 
%% Description: Cancels the ATM transaction no matter what state. 
%%------------------------------------------------ -------------------- 
cancel() -> 
  gen_fsm:send_all_state_event(?SERVER, cancel).

This message is sent handle_event/3, which we expand below:
Code:
handle_event(cancel, _StateName, _State) -> 
  {next_state, unauthorized, nobody}; 
handle_event(_Event, StateName, State) -> 
  {next_state, StateName, State}.

If we receive a cancel message, the server changes the state to unauthorized and the communication (data) to nobody: fresh ATM!

Final Notes​


In this article I showed how to create a simple ATM system built on a state machine using gen_fsm. I showed how to handle messages in different states, change state, change state by timeout, and send-to-all messages.

However, there are still a few warts in our system, and I will leave it to you to correct them. I have prepared 2 tasks for you if you want. Trust me, you can do them:
  1. Add error checking to deposit transactions. Make them come back {error, Reason}and {ok, Balance}instead of just “ok” all the time.
  2. Add balance check feature to ATM. It should only be accessible in the authorized state and should not terminate the transaction. This means that it should not transfer the state to thank_you. This is so because usually people, after checking their balance, want to withdraw or deposit money into their account.

These two features from the exercises will not be used in the future, and as such, I will not post answers here. You can test yourself by putting them to work! :)

The second part of these articles is finished. The third article is almost ready and will be published in the coming days. It will expand the gen_event topic. For some fun, you might want to consider what I'll add to Erlybank using gen_event! :D

I hope you enjoyed these introductory articles to Erlang/OTP as much as I enjoyed writing them. Thanks everyone for your support and good luck!
 
Top