#HSLIDE
#HSLIDE
- Какво е OTP?
- GenServer
- Supervisor
- Application
- Bonus - Tasks
#HSLIDE
#HSLIDE
- OTP е платформата, с която се разпространява Erlang.
- Версиите на Erlang са версии на OTP.
- OTP е стандартната библиотека на Erlang.
- OTP е Open Telecom Platform, но с времето OTP се е превърнала от платформа за писане на телекомуникационни програми в нещо повече.
#HSLIDE Платформата OTP идва с:
- Интерпретатор и компилатор на Erlang.
- Стандартните библиотеки на Erlang.
- Dialyzer.
- Mnesia - дистрибутирана база данни.
- ETS - база данни в паметта.
- Дебъгер.
- И много други...
#HSLIDE Ние ще разгледаме три абстракции от OTP:
- GenServer
- Supervisor
- Application
#HSLIDE
#HSLIDE
- GenServer представлява процес.
- Представен е от модул, който предлага функции за различни, често срещани случаи при работа с процеси.
#HSLIDE Какви функции?
- Функции за синхронна и/или асинхронна комуникация.
- Функции, които се извикват при създаване на процес или преди унищожаването му.
- Функции за нотифициране при грешки и за логване.
#HSLIDE
- Когато комбинираме GenServer-и със Supervisor процеси лесно можем да създадем fault-tolerant система.
#HSLIDE
- С помощта на мета-програмиране GenServer е специален тип поведение.
- Задава callback функции, които имат имплементации по подразбиране.
- За да направим от един модул GenServer, използваме "use GenServer".
#HSLIDE
defmodule MyWorker do
use GenServer
end#HSLIDE По този начин декларираме, че даден модул ще изпълни поведението GenServer. Това поведение и логиката около него ни дават следните възможности:
- Да стартираме процес.
- Да поддържаме състояние в този процес.
- Да получаваме request-и и да връщаме отговори.
- Да спираме процеса.
#HSLIDE
#HSLIDE
#HSLIDE
#HSLIDE
@type args :: any
@type state :: any
@type reason :: any
@type timeout :: non_neg_integer
@type init_result ::
{:ok, state} |
{:ok, state, timeout | :hibernate} |
:ignore |
{:stop, reason}
@spec init(args) :: init_result#HSLIDE
- Можем да приемем init/1 за конструктор на GenServer процеса.
- Тя приема аргументи от какъвто и да е тип и връща състоянието на процеса.
- За множество аргументи можем да използваме списък.
#HSLIDE
- Когато GenServer.start_link/3 се извика с даден модул, ако той дефинира init/1, то тя ще се извика като конструктор.
- Поведението по подразбиране, което се изпълнява ако не дефинираме тази функция, е да се използват аргументите като състояние:
#HSLIDE Ако върнем:
- {:ok, <състояние>}, процесът стартира с това състояние.
- {:ok, <състояние>, timeout-в-милисекунди}, процесът ще стартира със състоянието, и ако не получи съобщение за timeout време, ще получи автоматично съобщение :timeout.
- {:ok, <състояние>, :hibernate}, процесът ще хибернира.
#HSLIDE Ако върнем:
- :ignore, процесът ще излезе нормално и start_link ще върне :ignore.
- {:stop, reason}, процесът ще излезе с върнатата причина и start_link/3 ще върне {:error, reason}.
- Някой от {:ok, state} вариантите, GenServer.start_link/3 ще върне {:ok, pid}.
#HSLIDE
#HSLIDE
@type from :: {pid, ref}
@type handle_call_result ::
{:reply, reply, new_state} |
{:reply, reply, new_state, timeout | :hibernate} |
{:noreply, new_state} |
{:noreply, new_state, timeout | :hibernate} |
{:stop, reason, reply, new_state} |
{:stop, reason, new_state}
when reply: term, new_state: term, reason: term
@spec handle_call(request :: term, from, state :: term) ::
handle_call_result#HSLIDE
- Функциите handle_call се извикват когато GenServer-ът получи съобщение, изпратено от GenServer.call/3.
- Функцията GenServer.call/3 изпраща съобщение до GenServer процес и чака за отговор.
- Това е синхронна комуникация.
#HSLIDE GenServer.call/3
- Първият ѝ аргумент е pid или име на GenServer процес.
- Вторият - съобщението, което трябва да се изпрати.
- Tретият е timeout в милисекунди.
#HSLIDE
- Ако върнем с {:reply, <отговор>, <състояние>}, ще върнем отговорът като резултат на GenServer.call\3 и ще продължим със състоянието, което връщаме.
- Можем да очакваме подобно на init/1 поведение ако timeout или :hibernate са част от резултата.
#HSLIDE
- Ако отговорът е noreply, процесът извикал GenServer.call/3 няма да получи отговор и ще чака.
- Всеки процес може да отговори с GenServer.reply(from, reply).
- Нужно е само "from" да е точно тази наредена двойка, която е получена в handle_call.
#HSLIDE Има три основни причини да върнем noreply от handle_call:
- Защото сме отговорили с GenServer.reply/2 преди да върнем резултат.
- Защото ще отговори след като handle_call е свършила изпълнението си.
- Защото някой друг процес трябва да отговори.
#HSLIDE
- Връщаме :stop резултат, когато искаме да прекратим изпълнението на GenServer процеса.
- Ще се извика terminate(reason, state), ако е дефинирана, и процесът ще прекрати изпълнение с причина дадената причина.
#HSLIDE
#HSLIDE
@spec handle_cast(request :: term, state :: term) ::
{:noreply, new_state} |
{:noreply, new_state, timeout | :hibernate} |
{:stop, reason :: term, new_state} when new_state: term#HSLIDE
- handle_cast функциите се изпълняват при асинхронна комуникация.
- Това става чрез извикването на GenServer.cast(pid|name, request), която винаги връща :ok и не чака за отговор.
#HSLIDE
- handle_cast най-често се използват за промяна на състоянието.
#HSLIDE
#HSLIDE
@spec handle_info(msg :: :timeout | term, state :: term) ::
{:noreply, new_state} |
{:noreply, new_state, timeout | :hibernate} |
{:stop, reason :: term, new_state} when new_state: term#HSLIDE
- Използват се за прихващане на всякакви други съобщения - да речем такива, изпратени със send.
- Приемат и връщат аналогични параметри/резултати на тези на handle_cast/2.
#HSLIDE
#HSLIDE
@type reason :: :normal | :shutdown | {:shutdown, term} | term
@spec terminate(reason, state :: term) :: term#HSLIDE
- Извиква се преди терминиране на GenServer процес.
- Причината идва от резултат от типа {:stop, ...}, върнат от handle_* функциите.
- Може и да дойде от exit сигнал, ако GenServer процеса е системен процес.
- Supervisor процеси могат да пращат EXIT съобщения, които да се предадат на тази функция.
#HSLIDE
#HSLIDE
- Това е процес, чиято единствена роля е да наглежда други процеси и да се грижи за тях.
- С помощта на Supervisor по лесен начин можем да изградим fault-tolerant система.
- Идеологията около това поведение е лесна за възприемане.
- Трудното е да направим добър дизайн на такава система.
#HSLIDE
- Често сме споменавали за идеологията "Let it crash!".
- Тази мантра се базира върху следното:
#HSLIDE
- Важно е програмата да върви.
- Ако части от нея се сринат, не е проблем - нещо наблюдава тези части.
- Нещо ще се погрижи те да бъдат възстановени.
- Това нещо е Supervisor.
#HSLIDE
- Подобно на GenServer, Supervisor е поведение, за което callback функциите имат имплементации по подразбиране.
#HSLIDE
defmodule SomeSupervisor do
use Supervisor
end#HSLIDE В модула Supervisor има код, който може:
- Да инициализира и стартира Supervisor процес.
- Да осигури, че Supervisor процесът прихваща EXIT сигнали.
- Да стартира определен списък от процеси-деца, зададени на Supervisor-а и да ги link-не към него.
#HSLIDE Поведението Supervisor дава възможност:
- Ако някой от процесите-деца 'умре' непредвидено, Supervisor-ът ще получи сигнал и ще предприеме конкретно действие, зададено при имплементацията му.
- Ако Supervisor-ът бъде терминиран, всичките му процеси-деца биват 'убити'.
#HSLIDE
#HSLIDE
#HSLIDE :one_for_one
- Ако наблюдаван процес 'умре', той се рестартира.
- Другите не се влияят.
- Тази стратегия е добра за процеси, които нямат връзки и комуникация помежду си, които могат да загубят състоянието си без това да повлияе на другите процеси-деца на Supervisor-а им.
#HSLIDE :one_for_all
- Ако наблюдаван процес 'умре', всички наблюдавани процеси биват 'убити' и след това всички се стартират наново.
- Обикновено тази стратегия се използва за процеси, чиито състояния зависят доста едно от друго и ако само един от тях бъде рестартиран, ще е се наруши общата логика на програмата.
#HSLIDE :rest_for_one
- Ако наблюдаван процес 'умре', всички наблюдавани процеси стартирани СЛЕД него също 'умират'.
- Всички тези процеси, включително процесът-причина се рестартират по първоначалния си стартов ред.
- Тази стратегия е подходяща за ситуация като : процес 'A' няма зависимости, но процес 'Б' зависи от 'А', а има и процес 'В', който зависи както от 'Б', така и транзитивно от 'А'.
#HSLIDE :simple_one_for_one
- При тази стратегия даден Supervisor има право само на един тип процеси-деца.
- Обикновено стартира без процеси-деца, но знае как да си ги произведе.
- Процесите-деца се рестартират както при :one_for_one, но при много деца Supervisor-и с тази стратегия са по-бързи - не знаят реда на стартиране на процесите-си-деца.
- Подходяща е за ситуации, в които искаме да управляваме pool-ове от процеси.
#HSLIDE
- С опцията :max_restarts задаваме колко пъти може един процес да бъде рестартиран в даден период от време.
- По подразбиране е 3.
- Ако за :max_seconds секунди пробваме да рестартираме процеса :max_restarts пъти, трябва да се откажем.
#HSLIDE
#HSLIDE
@type spec :: {
child_id :: term,
start_fun :: {module, atom, [term]},
restart :: :permanent | :transient | :temporary,
shutdown :: :brutal_kill | :infinity | non_neg_integer,
worker :: :worker | :supervisor,
modules :: :dynamic | [module]
}#HSLIDE
- Функцията Supervisor.Spec.worker/3 взима модул за свой първи аргумент.
- Този модул обикновено имплементира GenServer.
- Вторият ѝ аргумент е списък от параметри.
- Третият е опции.
- Тези опции са:
#HSLIDE
[
restart: restart,
shutdown: shutdown,
id: term,
function: atom,
modules: modules
]#HSLIDE
- Това е стойност, която се ползва от Supervisor-ите вътрешно.
- Рядко ще я използваме за нещо, макар че можем да я подадем като опция на worker/3 с [id: id].
- Може да се ползва за debugging.
- Ако процесът има име - това е името му.
#HSLIDE
- Тази стойност е tupple съдържащ MFA.
- Използва се за стартиране на процеса-дете.
- Модулът, който съдържа логиката на процеса, се подава като първи аргумент на worker/3.
- Функцията за стартиране на процеса се подава от опциите на worker/3 чрез [function: atom-representing-public-function-from-the-module]. По подразбиране е :start_link.
#HSLIDE
- Задължително тази функция трябва да стартира процес и да го свързва със процеса, който я е извикал.
- Аргументите ще се подадат на зададената като атом функция при старт.
- Тези аргументи се подават във формата на списък на worker/3.
#HSLIDE
- Атом, който указва кога и дали 'терминиран' процес-дете ще се рестартира. Възможните стойности са:
#HSLIDE :permanent
- Процесът винаги се рестартира от Supervisor-а.
- Това е стойността по подразбиране на restart.
- Може да се зададе друга от опциите на worker/3 с [restart: :permanent | :transient | :temporary].
- Този начин на рестартиране е подходящ за дълго-живеещи процеси, които не трябва да 'умират'.
#HSLIDE :transient
- С тази опция, даденият процес-дете няма да бъде рестартиран ако излезе нормално - с exit(:normal) или просто завърши изпълнение.
- Ако обаче излезе с друга причина (exit(:reason)), ще бъде рестартиран.
#HSLIDE :temporary
- Процесът-дете няма да бъде рестартиран ако 'умре'.
- Няма значение дали е излязъл с грешка или не.
- Подходяща е за кратко-живеещи процеси, за които е очаквано, че могат да 'умрат' с грешка и няма много код, зависещ от тях.
#HSLIDE
- Когато Supervisor трябва да убие някои или всички свои процеси-деца, той извиква Process.exit(child_pid, :shutdown) за всеки от тях.
- Стойността, зададена като shutdown, се използва за timeout след като това се случи.
- По подразбиране е 5000, т.е. 5 секунди.
#HSLIDE
- Когато процес получи :shutdown, ако е Genserver, ще му се извика terminate/2 функцията.
- Изчистването на някакви ресурси може да отнеме време.
- Ако това време е повече от зададеното в shutdown, Supervisor-ът ще изпрати нов EXIT сигнал с Process.exit(child_pid, :kill).
#HSLIDE
- Ако зададем стойност :brutal_kill за shutdown, Supervisor-ът винаги ще терминира даденият процес направо с Process.exit(child_pid, :kill).
- Можем да зададем и :infinity, за да оставим процеса да си излезе спокойно.
#HSLIDE
- Това свойство определя дали процесът-дете е worker процес или Supervisor.
- Има функция Supervisor.Spec.supervisor/3, която генерира спецификация по същия начин като Supervisor.Spec.worker/3, но задава този worker да е supervisor.
#HSLIDE
- Трябва да е списък от един елемент - модул.
- Това е модулът, съдържащ callback функциите на GenServer имплементация или на Supervisor имплементация.
#HSLIDE
#HSLIDE
def start_supervising! do
import Supervisor.Spec, warn: false
children = [
worker(SomeModule, [])
worker(SomeOtherModule, [arg1, arg2])
supervisor(SomeModuleUsingSupervisor, [])
]
options = [strategy: one_for_one]
supervise(children, options)
end#HSLIDE
#HSLIDE
- Динамично добавя нова спецификация към Supervisor и стартира процес за нея.
- Първият аргумент е pid на Supervisor процес, а вторият - валидна Supervisor.Spec.spec.
#HSLIDE
Blogit.Components.Supervisor |> Supervisor.count_children
# %{active: 3, specs: 3, supervisors: 0, workers: 3}#HSLIDE
- active - това е броят на всички активни процеси-деца, които се управляват от подадения Supervisor.
- specs - това е броят на всички процеси-деца, няма значение дали са 'живи' или 'мъртви'.
#HSLIDE
- supervisors - броят на всички активни процеси-деца, които се управляват от подадения Supervisor и са Supervisor-и на свой ред. Няма значение дали са активни или не.
- workers - това е броят на всички активни процеси-деца, които се управляват от подадения Supervisor и не са Supervisor-и. Няма значение дали са активни или не.
#HSLIDE
- Връща списък с информация за всичките процеси-деца на Supervisor.
#HSLIDE
- Може да 'убие' процес-дете на Supervisor, подаден като първи аргумент.
- Ако стратегията е simple_one_for_one, процесът се подава като pid, при другите стратегии като child_id.
#HSLIDE
- Рестартира процес-дете, чиято спецификация се пази в подадения като първи аргумент Supervisor.
#HSLIDE
- Изтрива спецификация за дадено child_id. Не работи за simple_one_to_one стратегия.
#HSLIDE
- Спира подаденият като първи аргумент Supervisor с подадена като втори аргумент причина и изчаква с timeout - трети аргумент.
#HSLIDE
#HSLIDE
#HSLIDE
- Application е компонент в Elixir/Erlang, който може да бъде спиран и стартиран като едно цяло.
- Може да бъде използван от други Apllication-и.
- Един Application се грижи за едно supervision дърво и средата, в която то върви.
#HSLIDE
- Винаги, когато виртуалната машина стартира, специален процес, наречен 'application_controller' се стартира с нея.
- Този процес стои над всички Application-и, които вървят в тази виртуална машина.
- Mожем да го наречем Supervisor на Application-ите.
#HSLIDE
- Можем да приемем application_controller процеса за корена на дървото от процеси в един BEAM node.
- Под този 'корен' стоят различните Application-и, които са абстракция, обвиваща supervision дърво, която може да се стартира и спира като едно цяло.
- Те са като мега-процеси, управлявани от application_controller-a.
- Отвън те изглеждат като един процес, за който имаме функции start и stop.
#HSLIDE
- Когато се стартира един Application се създават два специални процеса, които заедно са наречени 'application master'.
- Тези два процеса създават Application-а и стоят между application_controller процеса и Supervisor-а, служещ за корен на supervision дървото.
#HSLIDE
#HSLIDE
#HSLIDE
- Извиква се при стартиране на Application-а.
- Очаква се да стартира процеса-корен на програмата, обикновено това е root Supervisor.
#HSLIDE
- Очаква се да върне {:ok, pid}, {:ok, pid, state} или {:error, reason}.
- Този state може да е каквото и да е, може да бъде пропуснат и ще е [].
- Той се подава на stop/1 callback функцията при спиране.
#HSLIDE На start/2 се подават два аргумента.
- Първият обикновено е атома :normal, но при дистрибутирана програма би могъл да е {:takeover, node} или {:failover, node}.
- Вторият са аргументи за програмата, които се задават при конфигурация.
#HSLIDE
- Когато Application-а бъде спрян, тази функция се извиква със състоянието върнато от start/2.
- Използва се за изчистване на ресурси и има имплементация по подразбиране, която просто връща :ok.
#HSLIDE
#HSLIDE
- Зарежда Application в паметта.
- Зарежда environment данните му и други свързани Application-и.
- Не го стартира.
#HSLIDE
- Стартира Application.
- За първи аргумент взима атом, идентифициращ Application.
- За втори типа на Application-а.
#HSLIDE Типът на програмата може да бъде: :permanent
- Ако Application-ът умре, всички други Application-и на node-а също умират.
- Няма значение дали Application-а е завършил нормално или не.
#HSLIDE Типът на програмата може да бъде: :transient
- Ако Application-ът умре с :normal причина, ще видим report за това, но другите Application-и на node-а няма да бъдат терминирани.
- Ако причината обаче е друга, всички други Application-и и целия node ще бъдат спрени.
#HSLIDE Типът на програмата може да бъде: :temporary
- Това е типът по подразбиране.
- С каквато и причина да спре един Application, другите ще продължат изпълнение.
#HSLIDE
- Ако спрем ръчно Application с функцията Application.stop/1, тези стратегии няма да се задействат.
#HSLIDE
- Прави същото като Application.start/2 и взима същия тип аргументи, но също стартира всички други Application-и, конфигурирани като зависимости.
#HSLIDE
- Взима модул, който представлява Application, тоест има "use Application".
- Връща атом представляващ Application.
- Няма значение дали този Application е активен или не.
- Важно е да е специфициран.
#HSLIDE
- Application environment е keyword list, който се конфигурира при дефиниране на Application.
- Има няколко функции за четене от него.
#HSLIDE
Тази функция има две версии.
- Първата взима само Application и връща цялата му спецификация.
- Втората взима Application и ключ в спецификацията, за да върне част от нея.
#HSLIDE
- Спира Application. Без да задейства стратегията му. Application-ът остава зареден в паметта.
#HSLIDE
- Премахва от паметта спрян Application и неговите зависимости, зададени като included_applications.
#HSLIDE
#HSLIDE
- OTP Application поведението и логиката около него идват от Erlang/OTP.
- Application-ите се конфигурират със специален .app файл, написан на Erlang.
- Той се слага при .beam файловете, които описва и след това може да се зареди на node, който има в пътя си директорията с него и тези .beam файлове.
#HSLIDE
mix new <app_project_name> --sup#HSLIDE
#HSLIDE
elixir -S mix run#HSLIDE
#HSLIDE
- Задачите са процеси, които правят главно едно нещо и не комуникират с други процеси.
- Освен когато свършат работата си.
- Тогава пращат съобщение на този процес, който е причинил тяхното създаване.
- Основната им цел е да превърнат прост sequential код в паралелен.
#HSLIDE
#HSLIDE
defmodule PEnum do
def map(enumerable, func) do
enumerable
|> Enum.map(&(Task.async(fn -> func.(&1) end)))
|> Enum.map(&Task.await/1)
end
end#HSLIDE Модулът Task има две важни функции :
#HSLIDE
- Task.async/1 (има и MFA версия - Task.async/3), която създава задача и я link-ва към текущия процес, като също така добавя монитор.
#HSLIDE 2. Task.await/2, която взима задача и опционално timeout (по подразбиране 5000, т.е. 5 секунди) и чака резултат от задачата.
#HSLIDE
#HSLIDE
Task.async(fn -> :nothing end)
# %Task{
# owner: #PID<0.2724.0>,
# pid: #PID<0.2727.0>,
# ref: #Reference<0.0.3.551>
# }#HSLIDE
- owner - Процесът, който ще получи съобщение, когато задачата приключи.
- pid - pid-ът на процеса на задачата.
- ref - Идва от монитора прикрепен към задачата.
#HSLIDE
#HSLIDE
- Защото ако той 'умре', задачата няма къде да върне резултата си, затова трябва да бъде изчистена.
- Ако пък задачата 'умре', означава, че нещо не е наред.
- Тя трябва да прави нещо наистина просто и не би трябвало да свърши живота си с причина, различна от :normal.
#HSLIDE
- Понякога искаме задачите да изпращат резултат, но да не се link-ват към текущия процес.
- Да речем това са задачи, които си говорят с отдалечен компонент/service и биха могли да получат грешка отвън.
#HSLIDE
- Бихме могли да стартираме специален simple_one_for_one Supervisor като част от нашия Application, който да отговаря за тези задачи.
- Този Supervisor ще създава задачи, които могат и да излязат с грешка, но няма да убият процеса, който ги използва.
- Даже ще връщат резултат, ако са създадени с правилната функция.
#HSLIDE
- Когато създадем задача в GenServer с Task.Supervisor.async_nolink, ако не използваме Task.await/2, можем да си дефинираме handle_info callback, който ще бъде извикан с резултата от задачата във формата
{ref, result}.
#HSLIDE
- Важното в подобни случаи е да дефинираме един допълнителен handle_info callback:
def handle_info({:DOWN, _ref, :process, _pid, _status}, state)#HSLIDE























