migmit: (Default)
[personal profile] migmit
Уже в нескольких местах рассказывал одну и ту же байку. Надо бы оформить постом, и потом только ссылку давать.

Итак. Года два-три назад писал я на Эрланге. И вот возник один баг: некий процесс, скажем, Игрек, через некоторое время после запуска вдруг падает. Причём, скотина, падает даже без каких-либо сообщений в логах. А так как занят он довольно важным делом — теряется куча информации. Соответственно, релиз пришлось откатить (то ещё удовольствие) и начать разбираться.

Как оказалось, падает Игрек только под нагрузкой, так что скормить ему какие-то простенькие входные данные и аккуратно отследить, где именно его скрючит, не представляется возможным. Добавленные во все мыслимые места логи показывают, что падает процесс, в общем-то, где попало. Точнее не совсем где попало, в некоей небольшой области, но не в строго определённом месте. Я поначалу грешил на оптимизатор (случалось сталкиваться с тем, что лог выведен, а предыдущая строчка таки не выполнилась из-за того, что процесс упал), но потом понял, что наш случай таки выходит за пределы бардака, который может устроить оптимизатор. А самое смешное, что, судя по логам git, ни сам Игрек, ни вообще что-либо в непосредственной близости от него, никто не трогал уже несколько месяцев. С чего он вдруг падать начал?

Что оказалось в итоге. Был некий другой процесс, скажем, Ипсилон. И он-то как раз занимался совершенно простенькой деятельностью; он принимал сообщение, смотрел, куда его сунуть, и отсылал дальше. Причём он дожидался ответа от конечного получателя ("ага, получили, спасибо"), но сам никакого ответа отправителю не возвращал. Реализован он был через gen_server, но реально никакого внутреннего состояния там не было; если точнее, внутреннее состояние всегда оставалось [].

Но какой-то гений, который его писал (я уже не помню, кто это был) слегка ошибся. Если кто не знает, то gen_server требует, чтобы в сервере была реализована функция handle_cast, которая, собственно, и решает, как именно следует реагировать на поступившее сообщение. Бизнес-логику реализует, если угодно. Так вот, эта функция должна была бы возвращать {noreply, []} (говоря тем самым "ответа не будет, новое состояние []"), но вместо этого возвращала {ok, []}. Нарушение типизации такое, ага.

gen_server такого ответа не понимает. Ну, тупой он, не знает, что ok — значит, типа, всё здорово, давай дальше в том же духе. Поэтому, по завершении handle_cast, он, в полном соответствии с идеологией Erlang "let it crash" (т.е., "гори оно огнём") падает. Но дальше, опять-таки в полном соответствии с упомянутой идеологией, в дело включается супервизор. Который видит, что Ипсилон упал — и перезапускает его. А так как состояния у Ипсилона нет — всё работает как и должно. Вместо того, чтобы сидеть и дожидаться нового сообщения, Ипсилон падает, стартует заново, и дальше сидит и ждёт нового сообщения. И так оно работало не то чтобы месяцами — годами.

И вот что изменилось: сам Ипсилон мы тоже не трогали — а вот в одном из конечных получателей кое-что таки подкрутили и пооптимизировали. В результате ответ Ипсилону стал приходить гораздо быстрее. И Ипсилон стал, в результате, работать быстрее. А так как его работа — это постоянные падения, то и падать он стал чаще. Если под нагрузкой, конечно; когда приходит одно сообщение в час, то он и падать будет раз в час.

Супервизор не железный, у него есть предел выносливости, заданный в настройках. Так что он увидел, что один из процессов, за которыми он следит, слишком часто падает, сказал "вы меня достали", убил всех своих подопечных, после чего убился сам. Ну и, как вы понимаете, одним из его подопечных оказался Игрек, который вообще ни в чём не виноват. И чем именно занимается Игрек в этот момент — предсказать невозможно, но, с хорошей вероятностью, тем, чем он занимается чаще всего — а это довольно небольшая область. И даже пискнуть Игрек не может, убиение его супервизором совершается мгновенно и безжалостно.

Date: 2016-02-01 01:40 pm (UTC)
From: [identity profile] jamhed.livejournal.com
только что попробовал -- показывает.

srv_cleanup.erl:35: The inferred return type of handle_cast/2 ({'ok',_}) has nothing in common with {'noreply',_} | {'noreply',_,'hibernate' | 'infinity' | non_neg_integer()} | {'stop',_,_}, which is the expected return type for the callback of gen_server behaviour

Хотя если на проекте нет практики применения dialyzer то какое-то время придется провести чтобы он начал показывать осмысленные результаты без лишнего шума.

Date: 2016-02-01 03:27 pm (UTC)
develop7: (dero)
From: [personal profile] develop7
странно, у меня в заглушке handle_call работает, а в нескольких клозах handle_cast — нет.

