Poolboyの使い方

社内勉強会で読んでる本にpoolboyが出てきたので、遊んでみて、いろいろ調べたのでこっちにも貼ります。

---

### [poolboy](https://github.com/devinus/poolboy)とは

プロセスプールを管理してくれるErlangのライブラリ。poolboyはワーカープロセスをいくつか予め起動しておく。それらを利用したいクライアントは、poolboyに「どのプロセスが暇ですか」と聞くと、「このプロセスは暇してます」とPIDを返してくれる。PIDを得たクライアントはワーカープロセスからサービスを受けられる。

#### 何が嬉しいか

##### 1. 並列度を制限できる

例えば、「画像URLを指定すると、インターネット上から画像を取得してディスク上に保存する」タスクを考える。並列度が制限されない場合、このタスクを必要な分だけ並列に実行できてしまうため、ネットワークやディスクに大きな負荷が掛かる可能性がある。あらかじめこのタスクを行うサーバプロセスの数を制限しておけばこの問題は回避できる。

##### 2. 状態を使いまわせる

TCPのコネクションなどを必要になる度に作るのは非効率的である場合がある。そういったものはワーカープロセスの内部状態として保持することで、無駄なリソース消費を抑えられる。poolboyはワーカープロセスを可能な限り立ち上げたままにしておくため、起動時の初期化処理にかかる時間も節約できる。

### 使い方

#### 起動

poolboyはワーカープロセスの再起動もやってくれるが、poolboy自体がクラッシュしてしまうとお終いなので、Supervision treeに入れるのが一般的。下の例では、`Pool.Worker`モジュールに`start_link/1`が定義されていることを前提に、`Pool.Worker`プロセスを4つ起動している。

```ruby
defmodule Pool do
  use Application

  @pool_name :my_first_pool

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    poolboy_config = [
      name: {:local, @pool_name},
      worker_module: Pool.Worker,
      size: 4,
      max_overflow: 1
    ]

    children = [
      :poolboy.child_spec(@pool_name, poolboy_config, :an_argument)
    ]

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

`:poolboy.child_spec/3`はプール名、オプション、ワーカーへの引数の3つを[child_spec](http://erlang.org/doc/man/supervisor.html#type-child_spec)形式で返してくれる便利な関数。第1引数は`:poolboy`(GenServerで実装されている)が起動するときに`:name`オプションに渡される名前。`{:local, atom}`, `{:global, term}`, `{:via, term}`の形式のいずれかで指定する。第2引数については後述する。第3引数は、ワーカーを起動する際にワーカーに渡す値。

#### PIDの取得

起動したpoolboyが管理しているワーカーにリクエストを送るためには、いずれかのワーカープロセスのPIDを取得する必要がある。PIDの取得方法は、`:poolboy.checkout/1, 2, 3`と`:poolboy.transaction/2, 3`がある。

`:poolboy.checkout/1`を呼ぶと、プールされたプロセスから1つが選ばれ、そのPIDが返ってくる。選ばれたプロセスは暇リストから一旦外される（[ソース](https://github.com/devinus/poolboy/blob/3bb3972617b3706b7fda6e264bf6bdaefbb833cd/src/poolboy.erl#L180-L201)）。クライアントは得られたPIDを用いて処理を行い、`:poolboy.checkin/2`で暇リストへ戻す。

```ruby
pid = :poolboy.checkout(:my_first_pool)

do_something_with(pid)

:poolboy.checkin(:my_first_pool, pid)
```

`:poolboy.checkout/2`の第2引数にはブロックするか否かをbooleanで指定できる。デフォルトは`true`である。第3引数には、ブロックする際のタイムアウト(デフォルト5秒)をミリ秒単位で指定する。暇リストが空のとき、第2引数が`false`ならすぐに`:full`が返され、第2引数が`true`なら`timeout`だけ待ち、それでも暇リストにワーカーが戻ってこなければ、エラーを返す。

`:poolboy.transaction/2`はプール名と無名関数を受け取り、`checkout`して得られたPIDを無名関数に渡して実行し、その後`checkin`する（[ソース](https://github.com/devinus/poolboy/blob/3bb3972617b3706b7fda6e264bf6bdaefbb833cd/src/poolboy.erl#L71-L79)）。第3引数には`timeout`を指定できる。

```ruby
:poolboy.transaction :my_first_pool, fn (pid) ->
  do_something_with(pid)
