📜 ⬆️ ⬇️

Use of controllers to keep ErlyBank afloat

This is the fourth article in the series “Introduction to OTP” . If you have just joined us, I recommend starting with the first part , which speaks about gen_server and lays the foundation of our banking system. If you are a capable student, you can take a look at the modules that are currently ready: eb_server.erl , eb_event_manager.erl , eb_withdrawal_handler.erl and eb_atm.erl .

Scenario : The moment we like at banks and ATMs is that they are always in the same place. Using an ATM, we can withdraw or deposit money whenever we want, 24 hours a day. Or go to any branch of the bank when it is open, knowing that we will have full access to our finances. To guarantee this, you need to be sure that our ErlyBank automation system always remains working: processes must be running all the time. ErlyBank instructed us to realize this goal. 100% uptime! (Or as close as we can provide)

Result : Using the supervisor OTP, we will create a process whose duty is to monitor the running processes and make sure that they are active.

What is a controller ?


A controller is a process that keeps track of what are called child processes. If the child process is “blown away”, the controller uses the restart strategy of this child to restart. This method can ensure the eternal performance of Erlang systems.
')
A controller is part of what is called a control tree . A well-written Erlang / OTP application starts, starting with the root controller, which monitors the child controllers, which, in turn, monitor additional controllers or processes. The idea is that if the controller crashes, the parent controller restarts it, and so on up to the root controller. In the Erlang runtime environment, there is an excellent mode in which the entire system is monitored and restarted if the root controller dies. Thus, the control tree will always work.

The controller has only one feedback method : init / 1 . His task is to return a list of child processes and restart strategies for each process, so that the controller knows what to follow and what to do if something goes wrong.

Separate eb_server and event manager


One of the things that I implemented in a previous article on gen_events was to explicitly start the event manager process in the initialization method of the eb_server module. Then it was the only opportunity that I had, if I really wanted to easily start the server with this dependency. But now, since we are going to implement start and stop using the controller, we have the opportunity to start the event manager in the control tree. So let's remove the launch of eb_event_manager from the server code.

To do this, simply remove line 84, which starts the event manager, from the eb_server module. I also added the add_handler call to this place to connect the eb_withdrawal_handler handler to the event manager ( if you consistently read and implement the translations described in this translation cycle, including the addendum to the previous article, then you do not need to add anything, because we have already done so ; after excluding the launch of the event manager, your initialization method code should look like the following - comment of the translator ). Now the init method of the eb_server module should look like this:

init([]) ->
eb_event_manager:add_handler(eb_withdrawal_handler),
{ok, dict:new()}.

Click here to see eb_server.erl after the changes are made.

Inspector's frame


The main framework for writing the controller can be seen here . As you can see, it contains a startup method and a basic initialization method, which currently returns a restart strategy and non-existent descendant specs. Restart strategies and restraints ( child specifications ) are covered in the following sections of this article.

Save the framework as eb_sup.erl. Naming this file is another convention. The controller of a particular group always has the suffix " _sup ." This is not a mandatory requirement, but is standard practice.

Restart strategies


The controller has a unified restart strategy, which it uses in conjunction with the descendants specifications to determine actions in case one of its descendants dies. The following list contains possible restart strategies:


The restart strategy is specified in the following format:

{RestartStrategy, MaxRetries, MaxTime}

It is very easy to understand by speaking in Russian: If the child is restarted more often than MaxRetries once in MaxTime seconds, then the controller terminates all child processes and then stops working itself. This is done to prevent an infinite loop of restarts of the child.

Syntax and basic descriptions of descendants


The init method of the controller is responsible for returning a list of descendant specifications. These specifications tell the controller what processes to run and how to do it. The controller starts the processes in the order of "left to right" (from the beginning of the list to its end). The restart strategy is a tuple with the following format:

{Id, StartFunc, Restart, Shutdown, Type, Modules}

:
Id = term()
StartFunc = {M,F,A}
M = F = atom()
A = [term()]
Restart = permanent | transient | temporary
Shutdown = brutal_kill | int()>=0 | infinity
Type = worker | supervisor
Modules = [Module] | dynamic
Module = atom()


Id is used only inside the controller to store the specification of the descendants, but the general agreement implies the use of the module name as the ID, unless you run multiple instances of the module; in the latter case, add to the ID number.

StartFunc is a tuple in the format {Module, Function, Args} , which indicates the function whose call starts the process. VERY IMPORTANT : the launch function must start the process and link (link) to it and must return {ok, Pid} , {ok, Pid, ​​Other} or {error, Reason} . The usual OTP start_link methods follow this rule. But if you implement a module that runs its own processes, make sure you use spawn_link to run them .

