Introduction to gen_server: Erlybank
- Transfer
Background
Introduction to the Open Telecom Platform / Open Telecommunication Platform (OTP / OTP)
This is the first article in a series of articles describing all the concepts that pertain to Erlang / OTP. So that you can find all these articles in the future, they are marked with a special tag: Otp introduction ( here I made a link to Habr tags ). As promised in the introduction to OTP , we will create a server serving fake bank accounts of people at Erlybank (yes, I like silly names).
Scenario:ErlyBank begins its activities, and managers need to get up on the right foot by creating a scalable system for managing bank accounts of their important customer base. Hearing about the power of Erlang, they hired us to do it! But to see what we are good for, they first want to see a simple server that can create and delete accounts, make a deposit and withdraw money. Customers only want a prototype, not something that they can put into production.
Purpose: we will create a simple server and client using gen_server . Since this is just a prototype, accounts will be stored in memory and identified by name. No other information will be needed to create an account. And of course, we will do a check for deposit and withdrawal operations.
Note: I assume that you already have an initial knowledge of Erlang syntax. If not, I recommend reading a short summary of resources for beginners to find a resource where you can learn Erlang.
If you are ready, click "Read more" to get started! (If you are not already reading the whole article :))
gen_server is an interface module for implementing client-server architecture. When you use this OTP module, you get a lot of goodies “for free”, but I'll talk about this later. Also, later in the series, I will talk about supervisors and error messages. And this module will hardly change.
Since gen_server is an interface, you need to implement a number of its methods or return functions (callbacks):
I always start writing with some generalized skeleton. You can watch it here .
Note: in order not to clutter up the space, I will not insert the contents of the files here, I will try to link everything as soon as possible. Here I will post important code snippets.
As you can see, the module is called eb_server . It implements all the callback methods that I indicated above and also adds another one:
This calls the start_link / 4 method of the gen_server module, which starts the server and registers the process with the atom defined by the macro
The server starts with the gen_server: start_link method, which calls the init method of our server. In it, you should initialize the state (data) of the server. State (data) can be anything: an atom, a list of values, a function, anything! This state (data) is transmitted to the server in each callback method. In our case, we would like to keep a list of all accounts and their balance values. Additionally, we would like to search for accounts by name. For this, I'm going to use the dict module , which stores key-value pairs in memory.
Note for object-oriented minds: if you came from OOP, you can take the server state for its instance variables. In each callback method, these variables will be available to you, and you can also change them.
So, the final form of the init method:
And really, it's that simple! So, for the Erlybank server, one of the expected values that init returns is this
Before we develop most of the server, I want to quickly go through the differences once again between the call and cast methods.
Call is a method that blocks a client. This means that when a client sends a message to the server, it waits for a response before proceeding further. You use call when you need an answer, such as requesting the balance of a specific account.
Cast is non-blockingor asynchronous method. This means that when the client sends a message to the server, it continues to work further, without waiting for the server to respond. Erlang now guarantees that all messages sent to processes reach the addressees, so if you clearly do not need a server response, you should use cast, this will allow your client to work further. That is, you do not need to make a call just to make sure your message has arrived - leave it to Erlang.
First start :) Erlybank needs a way to create new accounts. Quickly check yourself: if you had to create a bank account, what would you use: cast or call ? Think Qualitatively ... What Value Should Be Returned? If you chose call - you're right, although it's okay if not. You must be sure that the account was created successfully, instead of just relying on it. In our case, I'm going to execute it through cast, since we are not checking errors now.
First, I will create an API method that will be called from outside the module to create an account:
It sends a cast request to the server, which we registered as
Now we need to write a callback method for the server that will handle this cast:
As you can see, we just added one more definition for handle_cast to handle another request. Then we save it in an array with a value of 0, which displays the current account balance. handle_cast returns
Also, notice that I added a “catch-all” function to the end. This should be done not only here, but also in all functional languages in general. You can use this method only to swallow the message silently :) or you should throw an exception if you need to. In our case, I process it quietly.
The contents of the eb_server.erl file at the moment you can see here .
We promised our client, Erlybank, that we will add an API for depositing money and a basic check. So we need to write a deposit API method so that the server is required to check the account for existence before depositing money into the account. And again, check yourself
As before, I write the API method first:
Nothing outstanding: we just send a message to the server. The above should look familiar to you, as for now it’s almost the same as cast. But the differences appear in the server side:
Wow! A lot of new and incomprehensible! The method definition looks like handle_cast, except for a new argument
We promised Erlybank that we will check the existence of the account, and we do this in the first line of code. We are trying to find a value from the state (data) array that is equivalent to the user trying to make a deposit. The find method of the dict module returns one of 2 values: either
If an account exists, Value is equal to the current account balance - add the deposit amount to it. Then we save the new account balance in the array and assign it to a variable. I also save the server response in a variable, which just looks like a comment on the deposit API saying: that's it
On the other hand, if the account does not exist, we do not change the state (data) at all, but in response we send a tapple
Again, here is the updated version of eb_server.erl .
Now I’m going to leave the reader writing an API as an exercise to delete the account and withdraw money from the account. You have all the necessary knowledge to do this. If you need help with the dict module, refer to the dict API reference . When withdrawing money from an account, check the account for the existence and existence of the necessary amount of money to withdraw. You do not need to handle negative values.
When you are done, or if you are not done (I hope not!), You can find the answers here .
In this article, I showed the basics of gen_server and how to create client-server communication. I did not talk about all the possibilities of gen_server, which include such things as timeouts for messages and server shutdown, but I told you about a solid portion of this interface.
If you want to know more about callback methods, return values, and other more advanced gen_server things, read the gen_server documentation . Read it all, really.
Also, I know that I did not touch the code_change / 3 method, which for some reason is the most delicious for people. Do not worry, I already have sketches for an article (near the end of the series) devoted to upgrades on a working system (hot swapping code), and this method will play one of the main roles there.
The next article will be in a few days. It will be dedicated to gen_fsm. So if this article tickled your brain, “feel the freedom to jump into the manual” :) and act on your own. Maybe you can guess the continuation of the ErlyBank story, which I will do with gen_fsm .;)
Introduction to the Open Telecom Platform / Open Telecommunication Platform (OTP / OTP)
This is the first article in a series of articles describing all the concepts that pertain to Erlang / OTP. So that you can find all these articles in the future, they are marked with a special tag: Otp introduction ( here I made a link to Habr tags ). As promised in the introduction to OTP , we will create a server serving fake bank accounts of people at Erlybank (yes, I like silly names).
Scenario:ErlyBank begins its activities, and managers need to get up on the right foot by creating a scalable system for managing bank accounts of their important customer base. Hearing about the power of Erlang, they hired us to do it! But to see what we are good for, they first want to see a simple server that can create and delete accounts, make a deposit and withdraw money. Customers only want a prototype, not something that they can put into production.
Purpose: we will create a simple server and client using gen_server . Since this is just a prototype, accounts will be stored in memory and identified by name. No other information will be needed to create an account. And of course, we will do a check for deposit and withdrawal operations.
Note: I assume that you already have an initial knowledge of Erlang syntax. If not, I recommend reading a short summary of resources for beginners to find a resource where you can learn Erlang.
If you are ready, click "Read more" to get started! (If you are not already reading the whole article :))
What is in gen_server?
gen_server is an interface module for implementing client-server architecture. When you use this OTP module, you get a lot of goodies “for free”, but I'll talk about this later. Also, later in the series, I will talk about supervisors and error messages. And this module will hardly change.
Since gen_server is an interface, you need to implement a number of its methods or return functions (callbacks):
- init / 1 - Server initialization.
- handle_call / 3 - Handles a call request. The client who sends him a call request is blocked until he receives a response.
- handle_cast / 2 - Handles a cast request. A cast request is identical to a call request, except that it is asynchronous; the client continues to work during the cast request.
- handle_info / 2 - A kind of “catch all” method. If the server receives a message, and this is not a call or cast, then it will come here. An example of such a message is the EXIT process message if your server is connected (linked) to another process.
- terminate / 2 - Called when the server stops. Here you can do any operations necessary before exiting.
- code_change / 3 - Called when the server is updated in real time. You must put a stub in this method. This method will be discussed in detail in future articles.
Server skeleton
I always start writing with some generalized skeleton. You can watch it here .
Note: in order not to clutter up the space, I will not insert the contents of the files here, I will try to link everything as soon as possible. Here I will post important code snippets.
As you can see, the module is called eb_server . It implements all the callback methods that I indicated above and also adds another one:
start_link/0which will be used to start the server. I inserted a portion of this code below:start_link () ->
gen_server: start_link ({local,? SERVER},? MODULE, [], []).This calls the start_link / 4 method of the gen_server module, which starts the server and registers the process with the atom defined by the macro
SERVER, which by default is just the name of the module. The remaining arguments are the gen_server module, in this case it is itself, and any other arguments, and at the end of the option. We leave the arguments empty (we do not need them) and we do not specify any options. For a more detailed description of this method, see the gen_server manual page .Initializing Erlybank Server
The server starts with the gen_server: start_link method, which calls the init method of our server. In it, you should initialize the state (data) of the server. State (data) can be anything: an atom, a list of values, a function, anything! This state (data) is transmitted to the server in each callback method. In our case, we would like to keep a list of all accounts and their balance values. Additionally, we would like to search for accounts by name. For this, I'm going to use the dict module , which stores key-value pairs in memory.
Note for object-oriented minds: if you came from OOP, you can take the server state for its instance variables. In each callback method, these variables will be available to you, and you can also change them.
So, the final form of the init method:
init (_Args) ->
{ok, dict: new ()}.And really, it's that simple! So, for the Erlybank server, one of the expected values that init returns is this
{ok, State}. I just return ok and an empty associative array as state (data). And we do not pass arguments to init (which are still an empty array, remember, from start_link), so I precede the argument with the "_" symbol to indicate this.Call or Cast? Here is the question
Before we develop most of the server, I want to quickly go through the differences once again between the call and cast methods.
Call is a method that blocks a client. This means that when a client sends a message to the server, it waits for a response before proceeding further. You use call when you need an answer, such as requesting the balance of a specific account.
Cast is non-blockingor asynchronous method. This means that when the client sends a message to the server, it continues to work further, without waiting for the server to respond. Erlang now guarantees that all messages sent to processes reach the addressees, so if you clearly do not need a server response, you should use cast, this will allow your client to work further. That is, you do not need to make a call just to make sure your message has arrived - leave it to Erlang.
Create a bank account
First start :) Erlybank needs a way to create new accounts. Quickly check yourself: if you had to create a bank account, what would you use: cast or call ? Think Qualitatively ... What Value Should Be Returned? If you chose call - you're right, although it's okay if not. You must be sure that the account was created successfully, instead of just relying on it. In our case, I'm going to execute it through cast, since we are not checking errors now.
First, I will create an API method that will be called from outside the module to create an account:
%% ------------------------------------------------ --------------------
%% Function: create_account (Name) -> ok
%% Description: Creates a bank account for the person with name Name
%% ------------------------------------------------ --------------------
create_account (Name) ->
gen_server: cast (? SERVER, {create, Name}).It sends a cast request to the server, which we registered as
?SERVERin start_link. The request is a tapl {create, Name}. If cast is used, “ok” is immediately returned, which is also returned by our function. Now we need to write a callback method for the server that will handle this cast:
handle_cast ({create, Name}, State) ->
{noreply, dict: store (Name, 0, State)};
handle_cast (_Msg, State) ->
{noreply, State}.As you can see, we just added one more definition for handle_cast to handle another request. Then we save it in an array with a value of 0, which displays the current account balance. handle_cast returns
{noreply, State}where State is the new state (data) of the server. So this time we are returning a new array with an added account. Also, notice that I added a “catch-all” function to the end. This should be done not only here, but also in all functional languages in general. You can use this method only to swallow the message silently :) or you should throw an exception if you need to. In our case, I process it quietly.
The contents of the eb_server.erl file at the moment you can see here .
Cash deposit
We promised our client, Erlybank, that we will add an API for depositing money and a basic check. So we need to write a deposit API method so that the server is required to check the account for existence before depositing money into the account. And again, check yourself
cast или call:? The answer is simple: call. We must be sure that the money has reached and notify the user. As before, I write the API method first:
%% ------------------------------------------------ --------------------
%% Function: deposit (Name, Amount) -> {ok, Balance} | {error, Reason}
%% Description: Deposits Amount into Name's account. Returns the
%% balance if successful, otherwise returns an error and reason.
%% ------------------------------------------------ --------------------
deposit (Name, Amount) ->
gen_server: call (? SERVER, {deposit, Name, Amount}).
Nothing outstanding: we just send a message to the server. The above should look familiar to you, as for now it’s almost the same as cast. But the differences appear in the server side:
handle_call ({deposit, Name, Amount}, _From, State) ->
case dict: find (Name, State) of
{ok, Value} ->
NewBalance = Value + Amount,
Response = {ok, NewBalance},
NewState = dict: store (Name, NewBalance, State),
{reply, Response, NewState};
error ->
{reply, {error, account_does_not_exist}, State}
end;
handle_call (_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
Wow! A lot of new and incomprehensible! The method definition looks like handle_cast, except for a new argument
Fromthat we are not using. This is the pid identifier of the calling process, so we can send it an additional message if necessary. We promised Erlybank that we will check the existence of the account, and we do this in the first line of code. We are trying to find a value from the state (data) array that is equivalent to the user trying to make a deposit. The find method of the dict module returns one of 2 values: either
{ok, Value}, or error.If an account exists, Value is equal to the current account balance - add the deposit amount to it. Then we save the new account balance in the array and assign it to a variable. I also save the server response in a variable, which just looks like a comment on the deposit API saying: that's it
{ok, Balance}. Then, returning {reply, Reply, State}, the server sends a Reply back and saves the new state (data). On the other hand, if the account does not exist, we do not change the state (data) at all, but in response we send a tapple
{error, account_does_not_exist}, which again follows the specification in the deposit API comments. Again, here is the updated version of eb_server.erl .
Account deletion and withdrawal
Now I’m going to leave the reader writing an API as an exercise to delete the account and withdraw money from the account. You have all the necessary knowledge to do this. If you need help with the dict module, refer to the dict API reference . When withdrawing money from an account, check the account for the existence and existence of the necessary amount of money to withdraw. You do not need to handle negative values.
When you are done, or if you are not done (I hope not!), You can find the answers here .
Concluding notes
In this article, I showed the basics of gen_server and how to create client-server communication. I did not talk about all the possibilities of gen_server, which include such things as timeouts for messages and server shutdown, but I told you about a solid portion of this interface.
If you want to know more about callback methods, return values, and other more advanced gen_server things, read the gen_server documentation . Read it all, really.
Also, I know that I did not touch the code_change / 3 method, which for some reason is the most delicious for people. Do not worry, I already have sketches for an article (near the end of the series) devoted to upgrades on a working system (hot swapping code), and this method will play one of the main roles there.
The next article will be in a few days. It will be dedicated to gen_fsm. So if this article tickled your brain, “feel the freedom to jump into the manual” :) and act on your own. Maybe you can guess the continuation of the ErlyBank story, which I will do with gen_fsm .;)