Эрланг: как справиться с длительным обратным вызовом init?


У меня есть gen_server, который при запуске пытается запустить определенное количество дочерних процессов (обычно 10-20) под супервизором в дереве супервизии. Обратный вызов init gen_server вызывает supervisor:start_child/2 для каждого необходимого дочернего процесса. Вызов supervisor:start_child/2 является синхронным, поэтому он не возвращается, пока не запустится дочерний процесс. Все дочерние процессы также являются gen_servers, поэтому вызов start_link не возвращается до тех пор, пока не вернется обратный вызов init. При обратном вызове init выполняется вызов третьей стороне. система, которая может занять некоторое время, чтобы ответить (я обнаружил эту проблему, когда вызовы сторонней системы были тайм-аут после 60 секунд). Тем временем вызов init заблокирован, что означает, что supervisor:start_child/2 также заблокирован. Таким образом, все время процесс gen_server, который вызвал supervisor:start_child/2, не отвечает. Вызывает тайм-аут gen_server, пока он ожидает возврата функции on the start_child. Так как это может легко длиться в течение 60 секунд и более. Я хотел бы изменить это, поскольку моя заявка приостановлена в каком-то полузапущенном состоянии, пока он ждет.

Как лучше всего решить эту проблему?

Единственное решение, которое я могу придумать, - это переместить код, который взаимодействует со сторонней системой, из обратного вызова init в обратный вызов handle_cast. Это сделает обратный вызов init быстрее. Недостатком является то, что мне нужно будет вызвать gen_server:cast/2 после запуска всех дочерних процессов. Есть ли лучший способ сделать это?
1 5

1 ответ:

Один из подходов, который я видел, - это использование timeout init/1 и handle_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( .... 

Почти все результаты вы можете вернуть с дополнительным параметром timeout, который в основном время ожидания другого сообщения. Это заданное время проходит называется 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, которые регистрируются под некоторыми атом.


Отредактируйте После более тщательного изучения исходного кода OTP.

Такой подход достаточно хорош, когда вы общаетесь с вашим сервером через его pid. Главным образом потому, что pid возвращается после возврата ваших функций init/1. Но это немного отличается в случае gen_.., начатого с start/4 или start_link/4, где мы автоматически регистрируем процесс под тем же именем. Есть одно расовое условие, с которым вы можете столкнуться, которое я хотел бы объяснить немного подробнее деталь.

Если процесс регистровый, то он обычно упрощает все вызовы и приведение к серверу, например:

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

Где ?SERVER обычно имя модуля (atom) и который будет работать просто отлично, пока под этим именем не будет какой-то зарегистрированный (и живой) процесс. И конечно, под капотом это cast стандартное сообщение Эрланга, отправленное с !. В этом нет ничего волшебного, почти так же, как вы делаете в вашем init с self() ! {finish ....

Но в нашем случае мы предполагаем еще кое-что. Не только регистрационная часть, но и то, что наш сервер закончил его инициализацию. Конечно, поскольку мы имеем дело с коробкой сообщений, на самом деле не важно, сколько времени что-то занимает, но важно, какое сообщение мы получаем в первую очередь. Поэтому, если быть точным, мы хотели бы получить сообщение finish_init до получения сообщения count. К сожалению, такой сценарий мог произойти. Это связано с тем, что gen в OTP зарегистрированы ранее init/1 обратный вызов вызывается. Так что в теории пока один процесс вызывает функцию start, которая будет идти до регистрационной части, чем другой может найти наш сервер и отправить сообщение count, и сразу после этого функция init/1 будет вызвана с сообщением finish_init. Шансы невелики (очень, очень малы), но все же это может случиться.

Существует три варианта решения этой.

Во-первых, ничего не делать. В случае такого расового условия handle_cast потерпит неудачу (из-за предложения функции, так как наше состояние - not_state_yet атом), и супервайзер просто перезапустит все это.

Вторым случаем было бы игнорирование этого инцидента с плохим сообщением / состоянием. Это легко достигается с помощью

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

Как ваше последнее предложение. И, к сожалению, большинство людей, использующих шаблоны, используют такой неудачный (ИМХО) шаблон.

В обоих из них вы, возможно, могли бы потерять одно count сообщение. Если это действительно проблема, вы все еще можете попытаться исправить ее, изменив последнее предложение на

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

Но у этого есть и другие очевидные преимущества, я предпочел бы подход "пусть провалится".

Третий вариант-регистрация процесса чуть позже. Вместо того чтобы использовать start/4 и запрашивать автоматическую регистрацию, используйте start/3, получите pid и зарегистрируйте его самостоятельно.
start(Args) ->
   {ok, Pid} = gen_server:start(?MODULE, Args, []),
   register(?SERVER, Pid),
   {ok, Pid}.

Таким образом, мы посылаем finish_init сообщение до регистрации, и до того, как кто-либо другой может отправить и count сообщение.

Но у такого подхода есть свои недостатки, главным образом сама регистрация, которая может потерпеть неудачу несколькими различными способами. Всегда можно проверить, как это делается. OTP обрабатывает этот и дублирует этот код. Но это уже другая история. Так что в конечном итоге все зависит от того, что вам нужно, или даже от того, с какими проблемами вы столкнетесь в производстве. Важно иметь некоторое представление о том, что плохого может произойти, но я лично не буду пытаться исправить что-либо из этого, пока я действительно не буду страдать от такого состояния гонки.