`mix upload.hotswap` (kentaro/mix_tasks_upload_hotswap)の裏側

この記事は[#NervesJP Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nervesjp)の13日目です。

12日目は@torifukukaiouさんの「[「kentaro/mix_tasks_upload_hotswap」を試してみる！　ご本人が参加していらっしゃるカレンダーにて](https://qiita.com/torifukukaiou/items/6adf153ee3893fd1ad4d)」でした。さっそく使っていただき、ありがとうございます！この記事では、そのモジュールが何をやっているのかを見ていきたいと思います。

----

本記事では、`mix upload.hotswap`というmixタスクを提供する拙作のモジュール[kentaro/mix_tasks_upload_hotswap](https://github.com/kentaro/mix_tasks_upload_hotswap)について、裏側で何が行われているかを見ていきたいと思います。

## `mix upload.hotswap`とは？

`mix upload.hotswap`とは、いわゆるHot Code Swapping[^1]を行うためのmixタスクです。手元でコードに変更を施した後に、現に動いているElixirのアプリケーションを再起動することなく、その変更をデプロイし適用するという機能を提供しています。

そもそもなぜこれを作ったかということを説明するほうが、やりたいことの理解が進みそうです。このAdvent Calendarのお題である[Nerves](https://www.nerves-project.org/)でIoTデバイスを使ったアプリケーションのコードを書いている時に、ちょっとした変更を適用したいというだけでも、基本的にはファームウェアをビルドしなおして実機にアップロードし、再起動が完了して動作確認ができるようになるまで1分以上かかる[^2]のが面倒になってきました。そこで、Erlang/Elixirの提供するHot Code Swappingを使えばもっと楽できるのではないか？と思って作ったのがこのタスクです。実際、`mix upload.hotswap`では、後述する制約はあるものの数秒でデプロイが完了します。

Hot Code Swappingそのものについては、@kikuyutaさんによる「[はじめてな Elixir(30) プロセスのホットスワップをする](https://qiita.com/kikuyuta/items/94033d1da061109ea7e3)」が、`mix upload.hotswap`の使い方については前述の@torifukukaiouさんによる「[「kentaro/mix_tasks_upload_hotswap」を試してみる！　ご本人が参加していらっしゃるカレンダーにて](https://qiita.com/torifukukaiou/items/6adf153ee3893fd1ad4d)」がわかりやすいので、ぜひご覧ください。

## ElixirにおけるHot Code Swapping

そもそもHot Code SwappingはErlang/Elixirそのものが提供する機能です。その機能を利用したデプロイは、Elixir 1.9から提供されている`mix release`というタスクによってデプロイに必要な成果物（これを「リリース」といいます。`tar.gz`ファイルとしてパッケージングされています）を作成し、サーバに配置して実行することで、実現できもします[^3]。

ではなぜ`mix upload.hotswap`を作ったのか。前述の通り、そもそもの目的はNervesによるIoTデバイス/アプリケーション開発効率を向上することです[^4]。IoTデバイス上で動くコードを更新するには、一般に、施したコードの変更を元にファームウェアをビルドし直して、デバイス上のファームウェアを新しいもので置き換えて再起動するという工程があります。それはそれでよいのですが、前述の通り1回のデプロイで1分以上時間がかかります。そこで、リリースを用いたりファームウェアの更新をするのではない方法でHot Code Swappingを実現したかったのでした。

## `kentaro/mix_tasks_upload_hotswap`の実装

kentaro/mix_tasks_upload_hotswapの実装は、[hotswap.ex](https://github.com/kentaro/mix_tasks_upload_hotswap/blob/main/lib/mix/tasks/mix/tasks/upload/hotswap.ex)に含まれるコードが本質的な部分です。

```elixir
def run(_) do
  app_name = get_config_for(:app_name)
  nodes = get_config_for(:nodes)
  cookie = get_config_for(:cookie)

  System.cmd("epmd", ["-daemon"])
  {:ok, _} = Node.start(:me@localhost)
  Node.set_cookie(cookie)

  for node <- nodes do
    handle_connect(Node.connect(node), node)
  end

  {:ok, modules} = :application.get_key(app_name, :modules)

  for module <- modules do
    for node <- nodes do
      handle_load_module(IEx.Helpers.nl([node], module), module, node)
    end
  end
end
```

このコードでは、最初に設定ファイルから各種情報を取得します。次に、ノードを起動して、デプロイ先のIoTデバイスと通信できるようにします。また、IoTデバイスの方でも、前もってノードを起動しておく必要があります[^5]。そして、IoTアプリケーションを実装するコードに含まれるモジュールを取得して、IoTデバイスで動いているモジュールをそれらで置き換えるということをしています[^6]。

たったこれだけのことで、Hot Code Swappingが実現できているのは驚きですね。ただ、これだけだと何が起きているのかさっぱりわからないので、もう少し深堀りしてみましょう。

### `:application.get_key(APP_NAME, :modules)`

まず、ローカルでコードの変更を施したモジュールをリストアップする必要があります。`:application.get_key/2`[^7]に、Applicationの名前を示すatomと、`:modules`を渡してあげると取得できます。IExで試すと、以下のようになります。

```
$ iex -S mix

iex(1)> :application.get_key(:example, :modules)
{:ok, [Example, Example.Application, Example.Counter]}
```

`lib/`ディレクトリ以下のモジュールが取得できているようです。

### `IEx.Helpers.nl/2`

上記で取得したモジュールを、`IEx.Helpers.nl/2`によってIoTデバイス上で立ち上がっているノードで読み込みます。ところで、このメソッドは何をしているのでしょうか？[IEx.Helpers.nl/2のドキュメント](https://hexdocs.pm/iex/IEx.Helpers.html#nl/2)には、以下のように書かれています。

> Deploys a given module's BEAM code to a list of nodes.
> 
> This function is useful for development and debugging when you have code that has been compiled or updated locally that you want to run on other nodes.

受け取ったモジュールのBEAMコード（Erlang VMで実行するための内部表現。Javaのclassファイルみたいなものでしょうか）をノードにデプロイするということをするとのことです。また、開発時やデバッグ次に便利だという旨も書かれています。まさに、今回の目的に沿う機能であるといえます。

さらに深堀りしてみましょう。[Elixir v1.11.2におけるIEx.Helpersの実装](https://github.com/elixir-lang/elixir/blob/v1.11.2/lib/iex/lib/iex/helpers.ex#L1318)は以下の通りです。

```elixir
def nl(nodes \\ Node.list(), module) when is_list(nodes) and is_atom(module) do
  case :code.get_object_code(module) do
    {^module, bin, beam_path} ->
      results =
        for node <- nodes do
          case :rpc.call(node, :code, :load_binary, [module, beam_path, bin]) do
            {:module, _} -> {node, :loaded, module}
            {:badrpc, message} -> {node, :badrpc, message}
            {:error, message} -> {node, :error, message}
            unexpected -> {node, :error, unexpected}
          end
        end

      {:ok, results}

    _otherwise ->
      {:error, :nofile}
  end
end
```

まず、Erlangの`code.get_object_code/1`[^8]により、BEAMコードのバイナリを得ています。次に、引数としてわたされたノードたちに対して、これまたErlangの`rpc.call/4`[^9]によりデプロイし、ノードのコンテキストで`code.load_binary/3`[^10]を実行しています。そうしてモジュールをロードすることにより、Hot Code Swappingを実現していることがわかります。

それにしても、これだけの実装でHot Code Swappingができることには、あらためて驚きです。

## Hot Code Swappingの2つの方式

Erlang/Elixirのモジュールには、以下の2種類があります。

> **Residence module** - The module where a process has its tail-recursive loop function(s). If these functions are implemented in several modules, all those modules are residence modules for the process.
> **Functional module** - A module that is not a residence module for any process.
> 
> [Erlang -- Release Handling > Release Handling Instructions](https://erlang.org/doc/design_principles/release_handling.html#release-handling-instructions)より

Erlang/Elixirでは、関数の最後で自分自身を呼び出すことで再帰を実現できます[^11]。そして、再帰する関数を`Kernel.spawn/1`や`Kernel.spawn_link/1`でプロセスとして起動することで、並行実行されるプロセスとして処理をバックグラウンドで実行できます[^12]。上記のResidence moduleとは、そうしたモジュールのことをいいます。一方で、Residence moduleでないモジュールのことをFunctional moduleといいます。

また、上記で解説したのは、Hot Code Swappingの2つの方式の内のsimple code replacementと呼ばれるものです。もうひとつは、synchronized code replacementと呼ばれるもので、前述の「[はじめてな Elixir(30) プロセスのホットスワップをする](https://qiita.com/kikuyuta/items/94033d1da061109ea7e3)」で解説されている方式です。

`mix upload.hotswap`が対応しているのは、Functional moduleに対するsimple code replacementです[^13]。

> If a simple extension has been made to a functional module, it is sufficient to load the new version of the module into the system, and remove the old version. This is called **simple code replacement** and for this the following instruction is used:
> 
> [Erlang -- Release Handling > Release Handling Instructions](https://erlang.org/doc/design_principles/release_handling.html#release-handling-instructions)より

一般のHot Code Swappingにおいては、たとえばGenServerで実装したプロセスの状態（state）の構造に変更を加えたときなどに、`GenServer.code_change/3`[^14]を用いてsynchronized code replacementを実現できます。

一方で、`mix upload.hotswap`では、単にこのタスクを実行するだけでHot Code Swapingが実現できる利便性とsynchronized code replacementを実現できるうまい方法を思いつけていないために、現状ではsimple code replacementへの対応となっています[^15]。

## `mix upload.hotswap`の制約と使いどころ

`mix upload.hotswap`には、現状では以下の実装の詰めが甘いところや制約があります。

* `lib/`以下に含まれるモジュール全部を（更新があろうがなかろうが）デプロイするやり方になっています。本当は更新したコードだけデプロイできるといいのでしょうけれども……。
* 前述の通り、simple code replacementへの対応のみとなっています。そのため、GenServerで実装されたプロセスが持つ状態の構造を変更した場合は使えません。
* また、モジュール間で複雑に依存関係がある場合は、デプロイしたあとに不整合が起こる可能性があります[^16]。

そのため、`mix upload.hotswap`は使いどころを定めて利用するのがよいと思います。

* 依存モジュールを追加したり、GenServerで実装されたプロセスが持つ状態の構造を変更した場合などは、従来どおり`mix firmware && mix upload`によりファームウェアそのものを更新してください
* その他、モジュールの細部を更新したものをIoTデバイスに素早く反映させたい時に`mix upload.hotswap`を使うとよいでしょう。

というわけで、ふだんの開発時やデバッグ時のように、早いサイクルでPDCAを回したい時に用いていただけるとよいと思います。

## おわりに

本記事では、NervesでIoTデバイス/アプリケーションを開発するに際して感じたペインを解決する仕組みとして開発した[kentaro/mix_tasks_upload_hotswap](https://github.com/kentaro/mix_tasks_upload_hotswap)について、それが実際にはどういうことをやっているのかという裏側を解説してみました。

ぜひお使いいただきフィードバックをいただけると幸いです。

----

[#NervesJP Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nervesjp)の14日目は、これまた拙文の「[『プログラミングElixir 第2版』を読んでいまこそElixirに入門しよう](https://kentarokuribayashi.com/journal/2020/12/14/programming-elixir-1-6-ja)」です。引き続きお楽しみください。

[^1]: [GenServer.code_change/3のドキュメント](https://hexdocs.pm/elixir/GenServer.html#c:code_change/3)ではHot Code Swappingといわれていますが、Erlangのドキュメントだと[Code Loading](https://erlang.org/doc/reference_manual/code_loading.html)だとか、[Code Replacement](https://erlang.org/doc/design_principles/release_handling.html#release-handling-instructions)だとかいわれています。
[^2]: 実際には、必ずしもファームウェアをまるごと更新する必要はなかったりします。Nervesの1.7.0より正式にサポートされた[ファームウェアパッチによる更新](https://hexdocs.pm/nerves/experimental-features.html#firmware-patches)が可能です。
[^3]: 正確には、Elixirがオフィシャルにサポートしているわけではありません（[mix release — Mix v1.11.2](https://hexdocs.pm/mix/Mix.Tasks.Release.html#module-hot-code-upgrades)を参照のこと）。そのため、Hot Code Swappingの実現には、リリースを作るためのライブラリである[Distillery](https://github.com/bitwalker/distillery)の2.0以上のバージョンを使う必要があります。『プログラミングElixir 第2版』の382ページにそのあたりのことが書かれています。また、実際の利用のしかたは[How to perform Hot Code Swapping using Distillery— #2 — A (Live Demo) GenServer State update](https://medium.com/blackode/how-to-perform-hot-code-swapping-using-distillery-2-a-live-demo-genserver-state-update-53d8344d9147)を参照してください。
[^4]: とはいえ、この仕組み自体はNervesでしか使えないというものではなく、汎用的に使えるものではあります。
[^5]: そのあたりは[ドキュメント](https://github.com/kentaro/mix_tasks_upload_hotswap#2-start-nodes-on-your-devices-through-which-your-code-changes-are-deployed)に書いてありますので、ご参照ください。また、この記事ではノード間通信については扱わないので、詳しく知りたい方は「[はじめてな Elixir(27) ノード間の通信がどうなってるか調べる](https://qiita.com/kikuyuta/items/81423a8e7c054403a2b3)」を是非ご覧ください。
[^6]: ちなみに`[node]`としている部分は`nodes`をそのままわたすのでもいいのですが、ノード別・モジュール別にロードしたことを表示したかったので、冗長ですがそのように実装しています。
[^7]: http://erlang.org/doc/apps/kernel/application.html#get_key-2
[^8]: https://erlang.org/doc/man/code.html#get_object_code-1
[^9]: https://erlang.org/doc/man/rpc.html#call-4
[^10]: https://erlang.org/doc/man/code.html#load_binary-3
[^11]: Schemeなどのような関数型言語同様に末尾最適化が施されており、再帰呼び出しが関数の先頭へのジャンプ命令に変換されるため、スタックオーバーフローが起きないようになっています。
[^12]: 具体的には、「[ウェブチカでElixir/Nervesに入門する（2020年12月版）](https://qiita.com/kentaro/items/e8df79aa93b9fe9a567e)」で紹介した[ウェブチカのループを実装したコード](https://github.com/kentaro/nerves_webchika/blob/master/webchika_firmware/lib/webchika_firmware/worker.ex#L17)にシンプルな実装例がありますので、参照してください。
[^13]: Residence moduleでもできそうな気もするけど、できないのだろうか。試してみていないのでわからないです。
[^14]: https://hexdocs.pm/elixir/GenServer.html#c:code_change/3
[^15]: つまり、GenServerで実装されたプロセス持つ状態の構造を変更した場合には、従来どおり`mix firmware && mix upload`が必要であるということです。うまい実装をご存じの方は、ぜひ教えて下さい。
[^16]: このあたりの解決が困難なので、[mix releaseのドキュメント](https://hexdocs.pm/mix/Mix.Tasks.Release.html#module-hot-code-upgrades)にある通り、Hot Code SwappingはElixirの公式サポートからは外されてしまったのでした。