先日、ペアプロYouTuber第一話のなかでPattern
について解説しました。
この記事ではそのとき話した内容をもとに、Cizen/Patternの概要と今の設計になった経緯までを説明します。
TLDR
-
Pattern
はCizen.Filter
をもっと柔軟にしてCizen
から抽出したもの -
Pattern.new
はフィルタ関数を前置記法で表現した命令のツリーに変換する - 柔軟で直感的なクエリで、購読数が100万とかでもスケールする仕組みになった
Cizenとは
https://hexdocs.pm/cizen/readme.html
"Build highly concurrent, monitorable, and extensible applications with a collection of automata."
めちゃくちゃ雑に説明すると、
- プロセスをSagaという形式で統一的に管理し
- Sagaはイベントを購読しイベントを配信することで他のSagaに影響を与える
ような枠組みを提供するためのライブラリです。
イベントの購読時にはCizen.Filter
を使ってクエリを生成します。
近いうちにCizen
からCizen.Filter
を抽出したPattern
でCizen.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月に描いた図です。
コード例
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]
# ]
# ]}
# }
図
メリット
- スケールする
- 柔軟かつ直感的に書ける
デメリット
- 購読時と解除時のオーバーヘッド
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