Проверить

2 уникальных случая использования GenServer.reply | Deep Insights | Эксперт по эликсирам

Не бойтесь больше отвечать.

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

Ниже приведены два варианта использования, которые мы рассмотрим в этой статье.

  1. Многоузловая связь через серверные сообщения с использованием multi_call
  2. Преобразование асинхронного запроса в синхронный

О чем эта статья?

Здесь не говорится о GenServers и его использовании. Надеюсь, вы уже знаете о GenServer и его функциях. Если нет, ознакомьтесь со следующими статьями, чтобы освоить GenServer.

Ссылки на статьи для лучшего понимания GenServer







1. Многоузловая связь через зарегистрированный процесс | GenServer.multi_call

Требования к зданию

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

Давай построим.

Модули GenServer

Следующие ниже файлы GenServer не являются сложными, но упрощены в соответствии с нашими требованиями для демонстрационных целей. Единственная задача - отправить current_balance по запросу с сообщением :show_balance после ожидания 3 секунд .

bank1.ex

#bank1.ex
defmodule Bank1 do
  use GenServer
  def start_link(balance) do
    GenServer.start_link(__MODULE__, balance, name: Bank)
  end
  def init(balance) do
    {:ok, %{balance: balance}}
  end
  def handle_call(:show_balance, from, state) do
    Process.send_after(self(), {:reply, from}, 3_000)
    {:noreply, state}
  end
  def handle_info({:reply, bank_server}, %{balance: balance} = state) do
    GenServer.reply(bank_server, "The balance from Bank1 #{balance}")
    {:noreply, state}
  end
end

ban2.ex

#bank2.ex
defmodule Bank2 do
  use GenServer
  
  def start_link(balance) do
    GenServer.start_link(__MODULE__, balance, name: Bank)
  end
  
  def init(balance) do
    {:ok, %{balance: balance}}
  end
  
  def handle_call(:show_balance, from, state) do
    Process.send_after(self(), {:reply, from}, 3_000)
    {:noreply, state}
  end
  
  def handle_info({:reply, bank_server}, %{balance: balance} = state) do
    GenServer.reply(bank_server, "The balance from Bank2 #{balance}")
    {:noreply, state}
  end
end

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

Создание нескольких узлов IEx

Поскольку нам нужно запросить несколько узлов, мы создаем три iex узла с помощью флага --sname
Откройте свой терминал и создайте три вкладки и выполните следующие команды по одной на каждой вкладке.

$ iex --sname bank1
$ iex --sname bank2
$ iex --sname central_bank

Подключение узлов

Здесь я назвал узлы bank1, bank2 и central_bank. Узел central_bank соединится с двумя другими узлами bank1 и bank2.

Теперь войдите в свой iex узел с именем central_bank и подключитесь к оставшимся узлам, как показано ниже 👇

iex(central_bank@blackode)1> Node.connect :bank1@blackode
true
iex(central_bank@blackode)2> Node.connect :bank2@blackode
true

Node.list предоставит вам список подключенных узлов, а blackode - это имя моей машины.

iex(central_bank@blackode)3> Node.list
[:bank1@blackode, :bank2@blackode]

Посмотрите на следующий снимок экрана: соединение узлов.

Компиляция модулей GenServer

Следующим шагом будет загрузка созданных модулей GenServer из файлов bank1.ex и bank2.ex. Мы загружаем bank1.ex и bank2.ex в iex сеансах с именами bank1 и bank2 соответственно.

Загрузка Bank1

iex(bank1@blackode)1> c "bank1.ex"
[Bank1]

Загрузка Bank2

iex(bank2@blackode)1> c "bank2.ex"
[Bank2]

ПРИМЕЧАНИЕ.
Убедитесь, что вы указали правильные пути при компиляции файлов в соответствующих узлах. Я начал сеансы из той же папки, в которой существуют файлы. Итак, я просто передаю имена файлов вместо пути.

Если ваши сеансы и файлы GenServer находятся в разных каталогах, вам необходимо указать точный путь, как показано ниже.

iex> c "file/path/to/yourfile.ex"
example c "/home/code/bank1.ex"

Отправка запроса на несколько GenServer на разных узлах | GenServer.multi_call

Поскольку мы уже загрузили модули Bank1 и Bank2 в узлы bank1 и bank2 соответственно, нам необходимо запустить серверы, вызвав функцию start_link из их узлов независимо, как показано ниже.

iex(bank1@blackode) Bank1.start_link(1000)

В приведенной выше строке кода мы запускаем сервер с начальным балансом 1000 с узла bank1.

iex(bank2@blackode) Bank2.start_link(2000)

Точно так же мы запускаем сервер с начальным балансом 2000 с узла bank2.

Вызов функции GenServer.multi_call

Теперь переключитесь на узел central_bank и отправьте сообщение с помощью multi_call. Нам нужно передать подключенные узлы, зарегистрированное имя сервера (здесь Банк) и сообщение серверу (здесь: show_balance).

iex(central_bank@blackode)> nodes = Node.list
iex(central_bank@blackode)> GenSever.multi_call nodes, Bank, :show_balance
{[
   bank1@blackode: "The balance from Bank1 1000",
   bank2@blackode: "The balance from Bank2 2000"
 ], []}

Ответ multi_call представляет собой кортеж из двух списков {ответы, bad_nodes}

  • replies - это список {node, reply} кортежей, где node - ответивший узел, а reply - его ответ
  • bad_nodes - это список узлов, которые либо не существовали, либо сервер с указанным name не существовал или не отвечал
    Приведенные выше определения скопированы непосредственно из elixir docs

В нашем случае мы получили пустой список вместо bad_nodes, потому что все узлы ответили успешно.

