Процеси и OTP : Application

Application е още едно поведение, което идва с OTP.
Трите поведения - GenServer, Supervisor и Application са свързани.
Обикновено създаваме Application, който идва със свое дърво от Supervisor-и,
което както казахме в предишната статия, има за листа
GenServer процеси.
Какво е Application?
Както и при предишните две абстракции, които разгледахме, имаме логика, която работи с модул, имплементиращ дадено поведение.
Application е компонент в Elixir/Erlang, който може да бъде спиран и стартиран като едно цяло.
Също така може да бъде използван от други Apllication-и.
Един Application се грижи за едно supervision дърво и средата, в която то върви.
Винаги, когато виртуалната машина стартира, специален процес, наречен ‘application_controller’ се стартира с нея.
Този процес стои над всички Application-и, които вървят в тази виртуална машина, можем да го наречем Supervisor на Application-ите.
Разбира се самите Application-и могат да се управляват с различни стратегии, които ще разгледаме по-късно.
Както казахме можем да приемем application_controller процеса за корена на дървото от процеси в един BEAM node (макар и това да не е цялата истина, има известни изключения, за които, поне засега, няма да говорим).
Под този ‘корен’ стоят различните Application-и, които както казахме са абстракция, обвиваща supervision дърво, която може да се стартира и спира като едно цяло.
Представете си ги като мега-процеси, управлявани от application_controller-a. Поне отвън те изглеждат като един процес за който имаме функции start и stop.
Когато се стартира един Application се създават два специални процеса, които заедно са наречени ‘application master’.
Тези два процеса създават Application-а и стоят между application_controller процеса и Supervisor-а служещ за корен на supervision дървото на дадения Application.
Сега идва времето да разгледаме поведението Application и какво представлява и ни дава то.
Ще започнем, както и в предишните две статии, с пример.
Пример: Blogit
Досега давахме за пример различни части от Blogit.
Видяхме, че неговият root Supervisor се грижи за ‘мозъка’ му - Blogit.Server и
за друг Supervisor, управляващ малки компоненти, които могат да са достъпни за заявки отвън.
Нека сега да видим къде се създава този root Supervisor - Blogit Application-а:
defmodule Blogit do
use Application
alias Blogit.Components.Posts
alias Blogit.Components.PostsByDate
alias Blogit.Components.Configuration
@repository_provider Application.get_env(
:blogit, :repository_provider, Blogit.RepositoryProviders.Git
)
def start(_type, _args) do
Blogit.Supervisor.start_link(@repository_provider)
end
def list_posts(from \\ 0, size \\ 5) do
GenServer.call(Posts, {:list, from, size})
end
def list_pinned(), do: GenServer.call(Posts, :list_pinned)
def filter_posts(params, from \\ 0, size \\ 5) do
GenServer.call(Posts, {:filter, params, from, size})
end
def posts_by_dates, do: GenServer.call(PostsByDate, :get)
def post_by_name(name), do: GenServer.call(Posts, {:by_name, name})
def configuration do
GenServer.call(Configuration, :get)
end
end
Практика е Application-а да се слага в основния модул на проекта.
За проекта Blogit, този модул е Blogit.
Както казахме, Application е поведение и отново, чрез мета-програмиране, има имплементация по подразбиране за някои негови функции.
Всъщност callback функциите му са само две - Application.start/2 и Application.stop/1.
use Application задава имплементация по подразбиране само за Application.stop/1 - просто връща :ok.
Ние трябва да зададем имплементация на Application.start/2.
Application.start/2 има два аргумента - тип, който обикновено е :normal (ако програмата е дистрибутирана може и да е нещо друго) и аргументи, които след малко ще видим откъде идват.
Обикновено в тази функция построяваме supervision дървото и го стартираме.
В Blogit примера правим точно това. Стартираме Blogit.Supervisor, който както видяхме в предишната статия е коренът за Blogit.
В този пример можем да видим, че всеки Application си има environment, който можем да конфигурираме и използваме.
Да речем тук @repository_provider, който представлява способ за четене от някакво repository с публикации, се определя с помощта на Application.get_env/3.
Тази функция взима за първи аргумент атом, представляващ Application, за втори аргумент ключ към environment keyword list-а на този Application и за трети аргумент
стойност по подразбиране. След малко ще видим как задаваме настройки, които влизат в този environment keyword list.
Друга практика е клиентски функции, които правят заявки към различни процеси на Application-а да се дефинират тук.
Това е нещо като публичен интерфейс на Application-а. Можем да видим че Blogit ни дава възможност:
- Да вземем списък от публикации, който поддържа
pagination. - Да вземем списък от важни ‘pinned’ публикации.
- Да филтрираме публикации.
- Да вземем информация за публикациите по месец и година.
- Да вземем публикация по нейното име-идентификатор.
- Да вземем конфигурацията на блога.
Основните три компонента на Blogit стоят зад тези функции-заявки, но за публичния интерфейс, имплементацията не е важна.
Тук е добре да има добра и пълна документация, която ние сме премахнали за да направим примера кратък.
Ето какво представлява Application дървото на Blogit, представено от инструмента, който може да пуснете с :observer.start/0.

