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

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

Elixir の Plug の使い方の覚書

Elixir の Plug を使って HTTP サーバのモックを作る覚書です。

ドキュメントに詳しく書かれていますので、詳しい話はこちらを参照してください。

プロジェクトを用意する

新しいプロジェクトを作ります。

起動時に自動で Plug のアプリケーションを起動したいので --sup オプションを指定してアプリケーションの雛形を生成しておきます。

$ mix new mock_server --sup
$ cd mock_server

mix.exs を編集して依存する cowboyplug の記述を追加します。詳細は、公開されたパッケージを管理している Hex で確認できます。

この他にもコマンドラインから mix hex.info コマンドを使って調べることもできます。

$ mix hex.info cowboy
Small, fast, modular HTTP server.

Config: {:cowboy, "~> 2.4"}
Releases: 2.4.0, 2.3.0, 2.2.2, 2.2.1, 2.2.0, 2.1.0, 2.0.0, 1.1.2, ...

Licenses: ISC
Links:
  GitHub: https://github.com/ninenines/cowboy
$ mix hex.info plug
A specification and conveniences for composable modules between web applications

Config: {:plug, "~> 1.6"}
Releases: 1.6.2, 1.6.1, 1.6.0, 1.5.1, 1.5.0, 1.5.0-rc.2, 1.5.0-rc.1, 1.5.0-rc.0, ...

Licenses: Apache 2
Links:
  GitHub: https://github.com/elixir-plug/plug

ここでは表示された Config: の内容そのままに mix.exs に記述します。

  defp deps do
    [
      {:cowboy, "~> 2.4"},
      {:plug, "~> 1.6"}
    ]
  end

依存するパッケージの取得とコンパイル

$ mix do deps.get, deps.compile

ハンドラを書く

Plug の説明に倣ってハンドラを書きます。 何がリクエストされてもステータスコード 200 で Hello world を返すコードです。

# lib/mock_server.ex
defmodule MockServer do
  import Plug.Conn

  def init(options) do
    options
  end

  def call(conn, _opts) do
    conn
    |> send_resp(200, "Hello world\n")
  end
end

lib/mock_server/ に作成されているアプリケーションの雛形を編集して、 起動時に Plug のアプリケーションを起動するようにします。

# lib/mock_server/application.ex
defmodule MockServer.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      Plug.Adapters.Cowboy2.child_spec(scheme: :http, plug: MockServer, options: [port: 4000])
    ]

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

サーバを起動する

mix run コマンドでアプリケーションを起動します。コマンドはアプリケーションを起動するだけなので、そのままではアプリケーションが起動したらコマンド自体は終了してしまい一緒にアプリケーションも終了してしまいます。

アプリケーションが起動したあとコマンドが終了しないように --no-halt オプションを指定して実行します。

$ mix run --no-halt
Compiling 2 files (.ex)

最初の実行のときに表示されるメッセージから編集した二つのファイルがコンパイルされたことがわかります。

別のコンソールからリクエストを送ってみます。

$ curl http://localhost:4000
Hello world

応答が返りました。

Ctrl+C で終了します。

リクエストの状況を知る

どのようなリクエストを受けているか知りたいので IO.inspect/1 を挟んでみます。

MockServer.call/2 を編集して、パイプラインの途中に IO.inspect/1 を追加します。

  def call(conn, _opts) do
    conn
    |> IO.inspect()
    |> send_resp(200, "Hello world (#{next_count()})\n")
  end

実行。

$ mix run --no-halt
Compiling 1 file (.ex)

同じように別のコンソールからリクエストを送ると Plug.Conn 構造体の内容が表示されます。

%Plug.Conn{
  adapter: {Plug.Adapters.Cowboy2.Conn, :...},
  assigns: %{},
  before_send: [],
  body_params: %Plug.Conn.Unfetched{aspect: :body_params},
  cookies: %Plug.Conn.Unfetched{aspect: :cookies},
  halted: false,
  host: "localhost",
  method: "GET",
  owner: #PID<0.296.0>,
  params: %Plug.Conn.Unfetched{aspect: :params},
  path_info: [],
  path_params: %{},
  peer: {{127, 0, 0, 1}, 61667},
  port: 4000,
  private: %{},
  query_params: %Plug.Conn.Unfetched{aspect: :query_params},
  query_string: "",
  remote_ip: {127, 0, 0, 1},
  req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
  req_headers: [
    {"accept", "*/*"},
    {"host", "localhost:4000"},
    {"user-agent", "curl/7.54.0"}
  ],
  request_path: "/",
  resp_body: nil,
  resp_cookies: %{},
  resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
  scheme: :http,
  script_name: [],
  secret_key_base: nil,
  state: :unset,
  status: nil
}

バックエンドで実行する

--detached オプションを指定して elixir コマンドを利用して起動すると、コンソールから切り離して実行することができます。

オプションの内容はコマンドのヘルプで確認できます。

$ elixir --help
Usage: elixir [options] [.exs file] [data]

  -e COMMAND                  Evaluates the given command (*)
  ...
  --detached                  Starts the Erlang VM detached from console
  ...

実行。コマンドの実行後すぐにプロンプトが表示されますが、リクエストを送るとアプリケーションが起動していることがわかります。