Date: 2016-02-01 03:39 pm (UTC)
From: [identity profile] jamhed.livejournal.com
А можно подробнее где работает а где нет?

Date: 2016-02-01 06:58 pm (UTC)
develop7: (dero)
From: [personal profile] develop7
в общем, поигрался я немного. на
handle_cast(Request, State) ->
	_ = Request,
	{dafuq, State};
handle_cast(_Msg, State) ->
	{noreply, State}.
dialyzer ругается
teest.erl:34: The inferred return type of handle_cast/2 ({'dafuq',_}) has nothing in common with {'noreply',_} | {'noreply',_,'hibernate' | 'infinity' | non_neg_integer()} | {'stop',_,_}, which is the expected return type for the callback of gen_server behaviour
, как и требовалось. А вот на такое
handle_cast({dafuq, _Ugh}, State) ->
	{dafuq, State};
handle_cast(_Msg, State) ->
	{noreply, State}.
не ругается.
Т.е. клозы с паттернматчингом почему-то успешно пропускаются.

Вопрос — кто здесь неправ?

Date: 2016-02-01 07:04 pm (UTC)
From: [identity profile] jamhed.livejournal.com
Я так подозреваю что вызова handle_cast({dafuq, wtf}, State) не делалось? Вот и нет предупреждения потому что в первую клозу код не доходит, а вторая как более общая возвращает все правильно.

Date: 2016-02-01 07:20 pm (UTC)
develop7: (dero)
From: [personal profile] develop7
Конечно, не делалось — это коллбэк, он не вызывается вручную.

Date: 2016-02-01 07:23 pm (UTC)
From: [identity profile] jamhed.livejournal.com
Ну в смысле такая конструкция была?

api_handle_cast(Pid) -> gen_server:cast(Pid, {dafuq, wtf});
Edited Date: 2016-02-01 07:25 pm (UTC)

Date: 2016-02-01 07:32 pm (UTC)
From: [identity profile] kurilka.livejournal.com
Чтот не о том речь, там же тип определяется на уровне спек behavior, при чём здесь call-site для модуля gen_server?

Date: 2016-02-01 07:34 pm (UTC)
From: [identity profile] jamhed.livejournal.com
Ну вот если я правильно понимаю то dialyzer пытается выстроить дерево всех вызовов, потом по этому дереву сверяет типы где указано + inferred. Если совпадает то хорошо, если нет вывалит варнинг, если в ветку не попадает то не анализируется. Т.е. это не совсем statical type check, а где-то между.
Edited Date: 2016-02-01 07:36 pm (UTC)

Date: 2016-02-01 07:44 pm (UTC)
From: [identity profile] kurilka.livejournal.com
это былаб фантастика еслиб диалайзер смог из api-функции протянуть типы прям до handle_XXX через все кишки gen_XXX и не споткнувшись по пути...
ну и комбинаторный взрыв бы все асимптотам сделал грустно, думаю...
тут всёж тип задаётся на уровне колбэка, а почему диалайзер сдюживает былоб интересно узнать, конечно.

Date: 2016-02-01 07:53 pm (UTC)
From: [identity profile] jamhed.livejournal.com
> это былаб фантастика еслиб диалайзер смог из api-функции протянуть

Да, не срабатывает. Причем и даже если явно spec написать перед handle_cast.

Date: 2016-02-01 07:23 pm (UTC)
From: [identity profile] migmit.livejournal.com
А чего тогда в первом случае ругается?

Date: 2016-02-01 07:26 pm (UTC)
From: [identity profile] jamhed.livejournal.com
В первом две клозы неразличимы по сигнатуре, выполнится первая всегда с неверным типом.

Date: 2016-02-01 07:28 pm (UTC)
From: [identity profile] kurilka.livejournal.com
Думается, что это косяк success typing диалайзера, ссылку на точное описание не дам, но вроде как в нифига недоисанной книжке Лоика упоминается, что функция может иметь тип не только A -> B, но и A->B ; C -> D. Только вот почему второе может быть валидным подтипом первого не очень понятно.

Date: 2016-02-01 07:30 pm (UTC)
From: [identity profile] kurilka.livejournal.com
ну а в первом варианте 2-й клоз смысла же не имеет

Date: 2016-02-02 10:40 am (UTC)
develop7: (dero)
From: [personal profile] develop7
не имеет, верно. однако же и на
handle_cast({argh, _Ugh}, State) ->
	{noreply, dafuq, State};
handle_cast({dafuq, _Ugh}, State) ->
	{dafuq, State}.
всё равно не ругается.

Date: 2016-02-02 10:52 am (UTC)
From: [identity profile] kurilka.livejournal.com
Ну ситуация же про ту же странность варианта A->B ; C -> D по-моему, про которую я писал выше.
Былоб неплохо если кто-нибудь не поленился и спросил в рассылке, может Костис чего-нибудь бы и ответил :)