LoginSignup
7
0

More than 3 years have passed since last update.

Elixirでやばいマクロ(Pattern)を書いた理由【イベント購読】

Last updated at Posted at 2020-04-12

先日、ペアプロYouTuber第一話のなかでPatternについて解説しました。
この記事ではそのとき話した内容をもとに、Cizen/Patternの概要と今の設計になった経緯までを説明します。

TLDR

  • PatternCizen.Filterをもっと柔軟にしてCizenから抽出したもの
  • Pattern.newはフィルタ関数を前置記法で表現した命令のツリーに変換する
  • 柔軟で直感的なクエリで、購読数が100万とかでもスケールする仕組みになった

Cizenとは

https://hexdocs.pm/cizen/readme.html
"Build highly concurrent, monitorable, and extensible applications with a collection of automata."

めちゃくちゃ雑に説明すると、
1. プロセスをSagaという形式で統一的に管理し
2. Sagaはイベントを購読しイベントを配信することで他のSagaに影響を与える
ような枠組みを提供するためのライブラリです。

イベントの購読時にはCizen.Filterを使ってクエリを生成します。
近いうちにCizenからCizen.Filterを抽出したPatternCizen.Filterを置き換える予定です。

Cizen.Filterの要件

  • 柔軟なクエリでイベントを購読できる
  • 購読が数百万あってもスケールする

例: ライトが on off イベントを購読

ライトが複数あって、それぞれのライトが以下のようなイベントを購読します

%Event{body: %On{light_id: ^light_id}}
%Event{body: %Off{light_id: ^light_id}}

Cizen.Filter設計考察

現在の設計になるまでの流れを1から4の流れで説明します。

1. フィルタ関数で購読

もっとも簡単なのはフィルタ関数をクエリとするものです。

Dispatcherはフィルタ関数がtrueを返すような購読に対してイベントを配信します。

コード例

query = fn
  %Event{body: %On{light_id: ^light_id}} -> true
  _ -> false
end

メリット

  • 柔軟で直感的なクエリ

デメリット

  • スケールしない イベントごとに購読数分の関数を呼ぶ必要があるため

2. イベントボディーフィルタ

クエリはevent_type (OnやOffなど) と イベントボディーフィルタのリストを持ちます。

Dispatcherはevent_typeで購読を絞った後、イベントボディーフィルタがすべてtrueを返す購読に対してイベントを配信する。

コード例

query = %EventQuery{
  event_type: On,
  event_body_filters: [
    On.filter(light_id)
  ]
}

メリット

  • 同じevent_typeに購読が集中しなければスケールする

デメリット

  • フィルタ関数より煩雑
  • イベントボディーフィルタはスケールしない

3. パイプライン

パイプラインを指定して購読します。

DispatcherはPipeを木状に解釈(クエリを最適化)してイベントを配信します。

以下は2018年の9月に描いた図です。

IMG_8932.JPG

IMG_8934.JPG

コード例

query = %Pipeline{
  pipes: [
    Pipe.event_type(On),
    On.light_id(^light_id)
  ]
}

メリット

  • イベントボディーフィルタより柔軟
  • スケールしそう

デメリット

  • 実装によってはDispatcherがスマートになりすぎる
  • 実装によってはPipelineの最適化がスケールしない
  • フィルタ関数より煩雑
  • 最適化の部分がGCみたいにシステムの動作を不安定にしそうでこわい

4. 関数を実行時に評価できる形式に変換したものを木構造に挿入して購読

関数を実行時に評価できる形式に変換したものを木構造に挿入して購読、します。
やばいマクロでコンパイル時に頑張るということです。

Dispatcherは木構造からイベントにマッチする葉(購読者)を探してイベントを配信します。

コード例

query = Cizen.Filter.new(fn %Event{body: %On{light_id: ^light_id}} -> true end)
# コンパイル時にこうなる↓
# query = %Cizen.Filter{
#   code: {:and,
#    [
#      and: [is_map: [access: []], ==: [{:access, [:__struct__]}, Event]],
#      and: [
#        and: [
#          is_map: [access: [:body]],
#          ==: [{:access, [:body, :__struct__]}, On]
#        ],
#        ==: [{:access, [:body, :light_id]}, light_id]
#      ]
#    ]}
# }

2020_04_12 19_56 Office Lens_2.jpg

メリット

  • スケールする
  • 柔軟かつ直感的に書ける

デメリット

  • 購読時と解除時のオーバーヘッド

Pattern

  • CizenからCizen.Filterの機能を抽出して、明示的に別で管理するようにしたもの
  • リファクタリングを行い、鑑賞に堪えうるものにした
  • フィルタ関数だけでなくパターンマッチ的表現も許可

例えば、

Cizen.Filter.new(fn %Event{body: %On{light_id: ^light_id}} -> true end)

と書いていたところを、

Pattern.new(%Event{body: %On{light_id: ^light_id}})

と書けるようになりました。

関連リンク

Cizen: https://gitlab.com/cizen/cizen
Pattern: https://gitlab.com/cizen/pattern
ペアプロYouTuber第一話: https://youtu.be/WEWZoZkWKmE

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0