Эрланг: как справиться с длительным обратным вызовом 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 ответ:
Один из подходов, который я видел, - это использование 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}.
Таким образом, мы посылаем
Но у такого подхода есть свои недостатки, главным образом сама регистрация, которая может потерпеть неудачу несколькими различными способами. Всегда можно проверить, как это делается. OTP обрабатывает этот и дублирует этот код. Но это уже другая история. Так что в конечном итоге все зависит от того, что вам нужно, или даже от того, с какими проблемами вы столкнетесь в производстве. Важно иметь некоторое представление о том, что плохого может произойти, но я лично не буду пытаться исправить что-либо из этого, пока я действительно не буду страдать от такого состояния гонки.finish_init
сообщение до регистрации, и до того, как кто-либо другой может отправить иcount
сообщение.