PrehistoryIntroduction 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:
- state gen_fsm - The state of the finite state machine, the current "mode" of its operation. It has nothing to do with the state (data) of gen_server.
- state (data) - Server state data is what you learned from the previous article about gen_server.
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:
- init / 1 - Initializes the state machine server. Almost identical to gen_server.
- StateName / 2 - The 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 specific action is performed. This is an asynchronous callback method.
- handle_event / 3 - Same as StateName / 2, except that this method is triggered when a client calls gen_fsm: send_all_state_event, regardless of the current state of the machine. Again, asynchronous.
- StateName / 3 - The synchronous version of StateName / 2. The client waits for the server to respond.
- handle_sync_event / 4 - The synchronous version of handle_event / 3.
- handle_info / 3 - Equivalent to gen_server handle_info. This method receives all messages that were sent using non-standard gen_fsm tools. These can be timeout messages, process exit messages, or any other messages sent to the server process using "!".
- terminate / 3 - Called when the server is shutting down, in it you can free up occupied resources.
- code_change / 4 - Called when the server is updated in real time. We do not use it now, but it will be used in future articles.
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:
- Now, when creating an account, you need to require a PIN code that will be stored with your account, without encryption.
- 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:

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:
- Add error checking in operations with a deposit. Make them return
{error, Reason}
and {ok, Balance}
instead of just “ok” all the time. - 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!