MIX AND OTP vol 2

Mix and OTPの後半。

ETS

ここは、GenServer、Supervisor付近の説明から。

processによる状態の保持と、etsによる保持はいくつか違うところがあります。

processに状態を保持する場合、そのprocessが死んだらそれに保存されるデータも無くなります。一方、etsは複数のprocessに共有されるキャッシュのように扱うことができます。

共有キャッシュのように扱うことができるので、etsに対しては書き込み制限をしたりします。また、callbackに関しても同期的に行う必要がるなどの制約がでてきます。

ここら辺の説明がされますが、etsを生成したprocessが死んだ場合、etsも合わせて自動的に閉じられます。これは、etsはそれを起動したprocessにリンクされているから。

ということは、supervisorで起動させて、publicにするとprocessに依存して、それが死ぬとetsが閉じられるという事態も回避できそうです。

このページの最後の箇所の、 :ets.foldl() のfnへの引数で、 {name, pid} が与えられているのですがこれがどこから来たのかよくわからなかった…

  def init({table, events, buckets}) do
    refs = :ets.foldl(fn {name, pid}, acc ->
      HashDict.put(acc, Process.monitor(pid), name)
    end, HashDict.new, table)
    {:ok, %{names: table, refs: refs, events: events, buckets: buckets}}
  end

のですが、

  def handle_call({:create, name}, _from, state) do
...
        :ets.insert(state.names, {name, pid})
...
  end

の所を見て納得。 :ets(table) に保存していた値がfnの引数として与えられ、それに対して関数が適用されて出力が得られるというものなのですね。説明にもさらりと描かれているのですが、initの中に突然nameとpidが出てきたので???になりました。

このfoldlのErlangによるspecは以下。

foldl(Function, Acc0, Tab) -> Acc1

Types:

Function = fun((Element :: term(), AccIn) -> AccOut)
Tab = tab()
Acc0 = Acc1 = AccIn = AccOut = term()
Acc0 is returned if the table is empty. This function is similar to lists:foldl/3. The order in which the elements of the table are traversed is unspecified, except for tables of type ordered_set, for which they are traversed first to last.

If Function inserts objects into the table, or another process inserts objects into the table, those objects may (depending on key ordering) be included in the traversal.

pipelineでメソッドチェインぽく描くとき、 () はsつけるべきなのですね。そのほうが意図が伝わるコードになりますし。引数の数が違う同名の関数もあるので、これはすごく納得。

When using the |> operator, it is important to add parentheses to the function calls due to how operator precedence works. In particular, this code:

1..10 |> Enum.filter &(&1 <= 5) |> Enum.map &(&1 * 2)
Actually translates to:

1..10 |> Enum.filter(&(&1 <= 5) |> Enum.map(&(&1 * 2)))
Which is not what we want, since the function given to Enum.filter/2 is the one passed as first argument to Enum.map/2. The solution is to use explicit parentheses:

1..10 |> Enum.filter(&(&1 <= 5)) |> Enum.map(&(&1 * 2))

なるほどなるほど。

Task and gen-tcp

Taskは、非同期に処理を行うprocess。

processなので、そのprocessを監視するためにsupervisorへの登録が大事になってくるのですが、そこの詳しくはTask.Supervisorをみると良さそう。

こうざっと読んでみると、Supervisorが非常に重要な位置をしめて、そのsupervisorが監視する子プロセスへの再起動strategyも大事。これらがErlangというかElixirの安定性を支えているのですね。

結構、Unit testでロジックをしっかりかかないと、ここはdebugとかスキル必要そうだ。。。

Docs, tests and pipelines

ExUnit.DocTest、半端ない。ドキュメントがそのままiexのコマンドベースでのテストコードに置き換わるのですね。

http://elixir-lang.org/docs/stable/ex_unit/#!ExUnit.DocTest.html

例えば、以下の@docで囲んだドキュメントになる範囲の iex&gt; とその次の行がコマンドと期待結果になって、テストが実施される。APIとして目に見えるインタフェースでは、このテストかなり役立つのでは。autodocみたいなものですね。

defmodule KVServer.Command do
  @doc ~S"""
  Parses the given `line` into a command.

  ## Examples

      iex> KVServer.Command.parse "CREATE shopping\r\n"
      {:ok, {:create, "shopping"}}

      iex> KVServer.Command.parse "CREATE  shopping  \r\n"
      {:ok, {:create, "shopping"}}

      iex> KVServer.Command.parse "PUT shopping milk 1\r\n"
      {:ok, {:put, "shopping", "milk", "1"}}

      iex> KVServer.Command.parse "GET shopping milk\r\n"
      {:ok, {:get, "shopping", "milk"}}

      iex> KVServer.Command.parse "DELETE shopping eggs\r\n"
      {:ok, {:delete, "shopping", "eggs"}}

  Unknown commands or commands with the wrong number of
  arguments return an error:

      iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
      {:error, :unknown_command}

      iex> KVServer.Command.parse "GET shopping\r\n"
      {:error, :unknown_command}

  """
  def parse(line) do
    case String.split(line) do
      ["CREATE", bucket] -> {:ok, {:create, bucket}}
      ["GET", bucket, key] -> {:ok, {:get, bucket, key}}
      ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
      ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
      _ -> {:error, :unknown_command}
    end
  end
end

ここは、最後はどのようなテストコードを作っていくか、というのも含め、戦略はチームで形作っていく必要あるよね、ってもので終わってました。

Distributed tasks and configuration

最後は、異なるノード間での通信が話題です。ネットワーク越しに異なるノード間で通信をやりとりし、一方から送った処理をもう一方のノードで実行するということが可能です。

Node.spawn_link/2 を使うのですが、普通に使うとsupervisorの管理下に置くことができないので、以下の3通りが紹介されています。

  1. We could use Erlang’s :rpc module to execute functions on a remote node. Inside the bar@computer-name shell above, you can call :rpc.call(:”foo@computer-name”, Hello, :world, []) and it will print “hello world”
  2. We could have a server running on the other node and send requests to that node via the GenServer API. For example, you can call a remote named server using GenServer.call({name, node}, arg) or simply passing the remote process PID as first argument
  3. We could use tasks, which we have learned about in a previous chapter, as they can be spawned on both local and remote nodes

この中で、rpcとGenServerはリクエストに対する同期的な通信に向いていて、taskは非同期的な通信に効果的なようです。

その説明から、async/awaitの説明が行われます。

Taskは、このasync/awaitにおいてもsupervisorの恩恵を得ることができます。

Task.Supervisor.start_child/2 の代わりに、 Task.Supervisor.async/2 と、 Task.await/2 をつかうことでそれが実現されます。そして、Distributed Tasksはsupervised tasksと同じように扱うことができます。異なる点は、ノードの名前が変わる、ということだけです。

この複数間のノードの通信をテストを実施する場合、1つ以上、あらかじめノードを起動しておく必要があります。そのため、通常はこの類のテストを行わないためにタグでテスト対象を管理します。

@tag :distributed

そして、test/test_helper.exsに以下を記述することで、テスト対象からタグがついたものを外すことが可能。

exclude =
  if Node.alive?, do: [], else: [distributed: true]

ExUnit.start(exclude: exclude)

と、ざっと舐めてきましたが、ようやっと MIX AND OTP を終了。
基本的な書き方は馴染んできた感じがありますが、まだsupervisor付近の使い方が馴染んでいない。使い方というよりは、どういう構成にするべきか、という設計の話かな。

あとは Quote and unquote をざっと学んでみよう。それが終われば適当なライブラリ作ったり、かな。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中