ElixirのGenStageに入門する#2 バックプレッシャーを理解する

（この記事は、「Elixir or Phoenix Advent Calendar 2017」の10日目です）

前日は@tuchiroさんの[ElixirでSI開発入門 #2 Ectoで楽観的ロック](https://qiita.com/tuchiro/items/8949cc43ba90f4e68bf3) でした。

本掲載は[ElixirのGenStageに入門する#1](https://qiita.com/twinbee/items/12a61863ceef794996e0)の続きです。

前回は`:producer`, `:producer_consumer`, `:consumer` の三段階のステージで整数カウンターのストリーム処理を行いました。

## バックプレッシャー ([^1]背圧)

さて、前回`:producer`のところで、handle_demandというコールバックが登場したのを覚えてらっしゃいますでしょうか？ **生産者なのに需要がトリガーとなっています。**

つまり、GenStageは需要トリガーを元にストリームイベントを処理するライブラリなのです。需要トリガーで供給を引っ張ることを、ストリーム界隈(?)ではバックプレッシャー([^1]背圧)といいます。

「ストリームって、Twitterのタイムラインみたいに勝手に流れてくるもんじゃないの？消費者の都合で処理してたら取りこぼすよ？」と思われた方は鋭どいです。こういった場合は、`:producer`でのキューイングを自前で実装する必要があります。

GenStage公式ではキューイング実装のサンプルも載っているので、今回はこれを読み解きながらバックプレッシャー主体のストリームについて理解を深めていきます。

[^1]: 日本語の背圧は本来の用語はストリームのバックプレッシャーとは意味が異なるのですが、近年この意味も加わりつつあるので併記としています。

---

## ストリームを支える仕組み

ここまでは快調だったGenStage入門ですが、実際足を踏み入れるとサンプルが極端に少ない手探りの世界です。ストリームを支える仕組みを知るべく、まずは一番煩雑に見えるinitコールバックを図表現にしてみました。
<img src="https://qiita-image-store.s3.amazonaws.com/0/66748/c18acbd2-25bd-107a-d849-39728c629353.png" width="70%"> 


[公式のinit/1](https://hexdocs.pm/gen_stage/GenStage.html#c:init/1)を、多少ビジュアルにしただけですが、少しは先に進むモチベーションが上がるのではないでしょうか？

---

## ディスパッチャー
``:producer``と``:producer_consumer``のみで指定する、ディスパッチャーの種類を解説します。生産系ステージと消費系ステージ間でイベントのディスパッチ（配布）を行うのに、３種類の方法が用意されています。

### DemandDispatcher

デフォルトの`DemandDispatcher`です。demandはポンプの記号で表現しているつもりです。バックプレッシャー(背圧)によって、イベントが引っ張られる様子を図解しています。


![GenStage#1 Demand.png](https://qiita-image-store.s3.amazonaws.com/0/66748/c0bf9a09-f43d-7b5a-4e28-c524acd0d9ab.png)

---

### PartitionDispatcher

イベントデータの分類によって処理を変えたい場合は、PartitionDispacherの出番です。

PartitionDispatcherを選択した場合は、Hash関数を自作してイベント内容によって使用する消費者割り当てをカスタマイズできます。Hash関数を省略すると、デフォルトの:erlang.phash2が使われます。

（実は**Elixir Flowで登場するpartition関数**はこのDispatcherのことだったりします。）

![GenStage#PartitionDISP.png](https://qiita-image-store.s3.amazonaws.com/0/66748/a8275693-8efb-7148-491c-20cc73f8587d.png)

これは一つの例ですが、ディスパッチ途中でConsumer#2が落ちると、何らかの方法で#2配布分のイベント消費が行われない限り、本ステージ全体の消費が止まってしまいます。実装する場合は、こういったケースも考慮する必用があります。

---

### BroadcastDispatcher

Broadcastでは、購読している消費者に対してeventの同時配信を行います。

![GenStage#BoadDISP.png](https://qiita-image-store.s3.amazonaws.com/0/66748/4cc8eb16-995c-7071-cdb2-232d07e79154.png)

---

## 動かしてみる

### BroadcastDispatcherのサンプル

BroadcastDispatcherは公式にサンプルがあるので、動かしてみます。

このサンプルは単なるブロードキャストだけでなく、先に説明したproducer側キューイングを含んでいます。

```elixir

  @doc "Sends an event and returns only after the event is dispatched."
  def sync_notify(event, timeout \\ 5000) do
    GenStage.call(__MODULE__, {:notify, event}, timeout)
  end

  ## Callbacks

  def init(:ok) do
    {:producer, {:queue.new, 0}, dispatcher: GenStage.BroadcastDispatcher}
  end

  def handle_call({:notify, event}, from, {queue, pending_demand}) do
    queue = :queue.in({from, event}, queue)
    dispatch_events(queue, pending_demand, [])
  end

```

このコードの重要ポイントは、handle_callコールバックです。`handle_demand`とは**別の入り口**を作り、生産者からのイベントを**自前でキューイングするMyイベント**を定義しています。第一引数{:notify, events}でパターンマッチングを掛けて、Myイベントである`:notify`とペアにしたeventsを取り出してキューイングを行ってます。そのまま外部からGenStage.callで呼び出すのは、使いづらいのでsync_notify関数でラップをしています。

出典コードは[公式の``QueueBroadcaster``と ``Printer``の２つのモジュール]
(https://hexdocs.pm/gen_stage/GenStage.html#module-buffering-demand)以下です。
最後の```QueueBroadcaster.sync_notify(:hello_world)```でキューイングを行います。

<img src="https://qiita-image-store.s3.amazonaws.com/0/66748/7e140b1f-4437-b8d1-2866-0a96a1ebe966.png" width="70%">

次のアニメーションGIFでは、イベントとして文字列とリストを順次投下してます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/66748/4b77b7c0-6a86-93b5-e85d-65ddfe7b464d.gif" width="70%">


sync_notifyで投入したイベントが、購読しているステージにブロードキャストされたのがわかります。


次は、キューイングを試してみます。
購読する消費者を登録せずに、イベントを投入してみます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/66748/11a99260-cc6a-a264-6496-44cc6203ff78.gif" width="70%">



`sync_notify`は同期関数なので、イベントが消費されるまで待ちます（そういう仕様です）が、消費者不在ですのでタイムアウトで抜けてきます。（タイムアウトをキャッチする場合はtry catch で行いますが、今回は扱いません。）

その後で、Printer.start_link()で消費者を登録すると、先ほど投入したイベントが配信されてるのが判ります。
　


--- 
### PartitionDispatcherのサンプル


PartitionDispatcherは[GenStageのプロトタイプ版のサンプル](http://www.elixirfbp.org/2016/08/genstage-example-no-3-dispatching.html)しか見つからなかったので、現行世代のGenStageで動かしてみました。

1..10の整数を、３つに振り分ける簡単なHash関数を定義して３つの消費者に１秒毎に振り分けています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/66748/4629a96a-9d04-d81e-9154-31ddfb2518ac.gif" width="70%">

***冒頭の``chcp 65001``は、WindowsのUTF-8ワイド文字の文字化け対策です***
Consumerがtypoってますが、見逃して下さい　(*´ڡ`●)﻿

コードのポイントは、initコールバック中のpartitionsとhash関数定義部分です。partiotionの設定を見てわかるとおり、`:producer`側で自由にAtomのリストを設定しています。こうするとで、Hashによるパーティション分類を直観的なものにしています。この部分は単純な整数にすることも可能で、柔軟な設定が可能です。

```elixir
  def init(:ok) do
    hash_fun = fn event ->
      {event, case rem(event, 3) do
                 0 -> :第一
                 1 -> :第二
                 2 -> :第三
               end
      } end
    {:producer_consumer, %{},
      dispatcher: {GenStage.PartitionDispatcher,
                    partitions: [:第一, :第二, :第三],
                    hash: hash_fun}
    }
  end
```


[Gistに全体](https://gist.github.com/enpedasi/25fef9cc6d366da37850b59fb4f53bd5)をUPしました。


---

## 並列処理

前回の例では、生産者から消費者まで１プロセスずつで、各生産タイプは並列に動作するものの、シングルプロセスでした。　今回のBoadcastDispatcherやPartitionDispatcherでは、上流に対して複数の購読を行っており、その場合は生産者が並列動作します。もちろん、デフォルトであるDemandDispatcherでも並列で動作します。

アニメーションGIFでは、すべて違うPID(プロセスID）が割り当てられてるのが見て取れます。

## さらにGenStageを深めたい方

@kanmo さんの[GenStageを使ってRateLimiterを実装する](https://qiita.com/kanmo/items/1414e7ec73fcc97b2e5a)をご一読下さい。公式のRateLimiterをAWSのDynamoDb用に実装した実用的事例が投稿されています。本記事を書く上でも、かなり参考にさせて頂きました。ありがとうございます！

## 終わりに

ストリーミング中に消費者の一部が落ちた場合など、いろんなケースが考えられると思いますが、ドキュメントに言及してあるものの、実際はソースコードを読みながら解決していく必要があります。徐々に解読を進めたいと思います。

:stars: :stars: :stars: :stars: :stars: 

コミュニティ[fukuoka.ex](https://fukuokaex.connpass.com/)ではElixirを発展・進化させるべく定期的にイベントを行っています。興味のある皆様は気軽にご参加下さい！

:stars: :stars: :stars: :stars: :stars: 

GenStage入門は今回で一旦終了して、次回は[「Elixir並列処理「Flow」の2段ステージ構造を理解する」](https://qiita.com/twinbee/items/43b767520da666ee960d)になります。


明日は @zacky1972  さんの[「ZEAM開発ログv0.1.1 AI/MLを爆速にしたい！ Flow / GenStage でGPUを駆動できないの？」](https://qiita.com/zacky1972/items/140d2380dfdf727b22bc)です。