Правильный способ структурирования вызовов GenServer для себя

Я знаю, что практически невозможно иметь вызов процесса GenServer, потому что вы по существу попали в тупик. Но мне любопытно, есть ли предпочтительный способ сделать это.

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

def handle_call(:refill_queue, state) do
  new_state = put_some_stuff_in_queue(state)
  {:reply, new_state}
end

def handle_call(:pop, state) do
  if is_empty_queue(state) do
    GenServer.call(self, :refill_queue)
  end

  val,new_state = pop_something(state)

  {:reply, val, new_state}
end

большая проблема здесь в том, что это будет тупик, когда мы попытаемся пополнить очередь. Одно решение что я использовал в прошлом, чтобы использовать cast больше, чтобы он не тупик. Вот так (изменить call to cast для пополнения)

def handle_cast(:refill_queue, state) do

но в этом случае, я думаю, что это не сработает, так как асинхронный бросок для пополнения очереди может вернуться в pop case перед фактическим заполнением очереди означает, что я попытаюсь выскочить из пустой очереди.

в любом случае, основной вопрос: каков наилучший способ справиться с этим? я предполагаю, что ответ - просто позвонить put_some_stuff_in_queue прямо внутри pop звонок, но я хотел проверить. Другими словами, Кажется, что правильная вещь-это сделать handle_call и handle_cast как можно проще и в основном просто обертки для других функций, где происходит реальная работа. Затем создайте столько handle_* функции, как вам нужно, чтобы охватить все возможные случаи, с которыми вы будете иметь дело, а неhandle_call(:foo) в свою очередь, называют handle_call(:bar).

2 ответов


есть функция в GenServer модуль, называемый reply/2. Второй аргумент handle_call/3 обратный вызов-это соединение с клиентом. Вы можете создать новый процесс для обработки соединения и возврата {:noreply, state} в пункте обратного вызова. Используя Ваш пример:

defmodule Q do
  use GenServer

  ############
  # Public API

  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

  def push(pid, x) do
    GenServer.call(pid, {:push, x})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  ########
  # Helper

  # Creates a new process and does a request to
  # itself with the message `:refill`. Replies
  # to the client using `from`.
  defp refill(from) do
    pid = self()
    spawn_link fn ->
      result = GenServer.call(pid, :refill)
      GenServer.reply(from, result)
    end
  end

  ##########
  # Callback

  def handle_call(:refill, _from, []) do
    {:reply, 1, [2, 3]}
  end
  def handle_call(:refill, _from, [x | xs]) do
     {:reply, x, xs}
  end
  def handle_call({:push, x}, _from, xs) when is_list(xs) do
    {:reply, :ok, [x | xs]}
  end
  def handle_call(:pop, from, []) do
    # Handles refill and the reply to from.
    refill(from)
    # Returns nothing to the client, but unblocks the
    # server to get more requests.
    {:noreply, []}
  end
  def handle_call(:pop, _from, [x | xs]) do
    {:reply, x, xs}
  end
end

и вы получите следующее:

iex(1)> {:ok, pid} = Q.start_link()
{:ok, #PID<0.193.0>}
iex(2)> Q.pop(pid)
1
iex(3)> Q.pop(pid)
2
iex(4)> Q.pop(pid)
3
iex(5)> Q.pop(pid)
1
iex(6)> Q.pop(pid)
2
iex(7)> Q.pop(pid)
3
iex(8)> Q.push(pid, 4)
:ok
iex(9)> Q.pop(pid)    
4
iex(10)> Q.pop(pid)
1
iex(11)> Q.pop(pid)
2
iex(12)> Q.pop(pid)
3
iex(13)> tasks = for i <- 1..10 do
...(13)>   Task.async(fn -> {"Process #{inspect i}", Q.pop(pid)} end)
...(13)> end
(...)
iex(14)> for task <- tasks, do: Task.await(task)
[{"Process 1", 1}, {"Process 2", 2}, {"Process 3", 1}, {"Process 4", 2},
 {"Process 5", 3}, {"Process 6", 3}, {"Process 7", 2}, {"Process 8", 1},
 {"Process 9", 1}, {"Process 10", 3}]

таким образом, на самом деле GenServer может выполнять запросы к себе. Тебе просто нужно знать как.

надеюсь, это поможет.


почему вам нужно сделать GenServer.позвонить?

def handle_call(:pop, state) do
  new_state0 = if is_empty_queue(state) do
     put_some_stuff_in_queue(state)
  else
     state
  end
  {val,new_state} = pop_something(new_state0)

  {:reply, val, new_state}
end

или

def handle_call(:pop, state) do
  {val, new_state} = state
                     |> is_empty_queue
                     |> case do
                          true ->
                            put_some_stuff_in_queue(state)
                          false ->
                            state
                        end
                     |> pop_something

  {:reply, val, new_state}
end

таким образом, выполнение вызовов нет-нет, но вызов других функций полностью выполним.