6

監視ツリーの監視者gen_serverの下で、開始時に特定の数 (通常は 10 ~ 20) の子プロセスを開始しようとします。supervisor:start_child/2必要な子プロセスごとに、gen_server の init コールバックが呼び出されます。への呼び出しsupervisor:start_child/2は同期的であるため、子プロセスが開始されるまで戻りません。すべての子プロセスも gen_servers であるため、init コールバックが戻るまで start_link 呼び出しは戻りません。init コールバックでは、サードパーティ システムへの呼び出しが行われ、応答に時間がかかる場合があります (この問題は、サードパーティ システムへの呼び出しが 60 秒後にタイムアウトになったときに発見されました)。その間、init 呼び出しがブロックされました。つまり、supervisor:start_child/2もブロックされます。したがって、呼び出した gen_server プロセス全体supervisor:start_child/2無反応です。start_child 関数が戻るのを待っている間に gen_server タイムアウトを呼び出します。これは簡単に60秒以上続く可能性があるためです。アプリケーションが待機中に半開始状態で中断されているため、これを変更したいと思います。

この問題を解決する最善の方法は何ですか?

考えられる唯一の解決策は、サードパーティ システムと対話するコードを init コールバックから handle_cast コールバックに移動することです。これにより、init コールバックが高速になります。欠点は、gen_server:cast/2すべての子プロセスが開始された後に呼び出す必要があることです。

これを行うより良い方法はありますか?

4

1 に答える 1

9

私が見た 1 つのアプローチは、タイムアウトinit/1handle_info/2.

init(Args) ->
  {ok, {timeout_init, Args} = _State, 0 = _Timeout}.


...


handle_info( timeout, {timeout_init, Args}) ->
   %% do your inicialization
   {noreply, ActualServerState};  % this time no need for timeout 

handle_info( .... 

ほとんどすべての結果は、追加のタイムアウト パラメータを使用して返すことができます。これは基本的に別のメッセージを待つ時間です。時間が経過するhandle_info/2と、呼び出され、timeoutアトムとサーバーの状態で呼び出されます。私たちの場合、タイムアウトが 0 の場合、gen_server:start終了前でもタイムアウトが発生するはずです。handle_infoこれは、サーバーの pid を他の人に返す前に呼び出す必要があることを意味します。したがって、これtimeout_initはサーバーへの最初の呼び出しであり、他の処理を行う前に初期化を完了するという保証を与える必要があります。

このアプローチが気に入らない場合 (あまり読みにくい場合) は、init/1

init(Args) ->
   self() ! {finish_init, Args},
   {ok, no_state_yet}.

...


handle_info({finish_init, Args} = _Message, no_state_yet) ->
   %% finish whateva 
   {noreply, ActualServerState};

handle_info(  ... % other clauses 

繰り返しますが、初期化を終了するメッセージがこのサーバーにできるだけ早く送信されるようにしています。これは、いくつかのアトムの下に登録する gen_servers の場合に非常に重要です。


EDIT OTPソースコードをさらに注意深く調べた後。

このようなアプローチは、pid を介してサーバーと通信する場合に十分です。init/1主な理由は、関数が戻った後に pid が返されるためです。ただし、同じ名前でプロセスを自動的に登録する場合や、プロセスを自動的に登録する場合gen_..は少し異なります。発生する可能性のある競合状態が 1 つあります。これについてもう少し詳しく説明します。start/4start_link/4

プロセスが登録されている場合、通常、次のようにすべての呼び出しとサーバーへのキャストが簡素化されます。

count() ->
   gen_server:cast(?SERVER, count).

は通常、モジュール名 (アトム?SERVER) であり、この名前の下で登録された (そして生きている) プロセスになるまで問題なく動作します。そしてもちろん、内部では、これcastは で送信される標準の Erlang のメッセージ!です。initwith で行うのとほとんど同じですself() ! {finish ...

しかし、私たちの場合、もう 1 つ仮定します。登録部分だけでなく、サーバーが初期化を完了したことも確認します。もちろん、メッセージ ボックスを扱っているので、処理にかかる時間はそれほど重要ではありませんが、どのメッセージを最初に受け取るかが重要です。正確には、メッセージを受信finish_initする前にメッセージを受信したいと考えていcountます。

残念ながら、そのようなシナリオが発生する可能性があります。これは、コールバックが呼び出される前genに OTP 内の が登録されているためです。 init/1したがって、理論的には、あるプロセスがstart登録部分に進む関数を呼び出している間、別のプロセスがサーバーを見つけてcountメッセージを送信し、その直後にinit/1関数がfinish_initメッセージで呼び出されます。可能性は小さい (非常に小さい) ですが、それでも発生する可能性があります。

これには3つの解決策があります。

まずは何もしないことです。このような競合状態の場合、handle_cast関数句が原因で (状態がnot_state_yetアトムであるため) 失敗し、スーパーバイザーはすべてを再起動するだけです。

2 番目のケースは、この悪いメッセージ/状態インシデントを無視することです。これは簡単に達成できます

   ... ;
handle_cast( _, State) -> 
   {noreply, State}.

あなたの最後の句として。残念ながら、テンプレートを使用するほとんどの人は、そのような不幸な (IMHO) パターンを使用します。

それらの両方で、1 つのメッセージを失う可能性がありcountます。それが本当に問題である場合は、最後の句を次のように変更して修正を試みることができます

   ... ;
handle_cast(Message, no_state_yet) -> 
   gen_server:cast( ?SERVER, Message),
   {noreply, no_state_yet}.

しかし、これには他にも明らかな利点があります。私は「失敗させる」アプローチを好みます。

3 番目のオプションは、少し後でプロセスを登録することです。start/4自動登録を使用して要求するのではなく、 を使用してstart/3pid を受け取り、自分で登録します。

start(Args) ->
   {ok, Pid} = gen_server:start(?MODULE, Args, []),
   register(?SERVER, Pid),
   {ok, Pid}.

このようにしてfinish_init、登録前、および他の人が送信してcountメッセージを送信する前にメッセージを送信します。

しかし、そのようなアプローチには独自の欠点があり、主に登録自体がいくつかの異なる方法で失敗する可能性があります。OTP がそれをどのように処理するかを常に確認し、このコードを複製することができます。しかし、これは別の話です。

したがって、最終的には、何が必要か、または本番環境でどのような問題が発生するかによって異なります。何が悪いのかを考えておくことは重要ですが、個人的には、そのような競合状態に実際に苦しむまで、それを修正しようとはしません.

于 2014-11-07T20:38:08.353 に答える