Проверьте следующий снимок экрана выполнения

Что произойдет, если один из узлов не сможет ответить?

Если какой-либо из узлов не ответил, это не повлияет на ответы других узлов. Отказавший узел попадает в категорию bad_nodes в ответе на multi_call {replies, bad_nodes}.

Теперь мы вручную проверяем это, вызывая исключение из узла bank2. Итак, добавьте строку кода raise "I won't reply" в файл bank2.ex в функцию обратного вызова handle_call. Таким образом, он не может ответить. Мы намеренно вывели из строя сервер.

defmodule Bank2 do
  use GenServer
  
  ...
  
  def handle_call(:show_balance, from, state) do
    raise "I won't reply"
    Process.send_after(self(), {:reply, from}, 3_000)
    {:noreply, state}
  end
  
  ...
end

Теперь перекомпилируйте bank2.ex файл внутри узла bank2 c "bank2.ex" и снова вызовите функцию multi_call из узла central_bank.

iex(central_bank@blackode)> GenServer.multi_call nodes, Bank, :show_balance
{[bank1@blackode: "The balance from Bank1 1000"], [:bank2@blackode]}

На этот раз вы увидите ответ от узла bank1, а не от узла bank2. bank2 подпадает под bad_nodes.

Это все о двух вариантах использования GenServer.reply (). Надеюсь, вы поняли использование функции ответа вместо multi_call в GenServer.

2. Преобразование асинхронного запроса в синхронный запрос.

Требование

Предположим, что вам нужно обработать несколько запросов билетов какого-либо типа, и каждый запрос занимает больше времени. Итак, вам нужно освободить сервер, поместив задачу по времени в отдельную задачу (процесс), и нам также нужно отправить ответ после завершения задания по времени.

Здесь мы сосредоточимся на следующих моментах со стороны кодирования.

  • Продолжительность выполнения задания асинхронно
  • Ответ вызывающему абоненту после завершения длительного задания.

Уловка для кодовой игры

Поскольку нам нужно отправить ответ после того, как работа будет выполнена, мы определенно используем обратный вызов handle_call, поскольку он содержит информацию об отправителе, которому мы должны ответить. Но мы сохраняем логику учета времени в отдельном процессе, и нам также необходимо освободить обратный вызов. Итак, мы используем {:noreply, state} вместо обычного {:reply, reply, state}

Но кто тогда отправит ответ, если handle_call не отправляет?

Чтобы ответить отправителю, то есть от которого мы получили запрос, мы поддерживаем состояние сервера, добавляя информацию тикета при создании ссылки и кто просил вроде Map.put (tickets, ref, from).

Задание времени, которое мы выполняем в отдельном процессе, будет отчитываться на TicketServer, отправив сообщение {:ticket_processed, ref, response}, которое мы обрабатываем асинхронно. Итак, мы можем отправить ответ в виде ответа отправителю, выйдя из тикетов из состояния с помощью ключа ref, например {from, remaining_tickets} = Map.pop(tickets, ref), а затем мы используем GenServer.reply для отправки ответа вызывающему.

Это теория и несколько плавающих строк кода. Давайте погрузимся в реальный код.

#ticket_server.ex
defmodule TicketServer do
  use GenServer
  def start_link(options \\ []) do
    GenServer.start_link(__MODULE__, %{tickets: %{}}, options)
  end
  def init(state) do
    {:ok, state}
  end
  def process(pid, ticket) do
    GenServer.call(pid, {:process_ticket, ticket})
  end
  def handle_call({:process_ticket, ticket}, from, %{tickets: tickets} = state) do
    ref = make_ref()
    time_taking_job(self(), ref, ticket)
    tickets = Map.put(tickets, ref, from)
    state = %{state | tickets: tickets}
{:noreply, state}
  end
 def handle_cast({:ticket_processed, reference, response}, %{tickets: tickets} = state) do
    {from, remaining_tickets} = Map.pop(tickets, reference)
    GenServer.reply(from, response)
   state = %{state | tickets: remaining_tickets}
   {:noreply, state}
  end
  def time_taking_job(pid, reference, ticket) do
    Task.start fn -> 
      Process.sleep(3000)
      GenServer.cast(pid, {:ticket_processed, reference, "Got TicketResp: #{ticket}"})
    end
    IO.puts "#{ticket} has been processing...\n Please wait :)"
  end
end

Кратко о коде

Мы запускаем сервер, вызывая start_link, после чего получаем pid. После этого мы вызываем функцию process с pid и ticket. Билет здесь представляет собой просто строку. В свою очередь, функция process запускает GenServer.call с сообщением {:process_ticket, ticket}. Вызывается сопоставленный обратный вызов handle_call get, и именно здесь мы разделяем логику времени и отправку ответа.

Теперь давайте запустим сервер.

iex> {:ok, pid} = TicketServer.start_link() #starting
iex❯ TicketServer.process pid, "T1234"      #calling process
T1234 has been processing...
 Please wait :)
"Got TicketResp: T1234"                     #prints after 3 seconds

Надеюсь, вы поняли, как использовать GenServer.reply() в соответствии с вашими требованиями.

Удачного кодирования !!

Спасибо за чтение.

Присоединяйтесь к нашему каналу Telegram и поддержите нас.

Блэкодеры



«Blackoders
EAT 🍕 - CODE🐞 - SLEEP😴 Код, мысли и идеи Ресурсы по кодированию, советы, видео, статьи и новости Мы следим и собираем… t .меня"



Посетите репозиторий GitHub в разделе Советы по Killer Elixir

Рад, если вы внесете свой вклад в ★

TQ!