OTP Appup Cookbook

Материал из Erlang по-русски.

Оригинал этой статьи находится по адресу Appup Cookbook

Автор: Mirrorer
Дата: 12.11.2006
Версия: 1.0



Эта глава содержит примеры .appup файлов для типичных случаев обновлений\откатов до старой версии, которые производятся во время работы системы.


Содержание

Изменение функционального модуля

Когда производится изменение в функциональном модуле, например, при добавлении новой функции, или при исправлении ошибки, эффективной будет простая замена кода.

Пример:

{"2",
 [{"1", [{load_module, m}]}],
 [{"1", [{load_module, m}]}]
}.

Изменение резидентного модуля

В системе, реализованной согласно принципам разработки OTP, все процессы, за исключением системных и специальных процессов, живут в одном из следующих поведений: supervisor, gen_server, gen_fsm или gen_event. Они принадлежат приложению STDLIB, и обновление версии или откат до предыдущей версии обычно требует перезапуска эмулятора.

Таким образом, OTP обеспечивает поддержку изменения резидентных модулей, за исключением специальных процессов.

Изменение модуля обратного вызова

Модуль обратного вызова является функциональным модулем, и для изменения кода достаточно простой замены кода.

Пример: При добавлении функции в ch3, как описано в примере в главе Управление релизами, файл ch_app.appup выглядит следующим образом:

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.
 

OTP также поддерживает изменение внутреннего состояния процессов поведения, смотрите раздел Изменение внутреннего состояния.


Изменение внутреннего состояния

В этом случае простой замены кода недостаточно. Процесс должен явно изменить свое состояние при помощи функции обратного вызова code_change, перед переходом к новой версии модуля обратного вызова. Таким образом, используется синхронизированная замена кода.

Пример: Предположим, что у нас есть gen_server ch3 из главы Поведение Gen_Server. Внутренним состоянием будет терм Chs, представляющий количество возможных каналов. Предположим также, что хотим добавить счетчик N, который будет хранить количество пришедших запросов alloc. Это значит, что мы должны привести внутреннее состояние к виду {Chs,N}.

Файл .appup может выглядеть следующим образом:

{"2",
 [{"1", [{update, ch3, {advanced, []}}]}],
 [{"1", [{update, ch3, {advanced, []}}]}]
}.

Третьим элементом инструкции update является кортеж {advanced,Extra}, который говорит о том, что процессы, которые затрагивает изменение, должны изменить состояние перед загрузкой новой версии модуля. Это делается путем вызова процессом функции обратного вызова code_change (см. gen_server(3)). Терм Extra, в нашем случае [], передается в функцию без изменений.

-module(ch3).
...
-export([code_change/3]).
...
code_change({down, _Vsn}, {Chs, N}, _Extra) ->
    {ok, Chs};
code_change(_Vsn, Chs, _Extra) ->
    {ok, {Chs, 0}}.

Первый аргумент – это {down,Vsn} в случае отката до предыдущей версии, или Vsn, в случае обновления версии. Терм выталкивается из «оригинальной» версии модуля, т.е. версии, до которой мы обновляемся или откатываемся.

Версия указывается атрибутом модуля vsn, если таковой имеется. В ch3 такой атрибут отсутствует, так что в этом случае версией будет контрольная сумма (большое целочисленное значение) BEAM-файла, значение которой нам неинтересно, поэтому оно проигнорировано.

Функции обратного вызова ch3 в могут быть изменены в том случае, если будет добавлена новая функция интерфейса, но этот случай рассматриваться не будет.

Зависимости между модулями

Предположим, мы расширили модуль, добавив новую интерфейсную функцию, как в примере из Управление релизами, где функция available/0 была добавлена в ch3.

Если мы также добавим вызов этой функции, допустим в модуле m1, ошибка времени выполнения произойдет в процессе обновления релиза, если новая версия m1 будет загружена первой, и вызовет ch3:available/0 перед тем, как будет загружена новая версия ch3.

