📜 ⬆️ ⬇️

Introduction to gen_server: "Erlybank"

Prehistory
Introduction to the Open Telecom Platform / Open Telecommunication Platform (OTP / OTP)

This is the first article in a series of articles describing all concepts related 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 the Habr tags ). As promised in the introduction to OTP , we will create a server that serves the fake bank accounts of people at Erlybank (yes, I like stupid names).

Scenario: ErlyBank starts its activities, and managers need to stand with the right foot, 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 in order to see what we are fit 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 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 some basic knowledge of Erlang syntax. If not, I recommend reading a brief 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 a client-server architecture. When you use this OTP module, you get a lot of goodies “for free”, but I’ll tell you about this later. Also, later in the series, I will talk about supervisors and error messages. And this module is almost unchanged.

Since gen_server is an interface, you need to implement a number of its methods or return functions (callbacks):

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 together as soon as the opportunity arises. Here I will post important code snippets.

As you can see, the module is called eb_server . It implements all the callback methods I mentioned above and also adds another one: start_link/0 , which will be used to start the server. I pasted a portion of this code below:

  start_link () ->
   gen_server: start_link ({local,? SERVER},? MODULE, [], []). 

Here, the start_link / 4 method of the gen_server module is called, 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 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, refer to the gen_server manual page.

Initialization of Erlybank server


The server is started by the gen_server: start_link method, which calls our server’s init method. In it, you should initialize the state (data) of the server. A 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.

A note for object-oriented minds: if you come from OOP, you can take the state of the server as 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 easy! So, for the Erlybank server, one of the expected values ​​that init returns is {ok, State} . I simply return ok and the empty associative array as a state (data). And we don’t pass arguments to init (which is still an empty array, remember from start_link), so I preface the argument with a “_” to indicate this.

Call or Cast? Here is the question


Before we develop a large part of the server, I want to quickly take another look at the differences between 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 continuing on. You use call when you need an answer, such as a request for the balance of a specific account.

Cast is a non-blocking or asynchronous method. This means that when a client sends a message to the server, it continues to work further, without waiting for the server to respond. Now Erlang guarantees that all messages sent to processes will reach the recipients, so if you clearly do not need a server response, you should use a cast, this will allow your client to continue working. That is, you do not need to make a call just to make sure that your message has reached - leave this 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 to call - you are right, although it's okay if not. You need to be sure that the account is created successfully, instead of just relying on it. In our case, I'm going to execute it through cast, since we are not performing error checking right 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
 %% ------------------------------------------------ --------------------
 create_account (Name) ->
   gen_server: cast (? SERVER, {create, Name}). 

It sends a cast request to the server, which we registered as ?SERVER in start_link. The request is a tapple {create, Name} . In the case of a cast, “ok” is returned immediately, 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 another definition for handle_cast to process another request. Then we save it in an array with a value of 0, which reflects 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 the added account.

also note that I added the "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 file eb_server.erl at the moment you can see here .

Cash deposit


We promised our client, Erlybank, that we would add an API for depositing money and a basic check. So we need to write a deposit API method so that the server is obliged 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.
 %% ------------------------------------------------ --------------------
 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 long as it is practically the same as cast. But 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 Many new and incomprehensible! The method definition looks similar to handle_cast, excluding the new From argument, which we do not use. This is the pid of the calling process, so we can send it an additional message if needed.

We promised Erlybank that we would check the existence of the account, and we do this in the first line of the code. We are trying to find a value from the state array (data) equivalent to the user who is trying to make a deposit. The find method of the dict module returns one of 2 values: either {ok, Value} , or error .

In case the account exists, the Value equals the current account balance, we 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's response in a variable, which looks just like a comment to the deposit API, saying: everything is {ok, Balance} . Then, returning {reply, Reply, State} , the server sends back Reply 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, and in response we send the error {error, account_does_not_exist} , which again follows the specification in the comments of the deposit API.

Again, here is the updated version of eb_server.erl .

Deleting an account and withdrawing money


Now I'm going to leave as an exercise for the reader to write an API for deleting an account and withdrawing money from an account. You have all the necessary knowledge to do this. If you need help with the dict module, contact the dict API reference . When withdrawing money from your account, check your account for the existence and availability of the necessary amount for withdrawal. You do not need to handle negative values.

When you're done, or if you're not done (hopefully not!), You can find the answers here .

Final 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 features 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 learn 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 (closer to the end of the series), dedicated to conducting upgrades on a running system (hot code replacement), and this method will play a major role 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 free to jump into the manual” :) and act yourself. Maybe you can guess the continuation of the ErlyBank story, which I will do with gen_fsm .;)

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


All Articles