end
```

基本的には`checkout`ではなく`transaction`の方が、コードが簡潔になり`checkin`忘れも無いため推奨される。暇リストが空のときにすぐ`:full`を返してほしい場合は`checkout`を使うしかない。

#### オプション

`:poolboy.child_spec/3`の第2引数に与えられる設定群。以下のキーを持ったキーワードリスト。一部は省略可能。

- `worker_module`: ワーカープロセスとして起動するサーバが実装されたモジュール
- `size`: プールするワーカーの数
- `max_overflow`: ワーカープロセスが足りなくなったときに起動される追加ワーカープロセスの最大数
- `strategy`: プールからワーカーを選択するときの選び方。`:lifo`か`:fifo`。デフォルトは`:lifo`

[ソース](https://github.com/devinus/poolboy/blob/3bb3972617b3706b7fda6e264bf6bdaefbb833cd/src/poolboy.erl#L131-L141)

##### `size`と`max_overflow`

`size`が5, `max_overflow`が3に設定されているケースを例とすると、最初に起動されるのは`size`で指定された5プロセス。リクエストが一斉に5つ来ると、暇なワーカーがいなくなる。ここでさらにPIDを求められると、poolboyは新たなワーカープロセスを起動する。この追加プロセスの最大数を指定するのが`max_overflow`である。なのでこのケースでは、最大8つのワーカーが同時に存在することになる。なお、`checkin`時に`size`を超えてワーカーが存在する場合、`checkin`されたワーカーは廃棄される（[ソース](https://github.com/devinus/poolboy/blob/3bb3972617b3706b7fda6e264bf6bdaefbb833cd/src/poolboy.erl#L309)）。

##### `strategy`

暇なワーカーが複数存在するときに、`:poolboy.checkout`がどのワーカーのPIDを返すかを指定するオプション。デフォルトの`:lifo`なら、プールはスタックのような挙動となり、最も最近まで仕事をしていたワーカーが選ばれる。`:fifo`なら、プールはキューのように動作し、最も長い間暇なワーカーが選ばれる。なぜデフォルトが`:lifo`なのかは[このPR](https://github.com/devinus/poolboy/pull/50)に書いてある。一番ホットなワーカーを使うのが良い、という考え方。

### やってはいけないこと

http://blog.elixirsips.com/2014/07/16/errata-dont-use-cast-in-a-poolboy-transaction/

以下の様なコードを考える。

```ruby
def work
  :poolboy.transaction :my_pool, fn pid ->
    GenServer.cast(pid, :work)
  end
end

def handle_cast(:work, state) do
  time_consuming_task()
  {:noreply, state}
end
```

関数`work/0`を複数のプロセスが並行して実行すると、`:poolboy.transaction/2`が並列に呼び出される。`:poolboy.transaction`は以下の実装になっているので、`Fun(Worker)`を実行している間は暇リストから外される。

```erlang
transaction(Pool, Fun, Timeout) ->
    Worker = poolboy:checkout(Pool, true, Timeout),
    try
        Fun(Worker)
    after
        ok = poolboy:checkin(Pool, Worker)
    end.
```

`work/0`の場合、castは非同期呼び出しなので、すぐに`:ok`が返される。ワーカーは一瞬で暇リストへ戻されるので、デフォルトの`:lifo`設定では、そのワーカーが本当は`time_consuming_task()`を行っていて暇ではないのに、同ワーカーばかりが`checkout`される。

### 参考

- https://github.com/devinus/poolboy
- http://hashnuke.com/2013/10/03/managing-processes-with-poolboy-in-elixir.html
- http://blog.elixirsips.com/2014/07/16/errata-dont-use-cast-in-a-poolboy-transaction/