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を変更することです.
まぁ, まずやらないので大丈夫でしょう ^^;;;