На уровне синтаксиса языка код структурируется в функции и модули. На уровне потоков код структурируется в дерево супервизоров. Эти две структуры существуют независимо друг от друга. Но есть Application, которое связывает их вместе.
Во многих языка мы привыкли, что после функций и модулей, следующим уровнем идут пакеты. В эрланг нет пакетов, но приложение (Application) отчасти выполняет эту роль -- группирует несколько модулей в одну сущность. (К сожалению, Application не создает пространства имен. Имена всех модулей находятся в одной области видимости, и конфликты имен иногда случаются.)
С другой стороны, приложение контролирует часть дерева супервизоров и группирует потоки подобно тому, как пакет группирует модули. Эта группа (поддерево) может быть запущена и остановлена как единое целое.
Приложение следует рассматривать как некий компонент, предназначенный для повторного использования в разных проектах. Причем, для повторного использования предназначены обе структуры: и структура кода (функции-модули), и структура потоков (поддерево супервизоров).
Проект на эрланг обычно состоит из нескольких приложений:
Во-первых, это приложения, которые пишут разработчики -- непосредственно код проекта.
Во-вторых, это используемые библиотеки. Обычно каждая библиотека оформляется как Application. Например, библиотека для логирования lager, библиотека для сериализации JSON jiffy, драйвер для работы с PostgreSQL epgsql и другие.
В-третьих, это приложения, входящие в состав OTP. Например, приложение для работы с сетью inets, приложения, отвечающие за шифрование crypto и ssl, приложение для модульного тестирования eunit и другие.
Приложение состоит, как минимум, из главного модуля, реализующего behaviour(application), нескольких других модулей и файла ресурсов. (Полную структуру мы рассмотрим на следующем уроке).
Файл ресурсов (Application Resource File)
Начнем с файла, описывающего метаинформацию о приложении. Он должен называться по имени приложения и иметь расширение app. Например, my_cool_component.app.
Внутри он содержит кортеж из трех элементов:
{application, ApplicationName, Properties}.
ApplicationName -- имя приложения в виде атома. Например, my_cool_component.
Properties -- свойства приложения в виде proplist, где, обычно, присутствуют такие элементы:
- description -- краткое описание приложения одной строкой;
- vsn -- версия приложения, обычно в формате "major.minor.patch";
- modules -- список всех модулей, входящих в состав приложения;
- registered -- список всех имен под которыми регистрируются потоки;
- env -- настройки приложения в виде вложенного proplist;
- applications -- список других приложений, от которых зависит данное приложение;
- mod -- основной модуль приложения, реализующий behaviour(application).
Все опции считаются необязательными, но лучше указывать их явно. Большинство из них важны для сборки релиза. Инструменты, собирающие релиз, проверяют наличие указанных модулей, определяют очередность загрузки приложений, выявляют конфликты имен потоков. (Сборка релизов не входит в данный курс.)
Пример ресурс файла, взят из cowboy 1.0.1:
{application, cowboy, [
{description, "Small, fast, modular HTTP server."},
{vsn, "1.0.1"},
{id, "git"},
{modules, []},
{registered, [cowboy_clock, cowboy_sup]},
{applications, [
kernel,
stdlib,
ranch,
cowlib,
crypto
]},
{mod, {cowboy_app, []}},
{env, []}
]}.
Из этого файла видно следующее:
Ключ id не документирован, это авторы cowboy сами что-то придумали :)
Список модулей оставлен пустым. Его трудно поддерживать вручную, обычно он генерируется автоматически при сборке проекта.
Cowboy регистритует 2 потока с именами cowboy_clock и cowboy_sup.
Cowboy зависит от пяти других приложений. kernel, stdlib и crypto -- это часть OTP, ranch и cowlib -- это две библиотеки от тех же авторов.
Главный модуль -- cowboy_app.
Настроек тут нет, cowboy конфигурируется другим способом.
Запуск и остановка приложения
В эрланговской ноде всегда стартуют минимум 2 приложения: kernel и stdlib.
erl
Erlang/OTP 17 [erts-6.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V6.3 (abort with ^G)
1> application:which_applications().
[{stdlib,"ERTS CXC 138 10","2.3"},
{kernel,"ERTS CXC 138 10","3.1"}]
Один из потоков, которые запускает kernel, называется application_controller. Он отвечает за загрузку и запуск других приложений.
Чтобы запустить приложение, нужно вызвать application:start(my_app_name). При этом application_controller загружает метаданные приложения, проверяет, что все зависимые приложения уже запущены, и вызывает обработчик (callback) my_app_name:start/2.
Обработчик start/2 получает аргументы StartType и StartArgs. Они важны в распределенных приложениях, которые в данном курсе не описываются. (Но вам никто не запретит посмотреть документацию :) Здесь нужно запустить корневой супервизор приложения, и вернуть его Pid.
Чтобы остановить приложение, нужно вызывать application:stop(my_app_name). При этом будут вызваны обработчики my_app_name:pre_stop/1 до остановки приложения, и my_app_name:stop/1 после его остановки.
pre_stop/1 необязательный обработчик, так что его не нужно определять, если в нем не планируете ничего делать. А вот stop/1 -- обязательный обработчик, так что его всегда определяют, хотя чаще всего оставляют пустым.
При остановке приложения завершается его поддерево супервизоров в очередности, противоположной запуску. То есть, сперва завершаются рабочие потоки, потом дочерние супервизоры, и последним завершается корневой супервизор.
В процессе разработки на локальной машине приложения не редко запускают вручную, вызовом application:start/1. При этом нужно заботиться о том, чтобы запускать их в правильном порядке, иначе start вернет:
{error, {not_started, SomeOtherApp}}.
Запуск упрощается вызовом:
application:ensure_all_started(my_cool_app).
Этот вызов сперва проверяет, что все зависимые приложения запущены. Если не запущены, запускает их, и затем запускает my_cool_app.
1> application:start(ssl).
{error,{not_started,crypto}}
2> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
Здесь мы попытались запустить приложение ssl, но не получилось, потому что оно зависит от crypto и public_key. А public_key еще зависит от asn1. Вызов ensure_all_started запустил ssl и все эти зависимые приложения.
При использовании релизов запуск отличается. Здесь от разработчика требуется правильно указать зависимости приложений друг от друга в файле ресурсов. Затем автоматически генерируется скрипт запуска ноды, и там предусмотрен запуск всех приложений в правильном порядке.
Настройки
Приложение можно конфигурировать внешними настройками. Один источник таких настроек мы уже знаем -- это файл ресурсов.
Узел env в таком файле хранит настройки в виде proplist.
{application, my_cool_app,
[
{description, "The best app ever"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib]},
{mod, {my_cool_app, []}},
{env, [{key1, "value 1"},
{key2, 42},
{key3, [1,2,3,4]},
{key4, <<"value 4">>}
]}
]}.
Ключи должны быть атомами, а значения могут быть любого типа.
Другой источник, который используется чаще, это внешний конфигурационный файл. Он может иметь любое имя, но расширение должно быть .config.
В файле должен быть список кортежей вида {AppName, AppSettings}, где AppName -- атом, имя приложения, а AppSettings -- proplist, такой же, как в файле ресурсов.
%% file my_project.config
[
%% some app settings
{my_cool_app, [
{key1, "value 1"},
{key2, 42},
{key3, [1,2,3,4]},
{key4, <<"value 4">>}
]}
%% sasl app settings
{sasl, [
{errlog_type, error}
]},
%% lager app settings
{lager, [...]}
]
При запуске ноды нужно указать опцию -config my_project.
erl -config my_project ... other options
Для чтения настроек используются функции applications:get_env:
3> application:get_env(param1).
undefined
4> application:get_env(my_cool_app, param1).
{ok,"val1"}
5> application:get_env(my_cool_app, param2).
{ok,"val2"}
6> application:get_env(my_cool_app, param3).
undefined
7> application:get_env(my_cool_app, param3, "default value").
"default value"
get_env/1 работает внутри модуля, принадлежащего конкретному приложению, и возвращает настройку для этого приложения.
get_env/2 требует указать приложение и ключ, и возвращает {ok, Value} или undefined.
get_env/3 позволяет указать дефолтное значение на случай, если настройки нет в конфиге.
Есть нюанс, что в отличие от get_env/2, которая возвращает {ok, Value}, get_env/3 возвращает просто Value. Об этом нюансе нужно помнить, если у вас в коде изначально был вызов get_env/2, и вы дописали к нему 3-й аргумент. Тут, скорее всего, нужно будет поправить и код, принимающий значение из функции.
С настройками на рабочих серверах чаще имеют дело администраторы, чем разработчики. И для них такой синтаксис файла настроек неудобен. Честно говоря, этот синтаксис неудобен и для самих разработчиков -- легко можно ошибиться в запятых и скобках. Компилятор этот файл не проверяет, так что ошибка в синтаксисе проявится только при старте ноды.
Поэтому некоторые (и я в том числе), предпочитают использовать более привычные ini-файлы, или что-то подобное. Хотя тут придется приложить дополнительные усилия, чтобы загрузить и распарсить настройки.
Ну каждая команда в своем проекте делает выбор сама, так что я воздержусь от рекомендаций.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты