エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Elixir で escript からサーバにリクエストを送る覚書

事の起こり

NIF (Native Implemented Functions) を含むパッケージを escript で利用しようとしたのですが。

ビルドした実行ファイルを実行しても NIF のライブラリファイルを読み込めないとエラーになり。

検索してみたらこのとおり。


Unfortunately that's a limitation of escripts. They cannot access anything in priv and therefore they cannot access embedded .so files. In other words, it is not currently possible to build escripts with NIFs in them.

https://github.com/elixir-lang/elixir/issues/5444#issuecomment-260178836


You can not use NIFs in an escript.

https://elixirforum.com/t/escript-can-not-load-argon2-nif-wrong-approach-for-cli/25399/2


と、いうことなので。 NIF を含むサーバを起動しておいて escript の実行ファイルからプロセス間通信でリクエストを送ればよいじゃないか、と考えたしだい。

サーバを用意する

まずリクエストを受ける適当なサーバを用意します。

$ mix new my_server --sup

モジュールを用意する

適当なサーバモジュールとアプリケーションモジュールを用意します。

defmodule MyServer do
  use GenServer

  @name __MODULE__

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: @name)
  end

  def double(value) do
    GenServer.call(@name, {:double, value})
  end

  def init(_opts) do
    {:ok, :no_state}
  end

  def handle_call({:double, value}, _from, state) do
    {:reply, value * 2, state}
  end
end
defmodule MyServer.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      MyServer
    ]

    opts = [strategy: :one_for_one, name: MyServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

release 設定を用意する

ノード間で通信を行うには、ノードに名前を設定する必要があります。

まず mix release.init コマンドを実行して設定ファイルを生成します。

$ mix release.init
* creating rel/vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex

rel/env.sh.eex (MS Windows のばあいは rel/env.bat.eex )を編集し、ファイルの末尾の 2 行のコメントアウトを外します。

この行の直前のコメントに書かれているように、これでノード名が設定されます。 デフォルトではアプリケーション名 + 127.0.0.1 という名前になるので、今回は my_server@127.0.0.1 という名前が設定されます。

export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=<%= @release.name %>@127.0.0.1

cookie を設定する

安全のために cookie を設定します。

明示的に cookie を設定しない場合は自動的にランダムな値が設定されます。 その値を利用しても問題ないのですが、ここでは説明を兼ねて設定します。

mix.exs ファイルを開き、project/0 が返すキーワードリストに :release を追加します。

release には名前をつけて異なる設定で生成することができますが、ここではデフォルトのアプリケーション名に対して cookie の設定をします。

defmodule MyServer.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_server,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      releases: [
        my_server: [
          cookie: "MY_SERVER_COOKIE"
        ]
      ]
    ]
  end

  # 以下略

end

release を作成する

mix release を実行します。

$ mix release

実行に必要なファイルが _build/dev/rel/my_server に生成されます。 今回は MIX_ENV を指定していないのででデフォルトの dev で生成されています。

サーバを起動するにはこのディレクトリに含まれる _build/dev/rel/my_server/bin/my_server を利用します。

動作を確認する

start または daemon コマンドでサーバを起動します。

$ _build/dev/rel/my_server/bin/my_server start

または

$ _build/dev/rel/my_server/bin/my_server daemon

サーバが起動したら remote コマンドでサーバのノードに接続します。 start で起動したばあいは別のコンソールを開いてコマンドを実行してください。

$ _build/dev/rel/my_server/bin/my_server remote
iex(my_server@127.0.0.1)1> MyServer.double(123)
246

動作が確認できたら stop コマンドで停止します。 start で起動したばあいは Ctrl+C でも停止できます。

$ _build/dev/rel/my_server/bin/my_server stop

escript を用意する

次にリクエストを送る適当な escript を用意します

$ mix new my_cli

モジュールを用意する

エントリポイントになる関数 main/1 を定義したモジュールを用意します。

defmodule MyCli do
  def main(["double", arg]) do
    value = String.to_integer(arg)

    result = :rpc.call(:"my_server@127.0.0.1", MyServer, :double, [value])

    IO.puts("#{value} x2 = #{result}")
  end
end

リモート呼び出しにここでは Erlang のモジュール rpc の関数 call/4 を利用します。

まずサーバを起動します。

サーバのディレクトリでサーバを起動します。

$ _build/dev/rel/my_server/bin/my_server daemon

escript のディレクトリで iex を起動します。 このとき cookie はサーバで設定した内容に合わせます。

$ iex --name my_cli@127.0.0.1 --cookie MY_SERVER_COOKIE -S mix

関数を呼び出します。

iex(my_cli@127.0.0.1)1> MyCli.main(["double", "123"])
123 x2 = 246
:ok

無事サーバの関数を呼び出せることが確認できました。

動的にノード名と cookie を設定する

上の例では iex のオプションとしてノード名と cookie を指定していますが、escript として単独の実行ファイルとして作成したいので別の方法で設定する必要があります。

ノード名の設定は Node.start/3 を、cookie の設定は Node.set_cookie/2 を使うことで実現できます。

先の main/1 を編集して動的に設定するようにしてみます。

defmodule MyCli do
  def main(["double", arg]) do
    value = String.to_integer(arg)
    {:ok, _pid} = Node.start(:"my_cli@127.0.0.1")
    Node.set_cookie(:MY_SERVER_COOKIE)

    result = :rpc.call(:"my_server@127.0.0.1", MyServer, :double, [value])

    IO.puts("#{value} x2 = #{result}")
  end
end

start/3 に渡すノード名と set_cookie/2 に渡す cookie の値はどちらも atom である必要があります。

動作を確認します。 今回はノード名と cookie を指定せずに iex を起動します。

$ iex -S mix

main/1 を呼び出します。

iex(1)> MyCli.main(["double", "123"])
123 x2 = 246
:ok
iex(my_cli@127.0.0.1)2> 

今回も無事サーバの関数を呼び出せることが確認できました。 また main/1 が終了してもノード名が設定されたままになっていることがプロンプトからわかります。

これを終了するには関数 Node.stop/0 を呼び出します。

escript を生成する

mix.exs を編集して escript の設定を追加します。

defmodule MyCli.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_cli,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      escript: [main_module: MyCli]
    ]
  end

  # 以下略

end

escript をビルドします。

$ mix escript.build

実行します。 サーバを起動しておくことを忘れないでください。

$ ./my_cli double 123
123 x2 = 246

これでコマンドラインからサーバの機能を利用することができるようになりました。

いつか読むはずっと読まない:sense of wonder

原著が 1981 年、邦訳は 1982 年。 数年前にあった復刊フェアで手に入れました。

数学パズルものや論理パズルものの一つのつもりで手にしましたが、むしろ SF 仕立ての部分が刺さります。 古典的な SF が好きな人はどうぞ。

SFパズル

SFパズル