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

Elixir (Erlang/OTP) можно развернуть без остановки Erlang VM (BEAM). Эта функция называется Горячая перезагрузка кода / Горячая замена кода / Горячее развертывание кода. В этой статье мы будем называть это перезагрузкой горячего кода.

Какая польза?

Мы используем Elixir для разработки приложения oVice и используем Hot Code Reloading для развертывания кода. Это очень полезно и похоже на волшебство.

  1. Не останавливает процесс, поэтому сервер продолжает получать запросы
  2. Не останавливает сервер WebSocket, поэтому соединение не будет разорвано.
  3. Состояние процессов будет сохранено

Эти моменты важны для нас, потому что в нашем приложении мы используем и WebRTC, и WebSocket. Конечно, соединение WebSocket (используемое для передачи данных между внешним приложением и внутренним сервером) не разрывается во время развертывания, более того, соединение WebRTC (используемое для аудио/видеоданных) не разрывается.

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

Тем не менее, мы должны быть осторожны при написании кода Elixir, чтобы воспользоваться преимуществами Hot Code Reloading. Я объясню, как вы должны быть осторожны.

Basic: Что происходит во время перезагрузки горячего кода?

Erlang выполняет процесс перезагрузки в соответствии с relup (обновление выпуска). Например:

{"1.0.1",
 [{"1.0.0",[],
   [{load_object_code,
        {my_app,"1.0.1",
            ['Elixir.MyApp.Foo']}},
    point_of_no_return,
    {suspend,['Elixir.MyApp.Foo']},
    {load,
        {'Elixir.MyApp.Foo',brutal_purge,
            brutal_purge}},
    {code_change,up,[{'Elixir.MyApp.Foo',[]}]},
    {resume,['Elixir.MyApp.Foo']}]}],

Он приостановит текущий процесс, загрузит новый модуль, вызовет метод code_change и возобновит процесс. Я объясню code_change позже.

всн

Это не обязательно, это необязательно, но я рекомендую указать это при преобразовании состояния OTP.

Мы можем предоставить @vsn версию модуля, и она считывается во время Hot Code Reloading.

Например,

defmodule MyModule do
  @vsn "2"
  def init() do
  end
  #...
end

Если мы не укажем @vsn, версия будет определена автоматически из хэша MD5 модуля. Поэтому, если код изменится, вам не нужно указывать новый @vsn, потому что изменится MD5. Но если вы пишете code_change метод преобразования состояния OTP, необходимо указать @vsn. См. следующий раздел о преобразовании состояния OTP.

Когда мы должны указать vsn?

  • Используя gen_server или gen_statem .
  • Вы хотите перезагрузить модуль, не изменяя его. Например, когда вы обновляете библиотеки зависимостей, а библиотеки используются в модуле. Если вы не обновите @vsn и не измените модуль, модуль (процесс) не будет перезагружен.
  • Вы пишете code_change метод для преобразования состояния OTP.

Преобразование состояния

Обычно Erlang не изменяет состояние процесса во время Hot Code Reloading. Это означает, что мы не можем использовать горячую перезагрузку кода при изменении структуры модуля.

Но специальные процессы Erlang (например, gen_server и gen_statem) имеют функцию преобразования состояния процесса во время Hot Code Reloading. Вы можете использовать эту функцию, определив метод code_change. Он будет вызываться при обновлении и преобразовывать состояние вашего модуля из старой версии в новую версию.

Базовый

defmodule MyApp.Foo do
  @vsn "1"
  use GenServer
  defstruct [:foo]
  def init(state) do
    {:ok, state}
  end
  def handle_call(_, _from, state) do
    ## Some codes
  end
end

Когда вы изменяете структуру MyApp.Foo, добавляя :bar,

defmodule MyApp.Foo do
  @vsn "2"
  use GenServer
  defstruct [:foo, :bar]
  def init(state) do
    {:ok, state}
  end
  def handle_call(_, _from, state) do
    ## Some codes
  end
  def code_change("1" = vsn, state, _extra) do
    {:ok, %{ state | bar: "bar" }}
  end
end

обновите @vsn, определите метод code_change, и он вернет {:ok, new_state}.

Условия для выполнения code_change

  1. Процесс должен выполняться под главным супервизором приложения или в дереве супервизора.
  2. Это специальный процесс Erlang.


Первое очень важно. В приведенном выше примере вам нужно запустить MyApp.Foo в application.ex .

defmodule MyApp.Application do
  use Application
  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Foo
    ]
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Или вам нужно запустить MyApp.Foo под деревом наблюдения.

defmodule MyApp.Application do
  use Application
  @impl true
  def start(_type, _args) do
    children = [
      MyApp.MySupervisor
    ]
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
defmodule MyApp.Supervisor do
  use Supervisor
  def start_link(init_args) do
    Supervisor.start_link(__MODULE__, init_args, name: __MODULE__)
  end
  @impl Supervisor
  def init(_) do
    children = [
      MyApp.Foo
    ]
    Supervisor.init(children, strategy: :one_for_one)
  end
end

Примеры, когда code_change не будет вызываться

Не специальный процесс