Процесите с pid-ове 0.186.0 и 0.187.0 са application master абстракцията, под тях виждаме supervision дървото,
за което говорихме в предишната статия. Има един процес Elixir.Earmark.Global.Messages, който
идва от библиотеката с която преобразуваме markdown към HTML - Earmark, който се създава автоматично от нея и се link-ва към процеса,
който я ползва - Blogit.Server. Всичко останало е както го описахме.
Нека сега разгледаме двете функции на поведението Application.
Поведението Application
Както казахме това поведение има само две callback функции - start и stop.
start/2
Извиква се при стартиране на Application-а.
Очаква се да стартира процеса-корен на програмата, обикновено това е root или top-level Supervisor, зависи откъде го гледаме.
Очаква се да върне {:ok, pid}, {:ok, pid, state} или {:error, reason}, в зависимост от това дали и как се е стартирал този основен процес.
Този state може да е каквото и да е, по подразбиране може да бъде пропуснат и е [] (празен списък).
Той се подава на stop/1 callback функцията при спиране.
На start/2 се подават два аргумента. Първият обикновено е атома :normal, но при дистрибутирана програма би могъл да е {:takeover, node} или {:failover, node}.
Вторият са аргументи за програмата, които се задават при конфигурация, както ще видим по-късно.
stop/1
Когато Application-а бъде спрян, тази функция се извиква със състоянието върнато от start/2 или ако няма такова с [].
Използва се за изчистване на ресурси и има имплементация по подразбиране, която просто връща :ok.
Функциите на модула Application
Ще разгледаме няколко от функциите на модула, които управляват или дават информация за Application.
Application.load/1
Зарежда Application в паметта. Зарежда environment данните му и други свързани Application-и.
Не го стартира.
Application.start/2
Стартира Application. За първи аргумент взима атом идентифициращ Application-а, а за втори типа на Application-а.
Извиква Application.load/1 ако програмата не е заредена в паметта.
Важно е, други Application-и, от които зависи стартираната да са стартирани преди това, иначе функцията връща {:error, {:not_started, app}}.
Типът на програмата може да бъде:
:permanent- АкоApplication-ът умре, всички другиApplication-и наnode-а също умират. Няма значение далиApplication-а е завършил нормално или не.:transient- АкоApplication-ът умре с:normalпричина, ще видимreportза това, но другитеApplication-и наnode-а няма да бъдат терминирани. Ако причината обаче е друга, всички другиApplication-и и целияnodeще бъдат спрени.:temporary- Това е типът по подразбиране. С каквато и причина да спре единApplication, другите ще продължат изпълнение.
Application стратегиите не се опитват да спасят или рестартират нищо. Те задават дали и другите Application-и трябва да бъдат терминирани при проблем.
На този етап, ако Application-а ‘умира’ няма спасение.
Ако спрем ръчно Application с функцията Application.stop/1, тези стратегии няма да се задействат.
Application.ensure_all_started/2
Прави същото като Application.start/2 и взима същия тип аргументи, но допълнително стартира всички други Application-и, конфигурирани като зависимости на подадената ѝ като първи аргумент.
Application.get_application/1
Връща атом, представляващ Application. Няма значение дали този Application е активен или не. Важно е да е специфициран (ще видим как става това след малко).
Взима модул, който представлява Application, тоест има use Application. При Blogit:
Application.get_application(Blogit)
# :blogit
Функции за четене и писане на Application environment
Както казахме Application environment е keyword list, който се конфигурира при дефиниране на Application.
Има няколко функции свързани с него:
Application.fetch_env(app :: atom, key) :: {:ok, value} | :error- Взима стойност поApplicationатом и ключ, ако няма такъв ключ връща:error. Има версияfetch_env!, която при липса на ключ ‘вдига’ArgumentError.Application.get_all_env(app :: atom) :: [{key, value}]- Връща целияenvironmentсписък заApplicationпо атома, който го представлява.Application.get_env(app :: atom, key, value) :: value- Видяхме го в примера по-горе. Връща стойност по атом и ключ, или зададена стойност по подразбиране, която може да се пропусне и тогава ще еnil.Application.put_env(app :: atom, key, value, [timeout: timeout, persistent: boolean]) :: :ok- Добавя стойност къмenvironmentсписъка наApplication.Application.delete_env(app :: atom, key, [timeout: timeout, persistent: boolean]) :: :ok- Трие стойност отenvironmentсписъка.
Пример:
Application.get_all_env(:blogit)
# [
# assets_path: "assets", polling: true,
# repository_url: "git@github.com:meddle0x53/elixir-blog.git",
# posts_folder: ".", included_applications: [], poll_interval: 60000
# ]
Application.get_env(:blogit, :repository_url)
# "git@github.com:meddle0x53/elixir-blog.git"
Application.spec
Тази функция има две версии. Първата взима само Application и връща цялата му спецификация, а втората Application и ключ в спецификацията, за да върне част от нея.
Application.started_applications/0 и Application.loaded_applications/1
Връщат информация за Application-ите на node-а.
Application.stop/1
Спира Application, без да задейства стратегията му. Application-ът остава зареден в паметта.
Application.unload/1
Премахва от паметта спрян Application и неговите зависимости, зададени като included_applications.
Тези функции могат да бъдат разгледани по-подробно в документацията.
Обикновено няма да ползвате тези, които не са свързани с environment-а. Всичко би трябвало да бъде поето за вас от mix и Elixir.
Нека да видим как се създава OTP Application mix проект и как се конфигурира един Application.
Създаване и конфигурация на Application
Тези OTP Application поведения и логиката около тях идват от Erlang/OTP.
Те се конфигурират със специален .app файл, написан на Erlang, който се слага
при .beam файловете, които описва и след това може да се зареди на node, който има в пътя си
директорията с него и тези .beam файлове. Както сигурно се досещате, това става с Application.load/1.
Ето го и .app файла на Blogit:
{application,blogit,
[{description," Blogit is an OTP application for generating blog posts from a git\n repository containing markdown files.\n"},
{modules,['Elixir.Blogit',
'Elixir.Blogit.Components.Configuration',
'Elixir.Blogit.Components.Posts',
'Elixir.Blogit.Components.PostsByDate',
'Elixir.Blogit.Components.Supervisor',
'Elixir.Blogit.Logic.Search',
'Elixir.Blogit.Logic.Updater',
'Elixir.Blogit.Models.Configuration',
'Elixir.Blogit.Models.Post',
'Elixir.Blogit.Models.Post.Meta',
'Elixir.Blogit.RepositoryProvider',
'Elixir.Blogit.RepositoryProviders.Git',
'Elixir.Blogit.RepositoryProviders.Memory',
'Elixir.Blogit.RepositoryProviders.Memory.RawPost',
'Elixir.Blogit.Server','Elixir.Blogit.Supervisor']},
{registered,[]},
{vsn,"0.7.3"},
{applications,[kernel,stdlib,elixir,logger,yaml_elixir]},
{mod,{'Elixir.Blogit',[]}}]}.
Разбира се ние сме Elixir програмисти и няма защо да пишем този файл ръчно.
Можем да си конфигурираме тези неща в mix проект.
Ако генерираме mix проект с:
mix new <app_project_name> --sup
ще получим структура подходяща за OTP Application. Можем да използваме mix compile.app в директорията на този проект за да се генерираме .app файла.
mix файла на един Application
При създаване на нов --sup проект имаме по специфичен mix.exs файл, който трябва да допълним.
Нека разгледаме завършения файл за Blogit:
defmodule Blogit.Mixfile do
use Mix.Project
def project do
[app: :blogit,
version: "0.7.3",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
docs: [readme: true, main: "README.md"],
description: """
Blogit is an OTP application for generating blog posts from a git
repository containing markdown files.
""",
aliases: aliases(),
deps: deps()]
end
def application do
[applications: [:logger, :yaml_elixir],
mod: {Blogit, []}]
end
defp deps do
[
{:git_cli, "~> 0.2"},
{:earmark, "~> 1.1"},
{:yaml_elixir, "~> 1.3.0"},
{:calendar, "~> 0.16.1"},
{:ex_doc, ">= 0.15.0", only: :dev},
{:dialyxir, "~> 0.5", only: [:dev], runtime: false}
]
end
defp aliases do
[test: "test --no-start"]
end
end
Както виждате vsn и description, които видяхме в blogit.app файла идват от
project конфигурацията. Атомът, който идентифицира Application-а също. Задава се чрез app.
Интересна е функцията application/0. Тя задава специфични опции свързани с OTP програми.
Ключът mod сочи към наредена двойка - модулът на Application-а, който съдържа use Application и start(type, args) и списък от аргументите args, които се задават при извикване на start(type, args).
Списъкът, зададен под ключа applications, съдържа други Application-и, от които нашият зависи. Към тях при генериране на .app файла се добавят и други, които са задължителни - kernel, stdlib и elixir.
Тук можем да зададем и keyword list с ключ :env, за да дефинираме началния environment на Application-а, но има и по-добър начин.
Модулите част от .app файла са всички модули дефинирани в mix проекта.
Винаги, когато стартираме кода си чрез mix, да речем с iex -S mix или с mix test, Application-ът ще се стартира и всичките зависимости изброени в :extra_applications и :applications ще се стартират преди него.
Това поведение може да се избегне, като подадем --no-start на командата. Това правим и при mix test при примера горе.
Казваме всеки пък когато изпълним mix test, всъщност да изпълняваме mix test --no-start.
Конфигурация на Application
Казахме, че environment-ът може да се дефинира по по-добър начин от това да го слагаме направо в mix.exs файла.
Това става чрез дефинирането му в config/config.exs файла, който може да зарежда различни конфигурации, в зависимост от mode-а, в който е пуснат node-а.
Така, когато си пускаме тестовете в test mode ще имаме една конфигурация, а в dev mode или prod mode друга.
Ето как изглежда dev конфигурацията на Blogit:
use Mix.Config
config :blogit,
repository_url: "git@github.com:meddle0x53/elixir-blog.git",
polling: true, poll_interval: 60_000,
posts_folder: ".", assets_path: "assets"
Използваме функцията config, идваща от Mix.Config, подаваме атома на Application-а, в случая :blogit и списък от ключове и стойности.
Тези тук ги видяхме и когато извикахме Application.get_all_env(:blogit).
Как стартираме един Application?
Освен с iex или ръчно, с Application функциите, можем да пуснем Application и с elixir командата, отново с помощта на mix:
elixir -S mix run
Можем да си задаваме Application-ите като зависимости на други Application-и.
Прието е една библиотека в Elixir, която прави нещо в повече от един процес да е OTP Application.
Така нейните процеси ще могат да се ползват от други Application-и.
Заключение
Приемете че един OTP Application може да бъде както библиотека, така и executable в Elixir.
Много от зависимостите, които ще задавате в mix проект ще са също така Application-и.
Даже web framework-а Phoenix е всъщност няколко OTP Application-и.
Следващата статия ще е кратка и ще разгледа абстракцията за създаване на малки backgorund задачи в Elixir - Task.
С нея ще приключим (засега) темата за процесите.