Таким образом, ch3, должна быть загружена перед m1 в случае обновления версии, или, наоборот, в случае отката до предыдущей версии. Мы говорим, что m1 зависима от (is dependent on) ch3.

{load_module, Module, DepMods} {update, Module, {advanced, Extra}, DepMods}

DepMods – это список модулей, от которых зависит Module.

Пример: Модуль m1 в приложении myapp зависит от ch3 при обновлении от "1" к "2", или при откате от "2" к "1":

myapp.appup:

{"2",
 [{"1", [{load_module, m1, [ch3]}]}],
 [{"1", [{load_module, m1, [ch3]}]}]
}.


ch_app.appup:

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.
  

Если m1 и ch3 принадлежали одному приложению, файл .appup может выглядеть следующим образом:

{"2",
 [{"1",
   [{load_module, ch3},
    {load_module, m1, [ch3]}]}],
 [{"1",
   [{load_module, ch3},
    {load_module, m1, [ch3]}]}]
}.
  

Необходимо заметить, что m1 зависит от ch3 и при откате до предыдущей версии. systools понимает отличие между обновлением и откатом до предыдущей версии, и сгенерирует правильный relup, в котором ch3 будет загружаться перед m1 в случае обновления версии, но m1 будет загружаться перед ch3 в случае отката до предыдущей версии.

Изменение кода в специальных процессах

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

Image:note.gifИмя (имена) пользовательского резидентного модуля (модулей) должно быть в описано в части Modules спецификации потомков для специального процесса для того, чтобы менеджер релизов мог найти процесс.

Пример. Предположим у нас есть приложение ch4 из главы Библиотеки Sys и Proc_Lib . При запуске супервизора спецификация потомков может выглядеть так:

{ch4, {ch4, start_link, []},
 permanent, brutal_kill, worker, [ch4]}

Если ch4 является частью приложения sp_app и новая версия модуля должна быть загружена при обновлении с версии "1" до версии "2" приложения, файл может выглядеть следующим образом:

{"2",
 [{"1", [{update, ch4, {advanced, []}}]}],
 [{"1", [{update, ch4, {advanced, []}}]}]
}.

Функция update должна содержать кортеж {advanced,Extra}. Эта инструкция приведет к вызову функции обратного вызова system_code_change/4, которую должен реализовать пользователь, специальным процессом:

-module(ch4).
...
-export([system_code_change/4]).
...

system_code_change(Chs, _Module, _OldVsn, _Extra) ->
    {ok, Chs}.

Первый аргумент – это внутреннее состояние State, которое передается из функции sys:handle_system_msg(Request, From, Parent, Module, Deb, State), вызываемой системным процессом, при получении системного сообщения. В ch4 внутреннее состояние – это множество доступных каналов Chs.

Второй параметр – это имя модуля (ch4).

Третий аргумент – это Vsn или {down,Vsn}, описанные в gen_server:code_change/3.