defmodule MyApp.Websocket do
  @vsn "2"
  @behaviour :cowboy_websocket
  defstruct [:username]
  # Some methods
  # Will not be called
  def code_change("1" = vsn, %{username: username} = state, _extra) do
    {:ok, %{state | username: username <> "-user"}}
  end
end

GenServer не выполняется под управлением приложения

defmodule MyApp.Application do
  use Application
  @impl true
  def start(_type, _args) do
    children = [
      MyApp.MyServer
    ]
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
defmodule MyApp.MyServer do
  @vsn "1"
  use GenServer
  defstruct [:foo]
  def init(_state) do
    {:ok, pid} = GenServer.start_link(MyApp.Foo, %MyApp.Foo{})
    {:ok, %{foo: pid}}
  end
  # Some methods
end
defmodule MyApp.Foo do
  @vsn "2"
  use GenServer
  defstruct [:foo, :bar]
  # Some methods
  # Will not be called
  def code_change("1" = vsn, state, _extra) do
    {:ok, %{ state | bar: "bar" }}
  end
end

В этом случае MyApp.MyServer выполняется под супервизором приложений. Но MyApp.Foo не выполняется супервизором приложений и не принадлежит ни к какому дереву супервизии. Так что метод code_change вызываться не будет.

Модуль переименования

Будьте осторожны при переименовании модулей. Горячая перезагрузка кода не может обнаружить события переименования, поэтому лучше перезапустить виртуальную машину Erlang без горячей перезагрузки кода.

Что происходит?

Например, я меняю имя модуля Foo на Bar.

  • Старые процессы вызывают Foo, но в новом процессе нет модуля Foo. Так что не удалось вызвать Foo, и старые процессы рухнули. Если они являются членами какого-либо супервизора, они перезапускаются.
  • Если Foo и Barявляются GenServer, это сложнее. Пожалуйста, смотрите ниже.

GenServer запущен диспетчером приложений

Если вы начнете Fooв своем application.ex ,

defmodule MyApp.Application do
  use Application
  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Foo
    ]
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

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

children = [
  MyApp.Bar
]

MyApp.Bar не будет запускаться после перезагрузки горячего кода. Это означает, что он рухнет, когда вы вызовете его в кодах приложений.

defmodule MyApp.SomeModule do
  def init() do
    MyApp.Bar.baz() #=> (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
  end
end

И MyApp.Bar после этого не запустится и будет продолжать крашиться.

Таким образом, вы не можете использовать Hot Code Reloading в этом случае. Вам необходимо перезапустить виртуальную машину Erlang, если вы хотите переименовать модуль в диспетчере приложений.

GenServer запущен некоторыми другими процессами

Если вы начнете Fooв SomeModule,

defmodule MyApp.SomeModule do
  use GenServer
  def init(state) do
    {:ok, pid} = MyApp.Foo.start_link()
    {:ok, %{foo: pid}}
  end
  def handle_info(_, state) do
    MyApp.Foo.baz()
  end
end

и переименуйте его в Bar,

defmodule MyApp.SomeModule do
  use GenServer
  def init(state) do
    {:ok, pid} = MyApp.Bar.start_link()
    {:ok, %{foo: pid}}
  end
  def handle_info(_, state) do
    MyApp.Bar.baz() #=> (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
  end
end

MyApp.Bar.baz() вылетит, потому что Bar не запускается после Hot Code Reloading. Но если вы зарегистрируете SomeModule в диспетчере приложений,

children = [
  MyApp.SomeModule
]

MyApp.SomeModule процесс будет перезапущен после сбоя и будет вызван init. В это время будет вызван MyApp.Bar.start_link(), поэтому MyApp.Bar.baz() будет успешно выполнено.

Поэтому, если вы допускаете один сбой, вы можете использовать перезагрузку горячего кода в этом случае. Конечно, если не запускать MyApp.SomeModule в супервизоре, этот кейс нормально не заработает.

Вызов метода

Местный вызов против полного квалифицированного вызова

Местный звонок:

defmodule MyModule do
  def foo() do
  end
  def bar() do
    foo() # local call
  end
end

Полный квалифицированный вызов:

defmodule MyModule do
  def foo() do
  end
  def bar() do
    MyModule.foo() # full qualified call
  end
end

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

https://www.slideshare.net/Elixir-Meetup/hot-code-replacement-alexei-sholik/19

Изменение конфигурации

Большинство приложений Elixir используют Mix.Config или Config в config/${mix_env}.exs. Если вы измените эти файлы конфигурации, новая конфигурация не будет загружена после перезагрузки горячего кода. Конечно, у rel/config.exs и rel/vm.args такая же проблема.

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

Вещи, которые отлично работают при горячей перезагрузке кода

  1. Изменить аргументы и возвращаемые значения
  2. Переименуйте имя файла модуля
  3. Обновление библиотек

Эти действия не проблема, поэтому вам не нужно о них беспокоиться.

В заключение

Я представил несколько замечаний по написанию кода приложения, если вы используете горячую перезагрузку кода. Особенно сложны Erlang OTP и метод code_change. Вот репозиторий, который я создал, чтобы проверить и поэкспериментировать с этим поведением.



Горячая перезагрузка кода предоставляет потрясающие функции, подобные магии. Так что давайте наслаждаться Erlang/Elixir и Hot Code Reloading.