Check out the

2 Unique Use Cases of GenServer.reply | Deep Insights |Elixir Expert

Don’t fear to reply anymore.

Blackode
Blackode
May 10 · 8 min read
BG Photo by Nicolas Jehly on Unsplash

In this article, we are talking about two scenarios on how to use reply function in GenServer module explaining with live examples.

The following are the two use cases we are going to cover in this article.

  1. Multi Node Communication via server messages using multi_call
  2. Converting Asynchronous request to Synchronous

What this article isn’t about ?

It don’t talk about GenServers and its usage. I hope you already know about the GenServer and its functions. If not check the following articles to master GenServer.

Article References for better understanding GenServer

1. Multi Node Communication via Registered Process|GenServer.multi_call

Building Requirements

Before entering to actual coding, we need two GenServer modules to be registered with the same name but on two different nodes.

Let’s build it.

GenServer Modules

The following GenServer files are not complex but, simplified to our requirements for demo purpose. It’s only job is to send current_balance when requested with a message :show_balance after waiting for 3 seconds.

bank1.ex

#bank1.exdefmodule 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.exdefmodule 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

There isn’t much difference between these two files except the the reply message. I hope you’ll get the files with no ease.

Creating Multiple IEx Nodes

Since we need to request multiple nodes, we create three iex nodes using the flag --sname
Open your terminal and create three tabs and run following commands one in each tab.

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

Connecting the Nodes

Here, I named the nodes as bank1 , bank2 , and central_bank. The central_bank node will connect with other two nodes bank1 and bank2.

Now, be in your iex node, named as central_bank and connect to remaining nodes as shown in the below 👇

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

The Node.list will give you the list of connected nodes and blackode is my machine name here.

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

Check out the following screenshot: connecting nodes.

Connecting Nodes

Compiling GenServer Modules

The next step is loading created GenServer modules from files bank1.ex and bank2.ex . We load bank1.ex and bank2.ex in iex sessions named bank1 and bank2 respectively.

Loading Bank1


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

Loading Bank2

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

NOTE:
Make sure you give correct paths while compiling the files in their respective nodes. I have started the sessions from the same folder where the files exist. So, I am simply passing the file names instead of path.

If your sessions and GenServer files are in different directory then you need to give exact path like in the below


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

Sending request to multiple GenServers on different nodes | GenServer.multi_call

Since we already loaded the modules Bank1 and Bank2 in bank1 and bank2 nodes respectively, we need to start the servers by calling start_link function from their nodes independently as shown in below.

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

In the above line of code, we are starting the server with initial balance of 1000 from the node bank1.

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

Similarly, we are starting the server with initial balance of 2000 from the node bank2.

Calling GenServer.multi_call function

Now switch to central_bank node and send message using multi_call. We need to pass the connected nodes, the registered name of the server (here Bank) and message to the server(here :show_balance).

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

], []}

The response of multi_call is a tuple of two lists {replies, bad_nodes}

  • replies - is a list of {node, reply} tuples where node is the node that replied and reply is its reply
  • bad_nodes - is a list of nodes that either did not exist or where a server with the given name did not exist or did not reply
    Above definitions copied directly from elixir docs

In our case, we got the empty list in the place of bad_nodes because all nodes replied successfully.

Check the following screenshot of execution

Calling GenServer.multi_call

What happens if One Of The Nodes is Failed to reply?

If any one of the nodes failed to reply then it won’t affect the other nodes replying. The failed node falls under bad_nodes in the response of the multi_call {replies, bad_nodes}.

Now, we manually check this by raising exception from the node bank2. So, add the line of code raise "I won't reply" inside the bank2.ex file in handle_call callback function. So, that it fails to reply back. We failing the server on purpose.

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

Now, recompile the bank2.ex file inside the node bank2 c "bank2.ex" and call the multi_call function again from the central_bank node.

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

This time you’ll see the reply from bank1 and not from bank2 node. The bank2 falls under bad_nodes.

Failing Node to Check the reply

That’s all about the two use cases of GenServer.reply(). I hope you understood the using of reply function over multi_call in GenServer.

2. Converting Asynchronous request to Synchronous request

Requirement

Assume that you need to process multiple ticket requests of some kind and each request is taking more amount of time. So, you need to free the server by putting the time taking task in a separate task(process) and we also need to send reply after the time taking job has completed.

In this we focus on the following points from coding side.

  • Running Time Taking Job Asynchronously
  • Replying the caller once the long running job has completed.

Trick to Code Play

Since we need to send the reply once the job has done, definitely we go with handle_call callback as it has the sender information to whom we need to reply. But, we keep the time taking logic in a separate process and also need to free the callback. So, we use {:noreply, state} instead of a regular one {:reply, reply, state}

But Who will send reply then as handle_call is not sending?

To reply sender, i.e from whom we got the request, we are maintaining the state of a server by adding ticket information on creating a reference and who requested like Map.put (tickets, ref, from).

The time taking job which we are running in a separate process will report to TicketServer by sending a message {:ticket_processed, ref, response} which we handle asynchronously. So, we can send response as a reply to the sender by popping out from the tickets from state using the key ref like {from, remaining_tickets} = Map.pop(tickets, ref) and then we use GenServer.reply to send response to the caller.

That’s the theory and some floating lines of code. Let’s dive into the actual code.

#ticket_server.exdefmodule 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

Brief about code

We start the server by calling start_link then we get pid. After that, we call process function with pid and ticket. The ticket is just a string here. In return the process function triggers the GenServer.call with the message {:process_ticket, ticket}. The matched handle_call callback get’s called and it is where we are separating the time taking logic and sending reply.

Now, let’s execute the server.

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

I hope you understood how to use GenServer.reply() based on your requirements.

Happy Coding!!

Thanks for reading.

Join Our Telegram Channel and support us.

Blackoders

Check out the GitHub repository on Killer Elixir Tips

Glad if you can contribute with a ★

TQ!

blackode

Coding, thoughts, and ideas.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store