В этом случае все аргументы, кроме первого, игнорируются, и функция просто возвращает внутреннее состояние. Этого достаточно для обновления кода. Если бы мы хотели изменить внутреннее состояние (подобно примеру в Изменение внутреннего состояния, это было бы сделано этой функции, и результатом был бы {ok,Chs2}.

Смена супервизора

Поведение "supervisor" поддерживает изменение внутреннего состояния, т.е. изменение свойств стратегии перезапуска и максимальной частоты перезапуска, а также изменение существующих спецификаций дочерних процессов.

Добавление и удаление дочерних процессов также возможно, но не обрабатывается автоматически. Инструкции для таких действий должны быть описаны в .appup файле.

Изменение свойств

Поскольку супервизор должен изменять свое внутреннее состояние, требуется синхронизированная замена кода. Однако нужно использовать специальную инструкцию update.

Новая версия модуля обратного вызова должна загружаться первой и в случае обновления версии, и в случае отката до предыдущей версии. После этого можно проверять значение, возвращенное из init/1, и соответственно, изменять внутреннее состояние.

Следующая upgrade инструкция используется для супервизоров:

{update, Module, supervisor}

Пример: Предположим, мы хотим изменить стратегию перезапуска ch_sup из главы Поведение Супервизор с one_for_one на one_for_all. Мы изменяем функцию обратного вызова init/1 в ch_sup.erl:

-module(ch_sup).
...

init(_Args) ->
    {ok, {{one_for_all, 1, 60}, ...}}.
     

Файл ch_app.appup:

{"2",
 [{"1", [{update, ch_sup, supervisor}]}],
 [{"1", [{update, ch_sup, supervisor}]}]
}.

Изменение спецификаций потомков

При изменении спецификаций потомков инструкция и .appup файл точно такие же, как и при изменении свойств, описанном выше:

{"2",
 [{"1", [{update, ch_sup, supervisor}]}],
 [{"1", [{update, ch_sup, supervisor}]}]
}.

Процессы потомков не подвергаются изменениям. Например, изменение функции запуска определяет только, как должны запускаться дочерние процессы.

Заметьте, что идентификатор дочернего процесса не может быть изменен.

Необходимо заметить, что изменение поля Modules в спецификации потомка может повлиять на процесс управления релизами, поскольку это поле используется для определения процессов, подверженных изменениям при синхронизированной замене кода.

Добавление и удаление свойств потомков

Как было сказано выше, изменение спецификаций потомков не влияет на существующие процессы-потомки. Новые спецификации потомков автоматически добавляются, но не удаляются. Также, процессы-потомки автоматически не запускаются и не останавливаются. Это необходимо делать явно, при помощи функции apply.

Пример: Предположим, что мы хотим добавить новый дочерний процесс в m1, при обновлении версии ch_app с "1" до "2". Это означает, что m1 должен быть удален, при откате с версии "2" до версии "1":

{"2",
 [{"1",
   [{update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor}
   ]}]
}.

Заметьте, что порядок инструкций имеет значение.

Также заметьте, что супервизор должен быть зарегистрирован как ch_sup для того, чтобы скрипт работал нормально. Если супервизор не зарегистрирован, до него нельзя напрямую иметь доступ из скрипта. Вместо этого необходимо написать вспомогательную функцию, которая ищет pid супервизора, вызывает supervisor:restart_child и т.д., и эта функция должна быть вызвана из скрипта при помощи инструкции apply.

Если модуль m1 представлен в версии "2" приложения ch_app, он также должен быть загружен при обновлении версии, и удален при откате до предыдущей версии.

{"2",
 [{"1",
   [{add_module, m1},
    {update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor},
    {delete_module, m1}
   ]}]
}.

Необходимо опять отметить, что порядок инструкций очень важен. При обновлении m1 должен быть загружен, и спецификации потомков супервизора должны быть изменены до того, как будет запущен дочерний процесс. При откате до предыдущей версии дочерний процесс должен быть прерван до того, как будет изменена спецификация потомка и модуль будет удален.

Добавление и удаление модуля

Пример: Новый функциональный модуль m добавляется в ch_app:

{"2",
 [{"1", [{add_module, m}]}],
 [{"1", [{delete_module, m}]}]


Запуск и остановка процессов

В системе, имеющей структуру согласно принципам дизайна OTP, каждый процесс будет процессом-потомком, принадлежащим некоторому супервизору, см. раздел Смена супервизора выше.

Добавление и удаление приложения

При добавлении или удалении приложения файл .appup не нужен. При генерации relup будет проведено сравнение .rel файлов, и инструкции add_application и remove_application будут добавлены автоматически.

Перезапуск приложения

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

Пример: При добавлении нового дочернего процесса m1 к ch_sup, как в примере, описанном выше (example above), альтернативой обновления супервизора будет перезапуск приложения.

{"2",
 [{"1", [{restart_application, ch_app}]}],
 [{"1", [{restart_application, ch_app}]}]
}.

Изменение спецификации приложения

При установке релиза спецификации приложений автоматически обновляются перед выполнением скрипта relup. Таким образом, в файле .appup не нужно никаких инструкций:

{"2",
 [{"1", []}],
 [{"1", []}]
}.

Изменение конфигурации приложения

Изменение конфигурации приложения путем изменения ключа env в .app файле – это частный случай изменения спецификации приложения, см. выше (see above).

Конфигурационные параметры приложения также могут добавляться или изменяться в sys.config.

Изменение включенных приложений

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

Пример: Предположим, что у нас есть релиз, содержащий приложение prim_app, которое содержит супервизор prim_sup в своем дереве супервизоров.

В новой версии релиза приложение ch_app из нашего примера должно быть включено в prim_app. Таким образом, корневой супервизор ch_sup должен быть запущен как дочерний процесс prim_sup.

1) Отредактируйте код для prim_sup:

init(...) ->
    {ok, {...supervisor flags...,
          [...,
           {ch_sup, {ch_sup,start_link,[]},
            permanent,infinity,supervisor,[ch_sup]},
           ...]}}.
   

2) Отредактируйте .app файл для prim_app:

{application, prim_app,
 [...,
  {vsn, "2"},
  ...,
  {included_applications, [ch_app]},
  ...
 ]}.

3) Создайте новый .rel файл, содержащий ch_app:

{release,
 ...,
 [...,
  {prim_app, "2"},
  {ch_app, "1"}]}.


Перезапуск приложения

4a) Одним из способов перезапуска включенного приложения является перезапуск приложения prim_app целиком. Для этого следует использовать инструкцию restart_application в .appup файле для приложения prim_app.

Однако, если мы сделали так и потом сгенерировали файл relup, он будет содержать инструкции не только для перезапуска prim_app (т.е. удаления и добавления), а также и для запуска ch_app (и остановки этого приложения в случае отката до предыдущей версии). Такое поведение объясняется тем фактом, что ch_app включается в новый .rel файл, но не включается в старый файл.

Вместо этого корректный relup файл нужно создать вручную, либо с нуля, либо путем редактирования сгенерированной версии. Инструкции для запуска/остановки ch_app заменяются инструкциями для загрузки/выгрузки приложения:

{"B",
 [{"A",
   [],
   [{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
    {load_object_code,{prim_app,"2",[prim_app,prim_sup]}},
    point_of_no_return,
    {apply,{application,stop,[prim_app]}},
    {remove,{prim_app,brutal_purge,brutal_purge}},
    {remove,{prim_sup,brutal_purge,brutal_purge}},
    {purge,[prim_app,prim_sup]},
    {load,{prim_app,brutal_purge,brutal_purge}},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {load,{ch_sup,brutal_purge,brutal_purge}},
    {load,{ch3,brutal_purge,brutal_purge}},
    {apply,{application,load,[ch_app]}},
    {apply,{application,start,[prim_app,permanent]}}]}],
 [{"A",
   [],
   [{load_object_code,{prim_app,"1",[prim_app,prim_sup]}},
    point_of_no_return,
    {apply,{application,stop,[prim_app]}},
    {apply,{application,unload,[ch_app]}},
    {remove,{ch_sup,brutal_purge,brutal_purge}},
    {remove,{ch3,brutal_purge,brutal_purge}},
    {purge,[ch_sup,ch3]},
    {remove,{prim_app,brutal_purge,brutal_purge}},
    {remove,{prim_sup,brutal_purge,brutal_purge}},
    {purge,[prim_app,prim_sup]},
    {load,{prim_app,brutal_purge,brutal_purge}},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {apply,{application,start,[prim_app,permanent]}}]}]
}.
     

Смена супервизора

4b) Другой способ запуска включенного приложения (или остановки в случае отката до предыдущей версии) состоит в объединении инструкций для добавления и удаления дочерних процессов к/из prim_sup, с инструкциями для загрузки/выгрузки всего кода для ch_app и его спецификации.

