📜 ⬆️ ⬇️

Introduction to gen_fsm: Erlybank ATM

Prehistory
Introduction to the Open Telecom Platform / Open Telecommunication Platform (OTP / OTP)
Introduction to gen_server: "Erlybank"

This is the second article in the “Introduction to OTP” series. I recommend you read the first article , which talks about gen_server and lays the foundations of our banking system before reading this. If you quickly grab, you can look at the complete version of the server and move on.

Scenario: We delivered the ErlyBank server to customers, and they were very pleased. But the 21st century is outside, and they also want a safe and easy-to-use ATM, so they asked us to expand our server and create software for an ATM. User accounts must be protected with a 4-digit PIN code. At an ATM, you can log in using a previously created account, make a deposit or withdraw money from your account. It’s not necessary to make a beautiful interface; other people are doing it.
')
Purpose: First we will expand the server by adding PIN support for accounts and authorization via PIN. Then we will use gen_fsm to create an 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 state machine .

I apologize in advance, as in this article the concept of “state” will be used to refer to two things:

Not a bit agile, of course, but I will try to refer to them only in the context of the above conditions.

gen_fsm starts working in some state. Any call / cast calls to gen_fsm are handled in special callback methods, which should be called the current state name gen_fsm (state machine). Based on the action taken, the module can change the state ([current state, incoming symbol] -> new state, note). A textbook example of a finite 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 she continues to wait in the “closed” state. After entering 4 digits, if they are correct, the door changes its 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 guesses about how we will implement the state 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:

gen_fsm skeleton


Just as with gen_server, I start creating a state machine with some common skeleton. The skeleton for gen_fsm can be found here .

There is nothing extraordinary. start_link is similar to the one we created for gen_server. :) Save the skeleton as eb_atm.erl . And here we are ready to start!

The eb_server extension to create an account authorization mechanism.


This is another task that I leave to you. Changes that we need:

  1. Now, when creating an account, you need to require a PIN code that will be stored with your account, without encryption.
  2. Add the authorize / 2 method with the Name and PIN arguments. Return values ​​must be ok or {error, Reason} .

Also, it would be great to require a PIN-code for each deposit / withdrawal operation, but to save time, and also because the bank is fake (my heart is broken :( ha!), We will not do that.

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

After making changes, your eb_server.erl should look something like this . Please note that the messages you send to the server may be different, and this is normal. Thinking everyone is different. It is very important that the API displays the same data correctly. (The important thing is that the API outputs the same data, correctly, Eng.)

ATM Design Strategy (ATM Design Strategy)


I want to take a small “no-code” pause to tell the work plan of the ATM state machine. We are going to do it according to the diagram below:

Sequence Diagram for ATM

Three blue blocks represent different server states. 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 initialization is a little different.
  init ([]) ->
   {ok, unauthorized, nobody}. 

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

Account Authorization


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

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

The sync_send_event method is equivalent to the gen_server module's call method. It sends a message (the second argument) to the current state of the server (the first argument). Therefore, now we need to write a handler for this message:
  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 should receive a message when the server is in the unauthorized state. We make a pattern matching to process the tapes {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 ok client. Response format: {reply, Response, NewStateName, NewStateData} . In accordance with the format, we change the state to authorized and keep the account name in the (data) state.

If the account information was not correct, we send back the error and the cause of the error, the state and the state (the data) do not change.

In the end, we implement another “catch-all” function. You should always do this, but this is especially important here , as states can receive messages addressed to other states. For example: what will happen if for some reason someone tries to make a deposit in a non-categorized state? We need a "catch-all" method to send back the error message.

Deposit


As soon as we are transferred to the authorized state, the user is going to make a deposit or withdraw money from his bank account. We make a deposit using an asynchronous call to the server. Again, this is not very safe: we do not check if the deposit was successful, but since our bank is a fake, I will score for it. ;)

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

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

Again, everything is very simple. This method simply redirects information to the deposit method of the eb_server module, which also performs all the checks. But there is something unusual about the return value of the deposit method! Not only does the state change to thank_you, but also this number “5000” is there at the end. This is just a timeout . If no message is received within 5000 milliseconds (5 seconds), a - will be sent to the current state.
Kotor leads us to the next topic ...

Short “Thank You!” State


Many (or all) who used ATMs know that there is such a small “Thank You!” Screen that is shown for a short time. Actually, we could safely 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 working again with the next user. Here is the code:
 thank_you (timeout, _State) ->
   {next_state, unauthorized, nobody};
 thank_you (_Event, _State) ->
   {next_state, unauthorized, nobody}.

Note: A trained eye will notice that both methods are equivalent and there is no need for the first sample. It's true, I just turned on the first sample, to be sure that I would catch the thaumaut.

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

Withdrawal from the account


Again I will leave the development of methods for withdrawing money as an exercise for the reader. You can implement this puzzle as you want! Just make sure your real money is withdrawn;)

Here is my version of eb_atm.erl after implementing the withdrawal mechanisms. Please note that a successful operation will reset the machine to the thank_you state with a timeout.

“Cancel-No-Matter-What” button


One of the biggest problems with computers is the lack of a “Cancel” button that would interrupt everything you do. And although I know that the power off button on a computer is coping 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, make a cancel method that sends a cancel message. Then, in each state, you would process it and go back to an unauthorized state.

Witty, but not right, but this is not your fault! I did not indicate (or too briefly, you may have missed it) that there is a method gen_fsm:send_all_state_event/2 , which sends a message to the server regardless of the state of the server. We use it to keep our code clean .

Our API:
 %% ------------------------------------------------ --------------------
 %% 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 to handle_event/3 , which we expand below:
 handle_event (cancel, _StateName, _State) ->
   {next_state, unauthorized, nobody};
 handle_event (_Event, StateName, State) ->
   {next_state, StateName, State}.

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

As always, the current version of eb_atm.erl can be viewed here .

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 process messages in different states, change the state, change the state by timeout, and send-to-all messages.

However, there are still a few “warts” in our system, and I will leave you the opportunity to correct them. I prepared 2 tasks for you if you want. Believe me, you can do them:
  1. Add error checking in operations with a deposit. Make them return {error, Reason} and {ok, Balance} instead of just “ok” all the time.
  2. Add the function of checking balance in ATM. It should be available only in the authorized state and should not complete the transaction. This means that it should not transfer the state to thank_you. This is so because usually people, having hoisted their balance, want to withdraw or deposit money to themselves.

These two features of the exercises will not be used in the future, and if so, I will not post answers here. You can test yourself by making them work! :)

The second part of these articles is over. The third article is almost ready and will be published in the coming days. It will cover the topic of gen_event . To have fun, you might think about what I will add to Erlybank with the help of gen_event! : D

I hope you enjoyed these introductory articles in Erlang / OTP just as much as I liked writing them. Thank you all for your support and good luck!

Source: https://habr.com/ru/post/56196/


All Articles