Приложение

Приложение — целостная подсистема единой Системы, выполняющая определённый круг задач. То же: application.

Erlang/OTP можно мыслить как операционную систему в операционной системе (например, в Linux или Windows). Эрланг-программист может отвлечься от такой вещи как системный процесс и вместо него оперировать акторами. Данные в нашу Систему попадают разными путями, часто из файлов, но единожды попав в Систему, данные превращаются в термы и в таком виде циркулируют от актора к актору. Основным предметом работы программиста становятся акторы и термы. Новый и к тому же очень комфортный уровень абстракции, позволяющий делать всё что угодно, и позволяет рассматривать Erlang/OTP как операционную систему.

Сама по себе низкоуровневая операционная система (например, Linux или Windows) бесполезна для пользователя. Польза приходит тогда, когда в операционке запускают приложения. Аналогично, в Erlang/OTP основная польза появляется вместе с приложениями. Каждое приложение обладает внутренней целостностью, согласованностью, а также ценностью для пользователя (даёт некий важный функционал).

Тем не менее, можно в своей работе обойтись без написания приложений как таковых. Для каких-то своих целей можно подготовить модуль и функции в нём. Модуль — это ещё не приложение. Чтобы сделать именно приложение, потребуется ряд шагов.

Что же нам даёт приложение:

  1. ?
  2. ?
  3. ?
  4. ?

Минимальное приложение

Чтобы понять принципы OTP относительно создания приложения, нам придётся разработать минимальное приложение, причём двигаясь последовательно.

Создание приложения начинается с задания модулю поведения application. Создадим пока модуль лишь с этими двумя строчками:

-module(my_app).
-behaviour(application).

Это, конечно вызовет у компилятора предупреждение, потому что любое поведение требует, чтобы были определены соответствующие колбэки.

my_app.erl:2:2: Warning: undefined callback function start/2 (behaviour 'application')
%    2| -behaviour(application).
%     |  ^

my_app.erl:2:2: Warning: undefined callback function stop/1 (behaviour 'application')
%    2| -behaviour(application).
%     |  ^

Как видим, не хватает функции start с арностью 2 и функции stop с арностью 1. Нетрудно догадаться, что эти колбэки нужны для запуска и остановки нашего приложения.

-module(my_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_A,_B) ->
    ok.

stop(_A) ->
    ok.

Мы пока не знаем, что там за аргументы нужны для start и stop, поэтому просто их обозначили как _A и _B. Обе функции возвращают пока атом ok. Их нам пришлось экспортировать, чтобы компилятор перестал ругаться. Теперь этот модуль компилируется.

Приложения запускаются с помощью функции application:start/1,2, а останавливаются с помощью application:stop/1. При этом будут запущены функции (колбэки) start/2 и stop/1 из нашего модуля my_app.

При запуске в качестве аргумента должен быть атом — название нашего модуля-приложения.

1> application:start(my_app).
{error,{"no such file or directory","my_app.app"}}

Ресурсный файл приложения

Оказывается, нужен ещё файл с расширением *.app, который описывает наше приложение. Без него приложение не запускается. Придётся искать в документации — какое минимальное содержание должно быть в этом так называемом ресурсном файле приложения.

И тут возможно три варианта. Если мы создаём лишь библиотечное приложение, минимально ресурсный файл должен выглядеть так:

{application, myapp, []}.

myapp — атом названия нашего приложения (для пробы я вместо my_app (название модуля) написал myapp, ведь название приложения может не совпадать с именем главного модуля приложения). В квадратных скобках могли бы быть дополнительные опции, но тут без них можно обойтись.

2-й вариант это приложение с древом надзора. В квадратные скобки ресурсного файла добавляется одна минимальная опция.

{application, myapp, [
    {mod, {my_app,[]}}
    ]}.

Опция с ключом mod определяет базовый модуль (модуль с колбэками), с которого начинается запуск приложения. В квадратных скобках могут быть начальные аргументы при старте приложения. В данном случае их нет, и это означает, что приложение будет стартовать так:

my_app:start(normal, [])

Третий вариант — самый полный. Когда мы используем systools для упаковки кода в релиз, тогда потребуются ещё опции.

{application, myapp, [
    {description, "My first application"},
    {vsn, "0.01"},
    {modules, [my_app, my_sup, my_worker]},
    {registered, [my_worker]},
    {applications, [kernel, stdlib]},
    {mod, {my_app,[]}}
    ]}.

description — краткое описание приложения. vsn — номер версии приложения. modules — все модули, вводимые приложением. Поскольку в приложении потребуется модуль супервизии, я добавил в список модуль my_sup. Раз нужен хотя бы один работник, я добавил модуль my_worker. registered — те акторы, которые должны быть зарегистрированы в Системе. Я собираюсь работнику посылать задания, поэтому добавил здесь my_worker. applications — список зависимостей (других приложений). Как минимум везде требуются kernel и stdlib.

Опробуем все три варианта.

1-й вариант. Библиотечное приложение

Создадим файл myapp.app, добавим в него первый вариант и запустим из оболочки.

1> application:start(myapp).
ok
2> application:stop(myapp).
=INFO REPORT==== 6-Sep-2025::08:41:21.588706 ===
    application: myapp
    exited: stopped
    type: temporary

ok

Приложение запустилось и остановилось успешно.

2-й вариант. Приложение с древом надзора

Делаем второй вариант файла myapp.app.

1> application:start(myapp).
=INFO REPORT==== 6-Sep-2025::08:46:32.516203 ===
    application: myapp
    exited: {bad_return,{{my_app,start,[normal,[]]},ok}}
    type: temporary

{error,{bad_return,{{my_app,start,[normal,[]]},ok}}}
=CRASH REPORT==== 6-Sep-2025::08:46:32.516534 ===
  crasher:
  ...

Приложение рухнуло с ошибкой bad_return. Сразу ясно, что виновата функция my_app:start/2. У нас она возвращает атом ok, а должна — что-то другое. Придётся опять читать документацию.

Функция start должна иметь следующую сигнатуру:

start(StartType, StartArgs) -> {ok,Pid} | {ok,Pid,State}

Настало время создать модуль надзора (супервизии). Из функции my_app:start будет вызываться функция start_link модуля надзора. Создадим файл модуля my_sup и напишем пока опять только две строки.

-module(my_sup).
-behaviour(supervisor).

Компилятор на это ожидаемо ругается, что не хватает колбэков (точнее одного):

my_sup.erl:2:2: Warning: undefined callback function init/1 (behaviour 'supervisor')
%    2| -behaviour(supervisor).
%     |  ^

Находим в интернете пример init-функции и, пока особенно не думая, создаём недостающий колбэк. И, хотя нет такого требования поведения, создаём функцию start_link. Именно она запустит надзирателя, и при запуске как раз потребуется функция init.

-module(my_sup).
-behaviour(supervisor).
-export([init/1, start_link/0]).

init([]) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [
                  #{id => my_worker,
                    start => {my_worker, start_link, []},
                    type => worker,
                    restart => permanent,
                    shutdown => 5000,
                    auto_shutdown => brutal_kill,
                    critical => true}
                 ],
    {ok, {SupFlags,ChildSpecs}}.

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

Меняем и функцию my_app:start/2.

start(_A,_B) ->
    my_sup:start_link().

Компилируем и запускаем. Получаем в общем-то ожидаемую ошибку:

1> application:start(myapp).
=SUPERVISOR REPORT==== 6-Sep-2025::11:00:52.450215 ===
    supervisor: {local,my_sup}
    errorContext: start_error
    reason: {'EXIT',
                {undef,
                    [{my_worker,start_link,[],[]},
                    ...

Конечно, в модуле my_sup мы определили уже работника my_worker и что при старте у него должна выполняться функция start_link. Однако этого работника у нас ещё нет. Поэтому и получили ошибку undef, раз функция my_worker:start_link не найдена. Пора сделать работника.

Важный момент — работник должен обладать одним из следующих поведений: gen_event, gen_server или gen_statem. gen_event выглядит попроще, выберем его. У этого поведения должны быть три колбэка: init/1, handle_event/2 и handle_call/2. Плюс start_link, чтобы запустить надзор. Попробуем следующий вариант:

-module(my_worker).
-behaviour(gen_event).
-export([init/1, start_link/0, handle_event/2, handle_call/2]).

init(Args) ->
    {ok, 0}.

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

handle_event(Event, N) ->
    io:format("*** some event:~p~n",[Event]),
    {ok, N}.

handle_call(_Request, N) ->
    Reply = N,
    {ok, Reply, N}.

(Все поведенческие колбэки должны возвращать {ok, State}.)

Компилируем работника и запускаем приложение.

1> application:start(myapp).
ok

Ура! Наше приложение на этот раз запустилось. Смотрим список запущенных приложений и находим в нём своё:

2> application:which_applications().
[{myapp,[],[]},
 {stdlib,"ERTS  CXC 138 10","7.0.2"},
 {kernel,"ERTS  CXC 138 10","10.3.2"}]

Ради интереса можно запустить Обсервер:

3> observer:start().
ok

Так можно увидеть древо надзора.

Обсервер, приложения

Созданное нами приложение не только запускается, но и останавливается.

4> application:stop(myapp).
=INFO REPORT==== 6-Sep-2025::11:45:48.358534 ===
    application: myapp
    exited: stopped
    type: temporary

ok
5> application:which_applications().
[{stdlib,"ERTS  CXC 138 10","7.0.2"},
 {kernel,"ERTS  CXC 138 10","10.3.2"}]

Приложение создано, хотя как им пользоваться, пока не ясно.

3-й вариант. Приложение с systools

Делаем 3-й вариант файла myapp.app. Приложение продолжает запускаться. Похоже, выше мы сделали всё необходимое и для этого варианта. systools нужен для работы с релизами, но нам пока об этом думать рано.

1> application:start(myapp).
ok
2> application:which_applications().
[{myapp,"My first application","0.01"},
 {stdlib,"ERTS  CXC 138 10","7.0.2"},
 {kernel,"ERTS  CXC 138 10","10.3.2"}]

Отличие только в том, что в списке работающих приложений у myapp появился краткий комментарий, взятый из ресурсного файла.

Документация


Copyright © 2025 Алексей Карманов