Опять же, relup файл создается вручную. Либо с нуля, либо путем редактирования сгенерированной версии. Перед обновлением prim_sup необходимо загрузить весь код для ch_app, а также спецификацию для этого приложения (ch_app). При откате до предыдущей версии необходимо обновить prim_sup перед тем, как будут выгружен код для ch_app и его спецификация.

{"B",
 [{"A",
   [],
   [{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
    {load_object_code,{prim_app,"2",[prim_sup]}},
    point_of_no_return,
    {load,{ch_sup,brutal_purge,brutal_purge}},
    {load,{ch3,brutal_purge,brutal_purge}},
    {apply,{application,load,[ch_app]}},
    {suspend,[prim_sup]},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {code_change,up,[{prim_sup,[]}]},
    {resume,[prim_sup]},
    {apply,{supervisor,restart_child,[prim_sup,ch_sup]}}]}],
 [{"A",
   [],
   [{load_object_code,{prim_app,"1",[prim_sup]}},
    point_of_no_return,
    {apply,{supervisor,terminate_child,[prim_sup,ch_sup]}},
    {apply,{supervisor,delete_child,[prim_sup,ch_sup]}},
    {suspend,[prim_sup]},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {code_change,down,[{prim_sup,[]}]},
    {resume,[prim_sup]},
    {remove,{ch_sup,brutal_purge,brutal_purge}},
    {remove,{ch3,brutal_purge,brutal_purge}},
    {purge,[ch_sup,ch3]},
    {apply,{application,unload,[ch_app]}}]}]
}.

Замена не-Erlang кода

Замена кода для программы, написанной на языке, отличном от Erlang, например, программа порта, очень зависит от приложения, и OTP не обеспечивает специальной поддержки для этого.

Пример изменения кода для программы порта: Предположим, что Erlang-процесс, контролирующий порт – это gen_server portc, и этот порт открывается в функции обратного вызова init/1:

init(...) ->
   ...,
   PortPrg = filename:join(code:priv_dir(App), "portc"),
   Port = open_port({spawn,PortPrg}, [...]),
   ...,
   {ok, #state{port=Port, ...}}.

Если необходимо обновить программу, работающую с портом, мы можем расширить код gen_server при помощи функции code_change, которая закроет старый порт и откроет новый. (При необходимости gen_server может сначала запросить данные необходимые для сохранения у программы работающей с портом и передать эти данные в новый порт.)

code_change(_OldVsn, State, port) ->
   State#state.port ! close,
   receive
       {Port,close} ->
           true
   end,
   PortPrg = filename:join(code:priv_dir(App), "portc"),
   Port = open_port({spawn,PortPrg}, [...]),
   {ok, #state{port=Port, ...}}.

Обновите номер версии приложения в .app файле и запишите в .appup файл:

["2",
 [{"1", [{update, portc, {advanced,port}}]}],
 [{"1", [{update, portc, {advanced,port}}]}]
].

Убедитесь, что директория priv, в которой находится си-программа, включена в новый пакет релиза:

1> systools:make_tar("my_release", [{dirs,[priv]}]).
...

Перезапуск эмулятора

Если нужно перезапустить эмулятор, очень простой .relup файл можно создать вручную:

{"B",
 [{"A",
   [],
   [restart_new_emulator]}],
 [{"A",
   [],
   [restart_new_emulator]}]
}.

Таким образом, фреймворк менеджера релизов с автоматической паковкой и распаковкой пакетов релизов, автоматическим обновлением путей, и т.д. может использоваться без необходимости указывать .appup файлы.

Если необходимо сделать некоторое преобразование внешних данных, допустим содержимое базы данных, перед установкой новой версии релиза, инструкции могут быть добавлены в .relup файл.