A<->B<->C な gen_server groupを作成する
ある日アニメを見ていたら, twitterで面白そうなお題(?)が流れてきました.
Erlang のプロセス構成で最大の課題にぶつかっていてツライ。どうするかなー。A <-> B <-> C というプロセス構成はなかなか大変だ。いい案がないな。
— V (@voluntas) 2016年6月16日
自分も2, 3回こういった構成にせざるを得なくなり, 名前付けで逃げていました.
しかし, 起動タイミングの問題などで名前付けだけでは
init(_) ->
{ok, {{one_for_all, 5, 10},
[
%% NOTE: Bよりも先に起動しなければならない
{module_a, {module_a, start_link, []}, permanent, 5000, worker, [module_a]},
{module_b, {module_b, start_link, []}, permanent, 5000, worker, [module_b]}
]}.
のようにイケてない書き方をせざるを得なくなっていました. 本当にイケてません.
何がイケてないって, 起動直後にmodule_b -> module_aへの通信をしなければいけなくなった時に頭を抱えるのでイケてません (経験談)
ということで, イケてるやり方を思いついたので紹介します.
結論
init(SupPid) ->
ok = gen_server:cast(self(), {sync, SupPid}),
{ok, State}.
handle_cast({sync, SupPid}, State) ->
Pids = [Child || {_Id, Child, _, _} <- supervisor:which_children(SupPid)],
{noreply, State#?state{group_member = Pids}}.
-
one_for_all
のみを考慮しています. - 名前付けをする必要はなく, ChildIdと
pid/0
が分かれば良いものとします.
解説
supervisor | gen_server (A) | gen_server (B)
------------------------------------------------------------------
start_link
↓
init -------(a)------> init
<------(a)------ |
↓
<------(b)------ handle_cast
----------------(c)------------------> init
<---------------(c)------------------- |
↓
<---------------(d)------------------- handle_cast
----(b)----> (which_children reply)
----------------(d)----------------> (which_children reply)
gen_server の init処理
-
start_link
の返値が返る前にinit
は返値を返している. -
init
は新しく起動されたプロセス上で実行される.
この仕様から, init処理が重い場合にself()
にメッセージを送る事で, initの直後に実行する関数を指定することができます.
※ erlang:processes()
などの全体のプロセスを知る方法を使うとこの制約はなくなりますが, 調査以外でまず使うことはないので, この前提で問題が起きることはないはずです.
supervisor の 子の起動の仕方
-
one_for_all
,rest_for_one
の場合, 同時に再起動する対象について, 0 or N がsupervisor
の各APIが返すタイミングでは保証される (他の場合は, 同時であることを指定していない) -
start_child
の際に, 子の起動 (initの返値を得る) まではsupervisor
は他の動作をしない.
SupervisorModule:init
で指定された複数の子は順番に起動されるが, 全部の起動が終わるまでは他の処理を受け付けません.
これによって, supervisor:which_children
を返すことができるようになった時点で全ての子が「いる or いない」が保証できます.
また, 子自身がsupervisor:which_children
を呼び出した場合は前者が保証されます. (replyを得る前にsupervisorによってkillされる可能性はあります)
supervisor:which_children
の timeout
https://github.com/erlang/otp/blob/OTP-19.0-rc2/lib/stdlib/src/supervisor.erl#L235-L236
supervisorは全てのcallでinfinity
を使用しています (OTP19.0-rc2時点)
その為, which_children
でtimeoutになることはありません.
検証
理論的にはこれで動くので, 動作確認がてら汎用ライブラリ gcyclic を作りました.
検証コードはこちら
確認しているのは,
- 起動時にどのプロセスも残りの全プロセスの情報を取得できていること
- 再起動時にもどうようのことが成り立つこと
だけですが.
捕捉
其の壱
one_for_one
やrest_for_one
もこれを拡張することで実現はできそうです. ただ, 一部が存在しない状況を考慮するぐらいならone_for_all
にしてしまう方が良さそうです.
其の弐
この方法ではできなくなることが一つだけ存在します.
それは, ホットコードアップデートでsupervisor
のstrategy
を変更することです.
まぁ, まずやらないので大丈夫でしょう ^^;;;