Разрешение взаимоблокировки между двумя gen_tcp

Просматривая код приложения erlang, я столкнулся с интересной проблемой проектирования. Позвольте мне описать ситуацию, но я не могу опубликовать код из-за PIA, извините.

Код структурирован как OTP-приложение, в котором два модуля gen_server отвечают за выделение каких-то ресурсов. Приложение работает отлично в течение некоторого времени, и у нас не было больших проблем.

Сложная часть начинается, когда одному из первых gen_server нужно проверить, достаточно ли ресурсов осталось у второго. call выдается второму gen_server, который сам вызывает служебную библиотеку, которая (в очень особом случае) выдает call первому gen_server.

Я относительно новичок в erlang, но я думаю, что эта ситуация заставит два gen_server ждать друг друга.

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

Любая помощь будет оценена по достоинству.

РЕДАКТИРОВАТЬ: Подводя итоги ответов: Если у вас есть ситуация, когда два gen_servers call друг друга циклическим образом, вам лучше потратить еще немного времени на дизайн приложения.

Спасибо за вашу помощь :)


person sitifensys    schedule 17.05.2011    source источник


Ответы (3)


Это называется тупиком, и его можно/следует избегать на уровне проектирования. Ниже приведен возможный обходной путь и некоторые субъективные моменты, которые, надеюсь, помогут вам избежать ошибок.

Хотя есть способы обойти вашу проблему, «ожидание» — это именно то, что делает call.

Одним из возможных обходных путей может быть порождение процесса внутри A, который вызывает B, но не блокирует A от обработки вызова от B. Этот процесс будет отвечать непосредственно вызывающему.

На сервере А:

handle_call(do_spaghetti_call, From, State) ->
    spawn(fun() -> gen_server:reply(From, call_server_B(more_spaghetti)) end),
    {noreply, State};
handle_call(spaghetti_callback, _From, State) ->
    {reply, foobar, State}

На сервере Б:

handle_call(more_spaghetti, _From, State) ->
    {reply, gen_server:call(server_a, spaghetti_callback), State}

Для меня это очень сложно и сверхтрудно рассуждать. Я думаю, вы даже могли бы назвать это спагетти-кодом, никого не обидев.

С другой стороны, хотя приведенное выше может решить вашу проблему, вам следует хорошенько подумать о том, что на самом деле означает такой вызов. Например, что произойдет, если сервер А выполнит этот вызов много раз? Что произойдет, если в какой-то момент произойдет тайм-аут? Как настроить тайм-ауты, чтобы они имели смысл? (Самый внутренний вызов должен иметь более короткий тайм-аут, чем внешние вызовы и т.д.).

Я бы изменил дизайн, даже если это болезненно, потому что, когда вы позволяете этому существовать и обходите его, ваша система становится очень сложной для понимания. ИМХО, сложность - корень всех зол, и ее следует избегать любой ценой.

person knutin    schedule 17.05.2011
comment
Да, это делает тарелку спагетти немного дороже ;). Хотя я ясно вижу обходной механизм, я думаю, что в моем случае этого определенно следует избегать (я уверен, вы согласитесь, если увидите спагетти-код, который я вижу прямо сейчас ;)). Однозначно буду голосовать за редизайн. - person sitifensys; 18.05.2011

В основном это проблема дизайна, когда вам нужно убедиться, что нет длительных блокирующих вызовов от gen_server1. Это можно довольно легко сделать, запустив небольшую забаву, которая позаботится о вашем вызове gen_server2 и доставит результат на gen_server1, когда закончите.

Вам придется отслеживать тот факт, что gen_server1 ожидает ответа от gen_server2. Что-то вроде этого, может быть:

handle_call(Msg, From, S) ->
  Self = self(),
  spawn(fun() ->
    Res = gen_server:call(gen_server2, Msg),
    gen_server:cast(Self, {reply,Res})
  end),
{noreply, S#state{ from = From }}.

handle_cast({reply, Res}, S = #state{ from = From }) ->
  gen_server:reply(From, Res),
  {noreply, S#state{ from = undefiend}.

Таким образом, gen_server1 может обслуживать запросы от gen_server2 без зависаний. Вам, конечно, также нужно будет правильно распространять ошибки для небольшого процесса, но вы поняли общую идею.

person Lukas    schedule 17.05.2011

Другой способ сделать это, который я считаю лучшим, состоит в том, чтобы сделать эту (ресурсную) информацию, проходящую асинхронно. Каждый сервер реагирует и делает то, что должен, когда получает (асинхронное) сообщение my_resource_state от другого сервера. Он также может предложить другому серверу отправить состояние ресурсов с помощью send_me_your_resource_state асинхронного сообщения. Поскольку оба этих сообщения являются асинхронными, они никогда не будут блокироваться, и сервер может обрабатывать другие запросы, пока он ожидает сообщения my_resource_state от другого сервера после его запроса.

Еще одним преимуществом асинхронности сообщений является то, что серверы могут отправлять эту информацию без запроса, когда они считают это необходимым, например, "помогите мне, я работаю очень низко!" или «Я переполнен, хочешь немного?».

Два ответа от @Lukas и @knutin на самом деле делают это асинхронно, но они делают это, порождая временный процесс, который затем может выполнять синхронные вызовы, не блокируя серверы. Легче сразу использовать асинхронные сообщения, да и намерения яснее.

person rvirding    schedule 19.05.2011
comment
Спасибо за ваш ответ :) это кажется интересным и может идеально вписаться в способ ведения OTP, я думаю. Я попробую что-нибудь, используя этот подход, и соответствующим образом обновлю свой пост. - person sitifensys; 19.05.2011