$ elixir --detached -S mix run --no-halt
$ curl http://localhost:4000
Hello world

コンソールから切り離されているので、先ほどは表示されていた構造体の内容が今回は表示されません。

Logger を利用してログをファイルに出力する記事を書いていますのでこちらも参照してみてください。

また logger_file_backend などのパッケージが HEX に登録されていますのでこれらを利用できます。

終了は kill コマンドなどを駆使してください。

リモートシェルで状態を知る

remote shell を使うと実行しているノードに接続して状態を確認したり操作したりすることができます。

下準備として、状態を持つようにします。

エージェントとして振る舞う Counter モジュールを追加します。

# lib/mock_server/counter.ex
defmodule MockServer.Counter do
  use Agent

  @name __MODULE__

  def start_link(_) do
    Agent.start_link(fn -> 1 end, name: @name)
  end

  def next_count do
    Agent.get_and_update(@name, &{&1, &1 + 1})
  end
end

MockServer.Counter.start_link/1 で起動すると MockServer.Counter.next_count/0 を呼ぶごとに 1 ずつ大きくなる値を返します。

MockServer.Application.start/2 を編集して Counter モジュールも自動的に起動するようにします。

    children = [
      Plug.Adapters.Cowboy2.child_spec(scheme: :http, plug: DummyServer, options: [port: 4000]),
      MockServer.Counter
    ]

MockServer.call/2 を編集し MockServer.Counter.next_count/0 を利用してリクエストを受けた回数をレスポンスに含めるようにします。

  import MockServer.Counter

  def call(conn, _opts) do
    conn
    |> IO.inspect()
    |> send_resp(200, "Hello world (#{next_count()})\n")
  end

ノードに名前をつけて起動します。

$ elixir --sname foo -S mix run --no-halt

まず、状態を持つようになったことを確認します。

別のコンソールからリクエストを送ると、リクエストを送るたびに数が増えていくのがわかります。

$ curl http://localhost:4000
Hello world (1)
$ curl http://localhost:4000
Hello world (2)
$ curl http://localhost:4000
Hello world (3)

次に別のコンソールから、こちらも名前をつけて iex を起動します。

$ iex --sname bar
iex(bar@emattsan)1>

プロンプトに「コマンドラインで指定した名前 + @ + ホスト名」がノード名として表示されているのがわかります。 ここから foo に接続します。

まず Ctrl+G を押します。user switch command のプロンプトが表示されます。

iex(bar@emattsan)1>
User switch command
 -->

? を入力すると利用できるコマンドが表示されます。

User switch command
 --> ?
  c [nn]            - connect to job
  i [nn]            - interrupt job
  k [nn]            - kill job
  j                 - list all jobs
  s [shell]         - start local shell
  r [node [shell]]  - start remote shell
  q                 - quit erlang
  ? | h             - this message
 -->

リモートシェルを起動するには r を利用します。

接続先のノード foo をホスト名付きで指定します。シェルには Elixir.IEx を指定します。

ノード名とシェル名はアトムで指定する必要がありますが、この user switch command は Erlang の文脈で動いているので @ を含む文字列や大文字から始まる文字列がアトムとして扱われるようにシングルクォートで囲みます。 またデフォルトでは Erlang のシェルが起動するので IEx を明示的に指定します。

 --> r 'foo@emattsan' 'Elixir.IEx'
 -->

j で状態を確認します。1 が最初に起動したシェル、 2 が今回指定したシェルです。 * が表示されているので 2 がデフォルトになっているのがわかります。

 --> j
   1  {erlang,apply,[#Fun<Elixir.IEx.CLI.1.111201631>,[]]}
   2* {'foo@emattsan','Elixir.IEx',start,[]}
 -->

c でノードに接続します。番号を指定していないのでデフォルトが選択されます。

 --> c
Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@emattsan)1>

プロンプトの表示が foo に代わっていることがわかります。

…と、長々と iex を起動してからリモートシェルで接続する手順を書きましたが、--remsh オプションを使うことで起動時に接続先を指定することができます。

$ iex --sname bar --remsh foo@emattsan
Erlang/OTP 21 [erts-10.0.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@emattsan)1>

先ほどリクエストを送って Hello world (3) が表示された直後であれば、次のようにして状態が 4 になっていることが確認できます。

iex(foo@emattsan)1> Agent.get(MockServer.Counter, & &1)
4

もう一度リクエストを送ってみます。

$ curl http://localhost:4000
Hello world (4)

状態が更新されていることがわかります。

iex(foo@emattsans-MBP)2> Agent.get(DummyServer.Counter, & &1)
5

リモートシェルで状態を変更してみます。

iex(foo@emattsans-MBP)3> Agent.update(DummyServer.Counter, & &1 + 10)
:ok
iex(foo@emattsans-MBP)4> Agent.get(DummyServer.Counter, & &1)
15

リクエストを送ると値が変更されていることがわかります。

$ curl http://localhost:4000
Hello world (15)

いつか読むはずっと読まない:ひみつシリーズ「かはくのひみつ」

国立科学博物館のひみつ

国立科学博物館のひみつ

国立科学博物館のひみつ 地球館探検編

国立科学博物館のひみつ 地球館探検編