Restart is one of the three atoms ( atom ) described in the code block above. If the " permanent " atom is used as a restart , the process is always restarted. If the value is " temporary ", the process never starts again. And if this value is equal to " transient ", then the process is restarted only in case of unexpected completion.

Shutdown explains to the controller how to terminate child processes. The atom " brutal_kull " completes the child without calling its completion method. Any integer number above zero implies a timeout for correct completion. The atom " infinity " politely completes the process and will wait for it to stop forever.

Type tells the controller what the descendant is: another controller or any other process. If it is a controller, use the supervisor atom, otherwise use the worker atom.

Modules is either a list of modules that are affected by this process, or the atom “dynamic”. In 95% of the cases in the list, you will use a single OTP feedback module for this value. “Dynamic” is used if the process is a gen_event, since its effect on the modules is dynamic (various handlers that cannot be determined immediately). This list is used only for release management and is not important in the context of this article, but will be used in a future release management article.

Wow! So much information to learn in such a short time. It took me quite a lot of time to memorize the format of the descendants' specifications and different restart strategies, so don’t worry if you can’t do it right away. You can always refresh information in the manual for supervisors .

Speck of a descendant event manager


The first thing you need to start is the event manager, because the server depends on it. The child specification looks something like this:

EventManager = {eb_event_manager,{eb_event_manager, start_link,[]},
permanent,2000,worker,dynamic}.

After reading the section on descendant specification syntax, this piece of code should be fairly simple. You may need to go back and check the description to understand the effect of each parameter, and this is absolutely normal! It is better to linger and figure out the code than to leave your head and forget everything in a couple of minutes. I suppose that one “strange” thing in the specification description would be to specify the list of modules as “dynamic” ( dynamic; in this case, an atom - comment of the translator ). This is done because it is a gen_event, and the list of modules used in it is dynamic, since handlers are connected to it (the number of which may vary during the work - note of the translator ). In other cases, you must list all the modules that the process uses.

Here is the initialization method after the inclusion of specs of descendants into it:

init([]) ->
EventManager = {eb_event_manager,{eb_event_manager, start_link,[]},
permanent,2000,worker,dynamic},
{ok,{{one_for_one,5,10}, [EventManager]}}.

I prefer to assign each variable to a descendant of a variable, and then use the contents of these variables to return, rather than put the specs directly in the return value. One of the largest corns of Erlang appears at the moment when the programmer embeds lists and tuples so deeply that you cannot see where one ends and the other begins, therefore I recommend that you assign each variable to it.

If you compile and run the controller now (I think it's worth it!), Then after launching the start_link method belonging to the controller, enter whereis (eb_event_manager) and the command should return the pid of the event manager process. Then, if you kill the controller by executing exit (whereis (eb_sup), kill) , and then try to get the identifier eb_event_manager again, you should receive a message in response that it is not defined, because the process was killed.

Also, for fun, kill eb_event_manager while working under the control of the controller. Wait a few seconds and check the process. He must recover!

Server and ATM


With help on the specification of descendants and the example above, you should know enough to start the server and the bank. So if you feel this challenge is tempting, do it now. If not, then I gave the specifications for the one and the other below:

Server = {eb_server, {eb_server, start_link, []},
permanent,2000,worker,[eb_server]},
ATM = {eb_atm, {eb_atm, start_link, []},
permanent,2000,worker,[eb_atm]},


After creating these specifications, add them to the list returned by the initialization method. Be sure to post them after the event manager.

You can see the complete eb_sup.erl by clicking here .

Adding and removing children at runtime


Unfortunately, I could not come up with a witty script that allows you to insert this mechanism into ErlyBank, but I felt it was important to note the ability to dynamically add and delete descendants' specifications into the already started controller process using the start_child and delete_child methods .

They are fairly simple, so I will not repeat here the guide to which I referred; You can go directly to him and become familiar with these methods.

Conclusion


In this article on controllers, I introduced concepts such as the control tree, restart strategies, descendant specifications, and the dynamic addition and deletion of descendants.

This concludes the fourth article from the “Introduction to Erlang / OTP” series. The fifth article is ready, scheduled for publication in the next few days and will provide applications ( applications ).



Articles from the series

4. Use of controllers to keep ErlyBank afloat (current article)
3. Introduction to gen_event: Account Change Notifications

Translation by tiesto :
2. Introduction to gen_fsm: ErlyBank ATM
1. Introduction to gen_server: “ErlyBank”
0. Introduction to the Open Telecom Platform / Open Telecommunication Platform (OTP / OTP)
-one. Prehistory

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


All Articles