――― クリスマス。
街中が幻想的な光に包まれる、
一年に一度の特別な日。
そんな特別な日だからこそ、
あなたの大切な人に、
普段はなかなか言えない想いを
伝えるチャンスでしたよね。(過去形)
あなたの想い。大切な人に、伝わりましたか?
...あれ?
伝わらなかった?
なになに、ふむふむ、それは、もしかするに、もしかすると、cast していたせいかもしれませんよ。
読めた、という方も、なんのこっちゃ?! という方も、
そのままです。そのまま読み進めましょう。
気まぐれなあの人に、確かに想いを伝える方法
今日のお題はこれです。
なんのこっちゃ?! という方もいるでしょうから、仕方ない、エンジニア向けに言い直します。
必要な時だけ存在するような temporary worker に確実にリクエストを届ける方法
はい! ロマンティックでムーディーな雰囲気が一気にゼロになりました! はい終わった!
まぁまぁよくあるパターンとして、何らかの重たい処理を行う temporary worker があるとします。
例えばどんなケースがあるだろう。
例えば、山上会長率いるピアピア動画 とかで、ユーザが動画を投稿した後、ネットワーク帯域が細いユーザ向けに低画質動画をトランスコードする処理なんかはそういうパターンかもしれません。
例えばですが、ユーザーが動画を投稿した時、その動画を低画質にトランスコードするような temporary worker が起動するようなシステムが考えられます。
(もっとも、トランスコードクラスタの総 CPU パワーは限られているとすると、キューを介してトランスコーダを起動するようにしないと投稿が多い時にパンクしそうですが。)
さてピアピア動画に新機能を追加してみましょう。ユーザーが画質パラメータを細かく選んで動画の再トランスコードができる機能です。仮の話ですよ。
つまり、トランスコードシステムにはある動画に関するリクエストが不定期に来ることになります。
すると、こんな前提で設計/実装することになります。
- ある動画のトランスコードを管理する temporary worker はシステム上で高々 1 個存在する。そこには不定期に起動+トランスコード要求が来る
- その temporary worker はトランスコードが終わると終了する
さてあなたならどうやってこの挙動を実現しますか?
start_child + cast
supervisor:start_child/2
は既に temporary worker が起動している時は {error, {already_started, Pid}}
を返します。
なので、とりあえず毎回 supervisor:start_child/2
を呼んでから gen_server:cast/2
すれば良さそうに見えます。
supervisor にあまりにメッセージを送り過ぎると詰まってボトルネックになったりするので注意が必要ですが、今回のケースではユーザリクエストは一旦キューに積まれたあとでクラスタが空いてる時にゆっくりpopされる前提なのでおk
ということで サンプル を書いてみました。なんでまだ rebar2 なの🤔とかは言わないでください。
-
server_a
を callback module とするgen_server
は、先ほどの例で言うところのトランスコード処理を行う体のプロセスです。処理開始して 1 ミリ秒後にトランスコードが終わるので自動終了する ようになっています。すごいですね。 -
run(bad_sample, ensure_started_and_doit_badly, 0)
を実行すると start_child して cast を 1 秒間実行しまくります。トランスコード開始リクエストを送り続けるみたいな感じです。 - リクエストが全部処理されたかどうか確かめるために、リクエスト受理部分で ets を使ってカウントアップ して、最後にリクエスト送信回数と比較します。
badly とか言ってすでにダメな雰囲気が出てしまっていますが、実はこの実装だとタイミングによっては supervisor:start_child/2
と server_a:handle_cast/2
の間のどこかでプロセスが終わってしまうことがあるので cast が届かないことがあるんですね。
しかし大体の場合はうまくいくので、このままリリースすると「稀に処理されない」現象が発生し、エンジニアを困らせることになりましょう。
start_child + call + retry
ではどうすれば想いは伝わるのか?
伝わったと確証が得られるまで伝え続ければよいわけです。
※大切な人に想いを伝えるシーンで使ったらちょっとウザいかもしれません。
こんなコード になります。
ポイントは 2 点。
- 確実に呼ばれたことを保証するために
gen_server:call/2
を使う。call が成功で返ってきたら呼べたということです。書留みたいなもんですね。 - call に失敗したら リトライ する。
(書くの飽きてきた)
実行結果(例)
$ make start
==> Ensure_started_and_call (compile)
Compiled src/sample.erl
Compiled src/server_a.erl
erl -sname ensure_call@localhost -pz ebin deps/reloader/ebin -s reloader \
-eval 'application:ensure_all_started(ensure_call).'
Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Eshell V8.1 (abort with ^G)
(ensure_call@localhost)1> sample:a().
bad_sample finished. missing calls: 8519 (243434/251953)
good_sample finished. missing calls: 0 (219817/219817)
ok
start_child + cast 構成だと取りこぼしが発生しましたが、start_child + call + retry 構成にしたら全部到達しました。めでたしめでたし。
Elixir で書いてみる
ちょっと流行ってるようなので Elixir 化してみました。
まとめ
(書くの飽きてきた)
- 必要な時だけ存在するような temporary worker に確実にリクエストを届けたい時、
- start_child + cast だとタイミングによっては cast が届かないことがある。
- あまりにリトライが発生するような設計は避けたいところですが、基本的には start_child + call + retry でやってくといいんじゃないでしょうか。
- 原理を考えると当たり前の話ではある。
- erlang に限らず、マルチスレッドやマルチノード処理など並行/並列処理全般にあてはまります。
- こういうのは実際に起きてしまうと調査が面倒なので、設計段階、あるいは少なくともコードレビュー時に気づいておきたいですね!
ということで Erlang advent calender 2016 最終日の記事でした。
みなさま良いお年を。
Happy coding!